Adding voice record feature

This commit is contained in:
Selim Mustafaev 2020-06-27 18:56:17 +03:00
parent 936e20ac5e
commit 14357787dd
24 changed files with 1489 additions and 25 deletions

View File

@ -11,6 +11,9 @@
7A0516162414EC1200FC55AC /* Differentiator in Frameworks */ = {isa = PBXBuildFile; productRef = 7A0516152414EC1200FC55AC /* Differentiator */; };
7A0516182414EC1200FC55AC /* RxDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = 7A0516172414EC1200FC55AC /* RxDataSources */; };
7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0516192414FF0900FC55AC /* DateSection.swift */; };
7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */; };
7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */; };
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */; };
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470023FDE7E500B424AF /* AppDelegate.swift */; };
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470223FDE7E500B424AF /* SceneDelegate.swift */; };
7A11470823FDE7E500B424AF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470623FDE7E500B424AF /* Main.storyboard */; };
@ -31,11 +34,19 @@
7A11474923FF2B2D00B424AF /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474823FF2B2D00B424AF /* Response.swift */; };
7A11474B23FF368B00B424AF /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474A23FF368B00B424AF /* Settings.swift */; };
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADC6249D43210035F39E /* RegionsController.swift */; };
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; };
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; };
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF6249FEF690035F39E /* Recorder.swift */; };
7A27ADF924A09CAD0035F39E /* CocoaError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF824A09CAD0035F39E /* CocoaError.swift */; };
7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33381024990DAE00D878F1 /* FiltersController.swift */; };
7A333814249A532400D878F1 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A333813249A532400D878F1 /* Filter.swift */; };
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; };
7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; };
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A43F9F7246C8A6200BA5B49 /* JWT.swift */; };
7A488C3C24A74B990054D0B2 /* RxTableViewRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A488C3824A74B990054D0B2 /* RxTableViewRealmDataSource.swift */; };
7A488C3D24A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A488C3924A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift */; };
7A488C3E24A74B990054D0B2 /* Reactive+RxRealmDataSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A488C3A24A74B990054D0B2 /* Reactive+RxRealmDataSources.swift */; };
7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A488C3B24A74B990054D0B2 /* RealmBindObserver.swift */; };
7A530B78240010D900CBFE6E /* InputMask in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B77240010D900CBFE6E /* InputMask */; };
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7924001D3300CBFE6E /* CheckController.swift */; };
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */; };
@ -51,6 +62,8 @@
7A64AE812469E16100ABE48E /* ProgressAnimatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7B2469E16100ABE48E /* ProgressAnimatedView.swift */; };
7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */; };
7A64AE832469E16100ABE48E /* IHProgressHUD.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 7A64AE7D2469E16100ABE48E /* IHProgressHUD.bundle */; };
7A659B5924A2B1BA0043A0F2 /* AudioRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */; };
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A659B5A24A3768A0043A0F2 /* Substrings.swift */; };
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD902242BF4A5009DE740 /* PlateView.swift */; };
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90724329144009DE740 /* CenterTextLayer.swift */; };
7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; };
@ -80,6 +93,9 @@
/* Begin PBXFileReference section */
7A0516192414FF0900FC55AC /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = "<group>"; };
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordCell.swift; sourceTree = "<group>"; };
7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellProgressView.swift; sourceTree = "<group>"; };
7A1146FD23FDE7E500B424AF /* AutoCat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AutoCat.app; sourceTree = BUILT_PRODUCTS_DIR; };
7A11470023FDE7E500B424AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7A11470223FDE7E500B424AF /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
@ -96,11 +112,19 @@
7A11474A23FF368B00B424AF /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = "<group>"; };
7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SVProgressHUD.framework; path = Carthage/Build/iOS/SVProgressHUD.framework; sourceTree = "<group>"; };
7A27ADC6249D43210035F39E /* RegionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsController.swift; sourceTree = "<group>"; };
7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = "<group>"; };
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = "<group>"; };
7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = "<group>"; };
7A27ADF824A09CAD0035F39E /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = "<group>"; };
7A33381024990DAE00D878F1 /* FiltersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersController.swift; sourceTree = "<group>"; };
7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = "<group>"; };
7A3F07AC2436350B00E59687 /* SearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchController.swift; sourceTree = "<group>"; };
7A43F9F7246C8A6200BA5B49 /* JWT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = "<group>"; };
7A488C3824A74B990054D0B2 /* RxTableViewRealmDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxTableViewRealmDataSource.swift; sourceTree = "<group>"; };
7A488C3924A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxCollectionViewRealmDataSource.swift; sourceTree = "<group>"; };
7A488C3A24A74B990054D0B2 /* Reactive+RxRealmDataSources.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Reactive+RxRealmDataSources.swift"; sourceTree = "<group>"; };
7A488C3B24A74B990054D0B2 /* RealmBindObserver.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealmBindObserver.swift; sourceTree = "<group>"; };
7A530B7924001D3300CBFE6E /* CheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckController.swift; sourceTree = "<group>"; };
7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCell.swift; sourceTree = "<group>"; };
7A530B7F2401803A00CBFE6E /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
@ -115,6 +139,8 @@
7A64AE7B2469E16100ABE48E /* ProgressAnimatedView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProgressAnimatedView.swift; sourceTree = "<group>"; };
7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = IHProgressHUD.swift; sourceTree = "<group>"; };
7A64AE7D2469E16100ABE48E /* IHProgressHUD.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = IHProgressHUD.bundle; sourceTree = "<group>"; };
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecord.swift; sourceTree = "<group>"; };
7A659B5A24A3768A0043A0F2 /* Substrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Substrings.swift; sourceTree = "<group>"; };
7A6DD902242BF4A5009DE740 /* PlateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateView.swift; sourceTree = "<group>"; };
7A6DD90724329144009DE740 /* CenterTextLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CenterTextLayer.swift; sourceTree = "<group>"; };
7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = "<group>"; };
@ -219,6 +245,7 @@
7A6E03272485951700DB22ED /* OwnersController.swift */,
7A33381024990DAE00D878F1 /* FiltersController.swift */,
7A27ADC6249D43210035F39E /* RegionsController.swift */,
7A27ADF2249F8B650035F39E /* RecordsController.swift */,
);
path = Controllers;
sourceTree = "<group>";
@ -226,6 +253,7 @@
7A11472C23FECA3E00B424AF /* ThirdParty */ = {
isa = PBXGroup;
children = (
7A488C3724A74B990054D0B2 /* RxRealmDataSources */,
7A64AE772469E16100ABE48E /* IHProgressHUD */,
7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */,
7A6DD90724329144009DE740 /* CenterTextLayer.swift */,
@ -240,6 +268,8 @@
7A96AE30246B2FE400297C33 /* Constants.swift */,
7A43F9F7246C8A6200BA5B49 /* JWT.swift */,
7A11474323FF06CA00B424AF /* Api.swift */,
7A27ADF6249FEF690035F39E /* Recorder.swift */,
7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */,
);
path = Utils;
sourceTree = "<group>";
@ -255,6 +285,7 @@
7A6DD90D24337930009DE740 /* PlateNumber.swift */,
7A333813249A532400D878F1 /* Filter.swift */,
7AB562B9249C9E9B00473D53 /* Region.swift */,
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */,
);
path = Models;
sourceTree = "<group>";
@ -275,10 +306,24 @@
7A3F07AA24360DC800E59687 /* Dated.swift */,
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */,
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */,
7A27ADF824A09CAD0035F39E /* CocoaError.swift */,
7A659B5A24A3768A0043A0F2 /* Substrings.swift */,
);
path = Extensions;
sourceTree = "<group>";
};
7A488C3724A74B990054D0B2 /* RxRealmDataSources */ = {
isa = PBXGroup;
children = (
7A488C3824A74B990054D0B2 /* RxTableViewRealmDataSource.swift */,
7A488C3924A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift */,
7A488C3A24A74B990054D0B2 /* Reactive+RxRealmDataSources.swift */,
7A488C3B24A74B990054D0B2 /* RealmBindObserver.swift */,
);
path = RxRealmDataSources;
sourceTree = "<group>";
};
7A530B7C24017FBE00CBFE6E /* Cells */ = {
isa = PBXGroup;
children = (
@ -288,6 +333,7 @@
7A7547DB2403180A004E8406 /* SectionHeader.swift */,
7A7547DC2403180A004E8406 /* SectionHeader.xib */,
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */,
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */,
);
path = Cells;
sourceTree = "<group>";
@ -323,6 +369,7 @@
7A6DD90B24335A6D009DE740 /* FlagLayer.swift */,
7AB67E8B2435C38700258F61 /* CustomTextField.swift */,
7AB67E8D2435D1A000258F61 /* CustomButton.swift */,
7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */,
);
path = Views;
sourceTree = "<group>";
@ -446,16 +493,24 @@
7A96AE31246B2FE400297C33 /* Constants.swift in Sources */,
7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */,
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */,
7A27ADF924A09CAD0035F39E /* CocoaError.swift in Sources */,
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */,
7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */,
7AB562BA249C9E9B00473D53 /* Region.swift in Sources */,
7A659B5924A2B1BA0043A0F2 /* AudioRecord.swift in Sources */,
7A488C3C24A74B990054D0B2 /* RxTableViewRealmDataSource.swift in Sources */,
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */,
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */,
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */,
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */,
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */,
7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */,
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */,
7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */,
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */,
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */,
7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */,
7A11474923FF2B2D00B424AF /* Response.swift in Sources */,
7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */,
7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */,
@ -465,19 +520,24 @@
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */,
7A6E03282485951700DB22ED /* OwnersController.swift in Sources */,
7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */,
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */,
7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */,
7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */,
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */,
7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */,
7A11474723FF2AA500B424AF /* User.swift in Sources */,
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */,
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */,
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */,
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
7A11474423FF06CA00B424AF /* Api.swift in Sources */,
7A488C3D24A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift in Sources */,
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */,
7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */,
7A488C3E24A74B990054D0B2 /* Reactive+RxRealmDataSources.swift in Sources */,
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */,
7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */,
7A333814249A532400D878F1 /* Filter.swift in Sources */,
@ -722,7 +782,7 @@
repositoryURL = "https://github.com/realm/realm-cocoa";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 4.3.2;
minimumVersion = 5.0.0;
};
};
7A11472923FEA24D00B424AF /* XCRemoteSwiftPackageReference "Action" */ = {
@ -746,7 +806,7 @@
repositoryURL = "https://github.com/RxSwiftCommunity/RxRealm";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 2.0.0;
minimumVersion = 3.0.0;
};
};
7A8A220C248EF5830073DFD9 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {

View File

@ -87,8 +87,8 @@
"repositoryURL": "https://github.com/airbnb/MagazineLayout",
"state": {
"branch": null,
"revision": "4a91fb2fa75a3c498748466227fa115fd27bb100",
"version": "1.6.0"
"revision": "12dd2cc84b7f17c4f46c7d95cde64d521c588ee8",
"version": "1.6.1"
}
},
{
@ -96,8 +96,8 @@
"repositoryURL": "https://github.com/realm/realm-cocoa",
"state": {
"branch": null,
"revision": "fa43b8e2909334c79f233ce472332c136ca108da",
"version": "4.4.1"
"revision": "b3fa932233bfa53966c373933d60157545a3f09f",
"version": "5.1.0"
}
},
{
@ -105,8 +105,8 @@
"repositoryURL": "https://github.com/realm/realm-core",
"state": {
"branch": null,
"revision": "35662ff940e340bf630ad1d1d88acfc7af18bee6",
"version": "5.23.8"
"revision": "bc900a2a8e05722c1b42f95396adb3c99eeb500f",
"version": "6.0.6"
}
},
{
@ -123,8 +123,8 @@
"repositoryURL": "https://github.com/RxSwiftCommunity/RxRealm",
"state": {
"branch": null,
"revision": "70188d79fe2eb19b5013dd1deae33e9e53f10e76",
"version": "2.0.0"
"revision": "c4dcc49acbf8073a8a6481b571c640bb169650f1",
"version": "3.0.1"
}
},
{

View File

@ -40,5 +40,28 @@
landmarkType = "7">
</BreakpointContent>
</BreakpointProxy>
<BreakpointProxy
BreakpointExtensionID = "Xcode.Breakpoint.SymbolicBreakpoint">
<BreakpointContent
uuid = "B15A9E9C-A0CD-4FC9-8E24-DD93FB1B677F"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "UITableViewAlertForLayoutOutsideViewHierarchy"
moduleName = "">
<Locations>
<Location
uuid = "B15A9E9C-A0CD-4FC9-8E24-DD93FB1B677F - 620169ab4c7c265a"
shouldBeEnabled = "Yes"
ignoreCount = "0"
continueAfterRunningActions = "No"
symbolName = "UITableViewAlertForLayoutOutsideViewHierarchy"
moduleName = "UIKitCore"
usesParentBreakpointCondition = "Yes"
offsetFromSymbolStart = "0">
</Location>
</Locations>
</BreakpointContent>
</BreakpointProxy>
</Breakpoints>
</Bucket>

View File

@ -1,5 +1,7 @@
import UIKit
import RealmSwift
import RxSwift
import RxCocoa
import os.log
extension OSLog {
@ -9,6 +11,7 @@ extension OSLog {
enum QuickAction {
case none
case check
case addVoiceRecord
}
@UIApplicationMain
@ -19,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration(
schemaVersion: 6,
schemaVersion: 8,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion <= 3 {
var numbers: [String] = []
@ -36,10 +39,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
})
Realm.Configuration.defaultConfiguration = config
print(Realm.Configuration.defaultConfiguration.fileURL!)
IHProgressHUD.set(defaultStyle: .dark)
IHProgressHUD.set(defaultMaskType: .black)
Logging.URLRequests = { _ in false };
return true
}
@ -54,6 +60,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
if let shortcutItem = options.shortcutItem {
if shortcutItem.type == "CheckNumberAction" {
self.quickAction = .check
} else if shortcutItem.type == "AddVoiceRecordAction" {
self.quickAction = .addVoiceRecord
}
}

View File

@ -4,6 +4,8 @@
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>

View File

@ -193,7 +193,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="gzk-86-k5g" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1783.2" y="948.57571214392806"/>
<point key="canvasLocation" x="1095" y="965"/>
</scene>
<!--Owners Controller-->
<scene sceneID="0bv-cp-2uj">
@ -208,7 +208,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="URC-NW-y2j" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="2668" y="948.57571214392806"/>
<point key="canvasLocation" x="1881" y="965"/>
</scene>
<!--Search Controller-->
<scene sceneID="3Md-yW-a0R">
@ -380,7 +380,105 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="trD-gZ-yAv" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4200.8000000000002" y="892.80359820089961"/>
<point key="canvasLocation" x="3262" y="918"/>
</scene>
<!--Voice records-->
<scene sceneID="9pI-G0-wG0">
<objects>
<viewController id="JIE-9Y-R8R" customClass="RecordsController" customModule="AutoCat" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="VgQ-mW-DCS">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="28" sectionFooterHeight="28" translatesAutoresizingMaskIntoConstraints="NO" id="jeg-Q0-sHX">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<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="AudioRecordCell" id="mzE-bt-IiX" customClass="AudioRecordCell" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="28" width="375" height="44.5"/>
<autoresizingMask key="autoresizingMask"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="mzE-bt-IiX" id="bqu-eN-DJP">
<rect key="frame" x="0.0" y="0.0" width="375" height="44.5"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="fc7-Eb-6ms" customClass="CellProgressView" customModule="AutoCat" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="44.5"/>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
</view>
<stackView opaque="NO" contentMode="scaleToFill" spacing="12" translatesAutoresizingMaskIntoConstraints="NO" id="QSJ-FJ-C4c">
<rect key="frame" x="8" y="0.0" width="359" height="44.5"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="BKm-HE-Aor">
<rect key="frame" x="0.0" y="0.0" width="44.5" height="44.5"/>
<constraints>
<constraint firstAttribute="height" constant="44" id="I8M-qD-lKL"/>
<constraint firstAttribute="width" secondItem="BKm-HE-Aor" secondAttribute="height" multiplier="1:1" id="Y4G-Ki-oYP"/>
</constraints>
<state key="normal" image="play.fill" catalog="system"/>
<connections>
<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"/>
<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"/>
<fontDescription key="fontDescription" type="system" pointSize="20"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jgb-TO-YHq">
<rect key="frame" x="321.5" y="0.0" width="37.5" height="44.5"/>
<fontDescription key="fontDescription" type="system" pointSize="15"/>
<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>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="QSJ-FJ-C4c" secondAttribute="bottom" id="4Lk-6G-3mA"/>
<constraint firstAttribute="bottom" secondItem="fc7-Eb-6ms" secondAttribute="bottom" id="Ahl-q6-LA8"/>
<constraint firstItem="QSJ-FJ-C4c" firstAttribute="top" secondItem="bqu-eN-DJP" secondAttribute="top" id="Bb2-tQ-wiV"/>
<constraint firstItem="QSJ-FJ-C4c" firstAttribute="leading" secondItem="bqu-eN-DJP" secondAttribute="leading" constant="8" id="Cpo-2z-6oN"/>
<constraint firstItem="fc7-Eb-6ms" firstAttribute="top" secondItem="bqu-eN-DJP" secondAttribute="top" id="dWu-lm-R5w"/>
<constraint firstItem="fc7-Eb-6ms" firstAttribute="leading" secondItem="bqu-eN-DJP" secondAttribute="leading" id="ea6-NB-WO3"/>
<constraint firstAttribute="trailing" secondItem="fc7-Eb-6ms" secondAttribute="trailing" id="m2Y-kR-DqA"/>
<constraint firstAttribute="trailing" secondItem="QSJ-FJ-C4c" secondAttribute="trailing" constant="8" id="q9F-GK-Kuf"/>
</constraints>
</tableViewCellContentView>
<connections>
<outlet property="date" destination="Jgb-TO-YHq" id="sU2-F8-yS9"/>
<outlet property="duration" destination="bFQ-eU-5YJ" id="DLw-zt-4rW"/>
<outlet property="number" destination="MjS-Hy-iGH" id="2JD-dI-chu"/>
<outlet property="playButton" destination="BKm-HE-Aor" id="DnM-SL-ncO"/>
<outlet property="progressView" destination="fc7-Eb-6ms" id="dn3-JQ-7g0"/>
</connections>
</tableViewCell>
</prototypes>
</tableView>
</subviews>
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstAttribute="trailing" secondItem="jeg-Q0-sHX" secondAttribute="trailing" id="erL-GF-nQN"/>
<constraint firstAttribute="bottom" secondItem="jeg-Q0-sHX" secondAttribute="bottom" id="ijK-jl-ORp"/>
<constraint firstItem="jeg-Q0-sHX" firstAttribute="top" secondItem="VgQ-mW-DCS" secondAttribute="top" id="odG-hi-2mW"/>
<constraint firstItem="jeg-Q0-sHX" firstAttribute="leading" secondItem="VgQ-mW-DCS" secondAttribute="leading" id="xy9-Yb-4vc"/>
</constraints>
<viewLayoutGuide key="safeArea" id="TkD-O4-hix"/>
</view>
<navigationItem key="navigationItem" title="Voice records" id="lu2-xz-pMr"/>
<connections>
<outlet property="tableView" destination="jeg-Q0-sHX" id="ml7-mu-AmI"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Vum-9f-55l" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="4200.8000000000002" y="1660.1199400299852"/>
</scene>
<!--Check Controller-->
<scene sceneID="t7Z-yv-ZLH">
@ -552,6 +650,7 @@
</tabBar>
<connections>
<segue destination="TSb-ZG-qfD" kind="relationship" relationship="viewControllers" id="Bwf-98-gjF"/>
<segue destination="RK6-pn-2Bg" kind="relationship" relationship="viewControllers" id="KNz-WF-Kyy"/>
<segue destination="GCa-Re-j14" kind="relationship" relationship="viewControllers" id="FGp-f6-fUh"/>
<segue destination="4jU-Z3-PF2" kind="relationship" relationship="viewControllers" id="aH2-IT-86l"/>
</connections>
@ -634,7 +733,7 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Yso-GW-Nd2" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-262" y="144"/>
<point key="canvasLocation" x="-735" y="143"/>
</scene>
<!--Main Split Controller-->
<scene sceneID="10H-jh-3eN">
@ -647,7 +746,7 @@
</splitViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="zgS-sH-9QV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="844" y="145"/>
<point key="canvasLocation" x="199" y="143"/>
</scene>
<!--Check-->
<scene sceneID="pUX-kf-oY1">
@ -703,7 +802,26 @@
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="2VV-jB-JET" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="844" y="948.57571214392806"/>
<point key="canvasLocation" x="198" y="965"/>
</scene>
<!--Records-->
<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"/>
<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"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="JIE-9Y-R8R" kind="relationship" relationship="rootViewController" id="nMe-r8-0lu"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="elc-Dr-KxW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="3261.5999999999999" y="1660.1199400299852"/>
</scene>
</scenes>
<resources>
@ -713,5 +831,7 @@
<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"/>
</resources>
</document>

View File

@ -0,0 +1,80 @@
import UIKit
import RxSwift
class AudioRecordCell: UITableViewCell {
@IBOutlet weak var playButton: UIButton!
@IBOutlet weak var duration: UILabel!
@IBOutlet weak var number: UILabel!
@IBOutlet weak var date: UILabel!
@IBOutlet weak var progressView: CellProgressView!
let dateFormatter = DateFormatter()
let componentsFormatter = DateComponentsFormatter()
var stateDisposable: Disposable?
var progressDisposable: Disposable?
var record: AudioRecord?
override func awakeFromNib() {
super.awakeFromNib()
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeStyle = .short
self.componentsFormatter.unitsStyle = .abbreviated
self.componentsFormatter.allowedUnits = [.minute, .second]
self.componentsFormatter.zeroFormattingBehavior = .pad
self.progressView.progress = 0
}
override func prepareForReuse() {
super.prepareForReuse()
self.record = nil
self.stateDisposable?.dispose()
self.progressDisposable?.dispose()
self.progressView.progress = 0
}
func configure(with record: AudioRecord) {
self.record = record
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: record.addedDate))
self.number.text = record.number ?? "Unrecognized"
self.duration.text = self.componentsFormatter.string(from: record.duration)
self.stateDisposable = AudioPlayer.shared
.stateObservable()
.filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
.subscribe(onNext: { state in
let imgName = state == .playing ? "pause.fill" : "play.fill"
self.playButton.setImage(UIImage(systemName: imgName), for: .normal)
if state == .stopped {
self.progressView.progress = 0
}
}, onDisposed: {
self.playButton.setImage(UIImage(systemName: "play.fill"), for: .normal)
})
self.progressDisposable = AudioPlayer.shared
.progressObservable()
.filter { _ in AudioPlayer.shared.getUrl()?.lastPathComponent == record.path }
.subscribe(onNext: { progress in
self.progressView.progress = progress
}, onDisposed: {
self.progressView.progress = 0
})
}
@IBAction func onPlay(_ sender: UIButton) {
if let record = self.record {
do {
let url = try FileManager.default.url(for: record.path, in: "recordings")
try AudioPlayer.shared.play(url: url)
} catch {
print("Error playing audio record: \(error.localizedDescription)")
}
}
}
}

