Recording audio
This commit is contained in:
parent
c63ff50b00
commit
17fc96c06d
@ -68,7 +68,6 @@
|
||||
7A5911EE2D63226F00EC51BA /* SearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911ED2D63226F00EC51BA /* SearchScreen.swift */; };
|
||||
7A5911F02D63266B00EC51BA /* SearchViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911EF2D63266B00EC51BA /* SearchViewModel.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 */; };
|
||||
7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.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 */; };
|
||||
7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.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 */; };
|
||||
7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 7A6C4D9D2C56BCA600982597 /* SwiftLocation */; };
|
||||
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 */; };
|
||||
7ABD1B492D044A4700B43213 /* GalleryViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ABD1B482D044A4700B43213 /* GalleryViewModel.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 */; };
|
||||
7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3554B29696A1C00889457 /* MainTabController.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -397,7 +400,6 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -470,6 +472,12 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@ -794,7 +802,6 @@
|
||||
7ADF6CA02512244400F237B2 /* MapExt.swift */,
|
||||
7ADF6C92250B954900F237B2 /* Navigation.swift */,
|
||||
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */,
|
||||
7A659B5A24A3768A0043A0F2 /* Substrings.swift */,
|
||||
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */,
|
||||
7A761C0A267E8FF90005F28F /* Error.swift */,
|
||||
7AC76D7A270083AE0084DB27 /* TextView.swift */,
|
||||
@ -815,6 +822,7 @@
|
||||
7A45FB362C2706D000618694 /* Services */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7ABDA8012D8704C90083C715 /* VehicleRecordService */,
|
||||
7A9519772D80B3B200E69883 /* AudioRecordService */,
|
||||
7AB4E4392D3D3F390006D052 /* VehicleService */,
|
||||
7A06E0B12C707DD7005731AC /* SettingsService */,
|
||||
@ -1050,6 +1058,7 @@
|
||||
7A45FB372C27073700618694 /* StorageService.swift */,
|
||||
7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */,
|
||||
7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */,
|
||||
7ABDA80E2D8723F90083C715 /* StorageService+AudioRecords.swift */,
|
||||
7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */,
|
||||
);
|
||||
path = StorageService;
|
||||
@ -1095,6 +1104,16 @@
|
||||
path = GalleryScreen;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7ABDA8012D8704C90083C715 /* VehicleRecordService */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7ABDA8022D8704F70083C715 /* VehicleRecordService.swift */,
|
||||
7ABDA8042D8705210083C715 /* VehicleRecordServiceProtocol.swift */,
|
||||
7ABDA80A2D8715DC0083C715 /* VehicleRecordError.swift */,
|
||||
);
|
||||
path = VehicleRecordService;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7AC355552969742800889457 /* ACUIKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@ -1148,6 +1167,7 @@
|
||||
7A96AE30246B2FE400297C33 /* Constants.swift */,
|
||||
7A000AA124C2EEDE001F5B00 /* Location.swift */,
|
||||
7A64A2232C1A07EA00284124 /* Formatters.swift */,
|
||||
7ABDA8082D8710F80083C715 /* AutoCancellable.swift */,
|
||||
);
|
||||
path = Utils;
|
||||
sourceTree = "<group>";
|
||||
@ -1161,6 +1181,7 @@
|
||||
7A1CF81529A42117007962DA /* Realm.swift */,
|
||||
7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */,
|
||||
7A809F382D66755B00CF1B3C /* Error+Canceled.swift */,
|
||||
7ABDA80C2D8721B10083C715 /* Substrings.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
@ -1181,7 +1202,6 @@
|
||||
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */,
|
||||
7A912F362D381B7400002938 /* LicensePlateView.swift */,
|
||||
7AB4E42B2D397D8E0006D052 /* VehicleCellView.swift */,
|
||||
7A5912042D648A6000EC51BA /* AutoCancellable.swift */,
|
||||
7AC8B2752D6A01C700190706 /* UISearchTextField+Dumb.swift */,
|
||||
7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */,
|
||||
7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */,
|
||||
@ -1459,7 +1479,6 @@
|
||||
7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */,
|
||||
7A7158122C444A6400852088 /* AdsViewModel.swift in Sources */,
|
||||
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */,
|
||||
7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */,
|
||||
7A71580E2C4445A200852088 /* AdsCoordinator.swift in Sources */,
|
||||
7AB4E4662D58A16C0006D052 /* GenericError.swift in Sources */,
|
||||
7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */,
|
||||
@ -1520,7 +1539,6 @@
|
||||
7ABD1B472D044A3200B43213 /* GalleryScreen.swift in Sources */,
|
||||
7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */,
|
||||
7A71580C2C44453200852088 /* AdsScreen.swift in Sources */,
|
||||
7A5912052D648A6000EC51BA /* AutoCancellable.swift in Sources */,
|
||||
7A06E0B02C7065D8005731AC /* SettingsCoordinator.swift in Sources */,
|
||||
7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */,
|
||||
7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */,
|
||||
@ -1564,11 +1582,14 @@
|
||||
7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */,
|
||||
7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */,
|
||||
7A761C08267E8EA20005F28F /* JWT.swift in Sources */,
|
||||
7ABDA8032D8704F70083C715 /* VehicleRecordService.swift in Sources */,
|
||||
7A64A2242C1A07EA00284124 /* Formatters.swift in Sources */,
|
||||
7ABDA8092D8710F80083C715 /* AutoCancellable.swift in Sources */,
|
||||
7A5D84C02C1AE4DC00C2209B /* VehicleEngine.swift in Sources */,
|
||||
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */,
|
||||
7AF6D2282677C2DC0086EA64 /* Constants.swift in Sources */,
|
||||
7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */,
|
||||
7ABDA8052D8705210083C715 /* VehicleRecordServiceProtocol.swift in Sources */,
|
||||
7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */,
|
||||
7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */,
|
||||
7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */,
|
||||
@ -1583,6 +1604,7 @@
|
||||
7A761C042677F18E0005F28F /* ApiService.swift in Sources */,
|
||||
7A95197B2D80B41600E69883 /* AudioRecordServiceProtocol.swift in Sources */,
|
||||
7AF6D21C2677C1680086EA64 /* DebugInfo.swift in Sources */,
|
||||
7ABDA80D2D8721B10083C715 /* Substrings.swift in Sources */,
|
||||
7AF6D2122677C12E0086EA64 /* Location.swift in Sources */,
|
||||
7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */,
|
||||
7A64A2102C19E1EB00284124 /* VehicleBrandDto.swift in Sources */,
|
||||
@ -1592,6 +1614,7 @@
|
||||
7AF6D21E2677C1680086EA64 /* PlateNumber.swift in Sources */,
|
||||
7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */,
|
||||
7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */,
|
||||
7ABDA80B2D8715DC0083C715 /* VehicleRecordError.swift in Sources */,
|
||||
7A06E0B32C707E13005731AC /* SettingsServiceProtocol.swift in Sources */,
|
||||
7A06E0B52C707E2B005731AC /* SettingsService.swift in Sources */,
|
||||
7AF6D21F2677C1680086EA64 /* Response.swift in Sources */,
|
||||
@ -1615,6 +1638,7 @@
|
||||
7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */,
|
||||
7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */,
|
||||
7A1CF81629A42117007962DA /* Realm.swift in Sources */,
|
||||
7ABDA80F2D8723F90083C715 /* StorageService+AudioRecords.swift in Sources */,
|
||||
7A64A2142C19E3B700284124 /* VehicleEngineDto.swift in Sources */,
|
||||
7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */,
|
||||
7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */,
|
||||
|
||||
@ -18,7 +18,7 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate {
|
||||
}
|
||||
|
||||
addHistoryTab()
|
||||
//addRecordsTab()
|
||||
addRecordsTab()
|
||||
|
||||
#if !targetEnvironment(macCatalyst)
|
||||
addDummyTab()
|
||||
|
||||
@ -61,7 +61,15 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
locationService: locationService)
|
||||
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?) {
|
||||
|
||||
@ -19,9 +19,8 @@ final class RecordsCoordinator {
|
||||
|
||||
let resolver = ServiceContainer.shared
|
||||
let viewModel = RecordsViewModel(
|
||||
recordService: resolver.resolve(AudioRecordServiceProtocol.self),
|
||||
storageService: resolver.resolve(StorageServiceProtocol.self),
|
||||
locationService: resolver.resolve(LocationServiceProtocol.self)
|
||||
recordService: resolver.resolve(VehicleRecordServiceProtocol.self),
|
||||
storageService: resolver.resolve(StorageServiceProtocol.self)
|
||||
)
|
||||
|
||||
let view = RecordsScreen(viewModel: viewModel)
|
||||
|
||||
@ -13,6 +13,33 @@ struct RecordsScreen: View {
|
||||
@State var viewModel: RecordsViewModel
|
||||
|
||||
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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,18 +11,48 @@ import AutoCatCore
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class RecordsViewModel {
|
||||
final class RecordsViewModel: ACHudContainer {
|
||||
|
||||
let recordService: AudioRecordServiceProtocol
|
||||
let recordService: VehicleRecordServiceProtocol
|
||||
let storageService: StorageServiceProtocol
|
||||
let locationService: LocationServiceProtocol
|
||||
|
||||
init(recordService: AudioRecordServiceProtocol,
|
||||
storageService: StorageServiceProtocol,
|
||||
locationService: LocationServiceProtocol) {
|
||||
var hud: ACHud?
|
||||
var showRecordingAlert: Bool = false
|
||||
var records: [AudioRecordDto] = []
|
||||
|
||||
init(recordService: VehicleRecordServiceProtocol,
|
||||
storageService: StorageServiceProtocol) {
|
||||
|
||||
self.recordService = recordService
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -421,3 +421,5 @@
|
||||
"Something went wrong" = "Что-то пошло не так";
|
||||
|
||||
"Are you sure you want to delete this event?" = "Вы уверены, что хотите удалить это событие?";
|
||||
|
||||
"Voice records" = "Голосовые записи";
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public extension String {
|
||||
func index(from: Int) -> Index {
|
||||
return self.index(startIndex, offsetBy: from)
|
||||
}
|
||||
@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct AudioRecordDto: Decodable {
|
||||
public struct AudioRecordDto: Decodable, Sendable {
|
||||
|
||||
public var path: String = ""
|
||||
public var number: String?
|
||||
@ -17,13 +17,20 @@ public struct AudioRecordDto: Decodable {
|
||||
public var duration: TimeInterval = 0
|
||||
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.number = number
|
||||
self.duration = duration
|
||||
self.rawText = raw
|
||||
self.event = event
|
||||
self.addedDate = Date().timeIntervalSince1970
|
||||
self.addedDate = addedDate
|
||||
}
|
||||
|
||||
public func getAddedDate() -> TimeInterval {
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
import AVFoundation
|
||||
import Speech
|
||||
|
||||
public final class AudioRecordService {
|
||||
public actor AudioRecordService {
|
||||
|
||||
let audioFileSettings: [String : Any] = [
|
||||
AVFormatIDKey: kAudioFormatMPEG4AAC,
|
||||
@ -22,9 +22,17 @@ public final class AudioRecordService {
|
||||
public init() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
extension AudioRecordService: AudioRecordServiceProtocol {
|
||||
|
||||
public func requestRecordPermissions() async -> Bool {
|
||||
|
||||
await AVAudioApplication.requestRecordPermission()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func requestRecognitionAuthorization() async -> SFSpeechRecognizerAuthorizationStatus {
|
||||
public func requestRecognitionAuthorization() async -> SFSpeechRecognizerAuthorizationStatus {
|
||||
|
||||
let status = SFSpeechRecognizer.authorizationStatus()
|
||||
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 {
|
||||
guard AVAudioApplication.shared.recordPermission != .denied else {
|
||||
@ -74,10 +73,21 @@ extension AudioRecordService: AudioRecordServiceProtocol {
|
||||
recorder?.record()
|
||||
}
|
||||
|
||||
public func stopRecording() {
|
||||
public func stopRecording() async {
|
||||
|
||||
recorder?.stop()
|
||||
recorder = nil
|
||||
if recorder?.isRecording == true {
|
||||
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? {
|
||||
@ -87,6 +97,7 @@ extension AudioRecordService: AudioRecordServiceProtocol {
|
||||
}
|
||||
|
||||
let request = SFSpeechURLRecognitionRequest(url: url)
|
||||
request.shouldReportPartialResults = false
|
||||
|
||||
return await withCheckedContinuation { continuation 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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,12 +6,21 @@
|
||||
// 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 stopRecording()
|
||||
func stopRecording() async
|
||||
func cancelRecording() async
|
||||
func recognizeText(from url: URL) async -> String?
|
||||
func getDuration(from url: URL) async throws -> TimeInterval
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -16,7 +16,7 @@ extension StorageService {
|
||||
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 {
|
||||
vehicle.notes.append(note)
|
||||
|
||||
@ -31,4 +31,8 @@ public protocol StorageServiceProtocol: Sendable {
|
||||
func add(event: VehicleEventDto, to 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
|
||||
|
||||
// Audio records
|
||||
func add(record: AudioRecordDto) async throws
|
||||
func loadRecords() async throws -> [AudioRecordDto]
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -9,9 +9,9 @@
|
||||
import Foundation
|
||||
|
||||
@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 {
|
||||
if let oldValue, oldValue.isCancelled {
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -11,9 +11,27 @@ import Foundation
|
||||
public struct Formatters {
|
||||
|
||||
public static let standard: DateFormatter = {
|
||||
let f = DateFormatter()
|
||||
f.dateStyle = .medium
|
||||
f.timeStyle = .medium
|
||||
return f
|
||||
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateStyle = .medium
|
||||
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
|
||||
}()
|
||||
}
|
||||
|
||||
54
AutoCatCoreTests/VehicleRecordServiceTests.swift
Normal file
54
AutoCatCoreTests/VehicleRecordServiceTests.swift
Normal 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user