Voice recording fixes.

Adding experimental siri support
This commit is contained in:
Selim Mustafaev 2020-06-28 20:30:28 +03:00
parent 14357787dd
commit f513222d72
36 changed files with 416 additions and 104 deletions

2
.gitignore vendored
View File

@ -1 +1 @@
Carthage/
design/generated/

View File

@ -11,6 +11,7 @@ extension OSLog {
enum QuickAction {
case none
case check
case checkNumber(String)
case addVoiceRecord
}
@ -22,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration(
schemaVersion: 8,
schemaVersion: 9,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion <= 3 {
var numbers: [String] = []
@ -39,7 +40,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
})
Realm.Configuration.defaultConfiguration = config
print(Realm.Configuration.defaultConfiguration.fileURL!)
IHProgressHUD.set(defaultStyle: .dark)
IHProgressHUD.set(defaultMaskType: .black)

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "check-37.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "check-54.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "check-51.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "check-75.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "record-47.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "record-69.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "record-63.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "record-93.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "search-35.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "search-51.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 870 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "search-47.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "search-69.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "settings-37.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "settings-54.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -0,0 +1,25 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "settings-51.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "settings-75.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -223,14 +223,14 @@
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<prototypes>
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="blue" hidesAccessoryWhenEditing="NO" indentationLevel="1" indentationWidth="0.0" reuseIdentifier="VehicleCell" id="VEP-QD-i6y" customClass="VehicleCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="85.5"/>
<rect key="frame" x="0.0" y="28" width="375" height="85"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="VEP-QD-i6y" id="8hH-8I-XLB">
<rect key="frame" x="0.0" y="0.0" width="375" height="85.5"/>
<rect key="frame" x="0.0" y="0.0" width="375" height="85"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Kia (JF) Optima" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AQY-7N-q8D">
<rect key="frame" x="8" y="8" width="124" height="21.5"/>
<rect key="frame" x="8" y="8" width="124" height="21"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -242,7 +242,7 @@
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="cvf-vM-QnT" customClass="PlateView" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="8" y="37.5" width="317" height="40"/>
<rect key="frame" x="8" y="37" width="317" height="40"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="height" constant="40" id="Xoz-Iw-PCU"/>
@ -376,7 +376,7 @@
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<viewLayoutGuide key="safeArea" id="Uix-8K-fxh"/>
</view>
<tabBarItem key="tabBarItem" title="Settings" image="gear" catalog="system" id="zEL-ph-E2f"/>
<tabBarItem key="tabBarItem" title="Settings" image="settings" landscapeImage="settings-compact" id="zEL-ph-E2f"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="trD-gZ-yAv" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
@ -419,14 +419,14 @@
<action selector="onPlay:" destination="mzE-bt-IiX" eventType="touchUpInside" id="hwo-ns-0RK"/>
</connections>
</button>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="00:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bFQ-eU-5YJ">
<rect key="frame" x="56.5" y="0.0" width="46" height="44.5"/>
<label hidden="YES" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="252" verticalHuggingPriority="251" text="00:00" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bFQ-eU-5YJ">
<rect key="frame" x="50.5" y="0.0" width="0.0" height="44.5"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" systemColor="systemTealColor" red="0.35294117650000001" green="0.7843137255" blue="0.98039215690000003" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" verticalHuggingPriority="251" text="Label" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="MjS-Hy-iGH">
<rect key="frame" x="114.5" y="0.0" width="195" height="44.5"/>
<rect key="frame" x="56.5" y="0.0" width="253" height="44.5"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
@ -752,7 +752,7 @@
<scene sceneID="pUX-kf-oY1">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="TSb-ZG-qfD" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Check" image="eye" catalog="system" selectedImage="eye.fill" id="QJd-35-4OB"/>
<tabBarItem key="tabBarItem" title="Check" image="check" landscapeImage="check-compact" id="QJd-35-4OB"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="AAc-4d-GNh">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
@ -771,7 +771,7 @@
<scene sceneID="kiS-EQ-VFl">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="GCa-Re-j14" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Search" image="magnifyingglass" catalog="system" id="gDG-z8-R0t"/>
<tabBarItem key="tabBarItem" title="Search" image="search" landscapeImage="search-compact" id="gDG-z8-R0t"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="vdY-9n-hjX">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
@ -808,7 +808,7 @@
<scene sceneID="oyu-oz-pC4">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="RK6-pn-2Bg" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Records" image="recordingtape" catalog="system" id="lxF-EY-z8V"/>
<tabBarItem key="tabBarItem" title="Records" image="record" landscapeImage="record-compact" id="lxF-EY-z8V"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="8YG-pw-LE7">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
@ -825,13 +825,16 @@
</scene>
</scenes>
<resources>
<image name="check" width="25" height="25"/>
<image name="check-compact" width="18" height="18"/>
<image name="doc.on.doc" catalog="system" width="117" height="128"/>
<image name="eye" catalog="system" width="128" height="81"/>
<image name="eye.fill" catalog="system" width="128" height="78"/>
<image name="gear" catalog="system" width="128" height="119"/>
<image name="line.horizontal.3.decrease" catalog="system" width="128" height="73"/>
<image name="magnifyingglass" catalog="system" width="128" height="115"/>
<image name="play.fill" catalog="system" width="116" height="128"/>
<image name="recordingtape" catalog="system" width="128" height="60"/>
<image name="record" width="31" height="31"/>
<image name="record-compact" width="23" height="23"/>
<image name="search" width="23" height="23"/>
<image name="search-compact" width="17" height="17"/>
<image name="settings" width="25" height="25"/>
<image name="settings-compact" width="18" height="18"/>
</resources>
</document>

View File

@ -73,11 +73,20 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
func handleQuickActions() {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
if ad.quickAction == .check {
switch ad.quickAction {
case .check:
ad.quickAction = .none
self.number.becomeFirstResponder()
} else if ad.quickAction == .addVoiceRecord {
break
case .checkNumber(let number):
ad.quickAction = .none
self.check(number: number)
break
case .addVoiceRecord:
self.tabBarController?.selectedIndex = 1
break
default:
break
}
}
@ -85,19 +94,22 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
guard let number = self.number.text else { return }
let numberNormalized = number.filter { !$0.isWhitespace }.uppercased()
self.check(number: numberNormalized)
}
func check(number: String) {
self.number.resignFirstResponder()
self.number.text = nil
self.check.isEnabled = false
IHProgressHUD.show()
Api.checkVehicle(by: numberNormalized)
Api.checkVehicle(by: number)
.observeOn(MainScheduler.instance)
.subscribe(onNext: onReceivedVehicle(_:), onError: { err in
if let realm = try? Realm() {
try? realm.write {
realm.add(Vehicle(numberNormalized), update: .all)
}
}
if let realm = try? Realm() {
try? realm.write {
realm.add(Vehicle(number), update: .all)
}
}
IHProgressHUD.showError(withStatus: err.localizedDescription)
print(err.localizedDescription)
}).disposed(by: self.bag)

View File

@ -4,6 +4,9 @@ import RealmSwift
import RxSwift
import RxRealm
import RxDataSources
import Intents
import CoreSpotlight
import MobileCoreServices
class RecordsController: UIViewController, UITableViewDelegate {
@ -11,16 +14,16 @@ class RecordsController: UIViewController, UITableViewDelegate {
var recorder: Recorder?
var addButton: UIBarButtonItem!
var cancelButton: UIBarButtonItem!
let bag = DisposeBag()
let validLetters = ["А", "В", "Е", "К", "М", "Н", "О", "Р", "С", "Т", "У", "Х"]
override func viewDidLoad() {
super.viewDidLoad()
guard let realm = try? Realm() else { return }
self.addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(onAddVoiceRecord(_:)))
self.cancelButton = UIBarButtonItem(barButtonSystemItem: .cancel, target: self, action: #selector(onCancelRecording(_:)))
self.navigationItem.rightBarButtonItem = self.addButton
self.recorder = try? Recorder()
@ -57,6 +60,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
self.tableView.rx.setDelegate(self).disposed(by: self.bag)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.handleQuickActions()
@ -65,14 +69,36 @@ class RecordsController: UIViewController, UITableViewDelegate {
func handleQuickActions() {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
if ad.quickAction == .addVoiceRecord {
switch ad.quickAction {
case .addVoiceRecord:
ad.quickAction = .none
if let addButton = self.navigationItem.rightBarButtonItem {
self.onAddVoiceRecord(addButton)
}
break
default:
break
}
}
func donateUserActivity() {
let activityId = "pro.aliencat.autocat.addVoiceRecord"
let activity = NSUserActivity(activityType: activityId)
activity.persistentIdentifier = activityId
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
activity.title = "Add new audio record"
activity.suggestedInvocationPhrase = "Запиши номер"
let attributes = CSSearchableItemAttributeSet()
attributes.contentType = kUTTypeItem as String
attributes.contentDescription = "Add new plate number via audio recording"
activity.contentAttributeSet = attributes
self.userActivity = activity
activity.becomeCurrent()
}
// MARK: - Bar button handlers
@objc func onAddVoiceRecord(_ sender: UIBarButtonItem) {
@ -87,25 +113,24 @@ class RecordsController: UIViewController, UITableViewDelegate {
self.show(error: error)
} else {
do {
let alert = UIAlertController(title: "Recording...", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.recorder?.cancelRecording() }))
self.present(alert, animated: true)
let date = Date()
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
let url = try FileManager.default.url(for: fileName, in: "recordings")
try recorder.startRecording(to: url) { result in
self.navigationItem.rightBarButtonItem?.isEnabled = true
self.navigationItem.leftBarButtonItem = nil
self.title = "Voice recordings"
let asset = AVURLAsset(url: url)
let duration = TimeInterval(CMTimeGetSeconds(asset.duration))
let record = AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: result), duration: duration)
let record = AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: result), raw: result, duration: duration)
let realm = try? Realm()
try? realm?.write {
realm?.add(record)
}
alert.dismiss(animated: true)
}
self.title = "Recording..."
self.navigationItem.rightBarButtonItem?.isEnabled = false
self.navigationItem.leftBarButtonItem = self.cancelButton
self.donateUserActivity()
} catch {
IHProgressHUD.showError(withStatus: error.localizedDescription)
}
@ -114,69 +139,103 @@ class RecordsController: UIViewController, UITableViewDelegate {
}
}
@objc func onCancelRecording(_ sender: UIBarButtonItem) {
self.recorder?.cancelRecording()
self.navigationItem.rightBarButtonItem?.isEnabled = true
self.navigationItem.leftBarButtonItem = nil
self.title = "Voice recordings"
}
// MARK: - Processing
func getPlateNumber(from recognizedText: String) -> String? {
let trimmed = recognizedText.replacingOccurrences(of: " ", with: "").uppercased()
let trimmed = recognizedText
.replacingOccurrences(of: " ", with: "")
.uppercased()
.replacingOccurrences(of: "Ф", with: "В")
var result = ""
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
return String(trimmed[range])
result = String(trimmed[range])
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression) {
let n = String(trimmed[range])
return n.prefix(1) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
return String(trimmed[range]) + "161"
result = String(trimmed[range]) + "161"
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression) {
let n = String(trimmed[range])
return n.prefix(1) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + "161"
result = n.prefix(1) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + "161"
}
return nil
if !result.isEmpty && valid(number: result) {
return result
} else {
return nil
}
}
func valid(number: String) -> Bool {
let first = String(number.prefix(1))
let second = number.substring(with: 4..<5)
let third = number.substring(with: 5..<6)
return self.validLetters.contains(first)
&& self.validLetters.contains(second)
&& self.validLetters.contains(third)
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let record: AudioRecord = try? self.tableView.rx.model(at: indexPath) else { return nil }
/*
let deleteAction = UIContextualAction(style: .normal, title: "Delete") { action, view, completion in
do {
let realm = try Realm()
try realm.write {
realm.delete(record)
}
completion(true)
} catch {
print("Error deleting audio record: \(error.localizedDescription)")
completion(false)
}
}
deleteAction.image = UIImage(systemName: "trash")
deleteAction.backgroundColor = .systemRed
*/
guard let cell = tableView.cellForRow(at: indexPath) else { return nil }
let check = UIContextualAction(style: .normal, title: "Check") { action, view, completion in
if let number = record.number {
self.check(number: number)
}
completion(true)
}
check.backgroundColor = .systemGray2
check.image = UIImage(systemName: "eye")
let share = UIContextualAction(style: .normal, title: "Share") { action, view, completion in
let action = UIContextualAction(style: .normal, title: "Action") { action, view, completion in
self.moreActions(for: record, cell: cell)
completion(true)
}
share.backgroundColor = .systemGray2
action.backgroundColor = .systemGray2
action.image = UIImage(systemName: "ellipsis" /*"square.and.arrow.up"*/)
let delete = UIContextualAction(style: .destructive, title: "Delete") { action, view, completion in
self.tableView.dataSource?.tableView!(self.tableView, commit: .delete, forRowAt: indexPath)
completion(true)
}
delete.image = UIImage(systemName: "trash")
return UISwipeActionsConfiguration(actions: [delete, check, share])
let actions = record.number == nil ? [delete, action] : [delete, check, action]
return UISwipeActionsConfiguration(actions: actions)
}
func moreActions(for record: AudioRecord, cell: UITableViewCell) {
let sheet = UIAlertController(title: "More actions", message: nil, preferredStyle: .actionSheet)
let cancel = UIAlertAction(title: "Cancel", style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) }
let share = UIAlertAction(title: "Share", style: .default) { _ in
do {
let url = try FileManager.default.url(for: record.path, in: "recordings")
let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil)
self.present(controller, animated: true)
} catch {
print("Error sharing audio record: \(error.localizedDescription)")
IHProgressHUD.showError(withStatus: error.localizedDescription)
}
}
let showText = UIAlertAction(title: "Show recognized text", style: .default) { action in
self.showAlert(title: "Recognized text", message: record.rawText)
}
sheet.addAction(showText)
sheet.addAction(share)
sheet.addAction(cancel)
sheet.popoverPresentationController?.sourceView = cell
sheet.popoverPresentationController?.sourceRect = cell.frame
self.present(sheet, animated: true, completion: nil)
}
func check(number: String) {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
ad.quickAction = .checkNumber(number)
self.tabBarController?.selectedIndex = 0
}
}