View File

@ -42,12 +42,13 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
.subscribe(onNext: self.updateDetailController(with:))
.disposed(by: self.bag)
DispatchQueue.main.async {
Observable.collection(from: realm.objects(Vehicle.self)
.sorted(byKeyPath: "addedDate", ascending: false))
.map { $0.groupedByDate() }
.bind(to: self.history.rx.items(dataSource: ds))
.disposed(by: self.bag)
}
self.history.rx.setDelegate(self).disposed(by: self.bag)
}
@ -75,6 +76,8 @@ class CheckController: UIViewController, MaskedTextFieldDelegateListener, UITabl
if ad.quickAction == .check {
ad.quickAction = .none
self.number.becomeFirstResponder()
} else if ad.quickAction == .addVoiceRecord {
self.tabBarController?.selectedIndex = 1
}
}

View File

@ -0,0 +1,182 @@
import UIKit
import AVFoundation
import RealmSwift
import RxSwift
import RxRealm
import RxDataSources
class RecordsController: UIViewController, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
var recorder: Recorder?
var addButton: UIBarButtonItem!
var cancelButton: UIBarButtonItem!
let bag = DisposeBag()
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()
let ds = RxTableViewSectionedAnimatedDataSource<DateSection<AudioRecord>>(configureCell: { dataSource, tableView, indexPath, item in
if let cell = tableView.dequeueReusableCell(withIdentifier: "AudioRecordCell", for: indexPath) as? AudioRecordCell {
cell.configure(with: item)
return cell
} else {
return UITableViewCell()
}
}, canEditRowAtIndexPath: { _, _ in true })
ds.titleForHeaderInSection = { dataSourse, index in
return dataSourse.sectionModels[index].header
}
self.tableView.rx
.modelDeleted(AudioRecord.self)
.subscribe(onNext: { record in
try? realm.write {
realm.delete(record)
}
})
.disposed(by: self.bag)
DispatchQueue.main.async {
Observable.collection(from: realm.objects(AudioRecord.self)
.sorted(byKeyPath: "addedDate", ascending: false))
.map { $0.groupedByDate() }
.bind(to: self.tableView.rx.items(dataSource: ds))
.disposed(by: self.bag)
}
self.tableView.rx.setDelegate(self).disposed(by: self.bag)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
self.handleQuickActions()
}
func handleQuickActions() {
guard let ad = UIApplication.shared.delegate as? AppDelegate else { return }
if ad.quickAction == .addVoiceRecord {
ad.quickAction = .none
if let addButton = self.navigationItem.rightBarButtonItem {
self.onAddVoiceRecord(addButton)
}
}
}
// MARK: - Bar button handlers
@objc func onAddVoiceRecord(_ sender: UIBarButtonItem) {
guard let recorder = self.recorder else {
IHProgressHUD.showError(withStatus: "Audio recorder is not available")
return
}
recorder.requestPermissions { error in
DispatchQueue.main.async {
if let error = error {
self.show(error: error)
} else {
do {
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 realm = try? Realm()
try? realm?.write {
realm?.add(record)
}
}
self.title = "Recording..."
self.navigationItem.rightBarButtonItem?.isEnabled = false
self.navigationItem.leftBarButtonItem = self.cancelButton
} catch {
IHProgressHUD.showError(withStatus: error.localizedDescription)
}
}
}
}
}
@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()
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
return 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)
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
return 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"
}
return nil
}
// 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
*/
let check = UIContextualAction(style: .normal, title: "Check") { action, view, completion in
completion(true)
}
check.backgroundColor = .systemGray2
let share = UIContextualAction(style: .normal, title: "Share") { action, view, completion in
completion(true)
}
share.backgroundColor = .systemGray2
let delete = UIContextualAction(style: .destructive, title: "Delete") { action, view, completion in
self.tableView.dataSource?.tableView!(self.tableView, commit: .delete, forRowAt: indexPath)
completion(true)
}
return UISwipeActionsConfiguration(actions: [delete, check, share])
}
}

