Recording audio

This commit is contained in:
Selim Mustafaev 2025-03-16 22:23:03 +03:00
parent c63ff50b00
commit 17fc96c06d
20 changed files with 457 additions and 51 deletions

View File

@ -68,7 +68,6 @@
7A5911EE2D63226F00EC51BA /* SearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911ED2D63226F00EC51BA /* SearchScreen.swift */; }; 7A5911EE2D63226F00EC51BA /* SearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911ED2D63226F00EC51BA /* SearchScreen.swift */; };
7A5911F02D63266B00EC51BA /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911EF2D63266B00EC51BA /* SearchViewModel.swift */; }; 7A5911F02D63266B00EC51BA /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911EF2D63266B00EC51BA /* SearchViewModel.swift */; };
7A5911F22D63268400EC51BA /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911F12D63268400EC51BA /* SearchCoordinator.swift */; }; 7A5911F22D63268400EC51BA /* SearchCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911F12D63268400EC51BA /* SearchCoordinator.swift */; };
7A5912052D648A6000EC51BA /* AutoCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5912042D648A6000EC51BA /* AutoCancellable.swift */; };
7A599C362C18AC7F00D47C18 /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C352C18AC7F00D47C18 /* ApiError.swift */; }; 7A599C362C18AC7F00D47C18 /* ApiError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C352C18AC7F00D47C18 /* ApiError.swift */; };
7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */; }; 7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */; };
7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */; }; 7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */; };
@ -104,7 +103,6 @@
7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE702469DFB600ABE48E /* MediaContentView.swift */; }; 7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE702469DFB600ABE48E /* MediaContentView.swift */; };
7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */; }; 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */; };
7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */; }; 7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */; };
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A659B5A24A3768A0043A0F2 /* Substrings.swift */; };
7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */; }; 7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */; };
7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 7A6C4D9D2C56BCA600982597 /* SwiftLocation */; }; 7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 7A6C4D9D2C56BCA600982597 /* SwiftLocation */; };
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD902242BF4A5009DE740 /* PlateView.swift */; }; 7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD902242BF4A5009DE740 /* PlateView.swift */; };
@ -178,6 +176,12 @@
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */; }; 7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */; };
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */; }; 7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */; };
7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */; }; 7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */; };
7ABDA8032D8704F70083C715 /* VehicleRecordService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */; };
7ABDA8052D8705210083C715 /* VehicleRecordServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */; };
7ABDA8092D8710F80083C715 /* AutoCancellable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA8082D8710F80083C715 /* AutoCancellable.swift */; };
7ABDA80B2D8715DC0083C715 /* VehicleRecordError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA80A2D8715DC0083C715 /* VehicleRecordError.swift */; };
7ABDA80D2D8721B10083C715 /* Substrings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA80C2D8721B10083C715 /* Substrings.swift */; };
7ABDA80F2D8723F90083C715 /* StorageService+AudioRecords.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABDA80E2D8723F90083C715 /* StorageService+AudioRecords.swift */; };
7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AC355492969652F00889457 /* SwiftEntryKit */; }; 7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AC355492969652F00889457 /* SwiftEntryKit */; };
7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554B29696A1C00889457 /* MainTabController.swift */; }; 7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554B29696A1C00889457 /* MainTabController.swift */; };
7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554D29696C4500889457 /* DummyNewController.swift */; }; 7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554D29696C4500889457 /* DummyNewController.swift */; };
@ -354,7 +358,6 @@
7A5911ED2D63226F00EC51BA /* SearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreen.swift; sourceTree = "<group>"; }; 7A5911ED2D63226F00EC51BA /* SearchScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchScreen.swift; sourceTree = "<group>"; };
7A5911EF2D63266B00EC51BA /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; }; 7A5911EF2D63266B00EC51BA /* SearchViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewModel.swift; sourceTree = "<group>"; };
7A5911F12D63268400EC51BA /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = "<group>"; }; 7A5911F12D63268400EC51BA /* SearchCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchCoordinator.swift; sourceTree = "<group>"; };
7A5912042D648A6000EC51BA /* AutoCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellable.swift; sourceTree = "<group>"; };
7A599C352C18AC7F00D47C18 /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = "<group>"; }; 7A599C352C18AC7F00D47C18 /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = "<group>"; };
7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbRefreshTokenModel.swift; sourceTree = "<group>"; }; 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbRefreshTokenModel.swift; sourceTree = "<group>"; };
7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbVerifyTokenModel.swift; sourceTree = "<group>"; }; 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbVerifyTokenModel.swift; sourceTree = "<group>"; };
@ -397,7 +400,6 @@
7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBrowserViewController.swift; sourceTree = "<group>"; }; 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBrowserViewController.swift; sourceTree = "<group>"; };
7A64AE722469DFB600ABE48E /* ContentTransformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentTransformers.swift; sourceTree = "<group>"; }; 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentTransformers.swift; sourceTree = "<group>"; };
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecord.swift; 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>"; };
7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullifyDate.swift; sourceTree = "<group>"; }; 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullifyDate.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>"; };
@ -470,6 +472,12 @@
7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = "<group>"; }; 7ABD1B462D044A3200B43213 /* GalleryScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryScreen.swift; sourceTree = "<group>"; };
7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = "<group>"; }; 7ABD1B482D044A4700B43213 /* GalleryViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewModel.swift; sourceTree = "<group>"; };
7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = "<group>"; }; 7ABD1B4A2D044A7D00B43213 /* GalleryCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryCoordinator.swift; sourceTree = "<group>"; };
7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordService.swift; sourceTree = "<group>"; };
7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordServiceProtocol.swift; sourceTree = "<group>"; };
7ABDA8082D8710F80083C715 /* AutoCancellable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCancellable.swift; sourceTree = "<group>"; };
7ABDA80A2D8715DC0083C715 /* VehicleRecordError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRecordError.swift; sourceTree = "<group>"; };
7ABDA80C2D8721B10083C715 /* Substrings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Substrings.swift; sourceTree = "<group>"; };
7ABDA80E2D8723F90083C715 /* StorageService+AudioRecords.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageService+AudioRecords.swift"; sourceTree = "<group>"; };
7AC3554B29696A1C00889457 /* MainTabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabController.swift; sourceTree = "<group>"; }; 7AC3554B29696A1C00889457 /* MainTabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabController.swift; sourceTree = "<group>"; };
7AC3554D29696C4500889457 /* DummyNewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyNewController.swift; sourceTree = "<group>"; }; 7AC3554D29696C4500889457 /* DummyNewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DummyNewController.swift; sourceTree = "<group>"; };
7AC3554F29696D5A00889457 /* NewNumberController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNumberController.swift; sourceTree = "<group>"; }; 7AC3554F29696D5A00889457 /* NewNumberController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewNumberController.swift; sourceTree = "<group>"; };
@ -794,7 +802,6 @@
7ADF6CA02512244400F237B2 /* MapExt.swift */, 7ADF6CA02512244400F237B2 /* MapExt.swift */,
7ADF6C92250B954900F237B2 /* Navigation.swift */, 7ADF6C92250B954900F237B2 /* Navigation.swift */,
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */, 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
7A659B5A24A3768A0043A0F2 /* Substrings.swift */,
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */, 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */,
7A761C0A267E8FF90005F28F /* Error.swift */, 7A761C0A267E8FF90005F28F /* Error.swift */,
7AC76D7A270083AE0084DB27 /* TextView.swift */, 7AC76D7A270083AE0084DB27 /* TextView.swift */,
@ -815,6 +822,7 @@
7A45FB362C2706D000618694 /* Services */ = { 7A45FB362C2706D000618694 /* Services */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
7ABDA8012D8704C90083C715 /* VehicleRecordService */,
7A9519772D80B3B200E69883 /* AudioRecordService */, 7A9519772D80B3B200E69883 /* AudioRecordService */,
7AB4E4392D3D3F390006D052 /* VehicleService */, 7AB4E4392D3D3F390006D052 /* VehicleService */,
7A06E0B12C707DD7005731AC /* SettingsService */, 7A06E0B12C707DD7005731AC /* SettingsService */,
@ -1050,6 +1058,7 @@
7A45FB372C27073700618694 /* StorageService.swift */, 7A45FB372C27073700618694 /* StorageService.swift */,
7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */, 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */,
7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */, 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */,
7ABDA80E2D8723F90083C715 /* StorageService+AudioRecords.swift */,
7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */, 7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */,
); );
path = StorageService; path = StorageService;
@ -1095,6 +1104,16 @@
path = GalleryScreen; path = GalleryScreen;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
7ABDA8012D8704C90083C715 /* VehicleRecordService */ = {
isa = PBXGroup;
children = (
7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */,
7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */,
7ABDA80A2D8715DC0083C715 /* VehicleRecordError.swift */,
);
path = VehicleRecordService;
sourceTree = "<group>";
};
7AC355552969742800889457 /* ACUIKit */ = { 7AC355552969742800889457 /* ACUIKit */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -1148,6 +1167,7 @@
7A96AE30246B2FE400297C33 /* Constants.swift */, 7A96AE30246B2FE400297C33 /* Constants.swift */,
7A000AA124C2EEDE001F5B00 /* Location.swift */, 7A000AA124C2EEDE001F5B00 /* Location.swift */,
7A64A2232C1A07EA00284124 /* Formatters.swift */, 7A64A2232C1A07EA00284124 /* Formatters.swift */,
7ABDA8082D8710F80083C715 /* AutoCancellable.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1161,6 +1181,7 @@
7A1CF81529A42117007962DA /* Realm.swift */, 7A1CF81529A42117007962DA /* Realm.swift */,
7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */, 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */,
7A809F382D66755B00CF1B3C /* Error+Canceled.swift */, 7A809F382D66755B00CF1B3C /* Error+Canceled.swift */,
7ABDA80C2D8721B10083C715 /* Substrings.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -1181,7 +1202,6 @@
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */, 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
7A912F362D381B7400002938 /* LicensePlateView.swift */, 7A912F362D381B7400002938 /* LicensePlateView.swift */,
7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */, 7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */,
7A5912042D648A6000EC51BA /* AutoCancellable.swift */,
7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */, 7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */,
7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */, 7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */,
7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */, 7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */,
@ -1459,7 +1479,6 @@
7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */, 7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */,
7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */, 7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */,
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */, 7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */,
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */,
7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */, 7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */,
7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */, 7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */,
7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */,
@ -1520,7 +1539,6 @@
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */, 7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */,
7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */, 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */,
7A71580C2C44453200852088 /* AdsScreen.swift in Sources */, 7A71580C2C44453200852088 /* AdsScreen.swift in Sources */,
7A5912052D648A6000EC51BA /* AutoCancellable.swift in Sources */,
7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */, 7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */,
7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */, 7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */,
7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */, 7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */,
@ -1564,11 +1582,14 @@
7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */, 7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */,
7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */, 7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */,
7A761C08267E8EA20005F28F /* JWT.swift in Sources */, 7A761C08267E8EA20005F28F /* JWT.swift in Sources */,
7ABDA8032D8704F70083C715 /* VehicleRecordService.swift in Sources */,
7A64A2242C1A07EA00284124 /* Formatters.swift in Sources */, 7A64A2242C1A07EA00284124 /* Formatters.swift in Sources */,
7ABDA8092D8710F80083C715 /* AutoCancellable.swift in Sources */,
7A5D84C02C1AE4DC00C2209B /* VehicleEngine.swift in Sources */, 7A5D84C02C1AE4DC00C2209B /* VehicleEngine.swift in Sources */,
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */, 7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */,
7AF6D2282677C2DC0086EA64 /* Constants.swift in Sources */, 7AF6D2282677C2DC0086EA64 /* Constants.swift in Sources */,
7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */, 7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */,
7ABDA8052D8705210083C715 /* VehicleRecordServiceProtocol.swift in Sources */,
7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */, 7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */,
7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */, 7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */,
7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */, 7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */,
@ -1583,6 +1604,7 @@
7A761C042677F18E0005F28F /* ApiService.swift in Sources */, 7A761C042677F18E0005F28F /* ApiService.swift in Sources */,
7A95197B2D80B41600E69883 /* AudioRecordServiceProtocol.swift in Sources */, 7A95197B2D80B41600E69883 /* AudioRecordServiceProtocol.swift in Sources */,
7AF6D21C2677C1680086EA64 /* DebugInfo.swift in Sources */, 7AF6D21C2677C1680086EA64 /* DebugInfo.swift in Sources */,
7ABDA80D2D8721B10083C715 /* Substrings.swift in Sources */,
7AF6D2122677C12E0086EA64 /* Location.swift in Sources */, 7AF6D2122677C12E0086EA64 /* Location.swift in Sources */,
7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */, 7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */,
7A64A2102C19E1EB00284124 /* VehicleBrandDto.swift in Sources */, 7A64A2102C19E1EB00284124 /* VehicleBrandDto.swift in Sources */,
@ -1592,6 +1614,7 @@
7AF6D21E2677C1680086EA64 /* PlateNumber.swift in Sources */, 7AF6D21E2677C1680086EA64 /* PlateNumber.swift in Sources */,
7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */, 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */,
7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */, 7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */,
7ABDA80B2D8715DC0083C715 /* VehicleRecordError.swift in Sources */,
7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */, 7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */,
7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */, 7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */,
7AF6D21F2677C1680086EA64 /* Response.swift in Sources */, 7AF6D21F2677C1680086EA64 /* Response.swift in Sources */,
@ -1615,6 +1638,7 @@
7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */, 7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */,
7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */, 7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */,
7A1CF81629A42117007962DA /* Realm.swift in Sources */, 7A1CF81629A42117007962DA /* Realm.swift in Sources */,
7ABDA80F2D8723F90083C715 /* StorageService+AudioRecords.swift in Sources */,
7A64A2142C19E3B700284124 /* VehicleEngineDto.swift in Sources */, 7A64A2142C19E3B700284124 /* VehicleEngineDto.swift in Sources */,
7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */, 7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */,
7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */, 7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */,

View File

@ -18,7 +18,7 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate {
} }
addHistoryTab() addHistoryTab()
//addRecordsTab() addRecordsTab()
#if !targetEnvironment(macCatalyst) #if !targetEnvironment(macCatalyst)
addDummyTab() addDummyTab()

