Adding voice record feature
This commit is contained in:
parent
936e20ac5e
commit
14357787dd
@ -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" */ = {
|
||||
|
||||
@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
80
AutoCat/Cells/AudioRecordCell.swift
Normal file
80
AutoCat/Cells/AudioRecordCell.swift
Normal 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
182
AutoCat/Controllers/RecordsController.swift
Normal file
182
AutoCat/Controllers/RecordsController.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
29
AutoCat/Extensions/CocoaError.swift
Normal file
29
AutoCat/Extensions/CocoaError.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
|
||||
15
AutoCat/Extensions/FileManagerExt.swift
Normal file
15
AutoCat/Extensions/FileManagerExt.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
23
AutoCat/Extensions/Substrings.swift
Normal file
23
AutoCat/Extensions/Substrings.swift
Normal 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])
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
|
||||
32
AutoCat/Models/AudioRecord.swift
Normal file
32
AutoCat/Models/AudioRecord.swift
Normal 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"]
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
113
AutoCat/ThirdParty/RxRealmDataSources/Reactive+RxRealmDataSources.swift
vendored
Normal file
113
AutoCat/ThirdParty/RxRealmDataSources/Reactive+RxRealmDataSources.swift
vendored
Normal 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
|
||||
41
AutoCat/ThirdParty/RxRealmDataSources/RealmBindObserver.swift
vendored
Normal file
41
AutoCat/ThirdParty/RxRealmDataSources/RealmBindObserver.swift
vendored
Normal 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)
|
||||
}
|
||||
}
|
||||
210
AutoCat/ThirdParty/RxRealmDataSources/RxCollectionViewRealmDataSource.swift
vendored
Normal file
210
AutoCat/ThirdParty/RxRealmDataSources/RxCollectionViewRealmDataSource.swift
vendored
Normal 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
|
||||
223
AutoCat/ThirdParty/RxRealmDataSources/RxTableViewRealmDataSource.swift
vendored
Normal file
223
AutoCat/ThirdParty/RxRealmDataSources/RxTableViewRealmDataSource.swift
vendored
Normal 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
|
||||
107
AutoCat/Utils/AudioPlayer.swift
Normal file
107
AutoCat/Utils/AudioPlayer.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
|
||||
118
AutoCat/Utils/Recorder.swift
Normal file
118
AutoCat/Utils/Recorder.swift
Normal 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
|
||||
}
|
||||
}
|
||||
41
AutoCat/Views/CellProgressView.swift
Normal file
41
AutoCat/Views/CellProgressView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user