View File

@ -0,0 +1,29 @@
import UIKit
extension CocoaError {
static func error(_ description: String) -> NSError {
return error(Code(rawValue: 0), userInfo: [NSLocalizedDescriptionKey: description], url: nil) as NSError
}
static func error(_ description: String, suggestion: String) -> NSError {
let info = [
NSLocalizedDescriptionKey: description,
NSLocalizedRecoverySuggestionErrorKey: suggestion
]
return error(Code(rawValue: 0), userInfo: info, url: nil) as NSError
}
}
extension UIViewController {
func show(error: NSError) {
let alert = UIAlertController(title: error.localizedDescription, message: error.localizedRecoverySuggestion, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true)
}
func showAlert(title: String, message: String) {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil))
self.present(alert, animated: true)
}
}

View File

@ -12,6 +12,12 @@ extension Vehicle: Dated {
}
}
extension AudioRecord: Dated {
var date: Date {
Date(timeIntervalSince1970: self.addedDate)
}
}
extension RandomAccessCollection where Element: Dated & IdentifiableType & Equatable {
func groupedByDate() -> [DateSection<Element>] {
let now = Date()

View File

@ -0,0 +1,15 @@
import Foundation
extension FileManager {
func url(for file: String, in dir: String) throws -> URL {
guard let docUrl = self.urls(for: .documentDirectory, in: .userDomainMask).first else {
throw CocoaError(.fileReadNoSuchFile)
}
let folderUrl = docUrl.appendingPathComponent(dir, isDirectory: true)
if !self.fileExists(atPath: folderUrl.path) {
try self.createDirectory(at: folderUrl, withIntermediateDirectories: true, attributes: nil)
}
return folderUrl.appendingPathComponent(file)
}
}

View File

@ -0,0 +1,23 @@
import Foundation
extension String {
func index(from: Int) -> Index {
return self.index(startIndex, offsetBy: from)
}
func substring(from: Int) -> String {
let fromIndex = index(from: from)
return String(self[fromIndex...])
}
func substring(to: Int) -> String {
let toIndex = index(from: to)
return String(self[..<toIndex])
}
func substring(with r: Range<Int>) -> String {
let startIndex = index(from: r.lowerBound)
let endIndex = index(from: r.upperBound)
return String(self[startIndex..<endIndex])
}
}

View File

@ -2,6 +2,10 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Access is needed for recognizing plate numbers from voice recordings</string>
<key>NSMicrophoneUsageDescription</key>
<string>Access is needed for voice recordings</string>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
@ -69,6 +73,14 @@
</dict>
<key>UIApplicationShortcutItems</key>
<array>
<dict>
<key>UIApplicationShortcutItemType</key>
<string>AddVoiceRecordAction</string>
<key>UIApplicationShortcutItemTitle</key>
<string>Add voice record</string>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeAudio</string>
</dict>
<dict>
<key>UIApplicationShortcutItemIconType</key>
<string>UIApplicationShortcutIconTypeCompose</string>

View File

@ -0,0 +1,32 @@
import Foundation
import RealmSwift
import RxDataSources
class AudioRecord: Object, IdentifiableType {
@objc dynamic var path: String = ""
@objc dynamic var number: String?
@objc dynamic var addedDate: TimeInterval = Date().timeIntervalSince1970
@objc dynamic var duration: TimeInterval = 0
var identifier: TimeInterval = 0
var identity: TimeInterval {
if self.identifier == 0 {
self.identifier = self.addedDate
}
return self.identifier
}
init(path: String, number: String?, duration: TimeInterval) {
self.path = path
self.number = number
self.duration = duration
}
required init() {
}
override class func ignoredProperties() -> [String] {
return ["identity", "identifier"]
}
}

View File

@ -69,6 +69,22 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
tabvc.selectedIndex = 0
}
}
} else if shortcutItem.type == "AddVoiceRecordAction" {
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
}
}
}
}
}