View File

@ -61,7 +61,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
locationService: locationService) locationService: locationService)
container.register(VehicleServiceProtocol.self, instance: vehicleService) container.register(VehicleServiceProtocol.self, instance: vehicleService)
container.register(AudioRecordServiceProtocol.self, instance: AudioRecordService()) let audioRecordService = AudioRecordService()
container.register(AudioRecordServiceProtocol.self, instance: audioRecordService)
let vehicleRecordService = VehicleRecordService(
recordService: audioRecordService,
locationService: locationService,
settingsService: settingsService
)
container.register(VehicleRecordServiceProtocol.self, instance: vehicleRecordService)
} }
func setupRootController(scene: UIScene, openReport number: String?) { func setupRootController(scene: UIScene, openReport number: String?) {

View File

@ -19,9 +19,8 @@ final class RecordsCoordinator {
let resolver = ServiceContainer.shared let resolver = ServiceContainer.shared
let viewModel = RecordsViewModel( let viewModel = RecordsViewModel(
recordService: resolver.resolve(AudioRecordServiceProtocol.self), recordService: resolver.resolve(VehicleRecordServiceProtocol.self),
storageService: resolver.resolve(StorageServiceProtocol.self), storageService: resolver.resolve(StorageServiceProtocol.self)
locationService: resolver.resolve(LocationServiceProtocol.self)
) )
let view = RecordsScreen(viewModel: viewModel) let view = RecordsScreen(viewModel: viewModel)

View File

@ -13,6 +13,33 @@ struct RecordsScreen: View {
@State var viewModel: RecordsViewModel @State var viewModel: RecordsViewModel
var body: some View { var body: some View {
Text("Hello, World!") List {
ForEach(viewModel.records) { record in
Text(record.path)
}
}
.listStyle(.plain)
.hud($viewModel.hud)
.navigationTitle("Voice records")
.onAppear {
Task { await viewModel.onAppear() }
}
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
Task { await viewModel.startRecording() }
} label: {
Image(systemName: "plus")
}
.alert("Recording...", isPresented: $viewModel.showRecordingAlert) {
Button("Cancel", role: .cancel) {
Task { await viewModel.cancelRecording() }
}
Button("Done") {
Task { await viewModel.stopRecording() }
}
}
}
}
} }
} }

