diff --git a/AppCheck.xcodeproj/project.pbxproj b/AppCheck.xcodeproj/project.pbxproj index 470381d..35a7ff2 100644 --- a/AppCheck.xcodeproj/project.pbxproj +++ b/AppCheck.xcodeproj/project.pbxproj @@ -70,6 +70,7 @@ 545DDDD124436983003B6544 /* QuickUI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD024436983003B6544 /* QuickUI.swift */; }; 545DDDD424466D37003B6544 /* AutoLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 545DDDD324466D37003B6544 /* AutoLayout.swift */; }; 546063E523FEFAFE008F505A /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; }; + 54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54686A7524F8062C0084934D /* NotificationBanner.swift */; }; 54751E512423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; }; 54751E522423955100168273 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54751E502423955000168273 /* URL.swift */; }; 54953E3323DC752E0054345C /* DBCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54B7562223D7B2DC008F0C41 /* DBCore.swift */; }; @@ -268,6 +269,7 @@ 545DDDCE243E6267003B6544 /* TutorialSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TutorialSheet.swift; sourceTree = ""; }; 545DDDD024436983003B6544 /* QuickUI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickUI.swift; sourceTree = ""; }; 545DDDD324466D37003B6544 /* AutoLayout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoLayout.swift; sourceTree = ""; }; + 54686A7524F8062C0084934D /* NotificationBanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationBanner.swift; sourceTree = ""; }; 54751E502423955000168273 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 548B1F9423D338EC005B047C /* main.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = main.entitlements; sourceTree = ""; }; 54953E5E23DEBE840054345C /* TVCDomains.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TVCDomains.swift; sourceTree = ""; }; @@ -565,6 +567,7 @@ 541FC47524A12D01009154D8 /* IBViews.swift */, 5404AEEC24A95F3F003B2F54 /* SlideInAnimation.swift */, 541075D024CDBA0000D6F1BF /* ThrottledBatchQueue.swift */, + 54686A7524F8062C0084934D /* NotificationBanner.swift */, ); path = "Common Classes"; sourceTree = ""; @@ -1017,6 +1020,7 @@ 54B345A6241BB982004C53CC /* Notifications.swift in Sources */, 54448A2E2486464F00771C96 /* Array.swift in Sources */, 54E67E4B24A8C6370025D261 /* GlassVPN.swift in Sources */, + 54686A7624F8062C0084934D /* NotificationBanner.swift in Sources */, 541FC47824A1453F009154D8 /* VCCoOccurrence.swift in Sources */, 54B345AB241BBA5B004C53CC /* AlertSheet.swift in Sources */, 541DCA6124A6B0F6005F1A4B /* Color.swift in Sources */, diff --git a/main/Assets.xcassets/.DS_Store b/main/Assets.xcassets/.DS_Store index 2e96e05..ae9fe4d 100644 Binary files a/main/Assets.xcassets/.DS_Store and b/main/Assets.xcassets/.DS_Store differ diff --git a/main/Assets.xcassets/circle-check.imageset/Contents.json b/main/Assets.xcassets/circle-check.imageset/Contents.json new file mode 100644 index 0000000..20c5ee0 --- /dev/null +++ b/main/Assets.xcassets/circle-check.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "img.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "img@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "img@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/main/Assets.xcassets/circle-check.imageset/img.png b/main/Assets.xcassets/circle-check.imageset/img.png new file mode 100644 index 0000000..1d66b21 Binary files /dev/null and b/main/Assets.xcassets/circle-check.imageset/img.png differ diff --git a/main/Assets.xcassets/circle-check.imageset/img@2x.png b/main/Assets.xcassets/circle-check.imageset/img@2x.png new file mode 100644 index 0000000..6ebdd2c Binary files /dev/null and b/main/Assets.xcassets/circle-check.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/circle-check.imageset/img@3x.png b/main/Assets.xcassets/circle-check.imageset/img@3x.png new file mode 100644 index 0000000..36ed41f Binary files /dev/null and b/main/Assets.xcassets/circle-check.imageset/img@3x.png differ diff --git a/main/Assets.xcassets/circle-x.imageset/Contents.json b/main/Assets.xcassets/circle-x.imageset/Contents.json new file mode 100644 index 0000000..20c5ee0 --- /dev/null +++ b/main/Assets.xcassets/circle-x.imageset/Contents.json @@ -0,0 +1,26 @@ +{ + "images" : [ + { + "filename" : "img.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "img@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "img@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/main/Assets.xcassets/circle-x.imageset/img.png b/main/Assets.xcassets/circle-x.imageset/img.png new file mode 100644 index 0000000..9223a0f Binary files /dev/null and b/main/Assets.xcassets/circle-x.imageset/img.png differ diff --git a/main/Assets.xcassets/circle-x.imageset/img@2x.png b/main/Assets.xcassets/circle-x.imageset/img@2x.png new file mode 100644 index 0000000..8e715da Binary files /dev/null and b/main/Assets.xcassets/circle-x.imageset/img@2x.png differ diff --git a/main/Assets.xcassets/circle-x.imageset/img@3x.png b/main/Assets.xcassets/circle-x.imageset/img@3x.png new file mode 100644 index 0000000..9374c66 Binary files /dev/null and b/main/Assets.xcassets/circle-x.imageset/img@3x.png differ diff --git a/main/Common Classes/NotificationBanner.swift b/main/Common Classes/NotificationBanner.swift new file mode 100644 index 0000000..080923f --- /dev/null +++ b/main/Common Classes/NotificationBanner.swift @@ -0,0 +1,63 @@ +import UIKit + +struct NotificationBanner { + enum Style { + case fail, ok + } + + let view: UIView + + init(_ msg: String, style: Style) { + let bg, fg: UIColor + let imgName: String + switch style { + case .fail: + bg = .systemRed + fg = UIColor.black.withAlphaComponent(0.80) + imgName = "circle-x" + case .ok: + bg = .systemGreen + fg = UIColor.black.withAlphaComponent(0.65) + imgName = "circle-check" + } + view = UIView() + view.backgroundColor = bg + let lbl = QuickUI.label(msg, style: .callout) + lbl.textColor = fg + lbl.numberOfLines = 0 + lbl.font = lbl.font.bold() + let img = QuickUI.image(UIImage(named: imgName)) + img.tintColor = fg + view.addSubview(lbl) + view.addSubview(img) + img.anchor([.leading, .centerY], to: view.layoutMarginsGuide) + lbl.anchor([.top, .bottom, .trailing], to: view.layoutMarginsGuide) + img.widthAnchor =&= 25 + img.heightAnchor =&= 25 + lbl.leadingAnchor =&= img.trailingAnchor + 8 + img.bottomAnchor =<= view.bottomAnchor - 8 + lbl.bottomAnchor =<= view.bottomAnchor - 8 + } + + /// Animate header banner from the top of the view. Show for `delay` seconds and then hide again. + /// - Parameter onClose: Run after the close animation finishes. + func present(in vc: UIViewController, hideAfter delay: TimeInterval = 3, onClose: (() -> Void)? = nil) { + vc.view.addSubview(view) + view.anchor([.leading, .trailing], to: vc.view!) + vc.view.layoutIfNeeded() // sets the height + let h = view.frame.height + let constraint = view.topAnchor =&= vc.view.topAnchor - h + vc.view.layoutIfNeeded() // hide view + UIView.animate(withDuration: 0.3, animations: { + constraint.constant = 0 + vc.view.layoutIfNeeded() // animate view + UIView.animate(withDuration: 0.3, delay: delay, options: .curveLinear, animations: { + constraint.constant = -h + vc.view.layoutIfNeeded() // hide again + }, completion: { _ in + self.view.removeFromSuperview() + onClose?() + }) + }) + } +} diff --git a/main/DB/DBExtensions.swift b/main/DB/DBExtensions.swift index a2f5f52..a3bb384 100644 --- a/main/DB/DBExtensions.swift +++ b/main/DB/DBExtensions.swift @@ -33,7 +33,9 @@ extension FilterOptions { } extension Recording { - var fallbackTitle: String { get { "Unnamed Recording #\(id)" } } + var fallbackTitle: String { get { + isLongTerm ? "Background Recording" : "Unnamed Recording #\(id)" + } } var duration: Timestamp? { get { stop == nil ? nil : stop! - start } } var isLongTerm: Bool { (duration ?? 0) > Timestamp.hours(1) } } diff --git a/main/Extensions/AlertSheet.swift b/main/Extensions/AlertSheet.swift index bc0b1c0..a290e49 100644 --- a/main/Extensions/AlertSheet.swift +++ b/main/Extensions/AlertSheet.swift @@ -39,9 +39,13 @@ func AskAlert(title: String?, text: String?, buttonText: String = "Continue", bu /// Show alert hinting the user to go to system settings and re-enable notifications. func NotificationsDisabledAlert(presentIn viewController: UIViewController) { - Alert(title: "Notifications Disabled", - text: "Go to System Settings > Notifications > AppCheck to re-enable notifications." - ).presentIn(viewController) + 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) + } + }.presentIn(viewController) } // MARK: Alert with multiple options diff --git a/main/GUI/Base.lproj/Recordings.storyboard b/main/GUI/Base.lproj/Recordings.storyboard index 147b935..8018105 100644 --- a/main/GUI/Base.lproj/Recordings.storyboard +++ b/main/GUI/Base.lproj/Recordings.storyboard @@ -260,7 +260,7 @@ - + @@ -274,6 +274,9 @@ + @@ -283,11 +286,14 @@ + + + diff --git a/main/Recordings/TVCAppSearch.swift b/main/Recordings/TVCAppSearch.swift index 99c7690..abe3dcf 100644 --- a/main/Recordings/TVCAppSearch.swift +++ b/main/Recordings/TVCAppSearch.swift @@ -106,7 +106,7 @@ class TVCAppSearch: UITableViewController, UISearchBarDelegate { let alert = AskAlert(title: "App Name", text: "Be as descriptive as possible. Preferably use app bundle id if available. Alternatively use app name or a link to a public repository.", buttonText: "Set") { - self.delegate?.appSearch(didSelect: "un.known", appName: $0.textFields?.first?.text, developer: nil) + self.delegate?.appSearch(didSelect: "_manually", appName: $0.textFields?.first?.text, developer: nil) self.closeThis() } alert.addTextField { $0.placeholder = "com.apple.notes" } diff --git a/main/Recordings/VCEditRecording.swift b/main/Recordings/VCEditRecording.swift index a4a5fb2..7e64278 100644 --- a/main/Recordings/VCEditRecording.swift +++ b/main/Recordings/VCEditRecording.swift @@ -21,7 +21,7 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate if record.isLongTerm { appId = nil appIcon.image = nil - appTitle.text = "Background Recording" + appTitle.text = record.fallbackTitle appDeveloper.text = nil chooseAppTap.isEnabled = false } else { @@ -158,7 +158,7 @@ class VCEditRecording: UIViewController, UITextFieldDelegate, UITextViewDelegate private func validateSaveButton() { let changed = (appId != record.appId - || (appTitle.text != record.title && appTitle.text != "Tap here to choose app") + || (appTitle.text != record.title && appTitle.text != "Tap here to choose app" && appTitle.text != record.fallbackTitle) || appDeveloper.text != record.subtitle || inputNotes.text != record.notes ?? "") buttonSave.isEnabled = changed || deleteOnCancel // always allow save for new recordings diff --git a/main/Recordings/VCShareRecording.swift b/main/Recordings/VCShareRecording.swift index 0925efe..5d5fe9d 100644 --- a/main/Recordings/VCShareRecording.swift +++ b/main/Recordings/VCShareRecording.swift @@ -6,6 +6,7 @@ class VCShareRecording : UIViewController { private var jsonData: Data? @IBOutlet private var text : UITextView! + @IBOutlet private var sendActivity : UIActivityIndicatorView! override func viewDidLoad() { super.viewDidLoad() @@ -59,8 +60,68 @@ class VCShareRecording : UIViewController { dismiss(animated: true) } - @IBAction private func shareRecording() { - print("\(String(data: jsonData!, encoding: .utf8)!)") - Alert(title: "Not implemented yet", text: nil).presentIn(self) + @IBAction private func shareRecording(_ sender: UIBarButtonItem) { + sender.isEnabled = false + sendActivity.startAnimating() + + let url = URL(string: "http://127.0.0.1/api/v1/contribute/")! + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = jsonData + + URLSession.shared.dataTask(with: request) { data, response, error in + DispatchQueue.main.async { [weak self] in + sender.isEnabled = true + self?.sendActivity.stopAnimating() + + guard error == nil, let data = data, + let response = response as? HTTPURLResponse else { + self?.banner(.fail, "\(error?.localizedDescription ?? "Unkown error occurred")") + return + } + let json = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] + let status = json?["status"] as? String + let v = json?["v"] as? Int ?? 0 + guard v > 0, (200 ... 299) ~= response.statusCode else { + QLog.Warning("Couldn't contribute: \(status ?? "unkown reason")") + self?.banner(.fail, "Server couldn't parse request.\nTry again later.") + return + } + var autoHide = true + if v == 1, let urlStr = json?["url"] as? String { + let nextUpdateIn = json?["when"] as? Int + self?.showOpenResultsAlert(urlStr, when: nextUpdateIn) + autoHide = false + } + self?.banner(.ok, "Thank you for your contribution.", + autoHide ? { [weak self] in self?.closeView() } : nil) + } + }.resume() + } + + private func banner(_ style: NotificationBanner.Style, _ msg: String, _ closure: (() -> Void)? = nil) { + NotificationBanner(msg, style: style).present(in: self, onClose: closure) + } + + private func showOpenResultsAlert(_ urlStr: String, when: Int?) { + var msg = "Your contribution is being processed and will be available " + if let when = when { + if when < 61 { + msg += "in approx. \(when) sec. " + } else { + let fmt = TimeFormat.from(Timestamp(when)) + msg += "in \(fmt) min. " + } + } else { + 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 + if let url = URL(string: urlStr) { + UIApplication.shared.openURL(url) + } + }) + alert.presentIn(self) } } diff --git a/media/third-level.txt b/media/third-level.txt index eeca54e..da54ad3 100644 --- a/media/third-level.txt +++ b/media/third-level.txt @@ -84,6 +84,31 @@ br.vet br.vlog br.wiki br.zlg +co.a +co.b +co.com +co.edu +co.g +co.gov +co.inf +co.m +co.mil +co.net +co.ngo +co.nom +co.o +co.org +co.s +co.t +co.x +co.y +er.com +er.edu +er.gov +er.mil +er.net +er.org +er.ind es.com es.edu es.gob @@ -553,4 +578,4 @@ za.nom za.org za.school za.tm -za.web \ No newline at end of file +za.web