View File

@ -0,0 +1,113 @@
//
// RxRealm extensions
//
// Copyright (c) 2016 RxSwiftCommunity. All rights reserved.
// Check the LICENSE file for details
//
import Foundation
import RealmSwift
import RxSwift
import RxCocoa
import RxRealm
#if os(iOS)
// MARK: - iOS / UIKit
import UIKit
extension Reactive where Base: UITableView {
public func realmChanges<E>(_ dataSource: RxTableViewRealmDataSource<E>)
-> RealmBindObserver<E, AnyRealmCollection<E>, RxTableViewRealmDataSource<E>> {
return RealmBindObserver(dataSource: dataSource) {ds, results, changes in
if ds.tableView == nil {
ds.tableView = self.base
}
ds.tableView?.dataSource = ds
ds.applyChanges(items: AnyRealmCollection<E>(results), changes: changes)
}
}
public func realmModelSelected<E>(_ modelType: E.Type) -> ControlEvent<E> where E: RealmSwift.Object {
let source: Observable<E> = self.itemSelected.flatMap { [weak view = self.base as UITableView] indexPath -> Observable<E> in
guard let view = view, let ds = view.dataSource as? RxTableViewRealmDataSource<E> else {
return Observable.empty()
}
return Observable.just(ds.model(at: indexPath))
}
return ControlEvent(events: source)
}
}
extension Reactive where Base: UICollectionView {
public func realmChanges<E>(_ dataSource: RxCollectionViewRealmDataSource<E>)
-> RealmBindObserver<E, AnyRealmCollection<E>, RxCollectionViewRealmDataSource<E>> {
return RealmBindObserver(dataSource: dataSource) {ds, results, changes in
if ds.collectionView == nil {
ds.collectionView = self.base
}
ds.collectionView?.dataSource = ds
ds.applyChanges(items: AnyRealmCollection<E>(results), changes: changes)
}
}
public func realmModelSelected<E>(_ modelType: E.Type) -> ControlEvent<E> where E: RealmSwift.Object {
let source: Observable<E> = self.itemSelected.flatMap { [weak view = self.base as UICollectionView] indexPath -> Observable<E> in
guard let view = view, let ds = view.dataSource as? RxCollectionViewRealmDataSource<E> else {
return Observable.empty()
}
return Observable.just(ds.model(at: indexPath))
}
return ControlEvent(events: source)
}
}
#elseif os(OSX)
// MARK: - macOS / Cocoa
import Cocoa
extension Reactive where Base: NSTableView {
public func realmChanges<E>(_ dataSource: RxTableViewRealmDataSource<E>)
-> RealmBindObserver<E, AnyRealmCollection<E>, RxTableViewRealmDataSource<E>> {
base.delegate = dataSource
base.dataSource = dataSource
return RealmBindObserver(dataSource: dataSource) {ds, results, changes in
if dataSource.tableView == nil {
dataSource.tableView = self.base
}
ds.applyChanges(items: AnyRealmCollection<E>(results), changes: changes)
}
}
}
extension Reactive where Base: NSCollectionView {
public func realmChanges<E>(_ dataSource: RxCollectionViewRealmDataSource<E>)
-> RealmBindObserver<E, AnyRealmCollection<E>, RxCollectionViewRealmDataSource<E>> {
return RealmBindObserver(dataSource: dataSource) {ds, results, changes in
if ds.collectionView == nil {
ds.collectionView = self.base
}
ds.collectionView?.dataSource = ds
ds.applyChanges(items: AnyRealmCollection<E>(results), changes: changes)
}
}
}
#endif