View File

@ -120,8 +120,11 @@ class ReportController: UIViewController, UICollectionViewDataSource, UICollecti
self.navigationController?.setNavigationBarHidden(self.vehicle == nil, animated: animated)
if ad.quickAction == .check {
switch ad.quickAction {
case .check:
self.dismiss(animated: false, completion: nil)
default:
break
}
}

View File

@ -39,15 +39,17 @@ class SearchController: UIViewController, UISearchResultsUpdating {
.subscribe(onNext: self.updateDetailController(with:))
.disposed(by: self.bag)
self.filterRelay
//.throttle(.seconds(2), scheduler: MainScheduler.instance)
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.flatMap(Api.getVehicles)
.observeOn(MainScheduler.instance)
.do(onNext: { self.navigationItem.title = "\($0.count) vehicles found" })
.map { $0.groupedByDate() }
.bind(to: self.tableView.rx.items(dataSource: ds))
.disposed(by: self.bag)
DispatchQueue.main.async {
self.filterRelay
//.throttle(.seconds(2), scheduler: MainScheduler.instance)
.debounce(.milliseconds(500), scheduler: MainScheduler.instance)
.flatMap(Api.getVehicles)
.observeOn(MainScheduler.instance)
.do(onNext: { self.navigationItem.title = "\($0.count) vehicles found" })
.map { $0.groupedByDate() }
.bind(to: self.tableView.rx.items(dataSource: ds))
.disposed(by: self.bag)
}
}
// FIXME: Code duplication