View File

@ -11,18 +11,48 @@ import AutoCatCore
@MainActor @MainActor
@Observable @Observable
final class RecordsViewModel { final class RecordsViewModel: ACHudContainer {
let recordService: AudioRecordServiceProtocol let recordService: VehicleRecordServiceProtocol
let storageService: StorageServiceProtocol let storageService: StorageServiceProtocol
let locationService: LocationServiceProtocol
init(recordService: AudioRecordServiceProtocol, var hud: ACHud?
storageService: StorageServiceProtocol, var showRecordingAlert: Bool = false
locationService: LocationServiceProtocol) { var records: [AudioRecordDto] = []
init(recordService: VehicleRecordServiceProtocol,
storageService: StorageServiceProtocol) {
self.recordService = recordService self.recordService = recordService
self.storageService = storageService self.storageService = storageService
self.locationService = locationService }
func onAppear() async {
await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { return }
records = try await storageService.loadRecords()
}
await recordService.requestPermissionsIfNeeded()
}
func startRecording() async {
await wrapWithToast(showProgress: false) { [weak self] in
guard let self else { return }
try await recordService.startRecording()
showRecordingAlert = true
}
}
func stopRecording() async {
await wrapWithToast { [weak self] in
guard let self else { return }
let record = try await recordService.stopRecording()
try await storageService.add(record: record)
records = try await storageService.loadRecords()
}
}
func cancelRecording() async {
await recordService.cancelRecording()
} }
} }