View File

@ -0,0 +1,41 @@
//
// RxRealm extensions
//
// Copyright (c) 2016 RxSwiftCommunity. All rights reserved.
// Check the LICENSE file for details
//
import Foundation
import RealmSwift
import RxSwift
import RxCocoa
import RxRealm
public class RealmBindObserver<O: Object, C: RealmCollection, DS>: ObserverType {
typealias BindingType = (DS, C, RealmChangeset?) -> Void
public typealias E = (C, RealmChangeset?)
let dataSource: DS
let binding: BindingType
init(dataSource: DS, binding: @escaping BindingType) {
self.dataSource = dataSource
self.binding = binding
}
public func on(_ event: Event<E>) {
switch event {
case .next(let element):
binding(dataSource, element.0, element.1)
case .error:
return
case .completed:
return
}
}
func asObserver() -> AnyObserver<E> {
return AnyObserver(eventHandler: on)
}
}

View File

@ -0,0 +1,210 @@
//
// RxRealm extensions
//
// Copyright (c) 2016 RxSwiftCommunity. All rights reserved.
// Check the LICENSE file for details
//
import Foundation
import RealmSwift
import RxSwift
import RxCocoa
import RxRealm
#if os(iOS)
// MARK: - iOS / UIKit
import UIKit
public typealias CollectionCellFactory<E: Object> = (RxCollectionViewRealmDataSource<E>, UICollectionView, IndexPath, E) -> UICollectionViewCell
public typealias CollectionCellConfig<E: Object, CellType: UICollectionViewCell> = (CellType, IndexPath, E) -> Void
open class RxCollectionViewRealmDataSource <E: Object>: NSObject, UICollectionViewDataSource {
private var items: AnyRealmCollection<E>?
// MARK: - Configuration
public var collectionView: UICollectionView?
public var animated = true
// MARK: - Init
public let cellIdentifier: String
public let cellFactory: CollectionCellFactory<E>
public init(cellIdentifier: String, cellFactory: @escaping CollectionCellFactory<E>) {
self.cellIdentifier = cellIdentifier
self.cellFactory = cellFactory
}
public init<CellType>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping CollectionCellConfig<E, CellType>) where CellType: UICollectionViewCell {
self.cellIdentifier = cellIdentifier
self.cellFactory = {ds, cv, ip, model in
let cell = cv.dequeueReusableCell(withReuseIdentifier: cellIdentifier, for: ip) as! CellType
cellConfig(cell, ip, model)
return cell
}
}
// MARK: - Data access
public func model(at indexPath: IndexPath) -> E {
return items![indexPath.row]
}
// MARK: - UICollectionViewDataSource protocol
public func numberOfSections(in collectionView: UICollectionView) -> Int {
return 1
}
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return items?.count ?? 0
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
return cellFactory(self, collectionView, indexPath, items![indexPath.row])
}
// MARK: - Applying changeset to the collection view
private let fromRow = {(row: Int) in return IndexPath(row: row, section: 0)}
func applyChanges(items: AnyRealmCollection<E>, changes: RealmChangeset?) {
if self.items == nil {
self.items = items
}
guard let collectionView = collectionView else {
fatalError("You have to bind a collection view to the data source.")
}
guard animated else {
collectionView.reloadData()
return
}
guard let changes = changes else {
collectionView.reloadData()
return
}
let lastItemCount = collectionView.numberOfItems(inSection: 0)
guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else {
collectionView.reloadData()
return
}
collectionView.performBatchUpdates({[unowned self] in
collectionView.deleteItems(at: changes.deleted.map(self.fromRow))
collectionView.reloadItems(at: changes.updated.map(self.fromRow))
collectionView.insertItems(at: changes.inserted.map(self.fromRow))
}, completion: nil)
}
}
#elseif os(OSX)
// MARK: - macOS / Cocoa
import Cocoa
public typealias CollectionItemFactory<E: Object> = (RxCollectionViewRealmDataSource<E>, NSCollectionView, IndexPath, E) -> NSCollectionViewItem
public typealias CollectionItemConfig<E: Object, ItemType: NSCollectionViewItem> = (ItemType, IndexPath, E) -> Void
open class RxCollectionViewRealmDataSource <E: Object>: NSObject, NSCollectionViewDataSource {
private var items: AnyRealmCollection<E>?
// MARK: - Configuration
public var collectionView: NSCollectionView?
public var animated = true
// MARK: - Init
public let itemIdentifier: String
public let itemFactory: CollectionItemFactory<E>
public weak var delegate: NSCollectionViewDelegate?
public weak var dataSource: NSCollectionViewDataSource?
public init(itemIdentifier: String, itemFactory: @escaping CollectionItemFactory<E>) {
self.itemIdentifier = itemIdentifier
self.itemFactory = itemFactory
}
public init<ItemType>(itemIdentifier: String, itemType: ItemType.Type, itemConfig: @escaping CollectionItemConfig<E, ItemType>) where ItemType: NSCollectionViewItem {
self.itemIdentifier = itemIdentifier
self.itemFactory = { ds, cv, ip, model in
let item = cv.makeItem(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: itemIdentifier), for: ip) as! ItemType
itemConfig(item, ip, model)
return item
}
}
// MARK: - NSCollectionViewDataSource protocol
public func numberOfSections(in collectionView: NSCollectionView) -> Int {
return 1
}
public func collectionView(_ collectionView: NSCollectionView, numberOfItemsInSection section: Int) -> Int {
return items?.count ?? 0
}
@available(OSX 10.11, *)
public func collectionView(_ collectionView: NSCollectionView, itemForRepresentedObjectAt indexPath: IndexPath) -> NSCollectionViewItem {
return itemFactory(self, collectionView, indexPath, items![indexPath.item])
}
// MARK: - Proxy unimplemented data source and delegate methods
open override func responds(to aSelector: Selector!) -> Bool {
if RxCollectionViewRealmDataSource.instancesRespond(to: aSelector) {
return true
} else if let delegate = delegate {
return delegate.responds(to: aSelector)
} else if let dataSource = dataSource {
return dataSource.responds(to: aSelector)
} else {
return false
}
}
open override func forwardingTarget(for aSelector: Selector!) -> Any? {
return delegate ?? dataSource
}
// MARK: - Applying changeset to the collection view
private let fromRow = {(row: Int) in return IndexPath(item: row, section: 0)}
func applyChanges(items: AnyRealmCollection<E>, changes: RealmChangeset?) {
if self.items == nil {
self.items = items
}
guard let collectionView = collectionView else {
fatalError("You have to bind a collection view to the data source.")
}
guard animated else {
collectionView.reloadData()
return
}
guard let changes = changes else {
collectionView.reloadData()
return
}
let lastItemCount = collectionView.numberOfItems(inSection: 0)
guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else {
collectionView.reloadData()
return
}
collectionView.performBatchUpdates({[unowned self] in
//TODO: this should be animated, but doesn't seem to be?
collectionView.animator().deleteItems(at: Set(changes.deleted.map(self.fromRow)))
collectionView.animator().reloadItems(at: Set(changes.updated.map(self.fromRow)))
collectionView.animator().insertItems(at: Set(changes.inserted.map(self.fromRow)))
}, completionHandler: nil)
}
}
#endif