View File

@ -6,6 +6,7 @@ class AudioRecord: Object, IdentifiableType {
@objc dynamic var path: String = ""
@objc dynamic var number: String?
@objc dynamic var rawText: String = ""
@objc dynamic var addedDate: TimeInterval = Date().timeIntervalSince1970
@objc dynamic var duration: TimeInterval = 0
@ -17,13 +18,16 @@ class AudioRecord: Object, IdentifiableType {
return self.identifier
}
init(path: String, number: String?, duration: TimeInterval) {
init(path: String, number: String?, raw: String, duration: TimeInterval) {
self.path = path
self.number = number
self.duration = duration
self.rawText = raw
}
required init() {
super.init()
self.identifier = self.addedDate
}
override class func ignoredProperties() -> [String] {

View File

@ -1,4 +1,5 @@
import UIKit
import os.log
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
@ -9,6 +10,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
guard let windowScene = (scene as? UIWindowScene) else { return }
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
if let activity = connectionOptions.userActivities.first {
if activity.activityType == "pro.aliencat.autocat.addVoiceRecord" {
ad.quickAction = .addVoiceRecord
}
}
self.window = UIWindow(windowScene: windowScene)
let storyboard = UIStoryboard(name: "Main", bundle: nil)
@ -70,20 +78,31 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
} else if shortcutItem.type == "AddVoiceRecordAction" {
ad.quickAction = .addVoiceRecord
self.handleAddVoiceRecordAction()
}
}
if let split = self.window?.rootViewController as? MainSplitController, let tabvc = split.viewControllers.first as? UITabBarController {
if tabvc.selectedIndex == 1 {
if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController {
if let record = child as? RecordsController {
record.handleQuickActions()
} else {
nav.popToRootViewController(animated: false)
}
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
if userActivity.activityType == "pro.aliencat.autocat.addVoiceRecord" {
self.handleAddVoiceRecordAction()
}
}
func handleAddVoiceRecordAction() {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
ad.quickAction = .addVoiceRecord
if let split = self.window?.rootViewController as? MainSplitController, let tabvc = split.viewControllers.first as? UITabBarController {
if tabvc.selectedIndex == 1 {
if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController {
if let record = child as? RecordsController {
record.handleQuickActions()
} else {
nav.popToRootViewController(animated: false)
}
} else {
tabvc.selectedIndex = 1
}
} else {
tabvc.selectedIndex = 1
}
}
}

View File

@ -25,7 +25,7 @@ class Recorder {
init() throws {
try self.session.setCategory(.playAndRecord, mode: .spokenAudio, options: .mixWithOthers)
try self.session.setCategory(.record, mode: .spokenAudio, options: .mixWithOthers)
}
func requestPermissions(completion: @escaping (NSError?) -> Void) {
@ -72,6 +72,9 @@ class Recorder {
throw CocoaError.error(CocoaError.Code.fileWriteUnknown)
}
// Play sound (sms_alert_note.caf) to indicate start of recording
AudioServicesPlayAlertSound(SystemSoundID(4097))
ExtAudioFileSetProperty(fileRef, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size), inFormat.streamDescription)
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
@ -85,14 +88,14 @@ class Recorder {
self.result = transcription.formattedString
self.endRecognitionTimer?.invalidate()
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { timer in
self.cancelRecording()
self.stopRecording()
completion(self.result)
}
}
}
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
self.cancelRecording()
self.stopRecording()
completion(self.result)
}
@ -101,6 +104,12 @@ class Recorder {
}
func cancelRecording() {
self.stopRecording()
self.endRecognitionTimer?.invalidate()
self.endRecognitionTimer = nil
}
func stopRecording() {
self.engine.stop()
self.engine.inputNode.removeTap(onBus: 0)
self.request.endAudio()
@ -110,9 +119,4 @@ class Recorder {
ExtAudioFileDispose(fileRef)
}
}
func stopRecording() -> String {
self.cancelRecording()
return self.result
}
}

BIN
design/icons.sketch Normal file

Binary file not shown.