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 */; };
|
7A0516162414EC1200FC55AC /* Differentiator in Frameworks */ = {isa = PBXBuildFile; productRef = 7A0516152414EC1200FC55AC /* Differentiator */; };
|
||||||
7A0516182414EC1200FC55AC /* RxDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = 7A0516172414EC1200FC55AC /* RxDataSources */; };
|
7A0516182414EC1200FC55AC /* RxDataSources in Frameworks */ = {isa = PBXBuildFile; productRef = 7A0516172414EC1200FC55AC /* RxDataSources */; };
|
||||||
7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0516192414FF0900FC55AC /* DateSection.swift */; };
|
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 */; };
|
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470023FDE7E500B424AF /* AppDelegate.swift */; };
|
||||||
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470223FDE7E500B424AF /* SceneDelegate.swift */; };
|
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470223FDE7E500B424AF /* SceneDelegate.swift */; };
|
||||||
7A11470823FDE7E500B424AF /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7A11470623FDE7E500B424AF /* Main.storyboard */; };
|
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 */; };
|
7A11474923FF2B2D00B424AF /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474823FF2B2D00B424AF /* Response.swift */; };
|
||||||
7A11474B23FF368B00B424AF /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474A23FF368B00B424AF /* Settings.swift */; };
|
7A11474B23FF368B00B424AF /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474A23FF368B00B424AF /* Settings.swift */; };
|
||||||
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADC6249D43210035F39E /* RegionsController.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 */; };
|
7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33381024990DAE00D878F1 /* FiltersController.swift */; };
|
||||||
7A333814249A532400D878F1 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A333813249A532400D878F1 /* Filter.swift */; };
|
7A333814249A532400D878F1 /* Filter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A333813249A532400D878F1 /* Filter.swift */; };
|
||||||
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; };
|
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; };
|
||||||
7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; };
|
7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; };
|
||||||
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A43F9F7246C8A6200BA5B49 /* JWT.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 */; };
|
7A530B78240010D900CBFE6E /* InputMask in Frameworks */ = {isa = PBXBuildFile; productRef = 7A530B77240010D900CBFE6E /* InputMask */; };
|
||||||
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7924001D3300CBFE6E /* CheckController.swift */; };
|
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7924001D3300CBFE6E /* CheckController.swift */; };
|
||||||
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.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 */; };
|
7A64AE812469E16100ABE48E /* ProgressAnimatedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7B2469E16100ABE48E /* ProgressAnimatedView.swift */; };
|
||||||
7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */; };
|
7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE7C2469E16100ABE48E /* IHProgressHUD.swift */; };
|
||||||
7A64AE832469E16100ABE48E /* IHProgressHUD.bundle in Resources */ = {isa = PBXBuildFile; fileRef = 7A64AE7D2469E16100ABE48E /* IHProgressHUD.bundle */; };
|
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 */; };
|
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD902242BF4A5009DE740 /* PlateView.swift */; };
|
||||||
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90724329144009DE740 /* CenterTextLayer.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 */; };
|
7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; };
|
||||||
@ -80,6 +93,9 @@
|
|||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
7A0516192414FF0900FC55AC /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = "<group>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = "<group>"; };
|
||||||
@ -219,6 +245,7 @@
|
|||||||
7A6E03272485951700DB22ED /* OwnersController.swift */,
|
7A6E03272485951700DB22ED /* OwnersController.swift */,
|
||||||
7A33381024990DAE00D878F1 /* FiltersController.swift */,
|
7A33381024990DAE00D878F1 /* FiltersController.swift */,
|
||||||
7A27ADC6249D43210035F39E /* RegionsController.swift */,
|
7A27ADC6249D43210035F39E /* RegionsController.swift */,
|
||||||
|
7A27ADF2249F8B650035F39E /* RecordsController.swift */,
|
||||||
);
|
);
|
||||||
path = Controllers;
|
path = Controllers;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -226,6 +253,7 @@
|
|||||||
7A11472C23FECA3E00B424AF /* ThirdParty */ = {
|
7A11472C23FECA3E00B424AF /* ThirdParty */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
7A488C3724A74B990054D0B2 /* RxRealmDataSources */,
|
||||||
7A64AE772469E16100ABE48E /* IHProgressHUD */,
|
7A64AE772469E16100ABE48E /* IHProgressHUD */,
|
||||||
7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */,
|
7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */,
|
||||||
7A6DD90724329144009DE740 /* CenterTextLayer.swift */,
|
7A6DD90724329144009DE740 /* CenterTextLayer.swift */,
|
||||||
@ -240,6 +268,8 @@
|
|||||||
7A96AE30246B2FE400297C33 /* Constants.swift */,
|
7A96AE30246B2FE400297C33 /* Constants.swift */,
|
||||||
7A43F9F7246C8A6200BA5B49 /* JWT.swift */,
|
7A43F9F7246C8A6200BA5B49 /* JWT.swift */,
|
||||||
7A11474323FF06CA00B424AF /* Api.swift */,
|
7A11474323FF06CA00B424AF /* Api.swift */,
|
||||||
|
7A27ADF6249FEF690035F39E /* Recorder.swift */,
|
||||||
|
7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */,
|
||||||
);
|
);
|
||||||
path = Utils;
|
path = Utils;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -255,6 +285,7 @@
|
|||||||
7A6DD90D24337930009DE740 /* PlateNumber.swift */,
|
7A6DD90D24337930009DE740 /* PlateNumber.swift */,
|
||||||
7A333813249A532400D878F1 /* Filter.swift */,
|
7A333813249A532400D878F1 /* Filter.swift */,
|
||||||
7AB562B9249C9E9B00473D53 /* Region.swift */,
|
7AB562B9249C9E9B00473D53 /* Region.swift */,
|
||||||
|
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -275,10 +306,24 @@
|
|||||||
7A3F07AA24360DC800E59687 /* Dated.swift */,
|
7A3F07AA24360DC800E59687 /* Dated.swift */,
|
||||||
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
|
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
|
||||||
7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */,
|
7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */,
|
||||||
|
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */,
|
||||||
|
7A27ADF824A09CAD0035F39E /* CocoaError.swift */,
|
||||||
|
7A659B5A24A3768A0043A0F2 /* Substrings.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
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 */ = {
|
7A530B7C24017FBE00CBFE6E /* Cells */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -288,6 +333,7 @@
|
|||||||
7A7547DB2403180A004E8406 /* SectionHeader.swift */,
|
7A7547DB2403180A004E8406 /* SectionHeader.swift */,
|
||||||
7A7547DC2403180A004E8406 /* SectionHeader.xib */,
|
7A7547DC2403180A004E8406 /* SectionHeader.xib */,
|
||||||
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */,
|
7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */,
|
||||||
|
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */,
|
||||||
);
|
);
|
||||||
path = Cells;
|
path = Cells;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -323,6 +369,7 @@
|
|||||||
7A6DD90B24335A6D009DE740 /* FlagLayer.swift */,
|
7A6DD90B24335A6D009DE740 /* FlagLayer.swift */,
|
||||||
7AB67E8B2435C38700258F61 /* CustomTextField.swift */,
|
7AB67E8B2435C38700258F61 /* CustomTextField.swift */,
|
||||||
7AB67E8D2435D1A000258F61 /* CustomButton.swift */,
|
7AB67E8D2435D1A000258F61 /* CustomButton.swift */,
|
||||||
|
7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -446,16 +493,24 @@
|
|||||||
7A96AE31246B2FE400297C33 /* Constants.swift in Sources */,
|
7A96AE31246B2FE400297C33 /* Constants.swift in Sources */,
|
||||||
7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */,
|
7A64AE822469E16100ABE48E /* IHProgressHUD.swift in Sources */,
|
||||||
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */,
|
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */,
|
||||||
|
7A27ADF924A09CAD0035F39E /* CocoaError.swift in Sources */,
|
||||||
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */,
|
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */,
|
||||||
7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */,
|
7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */,
|
||||||
7AB562BA249C9E9B00473D53 /* Region.swift in Sources */,
|
7AB562BA249C9E9B00473D53 /* Region.swift in Sources */,
|
||||||
|
7A659B5924A2B1BA0043A0F2 /* AudioRecord.swift in Sources */,
|
||||||
|
7A488C3C24A74B990054D0B2 /* RxTableViewRealmDataSource.swift in Sources */,
|
||||||
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */,
|
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */,
|
||||||
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */,
|
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */,
|
||||||
|
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */,
|
||||||
|
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */,
|
||||||
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */,
|
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */,
|
||||||
7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */,
|
7A6DD90E24337930009DE740 /* PlateNumber.swift in Sources */,
|
||||||
|
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */,
|
||||||
7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */,
|
7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */,
|
||||||
|
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */,
|
||||||
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
|
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
|
||||||
7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */,
|
7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */,
|
||||||
|
7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */,
|
||||||
7A11474923FF2B2D00B424AF /* Response.swift in Sources */,
|
7A11474923FF2B2D00B424AF /* Response.swift in Sources */,
|
||||||
7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */,
|
7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */,
|
||||||
7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */,
|
7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */,
|
||||||
@ -465,19 +520,24 @@
|
|||||||
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */,
|
7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */,
|
||||||
7A6E03282485951700DB22ED /* OwnersController.swift in Sources */,
|
7A6E03282485951700DB22ED /* OwnersController.swift in Sources */,
|
||||||
7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */,
|
7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */,
|
||||||
|
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */,
|
||||||
7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */,
|
7A7547DD2403180A004E8406 /* SectionHeader.swift in Sources */,
|
||||||
7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */,
|
7AF58D58240309CA00CE01A0 /* VehicleTextParamCell.swift in Sources */,
|
||||||
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */,
|
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */,
|
||||||
|
7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */,
|
||||||
7A11474723FF2AA500B424AF /* User.swift in Sources */,
|
7A11474723FF2AA500B424AF /* User.swift in Sources */,
|
||||||
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
|
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
|
||||||
7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */,
|
7AF58D3124029E1000CE01A0 /* VehicleHeaderCell.swift in Sources */,
|
||||||
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */,
|
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */,
|
||||||
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
|
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
|
||||||
|
7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */,
|
||||||
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
|
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
|
||||||
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
|
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
|
||||||
7A11474423FF06CA00B424AF /* Api.swift in Sources */,
|
7A11474423FF06CA00B424AF /* Api.swift in Sources */,
|
||||||
|
7A488C3D24A74B990054D0B2 /* RxCollectionViewRealmDataSource.swift in Sources */,
|
||||||
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */,
|
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */,
|
||||||
7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */,
|
7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */,
|
||||||
|
7A488C3E24A74B990054D0B2 /* Reactive+RxRealmDataSources.swift in Sources */,
|
||||||
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */,
|
7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */,
|
||||||
7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */,
|
7A05161A2414FF0900FC55AC /* DateSection.swift in Sources */,
|
||||||
7A333814249A532400D878F1 /* Filter.swift in Sources */,
|
7A333814249A532400D878F1 /* Filter.swift in Sources */,
|
||||||
@ -722,7 +782,7 @@
|
|||||||
repositoryURL = "https://github.com/realm/realm-cocoa";
|
repositoryURL = "https://github.com/realm/realm-cocoa";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 4.3.2;
|
minimumVersion = 5.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
7A11472923FEA24D00B424AF /* XCRemoteSwiftPackageReference "Action" */ = {
|
7A11472923FEA24D00B424AF /* XCRemoteSwiftPackageReference "Action" */ = {
|
||||||
@ -746,7 +806,7 @@
|
|||||||
repositoryURL = "https://github.com/RxSwiftCommunity/RxRealm";
|
repositoryURL = "https://github.com/RxSwiftCommunity/RxRealm";
|
||||||
requirement = {
|
requirement = {
|
||||||
kind = upToNextMajorVersion;
|
kind = upToNextMajorVersion;
|
||||||
minimumVersion = 2.0.0;
|
minimumVersion = 3.0.0;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
7A8A220C248EF5830073DFD9 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {
|
7A8A220C248EF5830073DFD9 /* XCRemoteSwiftPackageReference "Swift-JWT" */ = {
|
||||||
|
|||||||
@ -87,8 +87,8 @@
|
|||||||
"repositoryURL": "https://github.com/airbnb/MagazineLayout",
|
"repositoryURL": "https://github.com/airbnb/MagazineLayout",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "4a91fb2fa75a3c498748466227fa115fd27bb100",
|
"revision": "12dd2cc84b7f17c4f46c7d95cde64d521c588ee8",
|
||||||
"version": "1.6.0"
|
"version": "1.6.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -96,8 +96,8 @@
|
|||||||
"repositoryURL": "https://github.com/realm/realm-cocoa",
|
"repositoryURL": "https://github.com/realm/realm-cocoa",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "fa43b8e2909334c79f233ce472332c136ca108da",
|
"revision": "b3fa932233bfa53966c373933d60157545a3f09f",
|
||||||
"version": "4.4.1"
|
"version": "5.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -105,8 +105,8 @@
|
|||||||
"repositoryURL": "https://github.com/realm/realm-core",
|
"repositoryURL": "https://github.com/realm/realm-core",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "35662ff940e340bf630ad1d1d88acfc7af18bee6",
|
"revision": "bc900a2a8e05722c1b42f95396adb3c99eeb500f",
|
||||||
"version": "5.23.8"
|
"version": "6.0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -123,8 +123,8 @@
|
|||||||
"repositoryURL": "https://github.com/RxSwiftCommunity/RxRealm",
|
"repositoryURL": "https://github.com/RxSwiftCommunity/RxRealm",
|
||||||
"state": {
|
"state": {
|
||||||
"branch": null,
|
"branch": null,
|
||||||
"revision": "70188d79fe2eb19b5013dd1deae33e9e53f10e76",
|
"revision": "c4dcc49acbf8073a8a6481b571c640bb169650f1",
|
||||||
"version": "2.0.0"
|
"version": "3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -40,5 +40,28 @@
|
|||||||
landmarkType = "7">
|
landmarkType = "7">
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</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>
|
</Breakpoints>
|
||||||
</Bucket>
|
</Bucket>
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import RealmSwift
|
import RealmSwift
|
||||||
|
import RxSwift
|
||||||
|
import RxCocoa
|
||||||
import os.log
|
import os.log
|
||||||
|
|
||||||
extension OSLog {
|
extension OSLog {
|
||||||
@ -9,6 +11,7 @@ extension OSLog {
|
|||||||
enum QuickAction {
|
enum QuickAction {
|
||||||
case none
|
case none
|
||||||
case check
|
case check
|
||||||
|
case addVoiceRecord
|
||||||
}
|
}
|
||||||
|
|
||||||
@UIApplicationMain
|
@UIApplicationMain
|
||||||
@ -19,7 +22,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
|
||||||
let config = Realm.Configuration(
|
let config = Realm.Configuration(
|
||||||
schemaVersion: 6,
|
schemaVersion: 8,
|
||||||
migrationBlock: { migration, oldSchemaVersion in
|
migrationBlock: { migration, oldSchemaVersion in
|
||||||
if oldSchemaVersion <= 3 {
|
if oldSchemaVersion <= 3 {
|
||||||
var numbers: [String] = []
|
var numbers: [String] = []
|
||||||
@ -36,10 +39,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Realm.Configuration.defaultConfiguration = config
|
Realm.Configuration.defaultConfiguration = config
|
||||||
|
print(Realm.Configuration.defaultConfiguration.fileURL!)
|
||||||
|
|
||||||
IHProgressHUD.set(defaultStyle: .dark)
|
IHProgressHUD.set(defaultStyle: .dark)
|
||||||
IHProgressHUD.set(defaultMaskType: .black)
|
IHProgressHUD.set(defaultMaskType: .black)
|
||||||
|
|
||||||
|
Logging.URLRequests = { _ in false };
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,6 +60,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
if let shortcutItem = options.shortcutItem {
|
if let shortcutItem = options.shortcutItem {
|
||||||
if shortcutItem.type == "CheckNumberAction" {
|
if shortcutItem.type == "CheckNumberAction" {
|
||||||
self.quickAction = .check
|
self.quickAction = .check
|
||||||
|
} else if shortcutItem.type == "AddVoiceRecordAction" {
|
||||||
|
self.quickAction = .addVoiceRecord
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,8 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>com.apple.security.app-sandbox</key>
|
<key>com.apple.security.app-sandbox</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>com.apple.security.device.audio-input</key>
|
||||||
|
<true/>
|
||||||
<key>com.apple.security.network.client</key>
|
<key>com.apple.security.network.client</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</dict>
|
||||||
|
|||||||
@ -193,7 +193,7 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="gzk-86-k5g" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="gzk-86-k5g" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1783.2" y="948.57571214392806"/>
|
<point key="canvasLocation" x="1095" y="965"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Owners Controller-->
|
<!--Owners Controller-->
|
||||||
<scene sceneID="0bv-cp-2uj">
|
<scene sceneID="0bv-cp-2uj">
|
||||||
@ -208,7 +208,7 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="URC-NW-y2j" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="URC-NW-y2j" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="2668" y="948.57571214392806"/>
|
<point key="canvasLocation" x="1881" y="965"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Search Controller-->
|
<!--Search Controller-->
|
||||||
<scene sceneID="3Md-yW-a0R">
|
<scene sceneID="3Md-yW-a0R">
|
||||||
@ -380,7 +380,105 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="trD-gZ-yAv" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="trD-gZ-yAv" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</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>
|
</scene>
|
||||||
<!--Check Controller-->
|
<!--Check Controller-->
|
||||||
<scene sceneID="t7Z-yv-ZLH">
|
<scene sceneID="t7Z-yv-ZLH">
|
||||||
@ -552,6 +650,7 @@
|
|||||||
</tabBar>
|
</tabBar>
|
||||||
<connections>
|
<connections>
|
||||||
<segue destination="TSb-ZG-qfD" kind="relationship" relationship="viewControllers" id="Bwf-98-gjF"/>
|
<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="GCa-Re-j14" kind="relationship" relationship="viewControllers" id="FGp-f6-fUh"/>
|
||||||
<segue destination="4jU-Z3-PF2" kind="relationship" relationship="viewControllers" id="aH2-IT-86l"/>
|
<segue destination="4jU-Z3-PF2" kind="relationship" relationship="viewControllers" id="aH2-IT-86l"/>
|
||||||
</connections>
|
</connections>
|
||||||
@ -634,7 +733,7 @@
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Yso-GW-Nd2" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="Yso-GW-Nd2" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="-262" y="144"/>
|
<point key="canvasLocation" x="-735" y="143"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Main Split Controller-->
|
<!--Main Split Controller-->
|
||||||
<scene sceneID="10H-jh-3eN">
|
<scene sceneID="10H-jh-3eN">
|
||||||
@ -647,7 +746,7 @@
|
|||||||
</splitViewController>
|
</splitViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="zgS-sH-9QV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="zgS-sH-9QV" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="844" y="145"/>
|
<point key="canvasLocation" x="199" y="143"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Check-->
|
<!--Check-->
|
||||||
<scene sceneID="pUX-kf-oY1">
|
<scene sceneID="pUX-kf-oY1">
|
||||||
@ -703,7 +802,26 @@
|
|||||||
</navigationController>
|
</navigationController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="2VV-jB-JET" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="2VV-jB-JET" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</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>
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
@ -713,5 +831,7 @@
|
|||||||
<image name="gear" catalog="system" width="128" height="119"/>
|
<image name="gear" catalog="system" width="128" height="119"/>
|
||||||
<image name="line.horizontal.3.decrease" catalog="system" width="128" height="73"/>
|
<image name="line.horizontal.3.decrease" catalog="system" width="128" height="73"/>
|
||||||
<image name="magnifyingglass" catalog="system" width="128" height="115"/>
|
<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>
|
</resources>
|
||||||
</document>
|
</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:))
|
.subscribe(onNext: self.updateDetailController(with:))
|
||||||
.disposed(by: self.bag)
|
.disposed(by: self.bag)
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
Observable.collection(from: realm.objects(Vehicle.self)
|
Observable.collection(from: realm.objects(Vehicle.self)
|
||||||
.sorted(byKeyPath: "addedDate", ascending: false))
|
.sorted(byKeyPath: "addedDate", ascending: false))
|
||||||
.map { $0.groupedByDate() }
|
.map { $0.groupedByDate() }
|
||||||
.bind(to: self.history.rx.items(dataSource: ds))
|
.bind(to: self.history.rx.items(dataSource: ds))
|
||||||
.disposed(by: self.bag)
|
.disposed(by: self.bag)
|
||||||
|
}
|
||||||
|
|
||||||
self.history.rx.setDelegate(self).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 {
|
if ad.quickAction == .check {
|
||||||
ad.quickAction = .none
|
ad.quickAction = .none
|
||||||
self.number.becomeFirstResponder()
|
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 {
|
extension RandomAccessCollection where Element: Dated & IdentifiableType & Equatable {
|
||||||
func groupedByDate() -> [DateSection<Element>] {
|
func groupedByDate() -> [DateSection<Element>] {
|
||||||
let now = Date()
|
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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<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>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
@ -69,6 +73,14 @@
|
|||||||
</dict>
|
</dict>
|
||||||
<key>UIApplicationShortcutItems</key>
|
<key>UIApplicationShortcutItems</key>
|
||||||
<array>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UIApplicationShortcutItemType</key>
|
||||||
|
<string>AddVoiceRecordAction</string>
|
||||||
|
<key>UIApplicationShortcutItemTitle</key>
|
||||||
|
<string>Add voice record</string>
|
||||||
|
<key>UIApplicationShortcutItemIconType</key>
|
||||||
|
<string>UIApplicationShortcutIconTypeAudio</string>
|
||||||
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
<key>UIApplicationShortcutItemIconType</key>
|
<key>UIApplicationShortcutItemIconType</key>
|
||||||
<string>UIApplicationShortcutIconTypeCompose</string>
|
<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
|
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 {
|
enum Constants {
|
||||||
static var baseUrl: String {
|
static var baseUrl: String {
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
return "http://127.0.0.1:3000/"
|
//return "http://127.0.0.1:3000/"
|
||||||
//return "https://vps.aliencat.pro:8443/"
|
return "https://vps.aliencat.pro:8443/"
|
||||||
#else
|
#else
|
||||||
return "https://vps.aliencat.pro:8443/"
|
return "https://vps.aliencat.pro:8443/"
|
||||||
#endif
|
#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