View File

@ -0,0 +1,223 @@
//
// RxRealm extensions
//
// Copyright (c) 2016 RxSwiftCommunity. All rights reserved.
// Check the LICENSE file for details
//
import Foundation
import RealmSwift
import RxSwift
import RxCocoa
import RxRealm
#if os(iOS)
// MARK: - iOS / UIKit
import UIKit
public typealias TableCellFactory<E: Object> = (RxTableViewRealmDataSource<E>, UITableView, IndexPath, E) -> UITableViewCell
public typealias TableCellConfig<E: Object, CellType: UITableViewCell> = (CellType, IndexPath, E) -> Void
open class RxTableViewRealmDataSource<E: Object>: NSObject, UITableViewDataSource {
private var items: AnyRealmCollection<E>?
// MARK: - Configuration
public var tableView: UITableView?
public var animated = true
public var rowAnimations = (
insert: UITableView.RowAnimation.automatic,
update: UITableView.RowAnimation.automatic,
delete: UITableView.RowAnimation.automatic)
public var headerTitle: String?
public var footerTitle: String?
// MARK: - Init
public let cellIdentifier: String
public let cellFactory: TableCellFactory<E>
public init(cellIdentifier: String, cellFactory: @escaping TableCellFactory<E>) {
self.cellIdentifier = cellIdentifier
self.cellFactory = cellFactory
}
public init<CellType>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping TableCellConfig<E, CellType>) where CellType: UITableViewCell {
self.cellIdentifier = cellIdentifier
self.cellFactory = {ds, tv, ip, model in
let cell = tv.dequeueReusableCell(withIdentifier: cellIdentifier, for: ip) as! CellType
cellConfig(cell, ip, model)
return cell
}
}
// MARK: - Data access
public func model(at indexPath: IndexPath) -> E {
return items![indexPath.row]
}
// MARK: - UITableViewDataSource protocol
public func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items?.count ?? 0
}
public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return cellFactory(self, tableView, indexPath, items![indexPath.row])
}
public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return headerTitle
}
public func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
return footerTitle
}
// MARK: - Applying changeset to the table view
private let fromRow = {(row: Int) in return IndexPath(row: row, section: 0)}
func applyChanges(items: AnyRealmCollection<E>, changes: RealmChangeset?) {
if self.items == nil {
self.items = items
}
guard let tableView = tableView else {
fatalError("You have to bind a table view to the data source.")
}
guard animated else {
tableView.reloadData()
return
}
guard let changes = changes else {
tableView.reloadData()
return
}
let lastItemCount = tableView.numberOfRows(inSection: 0)
guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else {
tableView.reloadData()
return
}
tableView.beginUpdates()
tableView.deleteRows(at: changes.deleted.map(fromRow), with: rowAnimations.delete)
tableView.insertRows(at: changes.inserted.map(fromRow), with: rowAnimations.insert)
tableView.reloadRows(at: changes.updated.map(fromRow), with: rowAnimations.update)
tableView.endUpdates()
}
}
#elseif os(OSX)
// MARK: - macOS / Cocoa
import Cocoa
public typealias TableCellFactory<E: Object> = (RxTableViewRealmDataSource<E>, NSTableView, Int, E) -> NSTableCellView
public typealias TableCellConfig<E: Object, CellType: NSTableCellView> = (CellType, Int, E) -> Void
open class RxTableViewRealmDataSource<E: Object>: NSObject, NSTableViewDataSource, NSTableViewDelegate {
private var items: AnyRealmCollection<E>?
// MARK: - Configuration
public var tableView: NSTableView?
public var animated = true
public var rowAnimations = (
insert: NSTableView.AnimationOptions.effectFade,
update: NSTableView.AnimationOptions.effectFade,
delete: NSTableView.AnimationOptions.effectFade)
public weak var delegate: NSTableViewDelegate?
public weak var dataSource: NSTableViewDataSource?
// MARK: - Init
public let cellIdentifier: String
public let cellFactory: TableCellFactory<E>
public init(cellIdentifier: String, cellFactory: @escaping TableCellFactory<E>) {
self.cellIdentifier = cellIdentifier
self.cellFactory = cellFactory
}
public init<CellType>(cellIdentifier: String, cellType: CellType.Type, cellConfig: @escaping TableCellConfig<E, CellType>) where CellType: NSTableCellView {
self.cellIdentifier = cellIdentifier
self.cellFactory = { ds, tv, row, model in
let cell = tv.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: cellIdentifier), owner: tv) as! CellType
cellConfig(cell, row, model)
return cell
}
}
// MARK: - UITableViewDataSource protocol
public func numberOfRows(in tableView: NSTableView) -> Int {
return items?.count ?? 0
}
public func tableView(_ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int) -> NSView? {
return cellFactory(self, tableView, row, items![row])
}
// MARK: - Proxy unimplemented data source and delegate methods
open override func responds(to aSelector: Selector!) -> Bool {
if RxTableViewRealmDataSource.instancesRespond(to: aSelector) {
return true
} else if let delegate = delegate {
return delegate.responds(to: aSelector)
} else if let dataSource = dataSource {
return dataSource.responds(to: aSelector)
} else {
return false
}
}
open override func forwardingTarget(for aSelector: Selector!) -> Any? {
return delegate ?? dataSource
}
// MARK: - Applying changeset to the table view
private let fromRow = {(row: Int) in return IndexPath(item: row, section: 0)}
func applyChanges(items: AnyRealmCollection<E>, changes: RealmChangeset?) {
if self.items == nil {
self.items = items
}
guard let tableView = tableView else {
fatalError("You have to bind a table view to the data source.")
}
guard animated else {
tableView.reloadData()
return
}
guard let changes = changes else {
tableView.reloadData()
return
}
let lastItemCount = tableView.numberOfRows
guard items.count == lastItemCount + changes.inserted.count - changes.deleted.count else {
tableView.reloadData()
return
}
tableView.beginUpdates()
tableView.removeRows(at: IndexSet(changes.deleted), withAnimation: rowAnimations.delete)
tableView.insertRows(at: IndexSet(changes.inserted), withAnimation: rowAnimations.insert)
tableView.reloadData(forRowIndexes: IndexSet(changes.updated), columnIndexes: IndexSet([0]))
tableView.endUpdates()
}
}
#endif