View File

@ -421,3 +421,5 @@
"Something went wrong" = "Что-то пошло не так"; "Something went wrong" = "Что-то пошло не так";
"Are you sure you want to delete this event?" = "Вы уверены, что хотите удалить это событие?"; "Are you sure you want to delete this event?" = "Вы уверены, что хотите удалить это событие?";
"Voice records" = "Голосовые записи";

View File

@ -1,6 +1,6 @@
import Foundation import Foundation
extension String { public extension String {
func index(from: Int) -> Index { func index(from: Int) -> Index {
return self.index(startIndex, offsetBy: from) return self.index(startIndex, offsetBy: from)
} }

View File

@ -8,7 +8,7 @@
import Foundation import Foundation
public struct AudioRecordDto: Decodable { public struct AudioRecordDto: Decodable, Sendable {
public var path: String = "" public var path: String = ""
public var number: String? public var number: String?
@ -17,13 +17,20 @@ public struct AudioRecordDto: Decodable {
public var duration: TimeInterval = 0 public var duration: TimeInterval = 0
public var event: VehicleEventDto? public var event: VehicleEventDto?
public init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEventDto?) { public init(
path: String,
number: String?,
raw: String,
addedDate: TimeInterval = Date().timeIntervalSince1970,
duration: TimeInterval,
event: VehicleEventDto?
) {
self.path = path self.path = path
self.number = number self.number = number
self.duration = duration self.duration = duration
self.rawText = raw self.rawText = raw
self.event = event self.event = event
self.addedDate = Date().timeIntervalSince1970 self.addedDate = addedDate
} }
public func getAddedDate() -> TimeInterval { public func getAddedDate() -> TimeInterval {

View File

@ -9,7 +9,7 @@
import AVFoundation import AVFoundation
import Speech import Speech
public final class AudioRecordService { public actor AudioRecordService {
let audioFileSettings: [String : Any] = [ let audioFileSettings: [String : Any] = [
AVFormatIDKey: kAudioFormatMPEG4AAC, AVFormatIDKey: kAudioFormatMPEG4AAC,
@ -22,9 +22,17 @@ public final class AudioRecordService {
public init() { public init() {
} }
}
extension AudioRecordService: AudioRecordServiceProtocol {
public func requestRecordPermissions() async -> Bool {
await AVAudioApplication.requestRecordPermission()
}
@discardableResult @discardableResult
func requestRecognitionAuthorization() async -> SFSpeechRecognizerAuthorizationStatus { public func requestRecognitionAuthorization() async -> SFSpeechRecognizerAuthorizationStatus {
let status = SFSpeechRecognizer.authorizationStatus() let status = SFSpeechRecognizer.authorizationStatus()
guard status == .notDetermined else { guard status == .notDetermined else {
@ -37,15 +45,6 @@ public final class AudioRecordService {
} }
} }
} }
}
extension AudioRecordService: AudioRecordServiceProtocol {
public func requestPermissions() async -> Bool {
await requestRecognitionAuthorization()
return await AVAudioApplication.requestRecordPermission()
}
public func startRecording(to url: URL) async throws { public func startRecording(to url: URL) async throws {
guard AVAudioApplication.shared.recordPermission != .denied else { guard AVAudioApplication.shared.recordPermission != .denied else {
@ -74,10 +73,21 @@ extension AudioRecordService: AudioRecordServiceProtocol {
recorder?.record() recorder?.record()
} }
public func stopRecording() { public func stopRecording() async {
recorder?.stop() if recorder?.isRecording == true {
recorder = nil recorder?.stop()
}
self.recorder = nil
}
public func cancelRecording() async {
if recorder?.isRecording == true {
recorder?.stop()
recorder?.deleteRecording()
}
self.recorder = nil
} }
public func recognizeText(from url: URL) async -> String? { public func recognizeText(from url: URL) async -> String? {
@ -87,6 +97,7 @@ extension AudioRecordService: AudioRecordServiceProtocol {
} }
let request = SFSpeechURLRecognitionRequest(url: url) let request = SFSpeechURLRecognitionRequest(url: url)
request.shouldReportPartialResults = false
return await withCheckedContinuation { continuation in return await withCheckedContinuation { continuation in
recognizer.recognitionTask(with: request) { result, error in recognizer.recognitionTask(with: request) { result, error in
@ -94,4 +105,11 @@ extension AudioRecordService: AudioRecordServiceProtocol {
} }
} }
} }
public func getDuration(from url: URL) async throws -> TimeInterval {
let asset = AVURLAsset(url: url)
let duration = try await asset.load(.duration)
return CMTimeGetSeconds(duration)
}
} }

View File

@ -6,12 +6,21 @@
// Copyright © 2025 Selim Mustafaev. All rights reserved. // Copyright © 2025 Selim Mustafaev. All rights reserved.
// //
import Foundation import Speech
import Mockable
public protocol AudioRecordServiceProtocol { @Mockable
public protocol AudioRecordServiceProtocol: Sendable {
@discardableResult
func requestRecordPermissions() async -> Bool
@discardableResult
func requestRecognitionAuthorization() async -> SFSpeechRecognizerAuthorizationStatus
func requestPermissions() async -> Bool
func startRecording(to url: URL) async throws func startRecording(to url: URL) async throws
func stopRecording() func stopRecording() async
func cancelRecording() async
func recognizeText(from url: URL) async -> String? func recognizeText(from url: URL) async -> String?
func getDuration(from url: URL) async throws -> TimeInterval
} }

View File

@ -0,0 +1,26 @@
//
// StorageService+AudioRecords.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 16.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Foundation
public extension StorageService {
func add(record: AudioRecordDto) async throws {
try await realm.asyncWrite {
realm.add(AudioRecord(dto: record))
}
}
func loadRecords() async throws -> [AudioRecordDto] {
realm.objects(AudioRecord.self)
.sorted(byKeyPath: "addedDate", ascending: false)
.map(\.dto)
}
}

View File

@ -16,7 +16,7 @@ extension StorageService {
throw StorageError.vehicleNotFound throw StorageError.vehicleNotFound
} }
let note = VehicleNote(text: text, user: await settingsService.user.email) let note = VehicleNote(text: text, user: settingsService.user.email)
try await realm.asyncWrite { try await realm.asyncWrite {
vehicle.notes.append(note) vehicle.notes.append(note)

View File

@ -31,4 +31,8 @@ public protocol StorageServiceProtocol: Sendable {
func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto
func remove(event id: String, from number: String) async throws -> VehicleDto func remove(event id: String, from number: String) async throws -> VehicleDto
func edit(event: VehicleEventDto, for number: String) async throws -> VehicleDto func edit(event: VehicleEventDto, for number: String) async throws -> VehicleDto
// Audio records
func add(record: AudioRecordDto) async throws
func loadRecords() async throws -> [AudioRecordDto]
} }

View File

@ -0,0 +1,21 @@
//
// VehicleRecordError.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 16.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Foundation
enum VehicleRecordError: LocalizedError {
case emptyUrl
var errorDescription: String? {
switch self {
case .emptyUrl:
return "Can't find record url"
}
}
}

View File

@ -0,0 +1,142 @@
//
// VehicleRecordService.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 16.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Foundation
public actor VehicleRecordService {
let recordService: AudioRecordServiceProtocol
let locationService: LocationServiceProtocol
let settingsService: SettingsServiceProtocol
let validLetters = Constants.pnLettersMap.keys.map(String.init)
var url: URL?
var date = Date()
var location: VehicleEventDto?
@AutoCancellable
var locationTask: Task<Void,Error>?
public init(
recordService: AudioRecordServiceProtocol,
locationService: LocationServiceProtocol,
settingsService: SettingsServiceProtocol
) {
self.recordService = recordService
self.locationService = locationService
self.settingsService = settingsService
}
func getPlateNumber(from recognizedText: String?) -> String? {
guard let recognizedText else {
return nil
}
let trimmed = recognizedText
.replacingOccurrences(of: " ", with: "")
.uppercased()
.replacingOccurrences(of: "Ф", with: "В")
.replacingOccurrences(of: "НОЛЬ", with: "0")
.replacingOccurrences(of: "Э", with: "")
var result = ""
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
result = String(trimmed[range])
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression), settingsService.recognizeAlternativeOrder {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S"#, options: .regularExpression), settingsService.recognizeShortenedNumbers {
result = String(trimmed[range]) + settingsService.defaultRegion
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression), settingsService.recognizeAlternativeOrder && settingsService.recognizeShortenedNumbers {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + settingsService.defaultRegion
}
if !result.isEmpty && valid(number: result) {
return result
} else {
return nil
}
}
func valid(number: String) -> Bool {
guard number.count >= 8 else { return false }
let first = String(number.prefix(1))
let second = number.substring(with: 4..<5)
let third = number.substring(with: 5..<6)
let digits = Int(number.substring(with: 1..<4))
let region = Int(number.substring(from: 6))
return self.validLetters.contains(first)
&& self.validLetters.contains(second)
&& self.validLetters.contains(third)
&& digits != nil
&& region != nil
&& region! < 1000
}
}
extension VehicleRecordService: VehicleRecordServiceProtocol {
public func requestPermissionsIfNeeded() async {
await recordService.requestRecordPermissions()
await recordService.requestRecognitionAuthorization()
}
public func startRecording() async throws {
date = Date()
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
let url = try FileManager.default.url(for: fileName, in: "recordings")
self.url = url
try await recordService.startRecording(to: url)
locationTask = Task {
location = try await locationService.getRecentLocation()
}
}
public func stopRecording() async throws -> AudioRecordDto {
guard let url else {
await recordService.cancelRecording()
throw VehicleRecordError.emptyUrl
}
await recordService.stopRecording()
async let recognitionTask = recordService.recognizeText(from: url)
async let durationTask = recordService.getDuration(from: url)
let (text, duration) = await (recognitionTask, try? durationTask)
locationTask?.cancel()
locationTask = nil
self.url = nil
let record = AudioRecordDto(
path: url.lastPathComponent,
number: getPlateNumber(from: text),
raw: text ?? "",
duration: duration ?? 0,
event: location
)
return record
}
public func cancelRecording() async {
await recordService.cancelRecording()
locationTask?.cancel()
locationTask = nil
url = nil
}
}

View File

@ -0,0 +1,17 @@
//
// VehicleRecordServiceProtocol.swift
// AutoCatCore
//
// Created by Selim Mustafaev on 16.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Foundation
public protocol VehicleRecordServiceProtocol: Sendable {
func requestPermissionsIfNeeded() async
func startRecording() async throws
func stopRecording() async throws -> AudioRecordDto
func cancelRecording() async
}

View File

@ -9,9 +9,9 @@
import Foundation import Foundation
@propertyWrapper @propertyWrapper
struct AutoCancellable<C: Sendable, E: Error> { public struct AutoCancellable<C: Sendable, E: Error> {
var wrappedValue: Task<C,E>? { public var wrappedValue: Task<C,E>? {
didSet { didSet {
if let oldValue, oldValue.isCancelled { if let oldValue, oldValue.isCancelled {
return return
@ -20,7 +20,7 @@ struct AutoCancellable<C: Sendable, E: Error> {
} }
} }
init(wrappedValue: Task<C,E>?) { public init(wrappedValue: Task<C,E>?) {
self.wrappedValue = wrappedValue self.wrappedValue = wrappedValue
} }
} }

View File

@ -11,9 +11,27 @@ import Foundation
public struct Formatters { public struct Formatters {
public static let standard: DateFormatter = { public static let standard: DateFormatter = {
let f = DateFormatter()
f.dateStyle = .medium let formatter = DateFormatter()
f.timeStyle = .medium formatter.dateStyle = .medium
return f formatter.timeStyle = .medium
return formatter
}()
public static let short: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .short
formatter.timeStyle = .short
return formatter
}()
public static let time: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.unitsStyle = .abbreviated
formatter.allowedUnits = [.minute, .second]
formatter.zeroFormattingBehavior = .pad
return formatter
}() }()
} }

View File

@ -0,0 +1,54 @@
//
// VehicleRecordServiceTests.swift
// AutoCatCoreTests
//
// Created by Selim Mustafaev on 16.03.2025.
// Copyright © 2025 Selim Mustafaev. All rights reserved.
//
import Testing
import Mockable
import AutoCatCore
struct VehicleRecordServiceTests {
let storageServiceMock = MockStorageServiceProtocol()
let locationServiceMock: MockLocationServiceProtocol
let audioRecordServiceMock = MockAudioRecordServiceProtocol()
let vehicleRecordService: VehicleRecordService
init() async {
self.locationServiceMock = await .init()
self.vehicleRecordService = .init(
recordService: audioRecordServiceMock,
storageService: storageServiceMock,
locationService: locationServiceMock
)
}
@Test("Requesting permissions")
func requestPermissions() async throws {
given(audioRecordServiceMock)
.requestRecordPermissions()
.willReturn(true)
given(audioRecordServiceMock)
.requestRecognitionAuthorization()
.willReturn(.authorized)
await vehicleRecordService.requestPermissionsIfNeeded()
verify(audioRecordServiceMock)
.requestRecordPermissions()
.called(.once)
verify(audioRecordServiceMock)
.requestRecognitionAuthorization()
.called(.once)
}
}