View File

@ -0,0 +1,107 @@
import Foundation
import AVFoundation
import RxSwift
import RxRelay
enum PlayerState {
case stopped
case paused
case playing
}
class AudioPlayer: NSObject, AVAudioPlayerDelegate {
static let shared = AudioPlayer()
private var player: AVAudioPlayer?
private var url: URL?
private var state = BehaviorRelay<PlayerState>(value: .stopped)
private var progress = BehaviorRelay<Double>(value: 0)
private var progressTimer: Timer?
func set(url: URL) throws {
if let curUrl = self.url, curUrl == url {
return
}
self.stop()
self.url = url
self.player = try AVAudioPlayer(contentsOf: url)
self.player?.delegate = self
}
func play() {
if let player = self.player {
if player.isPlaying {
player.pause()
self.state.accept(.paused)
} else {
player.play()
self.state.accept(.playing)
if self.progressTimer == nil {
self.progressTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(progressTick), userInfo: nil, repeats: true)
}
}
}
}
func play(url: URL) throws {
try self.set(url: url)
self.play()
}
func pause() {
if let player = self.player {
player.pause()
self.state.accept(.paused)
}
}
func stop() {
if let player = self.player {
player.stop()
self.state.accept(.stopped)
self.progressTimer?.invalidate()
self.progressTimer = nil
}
}
func getState() -> PlayerState {
return self.state.value
}
func getProgress() -> Double {
return self.progress.value
}
func getUrl() -> URL? {
return self.url
}
func duration() -> TimeInterval {
return self.player?.duration ?? 0
}
func stateObservable() -> Observable<PlayerState> {
return self.state.asObservable()
}
func progressObservable() -> Observable<Double> {
return self.progress.asObservable()
}
// MARK: - AVAudioPlayerDelegate
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
self.progress.accept(1)
self.stop()
self.state.accept(.stopped)
}
@objc func progressTick() {
if let player = self.player {
let progress = player.currentTime/player.duration
self.progress.accept(progress)
}
}
}

View File

@ -3,8 +3,8 @@ import Foundation
enum Constants {
static var baseUrl: String {
#if DEBUG
return "http://127.0.0.1:3000/"
//return "https://vps.aliencat.pro:8443/"
//return "http://127.0.0.1:3000/"
return "https://vps.aliencat.pro:8443/"
#else
return "https://vps.aliencat.pro:8443/"
#endif

View File

@ -0,0 +1,118 @@
import Foundation
import Speech
import AVFoundation
import AudioToolbox
class Recorder {
let session = AVAudioSession.sharedInstance()
let engine = AVAudioEngine()
var fileRef: ExtAudioFileRef? = nil
let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ru_RU"))
let request = SFSpeechAudioBufferRecognitionRequest()
var recognitionTask: SFSpeechRecognitionTask?
var endRecognitionTimer: Timer?
var result: String = ""
let recordingSettings: [String:Any] = [
AVFormatIDKey:kAudioFormatMPEG4AAC_HE,
AVSampleRateKey:44100.0,
AVNumberOfChannelsKey:2,
//AVEncoderBitRateKey:320*1024,
//AVLinearPCMBitDepthKey:16,
AVEncoderAudioQualityKey:AVAudioQuality.max.rawValue
]
init() throws {
try self.session.setCategory(.playAndRecord, mode: .spokenAudio, options: .mixWithOthers)
}
func requestPermissions(completion: @escaping (NSError?) -> Void) {
self.session.requestRecordPermission { allowed in
if allowed {
SFSpeechRecognizer.requestAuthorization { status in
switch status {
case .authorized:
completion(nil)
break
case .denied:
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use speech recognition in system settings")
completion(error)
break
case .restricted:
let error = CocoaError.error("Access restricted", suggestion: "Speech recognition is restricted on this device")
completion(error)
break
case .notDetermined:
let error = CocoaError.error("Access error", suggestion: "Speech recognition status is not yet determined")
completion(error)
break
@unknown default:
let error = CocoaError.error("Access error", suggestion: "Unknown error accessing speech recognizer")
completion(error)
break
}
}
} else {
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use microphone in system settings")
completion(error)
}
}
}
func startRecording(to file: URL, completion: @escaping (String) -> Void) throws {
let inFormat = self.engine.inputNode.outputFormat(forBus: 0)
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
throw CocoaError.error("Recording error", suggestion: "Format not supported")
}
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &fileRef)
guard let fileRef = self.fileRef else {
throw CocoaError.error(CocoaError.Code.fileWriteUnknown)
}
ExtAudioFileSetProperty(fileRef, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size), inFormat.streamDescription)
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
self.request.append(buffer)
//print(self.recognitionTask?.state.rawValue)
ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
}
self.recognitionTask = self.recognizer!.recognitionTask(with: self.request) { result, error in
if let transcription = result?.bestTranscription {
self.result = transcription.formattedString
self.endRecognitionTimer?.invalidate()
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { timer in
self.cancelRecording()
completion(self.result)
}
}
}
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
self.cancelRecording()
completion(self.result)
}
self.engine.prepare()
try self.engine.start()
}
func cancelRecording() {
self.engine.stop()
self.engine.inputNode.removeTap(onBus: 0)
self.request.endAudio()
self.recognitionTask?.cancel()
if let fileRef = self.fileRef {
ExtAudioFileDispose(fileRef)
}
}
func stopRecording() -> String {
self.cancelRecording()
return self.result
}
}

View File

@ -0,0 +1,41 @@
import UIKit
class CellProgressView: UIView {
let progressLayer = CAShapeLayer()
var progress: Double = 0 {
didSet {
let path = UIBezierPath(rect: self.rect(for: self.progress))
let pathFrom = self.progressLayer.path
self.progressLayer.path = path.cgPath
if self.progress != 0 {
let animation = CABasicAnimation(keyPath: "path")
animation.duration = 0.2
animation.fromValue = pathFrom
animation.toValue = path.cgPath
animation.timingFunction = CAMediaTimingFunction(name: .linear)
self.progressLayer.add(animation, forKey: "pathAnimation")
}
}
}
required init?(coder: NSCoder) {
super.init(coder: coder)
self.progressLayer.frame = self.layer.bounds
self.progressLayer.path = UIBezierPath(rect: self.layer.bounds).cgPath
self.progressLayer.fillColor = UIColor.systemBlue.cgColor
self.progressLayer.opacity = 0.3
self.layer.addSublayer(self.progressLayer)
}
override func layoutSubviews() {
super.layoutSubviews()
self.progressLayer.frame = rect(for: self.progress)
}
func rect(for progress: Double) -> CGRect {
return CGRect(x: 0, y: 0, width: self.bounds.width*CGFloat(progress), height: self.bounds.height)
}
}