diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 045e5e9..f16b15b 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -3,17 +3,11 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 70; objects = { /* Begin PBXBuildFile section */ 6841A85D4B60DB71D1E68DA0 /* ImageGrid.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841AC687EA6293A0757678C /* ImageGrid.swift */; }; - 7A0420AD2561A0B100034941 /* OsagoController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0420AC2561A0B100034941 /* OsagoController.swift */; }; - 7A0420B12561A0E100034941 /* OsagoAddController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0420B02561A0E100034941 /* OsagoAddController.swift */; }; - 7A0420B62568650C00034941 /* DkbmController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0420B52568650C00034941 /* DkbmController.swift */; }; - 7A0420BA25693D2C00034941 /* dkbm.js in Resources */ = {isa = PBXBuildFile; fileRef = 7A0420B925693D2C00034941 /* dkbm.js */; }; - 7A0B663729984201006F5189 /* DateCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0B663629984201006F5189 /* DateCache.swift */; }; - 7A0B96A0257D6D4B000B39AD /* MultilineLabelRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0B969F257D6D4B000B39AD /* MultilineLabelRow.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 */; }; @@ -25,33 +19,63 @@ 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471523FDEB2A00B424AF /* MainSplitController.swift */; }; 7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471723FDEBFA00B424AF /* ReportController.swift */; }; 7A11471A23FE839000B424AF /* AuthController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11471923FE839000B424AF /* AuthController.swift */; }; + 7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441652C297EDE00E79018 /* NotesScreen.swift */; }; + 7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1441672C297EFD00E79018 /* NotesViewModel.swift */; }; + 7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416B2C297F2100E79018 /* NotesCoordinator.swift */; }; + 7A14416E2C297F7C00E79018 /* Coordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416D2C297F7C00E79018 /* Coordinator.swift */; }; + 7A1441702C2998B200E79018 /* Formatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A14416F2C2998B200E79018 /* Formatters.swift */; }; + 7A176DB22C43071A00999D6B /* ApiServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A176DB12C43071A00999D6B /* ApiServiceStub.swift */; }; + 7A176DB72C432F8800999D6B /* Mockable in Frameworks */ = {isa = PBXBuildFile; productRef = 7A176DB62C432F8800999D6B /* Mockable */; }; + 7A176DB92C432F8800999D6B /* MockableTest in Frameworks */ = {isa = PBXBuildFile; productRef = 7A176DB82C432F8800999D6B /* MockableTest */; }; 7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17CE492A2E820300626A6E /* UIStackView.swift */; }; 7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A17CE4B2A2E850200626A6E /* UISegmentedControl.swift */; }; - 7A1CF80329A41C62007962DA /* Realm in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80229A41C62007962DA /* Realm */; }; 7A1CF80529A41C66007962DA /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80429A41C66007962DA /* RealmSwift */; }; - 7A1CF80829A41D58007962DA /* RxBlocking in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80729A41D58007962DA /* RxBlocking */; }; - 7A1CF80A29A41D58007962DA /* RxCocoa in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80929A41D58007962DA /* RxCocoa */; }; - 7A1CF80C29A41D58007962DA /* RxRelay in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80B29A41D58007962DA /* RxRelay */; }; - 7A1CF80E29A41D58007962DA /* RxSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80D29A41D58007962DA /* RxSwift */; }; 7A1CF81629A42117007962DA /* Realm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1CF81529A42117007962DA /* Realm.swift */; }; 7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */; }; - 7A21112A24FC3D7E003BBF6F /* AudioEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A21112924FC3D7E003BBF6F /* AudioEngine.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 */; }; + 7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */; }; 7A2DE69B25869ABD00A113FC /* AdsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2DE69A25869ABD00A113FC /* AdsController.swift */; }; 7A2DE69E2589606A00A113FC /* ImageGridRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2DE69D2589606A00A113FC /* ImageGridRow.swift */; }; + 7A2E6FA72C42B3AD00C40DA7 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; }; 7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A33381024990DAE00D878F1 /* FiltersController.swift */; }; 7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3399AA299063370087DF98 /* SearchControllerExt.swift */; }; 7A35177B27E23F8800DC538C /* Eureka in Frameworks */ = {isa = PBXBuildFile; productRef = 7A35177A27E23F8800DC538C /* Eureka */; }; + 7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E30F22C18840600567704 /* ActivityItemSource.swift */; }; 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; }; 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AC2436350B00E59687 /* SearchController.swift */; }; + 7A45FB382C27073700618694 /* StorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45FB372C27073700618694 /* StorageService.swift */; }; 7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7924001D3300CBFE6E /* CheckController.swift */; }; 7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.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 */; }; + 7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84B82C1AD3C200C2209B /* DtoConvertible.swift */; }; + 7A5D84BC2C1AD81000C2209B /* VehicleOwnershipPeriod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84BB2C1AD81000C2209B /* VehicleOwnershipPeriod.swift */; }; + 7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84BD2C1AE44700C2209B /* VehiclePhoto.swift */; }; + 7A5D84C02C1AE4DC00C2209B /* VehicleEngine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84BF2C1AE4DC00C2209B /* VehicleEngine.swift */; }; + 7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84C12C1AE5C900C2209B /* VehicleModel.swift */; }; + 7A5D84C42C1AE65C00C2209B /* VehicleBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84C32C1AE65C00C2209B /* VehicleBrand.swift */; }; + 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5D84C52C1AE72E00C2209B /* VehicleName.swift */; }; 7A61FF8B2575A2CD00D905D5 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7A61FF892575A2CD00D905D5 /* Localizable.strings */; }; 7A61FF912575A5B300D905D5 /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 7A61FF8F2575A5B300D905D5 /* InfoPlist.strings */; }; 7A61FFA0257D3CFC00D905D5 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 7A61FFA2257D3CFC00D905D5 /* Localizable.stringsdict */; }; + 7A64A2032C19DA1000284124 /* VehicleDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2022C19DA1000284124 /* VehicleDto.swift */; }; + 7A64A20A2C19E07100284124 /* VehicleNameDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2092C19E07100284124 /* VehicleNameDto.swift */; }; + 7A64A2102C19E1EB00284124 /* VehicleBrandDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A20F2C19E1EB00284124 /* VehicleBrandDto.swift */; }; + 7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2112C19E2A100284124 /* VehicleModelDto.swift */; }; + 7A64A2142C19E3B700284124 /* VehicleEngineDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2132C19E3B700284124 /* VehicleEngineDto.swift */; }; + 7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2152C19E4CF00284124 /* VehiclePhotoDto.swift */; }; + 7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2172C19E64800284124 /* VehicleOwnershipPeriodDto.swift */; }; + 7A64A21A2C19E6B300284124 /* VehicleEventDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2192C19E6B300284124 /* VehicleEventDto.swift */; }; + 7A64A21C2C19E87B00284124 /* OsagoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A21B2C19E87B00284124 /* OsagoDto.swift */; }; + 7A64A21E2C19E8D500284124 /* VehicleAdDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A21D2C19E8D500284124 /* VehicleAdDto.swift */; }; + 7A64A2202C19E93500284124 /* VehicleNoteDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A21F2C19E93500284124 /* VehicleNoteDto.swift */; }; + 7A64A2222C19E99E00284124 /* DebugInfoDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2212C19E99E00284124 /* DebugInfoDto.swift */; }; + 7A64A2242C1A07EA00284124 /* Formatters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2232C1A07EA00284124 /* Formatters.swift */; }; + 7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64A2252C1A32C800284124 /* AudioRecordDto.swift */; }; 7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */; }; 7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE702469DFB600ABE48E /* MediaContentView.swift */; }; 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */; }; @@ -61,11 +85,13 @@ 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90724329144009DE740 /* CenterTextLayer.swift */; }; 7A6DD90A24329541009DE740 /* RoadNumbers2.0.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */; }; 7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */; }; - 7A6E03282485951700DB22ED /* OwnersController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6E03272485951700DB22ED /* OwnersController.swift */; }; - 7A6F095E26DB9F85003A965D /* NotesController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F095D26DB9F85003A965D /* NotesController.swift */; }; 7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6F095F26DBF588003A965D /* VehicleNote.swift */; }; + 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */; }; + 7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */; }; + 7A7158072C44085600852088 /* OsagoScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158062C44085600852088 /* OsagoScreen.swift */; }; + 7A7158092C44087E00852088 /* OsagoCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7158082C44087E00852088 /* OsagoCoordinator.swift */; }; 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */; }; - 7A761C042677F18E0005F28F /* Api.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474323FF06CA00B424AF /* Api.swift */; }; + 7A761C042677F18E0005F28F /* ApiService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474323FF06CA00B424AF /* ApiService.swift */; }; 7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF824A09CAD0035F39E /* CocoaError.swift */; }; 7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A15051124DB3E3000F39631 /* AnyEncodable.swift */; }; 7A761C08267E8EA20005F28F /* JWT.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A43F9F7246C8A6200BA5B49 /* JWT.swift */; }; @@ -80,16 +106,23 @@ 7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A220A248D67B60073DFD9 /* VehicleReportImage.swift */; }; 7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */; }; 7A8AB76B25A1D95500ECF2C1 /* SourceStatusRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8AB76A25A1D95500ECF2C1 /* SourceStatusRow.swift */; }; + 7A8C4A5B2C1C55680052DDF3 /* SwiftLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 7A8C4A5A2C1C55680052DDF3 /* SwiftLocation */; }; 7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A91894E29A2BD8700519C74 /* GestureRecognizers.swift */; }; 7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */; }; 7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A96AE2E246B2BCD00297C33 /* WebKit.framework */; }; 7A99406426E4BFAE002E9CB6 /* VehicleNoteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */; }; 7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */; }; + 7AA363362C25B64A00851D6D /* RealmSwift in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 7A1CF80429A41C66007962DA /* RealmSwift */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 7AA7BC3325A5DFB80053A5D5 /* Kingfisher in Frameworks */ = {isa = PBXBuildFile; productRef = 7AF58D332402A91C00CE01A0 /* Kingfisher */; }; 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */ = {isa = PBXBuildFile; productRef = 7A813DC02508C4D900CC93B9 /* ExceptionCatcher */; }; 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABDE1C2532F3EB0041AFC6 /* PKHUD */; }; 7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABB1F1267E9CC800D7AB32 /* SwiftDate */; }; 7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; }; + 7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */; }; + 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */; }; + 7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */; }; + 7AB587372C42E3EC00FA7B66 /* StorageServiceStub.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587362C42E3EC00FA7B66 /* StorageServiceStub.swift */; }; + 7AB587412C42FFE200FA7B66 /* ApiServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */; }; 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; }; 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; }; 7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */ = {isa = PBXBuildFile; productRef = 7AC355492969652F00889457 /* SwiftEntryKit */; }; @@ -101,28 +134,24 @@ 7AC355592969746600889457 /* UIControl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC355582969746600889457 /* UIControl.swift */; }; 7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC3555A296995B200889457 /* UIEdgeInsets.swift */; }; 7AC76D7B270083AE0084DB27 /* TextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AC76D7A270083AE0084DB27 /* TextView.swift */; }; + 7ADF23062C25B5BF002624FF /* RealmSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 7ADF23052C25B5BF002624FF /* RealmSwift */; }; 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C92250B954900F237B2 /* Navigation.swift */; }; 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C94250D037700F237B2 /* ShowEventController.swift */; }; 7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */; }; 7ADF6C99250F872C00F237B2 /* RoadNumbers.otf in Resources */ = {isa = PBXBuildFile; fileRef = 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */; }; - 7ADF6C9D250FA96000F237B2 /* SwiftMaskTextfield.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C9C250FA96000F237B2 /* SwiftMaskTextfield.swift */; }; 7ADF6C9F251201D200F237B2 /* GlobalEventsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6C9E251201D200F237B2 /* GlobalEventsController.swift */; }; 7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7ADF6CA02512244400F237B2 /* MapExt.swift */; }; 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE24C5E251F1B4E00758E39 /* Buttons.swift */; }; 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */; }; 7AE26A3524F31B0700625033 /* EventsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE26A3424F31B0700625033 /* EventsController.swift */; }; - 7AE492A1259232F000322D2E /* MultilineLinkRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE492A0259232F000322D2E /* MultilineLinkRow.swift */; }; 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */; }; 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFE727240455E200910EB7 /* SettingsController.swift */; }; - 7AF6D1E02677A7E00086EA64 /* AutoCatTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6D1DF2677A7E00086EA64 /* AutoCatTests.swift */; }; - 7AF6D1E92677A8410086EA64 /* FakeLocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF6D1E82677A8410086EA64 /* FakeLocationManager.swift */; }; 7AF6D2042677C03B0086EA64 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; }; 7AF6D2052677C03B0086EA64 /* AutoCatCore.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 7AF6D2122677C12E0086EA64 /* Location.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A000AA124C2EEDE001F5B00 /* Location.swift */; }; 7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */; }; 7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */; }; 7AF6D2152677C1680086EA64 /* Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11474A23FF368B00B424AF /* Settings.swift */; }; - 7AF6D2162677C1680086EA64 /* Cloneable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AF12B1C258C9CFF0090F8B8 /* Cloneable.swift */; }; 7AF6D2172677C1680086EA64 /* VehicleRegion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */; }; 7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2DE69725868AC800A113FC /* VehicleAd.swift */; }; 7AF6D2192677C1680086EA64 /* DateSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A0516192414FF0900FC55AC /* DateSection.swift */; }; @@ -136,10 +165,23 @@ 7AF6D2212677C1680086EA64 /* PagedResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */; }; 7AF6D2282677C2DC0086EA64 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE30246B2FE400297C33 /* Constants.swift */; }; 7AF6D22A2677C3AD0086EA64 /* Exportable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AE8424D26109F78002F6B31 /* Exportable.swift */; }; + 7AFBE8C02C3024E5003C491D /* ACHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8BF2C3024E5003C491D /* ACHud.swift */; }; + 7AFBE8C42C302561003C491D /* ACHudContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8C32C302561003C491D /* ACHudContainer.swift */; }; + 7AFBE8C82C30816E003C491D /* ACProgressHud.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8C72C30816E003C491D /* ACProgressHud.swift */; }; + 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8C92C3081C7003C491D /* ACProgressHud+Modifiers.swift */; }; + 7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8CB2C3085C6003C491D /* ACProgressView.swift */; }; + 7AFBE8CE2C308B53003C491D /* ACMessageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AFBE8CD2C308B53003C491D /* ACMessageView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ - 7AF6D1E22677A7E00086EA64 /* PBXContainerItemProxy */ = { + 7A2E6FA82C42B3AD00C40DA7 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 7A1146F523FDE7E500B424AF /* Project object */; + proxyType = 1; + remoteGlobalIDString = 7AF6D1EE2677C03B0086EA64; + remoteInfo = AutoCatCore; + }; + 7AB587262C42D27F00FA7B66 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 7A1146F523FDE7E500B424AF /* Project object */; proxyType = 1; @@ -156,6 +198,17 @@ /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ + 7AA363372C25B64A00851D6D /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + 7AA363362C25B64A00851D6D /* RealmSwift in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; 7AF6D2092677C03B0086EA64 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -174,13 +227,7 @@ 6841AC687EA6293A0757678C /* ImageGrid.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ImageGrid.swift; sourceTree = ""; }; 7A000AA124C2EEDE001F5B00 /* Location.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Location.swift; sourceTree = ""; }; 7A0420A925619AEC00034941 /* Osago.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Osago.swift; sourceTree = ""; }; - 7A0420AC2561A0B100034941 /* OsagoController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoController.swift; sourceTree = ""; }; - 7A0420B02561A0E100034941 /* OsagoAddController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoAddController.swift; sourceTree = ""; }; - 7A0420B52568650C00034941 /* DkbmController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DkbmController.swift; sourceTree = ""; }; - 7A0420B925693D2C00034941 /* dkbm.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = dkbm.js; sourceTree = ""; }; 7A0516192414FF0900FC55AC /* DateSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateSection.swift; sourceTree = ""; }; - 7A0B663629984201006F5189 /* DateCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateCache.swift; sourceTree = ""; }; - 7A0B969F257D6D4B000B39AD /* MultilineLabelRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineLabelRow.swift; sourceTree = ""; }; 7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordCell.swift; sourceTree = ""; }; 7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = ""; }; 7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellProgressView.swift; sourceTree = ""; }; @@ -194,35 +241,54 @@ 7A11471523FDEB2A00B424AF /* MainSplitController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSplitController.swift; sourceTree = ""; }; 7A11471723FDEBFA00B424AF /* ReportController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportController.swift; sourceTree = ""; }; 7A11471923FE839000B424AF /* AuthController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthController.swift; sourceTree = ""; }; - 7A11474323FF06CA00B424AF /* Api.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Api.swift; sourceTree = ""; }; + 7A11474323FF06CA00B424AF /* ApiService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiService.swift; sourceTree = ""; }; 7A11474623FF2AA500B424AF /* User.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = User.swift; sourceTree = ""; }; 7A11474823FF2B2D00B424AF /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = ""; }; 7A11474A23FF368B00B424AF /* Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Settings.swift; sourceTree = ""; }; 7A11474D23FFEE8800B424AF /* SVProgressHUD.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SVProgressHUD.framework; path = Carthage/Build/iOS/SVProgressHUD.framework; sourceTree = ""; }; + 7A1441652C297EDE00E79018 /* NotesScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesScreen.swift; sourceTree = ""; }; + 7A1441672C297EFD00E79018 /* NotesViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesViewModel.swift; sourceTree = ""; }; + 7A14416B2C297F2100E79018 /* NotesCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesCoordinator.swift; sourceTree = ""; }; + 7A14416D2C297F7C00E79018 /* Coordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Coordinator.swift; sourceTree = ""; }; + 7A14416F2C2998B200E79018 /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = ""; }; 7A15051124DB3E3000F39631 /* AnyEncodable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnyEncodable.swift; sourceTree = ""; }; + 7A176DB12C43071A00999D6B /* ApiServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceStub.swift; sourceTree = ""; }; 7A17CE492A2E820300626A6E /* UIStackView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIStackView.swift; sourceTree = ""; }; 7A17CE4B2A2E850200626A6E /* UISegmentedControl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UISegmentedControl.swift; sourceTree = ""; }; 7A1CF81529A42117007962DA /* Realm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Realm.swift; sourceTree = ""; }; 7A1DC38D2517ED98002E9C99 /* BlockBarButtonItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockBarButtonItem.swift; sourceTree = ""; }; - 7A21112924FC3D7E003BBF6F /* AudioEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioEngine.swift; sourceTree = ""; }; 7A27ADC6249D43210035F39E /* RegionsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegionsController.swift; sourceTree = ""; }; 7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = ""; }; 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = ""; }; 7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; }; 7A27ADF824A09CAD0035F39E /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = ""; }; + 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteAlertModifier.swift; sourceTree = ""; }; 7A2DE69725868AC800A113FC /* VehicleAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleAd.swift; sourceTree = ""; }; 7A2DE69A25869ABD00A113FC /* AdsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdsController.swift; sourceTree = ""; }; 7A2DE69D2589606A00A113FC /* ImageGridRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGridRow.swift; sourceTree = ""; }; + 7A2E6FA32C42B3AD00C40DA7 /* AutoCatCoreTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatCoreTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 7A33381024990DAE00D878F1 /* FiltersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersController.swift; sourceTree = ""; }; 7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = ""; }; 7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = ""; }; + 7A3E30F22C18840600567704 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = ""; }; 7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = ""; }; 7A3F07AC2436350B00E59687 /* SearchController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchController.swift; sourceTree = ""; }; 7A43F9F7246C8A6200BA5B49 /* JWT.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JWT.swift; sourceTree = ""; }; + 7A45FB372C27073700618694 /* StorageService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageService.swift; sourceTree = ""; }; 7A52AB292580112E002CD910 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 7A530B7924001D3300CBFE6E /* CheckController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckController.swift; sourceTree = ""; }; 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCell.swift; sourceTree = ""; }; 7A530B7F2401803A00CBFE6E /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = ""; }; + 7A599C352C18AC7F00D47C18 /* ApiError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiError.swift; sourceTree = ""; }; + 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbRefreshTokenModel.swift; sourceTree = ""; }; + 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FbVerifyTokenModel.swift; sourceTree = ""; }; + 7A5D84B82C1AD3C200C2209B /* DtoConvertible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DtoConvertible.swift; sourceTree = ""; }; + 7A5D84BB2C1AD81000C2209B /* VehicleOwnershipPeriod.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleOwnershipPeriod.swift; sourceTree = ""; }; + 7A5D84BD2C1AE44700C2209B /* VehiclePhoto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhoto.swift; sourceTree = ""; }; + 7A5D84BF2C1AE4DC00C2209B /* VehicleEngine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEngine.swift; sourceTree = ""; }; + 7A5D84C12C1AE5C900C2209B /* VehicleModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleModel.swift; sourceTree = ""; }; + 7A5D84C32C1AE65C00C2209B /* VehicleBrand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleBrand.swift; sourceTree = ""; }; + 7A5D84C52C1AE72E00C2209B /* VehicleName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleName.swift; sourceTree = ""; }; 7A61FF8325759DE700D905D5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/LaunchScreen.strings; sourceTree = ""; }; 7A61FF8A2575A2CD00D905D5 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = Base; path = Base.lproj/Localizable.strings; sourceTree = ""; }; 7A61FF8D2575A2F900D905D5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Localizable.strings; sourceTree = ""; }; @@ -231,6 +297,20 @@ 7A61FF962576C16400D905D5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ru; path = ru.lproj/Main.strings; sourceTree = ""; }; 7A61FFA1257D3CFC00D905D5 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = ""; }; 7A61FFA4257D3D0200D905D5 /* ru */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = ru; path = ru.lproj/Localizable.stringsdict; sourceTree = ""; }; + 7A64A2022C19DA1000284124 /* VehicleDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleDto.swift; sourceTree = ""; }; + 7A64A2092C19E07100284124 /* VehicleNameDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNameDto.swift; sourceTree = ""; }; + 7A64A20F2C19E1EB00284124 /* VehicleBrandDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleBrandDto.swift; sourceTree = ""; }; + 7A64A2112C19E2A100284124 /* VehicleModelDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleModelDto.swift; sourceTree = ""; }; + 7A64A2132C19E3B700284124 /* VehicleEngineDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEngineDto.swift; sourceTree = ""; }; + 7A64A2152C19E4CF00284124 /* VehiclePhotoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoDto.swift; sourceTree = ""; }; + 7A64A2172C19E64800284124 /* VehicleOwnershipPeriodDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleOwnershipPeriodDto.swift; sourceTree = ""; }; + 7A64A2192C19E6B300284124 /* VehicleEventDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEventDto.swift; sourceTree = ""; }; + 7A64A21B2C19E87B00284124 /* OsagoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoDto.swift; sourceTree = ""; }; + 7A64A21D2C19E8D500284124 /* VehicleAdDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleAdDto.swift; sourceTree = ""; }; + 7A64A21F2C19E93500284124 /* VehicleNoteDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNoteDto.swift; sourceTree = ""; }; + 7A64A2212C19E99E00284124 /* DebugInfoDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInfoDto.swift; sourceTree = ""; }; + 7A64A2232C1A07EA00284124 /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = ""; }; + 7A64A2252C1A32C800284124 /* AudioRecordDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordDto.swift; sourceTree = ""; }; 7A64AE6B2469DC6900ABE48E /* AutoCat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AutoCat.entitlements; sourceTree = ""; }; 7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissAnimationController.swift; sourceTree = ""; }; 7A64AE702469DFB600ABE48E /* MediaContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaContentView.swift; sourceTree = ""; }; @@ -243,9 +323,11 @@ 7A6DD90924329541009DE740 /* RoadNumbers2.0.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers2.0.otf; sourceTree = ""; }; 7A6DD90B24335A6D009DE740 /* FlagLayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlagLayer.swift; sourceTree = ""; }; 7A6DD90D24337930009DE740 /* PlateNumber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlateNumber.swift; sourceTree = ""; }; - 7A6E03272485951700DB22ED /* OwnersController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersController.swift; sourceTree = ""; }; - 7A6F095D26DB9F85003A965D /* NotesController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesController.swift; sourceTree = ""; }; 7A6F095F26DBF588003A965D /* VehicleNote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNote.swift; sourceTree = ""; }; + 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersScreen.swift; sourceTree = ""; }; + 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OwnersCoordinator.swift; sourceTree = ""; }; + 7A7158062C44085600852088 /* OsagoScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoScreen.swift; sourceTree = ""; }; + 7A7158082C44087E00852088 /* OsagoCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OsagoCoordinator.swift; sourceTree = ""; }; 7A7547DF24032CB6004E8406 /* VehiclePhotoCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehiclePhotoCell.swift; sourceTree = ""; }; 7A761C0A267E8FF90005F28F /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; 7A813DBD2506A57100CC93B9 /* AuthenticationServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AuthenticationServices.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/AuthenticationServices.framework; sourceTree = DEVELOPER_DIR; }; @@ -269,6 +351,11 @@ 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxSectionedDataSource.swift; sourceTree = ""; }; 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = ""; }; 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleRegion.swift; sourceTree = ""; }; + 7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceProtocol.swift; sourceTree = ""; }; + 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageService+Notes.swift"; sourceTree = ""; }; + 7AB587362C42E3EC00FA7B66 /* StorageServiceStub.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorageServiceStub.swift; sourceTree = ""; }; + 7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiServiceProtocol.swift; sourceTree = ""; }; 7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = ""; }; 7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = ""; }; 7AC3554B29696A1C00889457 /* MainTabController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabController.swift; sourceTree = ""; }; @@ -283,26 +370,30 @@ 7ADF6C94250D037700F237B2 /* ShowEventController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShowEventController.swift; sourceTree = ""; }; 7ADF6C96250F41B000F237B2 /* PNKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PNKeyboard.swift; sourceTree = ""; }; 7ADF6C98250F872C00F237B2 /* RoadNumbers.otf */ = {isa = PBXFileReference; lastKnownFileType = file; path = RoadNumbers.otf; sourceTree = ""; }; - 7ADF6C9C250FA96000F237B2 /* SwiftMaskTextfield.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftMaskTextfield.swift; sourceTree = ""; }; 7ADF6C9E251201D200F237B2 /* GlobalEventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlobalEventsController.swift; sourceTree = ""; }; 7ADF6CA02512244400F237B2 /* MapExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapExt.swift; sourceTree = ""; }; 7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = ""; }; 7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = ""; }; 7AE26A3424F31B0700625033 /* EventsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsController.swift; sourceTree = ""; }; - 7AE492A0259232F000322D2E /* MultilineLinkRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineLinkRow.swift; sourceTree = ""; }; 7AE8424D26109F78002F6B31 /* Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exportable.swift; sourceTree = ""; }; 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = ""; }; 7AEFE727240455E200910EB7 /* SettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = ""; }; - 7AF12B1C258C9CFF0090F8B8 /* Cloneable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cloneable.swift; sourceTree = ""; }; - 7AF6D1DD2677A7E00086EA64 /* AutoCatTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AutoCatTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AF6D1DF2677A7E00086EA64 /* AutoCatTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AutoCatTests.swift; sourceTree = ""; }; - 7AF6D1E12677A7E00086EA64 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 7AF6D1E82677A8410086EA64 /* FakeLocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FakeLocationManager.swift; sourceTree = ""; }; 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = AutoCatCore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AutoCatCore.h; sourceTree = ""; }; 7AF6D1F22677C03B0086EA64 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7AFBE8BF2C3024E5003C491D /* ACHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACHud.swift; sourceTree = ""; }; + 7AFBE8C32C302561003C491D /* ACHudContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACHudContainer.swift; sourceTree = ""; }; + 7AFBE8C72C30816E003C491D /* ACProgressHud.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACProgressHud.swift; sourceTree = ""; }; + 7AFBE8C92C3081C7003C491D /* ACProgressHud+Modifiers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACProgressHud+Modifiers.swift"; sourceTree = ""; }; + 7AFBE8CB2C3085C6003C491D /* ACProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACProgressView.swift; sourceTree = ""; }; + 7AFBE8CD2C308B53003C491D /* ACMessageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACMessageView.swift; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 7A2E6FA42C42B3AD00C40DA7 /* AutoCatCoreTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AutoCatCoreTests; sourceTree = ""; }; + 7AB587232C42D27F00FA7B66 /* AutoCatTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = AutoCatTests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 7A1146FA23FDE7E500B424AF /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -311,6 +402,7 @@ 7AA7BC3525A5DFB80053A5D5 /* ExceptionCatcher in Frameworks */, 7AA7BC3325A5DFB80053A5D5 /* Kingfisher in Frameworks */, 7A813DBE2506A57100CC93B9 /* AuthenticationServices.framework in Frameworks */, + 7ADF23062C25B5BF002624FF /* RealmSwift in Frameworks */, 7AA7BC3625A5DFB80053A5D5 /* PKHUD in Frameworks */, 7AC3554A2969652F00889457 /* SwiftEntryKit in Frameworks */, 7AF6D2042677C03B0086EA64 /* AutoCatCore.framework in Frameworks */, @@ -319,10 +411,20 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7AF6D1DA2677A7E00086EA64 /* Frameworks */ = { + 7A2E6FA02C42B3AD00C40DA7 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 7AB5871D2C42C1CF00FA7B66 /* RealmSwift in Frameworks */, + 7A2E6FA72C42B3AD00C40DA7 /* AutoCatCore.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7AB5871F2C42D27F00FA7B66 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7A176DB92C432F8800999D6B /* MockableTest in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -330,43 +432,20 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 7A1CF80C29A41D58007962DA /* RxRelay in Frameworks */, 7A1CF80529A41C66007962DA /* RealmSwift in Frameworks */, - 7A1CF80829A41D58007962DA /* RxBlocking in Frameworks */, - 7A1CF80A29A41D58007962DA /* RxCocoa in Frameworks */, - 7A1CF80329A41C62007962DA /* Realm in Frameworks */, 7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */, - 7A1CF80E29A41D58007962DA /* RxSwift in Frameworks */, + 7A176DB72C432F8800999D6B /* Mockable in Frameworks */, + 7A8C4A5B2C1C55680052DDF3 /* SwiftLocation in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 7A0420AF2561A0C100034941 /* Osago */ = { - isa = PBXGroup; - children = ( - 7A0420AC2561A0B100034941 /* OsagoController.swift */, - 7A0420B02561A0E100034941 /* OsagoAddController.swift */, - 7A0420B52568650C00034941 /* DkbmController.swift */, - ); - path = Osago; - sourceTree = ""; - }; - 7A0420B825693CEE00034941 /* JS */ = { - isa = PBXGroup; - children = ( - 7A0420B925693D2C00034941 /* dkbm.js */, - ); - path = JS; - sourceTree = ""; - }; 7A0B969D257D6CB3000B39AD /* eureka */ = { isa = PBXGroup; children = ( - 7A0B969F257D6D4B000B39AD /* MultilineLabelRow.swift */, 7A2DE69D2589606A00A113FC /* ImageGridRow.swift */, - 7AE492A0259232F000322D2E /* MultilineLinkRow.swift */, 7A8AB76A25A1D95500ECF2C1 /* SourceStatusRow.swift */, ); path = eureka; @@ -376,8 +455,9 @@ isa = PBXGroup; children = ( 7A1146FF23FDE7E500B424AF /* AutoCat */, - 7AF6D1DE2677A7E00086EA64 /* AutoCatTests */, 7AF6D1F02677C03B0086EA64 /* AutoCatCore */, + 7A2E6FA42C42B3AD00C40DA7 /* AutoCatCoreTests */, + 7AB587232C42D27F00FA7B66 /* AutoCatTests */, 7A1146FE23FDE7E500B424AF /* Products */, 7A11474C23FFEE8700B424AF /* Frameworks */, ); @@ -387,8 +467,9 @@ isa = PBXGroup; children = ( 7A1146FD23FDE7E500B424AF /* AutoCat.app */, - 7AF6D1DD2677A7E00086EA64 /* AutoCatTests.xctest */, 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */, + 7A2E6FA32C42B3AD00C40DA7 /* AutoCatCoreTests.xctest */, + 7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */, ); name = Products; sourceTree = ""; @@ -396,12 +477,14 @@ 7A1146FF23FDE7E500B424AF /* AutoCat */ = { isa = PBXGroup; children = ( + 7AB587352C42E3BF00FA7B66 /* Preview */, + 7AFBE8C52C30812E003C491D /* SwiftUI */, 7AC355552969742800889457 /* ACUIKit */, 7A530B7C24017FBE00CBFE6E /* Cells */, 7A11471423FDEAF800B424AF /* Controllers */, + 7A1441632C297E9800E79018 /* Screens */, 7A3F07A924360D9100E59687 /* Extensions */, 7A6DD90424326788009DE740 /* Fonts */, - 7A0420B825693CEE00034941 /* JS */, 7A11472C23FECA3E00B424AF /* ThirdParty */, 7A11474223FF06B600B424AF /* Utils */, 7A6DD901242BF48D009DE740 /* Views */, @@ -423,20 +506,17 @@ isa = PBXGroup; children = ( 7A813DC7250B5C6E00CC93B9 /* Location */, - 7A0420AF2561A0C100034941 /* Osago */, 7A2DE69A25869ABD00A113FC /* AdsController.swift */, 7A11471923FE839000B424AF /* AuthController.swift */, 7A530B7924001D3300CBFE6E /* CheckController.swift */, 7A33381024990DAE00D878F1 /* FiltersController.swift */, 7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */, 7A11471523FDEB2A00B424AF /* MainSplitController.swift */, - 7A6E03272485951700DB22ED /* OwnersController.swift */, 7A27ADF2249F8B650035F39E /* RecordsController.swift */, 7A27ADC6249D43210035F39E /* RegionsController.swift */, 7A11471723FDEBFA00B424AF /* ReportController.swift */, 7A3F07AC2436350B00E59687 /* SearchController.swift */, 7AEFE727240455E200910EB7 /* SettingsController.swift */, - 7A6F095D26DB9F85003A965D /* NotesController.swift */, 7AC3554B29696A1C00889457 /* MainTabController.swift */, 7AC3554D29696C4500889457 /* DummyNewController.swift */, 7AC3554F29696D5A00889457 /* NewNumberController.swift */, @@ -449,7 +529,6 @@ children = ( 7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */, 7A6DD90724329144009DE740 /* CenterTextLayer.swift */, - 7ADF6C9C250FA96000F237B2 /* SwiftMaskTextfield.swift */, ); path = ThirdParty; sourceTree = ""; @@ -461,6 +540,9 @@ 7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */, 7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */, 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */, + 7A3E30F22C18840600567704 /* ActivityItemSource.swift */, + 7A14416D2C297F7C00E79018 /* Coordinator.swift */, + 7A14416F2C2998B200E79018 /* Formatters.swift */, ); path = Utils; sourceTree = ""; @@ -468,22 +550,18 @@ 7A11474523FF2A9000B424AF /* Models */ = { isa = PBXGroup; children = ( - 7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */, - 7AF12B1C258C9CFF0090F8B8 /* Cloneable.swift */, + 7A5D84BA2C1AD80400C2209B /* Realm */, + 7A64A2042C19DA2D00284124 /* Protocols */, + 7A64A2012C19D99D00284124 /* DTO */, + 7A599C372C18B21200D47C18 /* Firebase */, 7A0516192414FF0900FC55AC /* DateSection.swift */, - 7A8AB76725A0DC8200ECF2C1 /* DebugInfo.swift */, 7A333813249A532400D878F1 /* Filter.swift */, - 7A0420A925619AEC00034941 /* Osago.swift */, 6841A913FABBB0AB20DEF4FC /* PagedResponse.swift */, 7A6DD90D24337930009DE740 /* PlateNumber.swift */, 7A11474823FF2B2D00B424AF /* Response.swift */, 7A11474A23FF368B00B424AF /* Settings.swift */, 7A11474623FF2AA500B424AF /* User.swift */, - 7A530B7F2401803A00CBFE6E /* Vehicle.swift */, - 7A2DE69725868AC800A113FC /* VehicleAd.swift */, - 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */, 7AB562B9249C9E9B00473D53 /* VehicleRegion.swift */, - 7A6F095F26DBF588003A965D /* VehicleNote.swift */, ); path = Models; sourceTree = ""; @@ -499,10 +577,30 @@ name = Frameworks; sourceTree = ""; }; + 7A1441632C297E9800E79018 /* Screens */ = { + isa = PBXGroup; + children = ( + 7A7158052C44083F00852088 /* OsagoScreen */, + 7A7157FE2C43EA5200852088 /* OwnersScreen */, + 7A1441642C297EA800E79018 /* NotesScreen */, + ); + path = Screens; + sourceTree = ""; + }; + 7A1441642C297EA800E79018 /* NotesScreen */ = { + isa = PBXGroup; + children = ( + 7A1441652C297EDE00E79018 /* NotesScreen.swift */, + 7A1441672C297EFD00E79018 /* NotesViewModel.swift */, + 7A14416B2C297F2100E79018 /* NotesCoordinator.swift */, + 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */, + ); + path = NotesScreen; + sourceTree = ""; + }; 7A3F07A924360D9100E59687 /* Extensions */ = { isa = PBXGroup; children = ( - 7A21112924FC3D7E003BBF6F /* AudioEngine.swift */, 7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */, 7AE24C5E251F1B4E00758E39 /* Buttons.swift */, 7A3F07AA24360DC800E59687 /* Dated.swift */, @@ -519,6 +617,15 @@ path = Extensions; sourceTree = ""; }; + 7A45FB362C2706D000618694 /* Services */ = { + isa = PBXGroup; + children = ( + 7AB5873D2C42FF4000FA7B66 /* ApiService */, + 7AB587302C42D35900FA7B66 /* StorageService */, + ); + path = Services; + sourceTree = ""; + }; 7A530B7C24017FBE00CBFE6E /* Cells */ = { isa = PBXGroup; children = ( @@ -532,6 +639,63 @@ path = Cells; sourceTree = ""; }; + 7A599C372C18B21200D47C18 /* Firebase */ = { + isa = PBXGroup; + children = ( + 7A599C382C18B22900D47C18 /* FbRefreshTokenModel.swift */, + 7A599C3A2C18B36A00D47C18 /* FbVerifyTokenModel.swift */, + ); + path = Firebase; + sourceTree = ""; + }; + 7A5D84BA2C1AD80400C2209B /* Realm */ = { + isa = PBXGroup; + children = ( + 7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */, + 7A5D84BB2C1AD81000C2209B /* VehicleOwnershipPeriod.swift */, + 7A5D84BD2C1AE44700C2209B /* VehiclePhoto.swift */, + 7A5D84BF2C1AE4DC00C2209B /* VehicleEngine.swift */, + 7A5D84C12C1AE5C900C2209B /* VehicleModel.swift */, + 7A5D84C32C1AE65C00C2209B /* VehicleBrand.swift */, + 7A5D84C52C1AE72E00C2209B /* VehicleName.swift */, + 7A530B7F2401803A00CBFE6E /* Vehicle.swift */, + 7A2DE69725868AC800A113FC /* VehicleAd.swift */, + 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */, + 7A6F095F26DBF588003A965D /* VehicleNote.swift */, + 7A0420A925619AEC00034941 /* Osago.swift */, + 7A8AB76725A0DC8200ECF2C1 /* DebugInfo.swift */, + ); + path = Realm; + sourceTree = ""; + }; + 7A64A2012C19D99D00284124 /* DTO */ = { + isa = PBXGroup; + children = ( + 7A64A2022C19DA1000284124 /* VehicleDto.swift */, + 7A64A2092C19E07100284124 /* VehicleNameDto.swift */, + 7A64A20F2C19E1EB00284124 /* VehicleBrandDto.swift */, + 7A64A2112C19E2A100284124 /* VehicleModelDto.swift */, + 7A64A2132C19E3B700284124 /* VehicleEngineDto.swift */, + 7A64A2152C19E4CF00284124 /* VehiclePhotoDto.swift */, + 7A64A2172C19E64800284124 /* VehicleOwnershipPeriodDto.swift */, + 7A64A2192C19E6B300284124 /* VehicleEventDto.swift */, + 7A64A21B2C19E87B00284124 /* OsagoDto.swift */, + 7A64A21D2C19E8D500284124 /* VehicleAdDto.swift */, + 7A64A21F2C19E93500284124 /* VehicleNoteDto.swift */, + 7A64A2212C19E99E00284124 /* DebugInfoDto.swift */, + 7A64A2252C1A32C800284124 /* AudioRecordDto.swift */, + ); + path = DTO; + sourceTree = ""; + }; + 7A64A2042C19DA2D00284124 /* Protocols */ = { + isa = PBXGroup; + children = ( + 7A5D84B82C1AD3C200C2209B /* DtoConvertible.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */ = { isa = PBXGroup; children = ( @@ -568,6 +732,24 @@ path = Fonts; sourceTree = ""; }; + 7A7157FE2C43EA5200852088 /* OwnersScreen */ = { + isa = PBXGroup; + children = ( + 7A7157FF2C43EA6900852088 /* OwnersScreen.swift */, + 7A7158032C43EAA200852088 /* OwnersCoordinator.swift */, + ); + path = OwnersScreen; + sourceTree = ""; + }; + 7A7158052C44083F00852088 /* OsagoScreen */ = { + isa = PBXGroup; + children = ( + 7A7158062C44085600852088 /* OsagoScreen.swift */, + 7A7158082C44087E00852088 /* OsagoCoordinator.swift */, + ); + path = OsagoScreen; + sourceTree = ""; + }; 7A761C06267E8E720005F28F /* ThirdParty */ = { isa = PBXGroup; children = ( @@ -590,6 +772,35 @@ path = Location; sourceTree = ""; }; + 7AB587302C42D35900FA7B66 /* StorageService */ = { + isa = PBXGroup; + children = ( + 7AB587312C42D38E00FA7B66 /* StorageServiceProtocol.swift */, + 7A45FB372C27073700618694 /* StorageService.swift */, + 7AB587332C42D3FA00FA7B66 /* StorageService+Notes.swift */, + ); + path = StorageService; + sourceTree = ""; + }; + 7AB587352C42E3BF00FA7B66 /* Preview */ = { + isa = PBXGroup; + children = ( + 7AB587362C42E3EC00FA7B66 /* StorageServiceStub.swift */, + 7A176DB12C43071A00999D6B /* ApiServiceStub.swift */, + ); + path = Preview; + sourceTree = ""; + }; + 7AB5873D2C42FF4000FA7B66 /* ApiService */ = { + isa = PBXGroup; + children = ( + 7AB587402C42FFE200FA7B66 /* ApiServiceProtocol.swift */, + 7A11474323FF06CA00B424AF /* ApiService.swift */, + 7A599C352C18AC7F00D47C18 /* ApiError.swift */, + ); + path = ApiService; + sourceTree = ""; + }; 7AC355552969742800889457 /* ACUIKit */ = { isa = PBXGroup; children = ( @@ -621,27 +832,10 @@ path = Extensions; sourceTree = ""; }; - 7AF6D1DE2677A7E00086EA64 /* AutoCatTests */ = { - isa = PBXGroup; - children = ( - 7AF6D1E72677A81F0086EA64 /* Mocks */, - 7AF6D1DF2677A7E00086EA64 /* AutoCatTests.swift */, - 7AF6D1E12677A7E00086EA64 /* Info.plist */, - ); - path = AutoCatTests; - sourceTree = ""; - }; - 7AF6D1E72677A81F0086EA64 /* Mocks */ = { - isa = PBXGroup; - children = ( - 7AF6D1E82677A8410086EA64 /* FakeLocationManager.swift */, - ); - path = Mocks; - sourceTree = ""; - }; 7AF6D1F02677C03B0086EA64 /* AutoCatCore */ = { isa = PBXGroup; children = ( + 7A45FB362C2706D000618694 /* Services */, 7A761C06267E8E720005F28F /* ThirdParty */, 7AF6D2292677C3950086EA64 /* Extensions */, 7A11474523FF2A9000B424AF /* Models */, @@ -656,10 +850,9 @@ isa = PBXGroup; children = ( 7A43F9F7246C8A6200BA5B49 /* JWT.swift */, - 7A11474323FF06CA00B424AF /* Api.swift */, 7A96AE30246B2FE400297C33 /* Constants.swift */, 7A000AA124C2EEDE001F5B00 /* Location.swift */, - 7A0B663629984201006F5189 /* DateCache.swift */, + 7A64A2232C1A07EA00284124 /* Formatters.swift */, ); path = Utils; sourceTree = ""; @@ -674,6 +867,27 @@ path = Extensions; sourceTree = ""; }; + 7AFBE8C52C30812E003C491D /* SwiftUI */ = { + isa = PBXGroup; + children = ( + 7AFBE8C62C30814E003C491D /* ACProgressHud */, + ); + path = SwiftUI; + sourceTree = ""; + }; + 7AFBE8C62C30814E003C491D /* ACProgressHud */ = { + isa = PBXGroup; + children = ( + 7AFBE8BF2C3024E5003C491D /* ACHud.swift */, + 7AFBE8C32C302561003C491D /* ACHudContainer.swift */, + 7AFBE8C72C30816E003C491D /* ACProgressHud.swift */, + 7AFBE8C92C3081C7003C491D /* ACProgressHud+Modifiers.swift */, + 7AFBE8CB2C3085C6003C491D /* ACProgressView.swift */, + 7AFBE8CD2C308B53003C491D /* ACMessageView.swift */, + ); + path = ACProgressHud; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -708,27 +922,58 @@ 7AABDE1C2532F3EB0041AFC6 /* PKHUD */, 7A35177A27E23F8800DC538C /* Eureka */, 7AC355492969652F00889457 /* SwiftEntryKit */, + 7ADF23052C25B5BF002624FF /* RealmSwift */, ); productName = AutoCat; productReference = 7A1146FD23FDE7E500B424AF /* AutoCat.app */; productType = "com.apple.product-type.application"; }; - 7AF6D1DC2677A7E00086EA64 /* AutoCatTests */ = { + 7A2E6FA22C42B3AD00C40DA7 /* AutoCatCoreTests */ = { isa = PBXNativeTarget; - buildConfigurationList = 7AF6D1E62677A7E00086EA64 /* Build configuration list for PBXNativeTarget "AutoCatTests" */; + buildConfigurationList = 7A2E6FAC2C42B3AD00C40DA7 /* Build configuration list for PBXNativeTarget "AutoCatCoreTests" */; buildPhases = ( - 7AF6D1D92677A7E00086EA64 /* Sources */, - 7AF6D1DA2677A7E00086EA64 /* Frameworks */, - 7AF6D1DB2677A7E00086EA64 /* Resources */, + 7A2E6F9F2C42B3AD00C40DA7 /* Sources */, + 7A2E6FA02C42B3AD00C40DA7 /* Frameworks */, + 7A2E6FA12C42B3AD00C40DA7 /* Resources */, ); buildRules = ( ); dependencies = ( - 7AF6D1E32677A7E00086EA64 /* PBXTargetDependency */, + 7A2E6FA92C42B3AD00C40DA7 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7A2E6FA42C42B3AD00C40DA7 /* AutoCatCoreTests */, + ); + name = AutoCatCoreTests; + packageProductDependencies = ( + 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */, + ); + productName = AutoCatCoreTests; + productReference = 7A2E6FA32C42B3AD00C40DA7 /* AutoCatCoreTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 7AB587212C42D27F00FA7B66 /* AutoCatTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7AB587282C42D27F00FA7B66 /* Build configuration list for PBXNativeTarget "AutoCatTests" */; + buildPhases = ( + 7AB5871E2C42D27F00FA7B66 /* Sources */, + 7AB5871F2C42D27F00FA7B66 /* Frameworks */, + 7AB587202C42D27F00FA7B66 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 7AB587272C42D27F00FA7B66 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 7AB587232C42D27F00FA7B66 /* AutoCatTests */, ); name = AutoCatTests; + packageProductDependencies = ( + 7A176DB82C432F8800999D6B /* MockableTest */, + ); productName = AutoCatTests; - productReference = 7AF6D1DD2677A7E00086EA64 /* AutoCatTests.xctest */; + productReference = 7AB587222C42D27F00FA7B66 /* AutoCatTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; 7AF6D1EE2677C03B0086EA64 /* AutoCatCore */ = { @@ -739,6 +984,7 @@ 7AF6D1EB2677C03B0086EA64 /* Sources */, 7AF6D1EC2677C03B0086EA64 /* Frameworks */, 7AF6D1ED2677C03B0086EA64 /* Resources */, + 7AA363372C25B64A00851D6D /* Embed Frameworks */, ); buildRules = ( ); @@ -747,12 +993,9 @@ name = AutoCatCore; packageProductDependencies = ( 7AABB1F1267E9CC800D7AB32 /* SwiftDate */, - 7A1CF80229A41C62007962DA /* Realm */, 7A1CF80429A41C66007962DA /* RealmSwift */, - 7A1CF80729A41D58007962DA /* RxBlocking */, - 7A1CF80929A41D58007962DA /* RxCocoa */, - 7A1CF80B29A41D58007962DA /* RxRelay */, - 7A1CF80D29A41D58007962DA /* RxSwift */, + 7A8C4A5A2C1C55680052DDF3 /* SwiftLocation */, + 7A176DB62C432F8800999D6B /* Mockable */, ); productName = AutoCatCore; productReference = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; @@ -764,15 +1007,18 @@ 7A1146F523FDE7E500B424AF /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1250; + LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1130; ORGANIZATIONNAME = "Selim Mustafaev"; TargetAttributes = { 7A1146FC23FDE7E500B424AF = { CreatedOnToolsVersion = 11.3.1; }; - 7AF6D1DC2677A7E00086EA64 = { - CreatedOnToolsVersion = 12.5; + 7A2E6FA22C42B3AD00C40DA7 = { + CreatedOnToolsVersion = 16.0; + }; + 7AB587212C42D27F00FA7B66 = { + CreatedOnToolsVersion = 16.0; TestTargetID = 7A1146FC23FDE7E500B424AF; }; 7AF6D1EE2677C03B0086EA64 = { @@ -798,7 +1044,8 @@ 7A35177927E23F8800DC538C /* XCRemoteSwiftPackageReference "Eureka" */, 7AC355482969652F00889457 /* XCRemoteSwiftPackageReference "SwiftEntryKit" */, 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */, - 7A1CF80629A41D58007962DA /* XCRemoteSwiftPackageReference "RxSwift" */, + 7AF336F72C1C54EC002FB8A3 /* XCRemoteSwiftPackageReference "SwiftLocation" */, + 7A176DB52C432F8800999D6B /* XCRemoteSwiftPackageReference "Mockable" */, ); productRefGroup = 7A1146FE23FDE7E500B424AF /* Products */; projectDirPath = ""; @@ -806,7 +1053,8 @@ targets = ( 7A1146FC23FDE7E500B424AF /* AutoCat */, 7AF6D1EE2677C03B0086EA64 /* AutoCatCore */, - 7AF6D1DC2677A7E00086EA64 /* AutoCatTests */, + 7A2E6FA22C42B3AD00C40DA7 /* AutoCatCoreTests */, + 7AB587212C42D27F00FA7B66 /* AutoCatTests */, ); }; /* End PBXProject section */ @@ -816,7 +1064,6 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7A0420BA25693D2C00034941 /* dkbm.js in Resources */, 7A61FF912575A5B300D905D5 /* InfoPlist.strings in Resources */, 7A61FFA0257D3CFC00D905D5 /* Localizable.stringsdict in Resources */, 7ADF6C99250F872C00F237B2 /* RoadNumbers.otf in Resources */, @@ -828,7 +1075,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 7AF6D1DB2677A7E00086EA64 /* Resources */ = { + 7A2E6FA12C42B3AD00C40DA7 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7AB587202C42D27F00FA7B66 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( @@ -850,90 +1104,105 @@ buildActionMask = 2147483647; files = ( 7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */, + 7A7158092C44087E00852088 /* OsagoCoordinator.swift in Sources */, + 7A1441662C297EDE00E79018 /* NotesScreen.swift in Sources */, 7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */, - 7ADF6C9D250FA96000F237B2 /* SwiftMaskTextfield.swift in Sources */, 7A813DC9250B5C9700CC93B9 /* LocationRow.swift in Sources */, 7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */, 7A2DE69B25869ABD00A113FC /* AdsController.swift in Sources */, + 7A14416E2C297F7C00E79018 /* Coordinator.swift in Sources */, 7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */, 7A99406426E4BFAE002E9CB6 /* VehicleNoteCell.swift in Sources */, 7A8AB76B25A1D95500ECF2C1 /* SourceStatusRow.swift in Sources */, + 7AFBE8C82C30816E003C491D /* ACProgressHud.swift in Sources */, 7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */, 7A813DC32508EE4F00CC93B9 /* EventCell.swift in Sources */, + 7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */, + 7AFBE8C02C3024E5003C491D /* ACHud.swift in Sources */, 7A3F07AD2436350B00E59687 /* SearchController.swift in Sources */, 7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */, - 7A0B96A0257D6D4B000B39AD /* MultilineLabelRow.swift in Sources */, 7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */, 7A761C0B267E8FF90005F28F /* Error.swift in Sources */, 7AC3555029696D5A00889457 /* NewNumberController.swift in Sources */, 7AE26A3524F31B0700625033 /* EventsController.swift in Sources */, 7A2DE69E2589606A00A113FC /* ImageGridRow.swift in Sources */, 7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */, - 7A0420B62568650C00034941 /* DkbmController.swift in Sources */, 7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */, 7A17CE4A2A2E820300626A6E /* UIStackView.swift in Sources */, 7A1DC38E2517ED98002E9C99 /* BlockBarButtonItem.swift in Sources */, 7AE26A3324EEF9EC00625033 /* UIViewControllerExt.swift in Sources */, 7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */, + 7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */, 7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */, 7ADF6CA12512244400F237B2 /* MapExt.swift in Sources */, 7AC3554E29696C4500889457 /* DummyNewController.swift in Sources */, 7A659B5B24A3768A0043A0F2 /* Substrings.swift in Sources */, 7AEFE728240455E200910EB7 /* SettingsController.swift in Sources */, + 7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */, 7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */, + 7A7158072C44085600852088 /* OsagoScreen.swift in Sources */, 7A3F07AB24360DC800E59687 /* Dated.swift in Sources */, 7A33381124990DAE00D878F1 /* FiltersController.swift in Sources */, 7AC76D7B270083AE0084DB27 /* TextView.swift in Sources */, 7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */, + 7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */, 7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */, 7A11471823FDEBFA00B424AF /* ReportController.swift in Sources */, 7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */, 7A11471A23FE839000B424AF /* AuthController.swift in Sources */, 7A530B7A24001D3300CBFE6E /* CheckController.swift in Sources */, - 7A6F095E26DB9F85003A965D /* NotesController.swift in Sources */, - 7A6E03282485951700DB22ED /* OwnersController.swift in Sources */, 7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */, 7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */, 7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */, + 7A176DB22C43071A00999D6B /* ApiServiceStub.swift in Sources */, 7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */, - 7A0420B12561A0E100034941 /* OsagoAddController.swift in Sources */, 7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */, 7A813DC5250AAF3C00CC93B9 /* LocationEditController.swift in Sources */, 7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */, 7ADF6C9F251201D200F237B2 /* GlobalEventsController.swift in Sources */, 7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */, 7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */, + 7AFBE8CE2C308B53003C491D /* ACMessageView.swift in Sources */, + 7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */, 7A813DCB250B5DC900CC93B9 /* LocationPickerController.swift in Sources */, 7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */, 7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */, 7AC3555229696E3F00889457 /* UIView+layout.swift in Sources */, - 7A0420AD2561A0B100034941 /* OsagoController.swift in Sources */, 7AC355592969746600889457 /* UIControl.swift in Sources */, 7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */, - 7A21112A24FC3D7E003BBF6F /* AudioEngine.swift in Sources */, 7AC35554296973E100889457 /* ACButton.swift in Sources */, 7A8A220B248D67B60073DFD9 /* VehicleReportImage.swift in Sources */, + 7AFBE8C42C302561003C491D /* ACHudContainer.swift in Sources */, 7AC3555B296995B200889457 /* UIEdgeInsets.swift in Sources */, + 7A7158002C43EA6900852088 /* OwnersScreen.swift in Sources */, + 7A1441702C2998B200E79018 /* Formatters.swift in Sources */, 7ADF6C95250D037700F237B2 /* ShowEventController.swift in Sources */, 7A91894F29A2BD8700519C74 /* GestureRecognizers.swift in Sources */, 7A27ADC7249D43210035F39E /* RegionsController.swift in Sources */, - 7AE492A1259232F000322D2E /* MultilineLinkRow.swift in Sources */, + 7AFBE8CC2C3085C6003C491D /* ACProgressView.swift in Sources */, 7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */, + 7AB587372C42E3EC00FA7B66 /* StorageServiceStub.swift in Sources */, 7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */, 7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */, 7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */, + 7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */, 7A7547E024032CB6004E8406 /* VehiclePhotoCell.swift in Sources */, 7A17CE4C2A2E850200626A6E /* UISegmentedControl.swift in Sources */, 6841A85D4B60DB71D1E68DA0 /* ImageGrid.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; - 7AF6D1D92677A7E00086EA64 /* Sources */ = { + 7A2E6F9F2C42B3AD00C40DA7 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 7AB5871E2C42D27F00FA7B66 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 7AF6D1E02677A7E00086EA64 /* AutoCatTests.swift in Sources */, - 7AF6D1E92677A8410086EA64 /* FakeLocationManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -941,29 +1210,55 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */, + 7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */, 7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */, 7A761C08267E8EA20005F28F /* JWT.swift in Sources */, + 7A64A2242C1A07EA00284124 /* Formatters.swift in Sources */, + 7A5D84C02C1AE4DC00C2209B /* VehicleEngine.swift in Sources */, 7AF6D2282677C2DC0086EA64 /* Constants.swift in Sources */, + 7A64A2182C19E64800284124 /* VehicleOwnershipPeriodDto.swift in Sources */, + 7A599C3B2C18B36A00D47C18 /* FbVerifyTokenModel.swift in Sources */, + 7A64A2162C19E4CF00284124 /* VehiclePhotoDto.swift in Sources */, + 7A5D84BE2C1AE44700C2209B /* VehiclePhoto.swift in Sources */, + 7A64A2262C1A32C800284124 /* AudioRecordDto.swift in Sources */, 7A761C09267E8EE40005F28F /* Base64FS.swift in Sources */, + 7A64A21E2C19E8D500284124 /* VehicleAdDto.swift in Sources */, 7AF6D2202677C1680086EA64 /* Filter.swift in Sources */, - 7A761C042677F18E0005F28F /* Api.swift in Sources */, + 7A761C042677F18E0005F28F /* ApiService.swift in Sources */, 7AF6D21C2677C1680086EA64 /* DebugInfo.swift in Sources */, 7AF6D2122677C12E0086EA64 /* Location.swift in Sources */, 7AF6D2142677C1680086EA64 /* VehicleEvent.swift in Sources */, + 7A64A2102C19E1EB00284124 /* VehicleBrandDto.swift in Sources */, + 7A599C392C18B22900D47C18 /* FbRefreshTokenModel.swift in Sources */, 7AF6D2172677C1680086EA64 /* VehicleRegion.swift in Sources */, 7A6F096026DBF588003A965D /* VehicleNote.swift in Sources */, 7AF6D21E2677C1680086EA64 /* PlateNumber.swift in Sources */, + 7A5D84C62C1AE72E00C2209B /* VehicleName.swift in Sources */, + 7A64A2122C19E2A100284124 /* VehicleModelDto.swift in Sources */, 7AF6D21F2677C1680086EA64 /* Response.swift in Sources */, 7A761C07267E8E7F0005F28F /* AnyEncodable.swift in Sources */, - 7AF6D2162677C1680086EA64 /* Cloneable.swift in Sources */, + 7A64A2032C19DA1000284124 /* VehicleDto.swift in Sources */, + 7AB587322C42D38E00FA7B66 /* StorageServiceProtocol.swift in Sources */, + 7A64A2222C19E99E00284124 /* DebugInfoDto.swift in Sources */, + 7A5D84BC2C1AD81000C2209B /* VehicleOwnershipPeriod.swift in Sources */, + 7A64A2202C19E93500284124 /* VehicleNoteDto.swift in Sources */, 7AF6D21A2677C1680086EA64 /* User.swift in Sources */, - 7A0B663729984201006F5189 /* DateCache.swift in Sources */, + 7A64A21C2C19E87B00284124 /* OsagoDto.swift in Sources */, 7AF6D21D2677C1680086EA64 /* Osago.swift in Sources */, 7AF6D2152677C1680086EA64 /* Settings.swift in Sources */, 7A1CF81629A42117007962DA /* Realm.swift in Sources */, + 7A64A2142C19E3B700284124 /* VehicleEngineDto.swift in Sources */, 7A761C052677F1BC0005F28F /* CocoaError.swift in Sources */, 7AF6D2132677C15A0086EA64 /* AudioRecord.swift in Sources */, + 7A64A21A2C19E6B300284124 /* VehicleEventDto.swift in Sources */, + 7AB587342C42D3FA00FA7B66 /* StorageService+Notes.swift in Sources */, 7AF6D21B2677C1680086EA64 /* Vehicle.swift in Sources */, + 7AB587412C42FFE200FA7B66 /* ApiServiceProtocol.swift in Sources */, + 7A5D84C42C1AE65C00C2209B /* VehicleBrand.swift in Sources */, + 7A599C362C18AC7F00D47C18 /* ApiError.swift in Sources */, + 7A45FB382C27073700618694 /* StorageService.swift in Sources */, + 7A64A20A2C19E07100284124 /* VehicleNameDto.swift in Sources */, 7AF6D22A2677C3AD0086EA64 /* Exportable.swift in Sources */, 7AF6D2212677C1680086EA64 /* PagedResponse.swift in Sources */, 7AF6D2192677C1680086EA64 /* DateSection.swift in Sources */, @@ -973,10 +1268,15 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ - 7AF6D1E32677A7E00086EA64 /* PBXTargetDependency */ = { + 7A2E6FA92C42B3AD00C40DA7 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 7AF6D1EE2677C03B0086EA64 /* AutoCatCore */; + targetProxy = 7A2E6FA82C42B3AD00C40DA7 /* PBXContainerItemProxy */; + }; + 7AB587272C42D27F00FA7B66 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 7A1146FC23FDE7E500B424AF /* AutoCat */; - targetProxy = 7AF6D1E22677A7E00086EA64 /* PBXContainerItemProxy */; + targetProxy = 7AB587262C42D27F00FA7B66 /* PBXContainerItemProxy */; }; 7AF6D2032677C03B0086EA64 /* PBXTargetDependency */ = { isa = PBXTargetDependency; @@ -1154,7 +1454,6 @@ 7A11471223FDE7E600B424AF /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; @@ -1163,8 +1462,7 @@ INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1174,7 +1472,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; - SWIFT_VERSION = 5.0; + SWIFT_VERSION = 6.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; @@ -1182,7 +1480,6 @@ 7A11471323FDE7E600B424AF /* Release */ = { isa = XCBuildConfiguration; buildSettings = { - ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_ENTITLEMENTS = AutoCat/AutoCat.entitlements; CODE_SIGN_STYLE = Automatic; @@ -1191,8 +1488,7 @@ INFOPLIST_FILE = AutoCat/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = AutoCat; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - "IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1202,50 +1498,106 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTS_MACCATALYST = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 7A2E6FAA2C42B3AD00C40DA7 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 46DTTB8X4S; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7A2E6FAB2C42B3AD00C40DA7 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 46DTTB8X4S; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; name = Release; }; - 7AF6D1E42677A7E00086EA64 /* Debug */ = { + 7AB587292C42D27F00FA7B66 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = AutoCatTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 46DTTB8X4S; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/AutoCat"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat"; }; name = Debug; }; - 7AF6D1E52677A7E00086EA64 /* Release */ = { + 7AB5872A2C42D27F00FA7B66 /* Release */ = { isa = XCBuildConfiguration; buildSettings = { + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CODE_SIGN_STYLE = Automatic; - INFOPLIST_FILE = AutoCatTests/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 14.5; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@loader_path/Frameworks", - ); + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 46DTTB8X4S; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/AutoCat"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat"; }; name = Release; }; @@ -1262,7 +1614,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AutoCatCore/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1271,6 +1623,7 @@ PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCore; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MOCKING"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1291,7 +1644,7 @@ DYLIB_INSTALL_NAME_BASE = "@rpath"; INFOPLIST_FILE = AutoCatCore/Info.plist; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -1328,11 +1681,20 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 7AF6D1E62677A7E00086EA64 /* Build configuration list for PBXNativeTarget "AutoCatTests" */ = { + 7A2E6FAC2C42B3AD00C40DA7 /* Build configuration list for PBXNativeTarget "AutoCatCoreTests" */ = { isa = XCConfigurationList; buildConfigurations = ( - 7AF6D1E42677A7E00086EA64 /* Debug */, - 7AF6D1E52677A7E00086EA64 /* Release */, + 7A2E6FAA2C42B3AD00C40DA7 /* Debug */, + 7A2E6FAB2C42B3AD00C40DA7 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7AB587282C42D27F00FA7B66 /* Build configuration list for PBXNativeTarget "AutoCatTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7AB587292C42D27F00FA7B66 /* Debug */, + 7AB5872A2C42D27F00FA7B66 /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; @@ -1357,6 +1719,14 @@ minimumVersion = 6.1.0; }; }; + 7A176DB52C432F8800999D6B /* XCRemoteSwiftPackageReference "Mockable" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Kolos65/Mockable"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.9; + }; + }; 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/realm/realm-swift.git"; @@ -1365,14 +1735,6 @@ minimumVersion = 10.36.0; }; }; - 7A1CF80629A41D58007962DA /* XCRemoteSwiftPackageReference "RxSwift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/ReactiveX/RxSwift.git"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 6.0.0; - }; - }; 7A35177927E23F8800DC538C /* XCRemoteSwiftPackageReference "Eureka" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/xmartlabs/Eureka"; @@ -1405,47 +1767,40 @@ minimumVersion = 2.0.0; }; }; + 7AF336F72C1C54EC002FB8A3 /* XCRemoteSwiftPackageReference "SwiftLocation" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/malcommac/SwiftLocation.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 6.0.0; + }; + }; 7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/onevcat/Kingfisher"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 5.13.1; + branch = "8.0.0-alpha.1"; + kind = branch; }; }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ - 7A1CF80229A41C62007962DA /* Realm */ = { + 7A176DB62C432F8800999D6B /* Mockable */ = { isa = XCSwiftPackageProductDependency; - package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; - productName = Realm; + package = 7A176DB52C432F8800999D6B /* XCRemoteSwiftPackageReference "Mockable" */; + productName = Mockable; + }; + 7A176DB82C432F8800999D6B /* MockableTest */ = { + isa = XCSwiftPackageProductDependency; + package = 7A176DB52C432F8800999D6B /* XCRemoteSwiftPackageReference "Mockable" */; + productName = MockableTest; }; 7A1CF80429A41C66007962DA /* RealmSwift */ = { isa = XCSwiftPackageProductDependency; package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; productName = RealmSwift; }; - 7A1CF80729A41D58007962DA /* RxBlocking */ = { - isa = XCSwiftPackageProductDependency; - package = 7A1CF80629A41D58007962DA /* XCRemoteSwiftPackageReference "RxSwift" */; - productName = RxBlocking; - }; - 7A1CF80929A41D58007962DA /* RxCocoa */ = { - isa = XCSwiftPackageProductDependency; - package = 7A1CF80629A41D58007962DA /* XCRemoteSwiftPackageReference "RxSwift" */; - productName = RxCocoa; - }; - 7A1CF80B29A41D58007962DA /* RxRelay */ = { - isa = XCSwiftPackageProductDependency; - package = 7A1CF80629A41D58007962DA /* XCRemoteSwiftPackageReference "RxSwift" */; - productName = RxRelay; - }; - 7A1CF80D29A41D58007962DA /* RxSwift */ = { - isa = XCSwiftPackageProductDependency; - package = 7A1CF80629A41D58007962DA /* XCRemoteSwiftPackageReference "RxSwift" */; - productName = RxSwift; - }; 7A35177A27E23F8800DC538C /* Eureka */ = { isa = XCSwiftPackageProductDependency; package = 7A35177927E23F8800DC538C /* XCRemoteSwiftPackageReference "Eureka" */; @@ -1456,6 +1811,11 @@ package = 7A813DBF2508C4D900CC93B9 /* XCRemoteSwiftPackageReference "ExceptionCatcher" */; productName = ExceptionCatcher; }; + 7A8C4A5A2C1C55680052DDF3 /* SwiftLocation */ = { + isa = XCSwiftPackageProductDependency; + package = 7AF336F72C1C54EC002FB8A3 /* XCRemoteSwiftPackageReference "SwiftLocation" */; + productName = SwiftLocation; + }; 7AABB1F1267E9CC800D7AB32 /* SwiftDate */ = { isa = XCSwiftPackageProductDependency; package = 7A05160F241412CA00FC55AC /* XCRemoteSwiftPackageReference "SwiftDate" */; @@ -1466,11 +1826,21 @@ package = 7AABDE1B2532F3EB0041AFC6 /* XCRemoteSwiftPackageReference "PKHUD" */; productName = PKHUD; }; + 7AB5871C2C42C1CF00FA7B66 /* RealmSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; + productName = RealmSwift; + }; 7AC355492969652F00889457 /* SwiftEntryKit */ = { isa = XCSwiftPackageProductDependency; package = 7AC355482969652F00889457 /* XCRemoteSwiftPackageReference "SwiftEntryKit" */; productName = SwiftEntryKit; }; + 7ADF23052C25B5BF002624FF /* RealmSwift */ = { + isa = XCSwiftPackageProductDependency; + package = 7A1CF7FD29A41C2F007962DA /* XCRemoteSwiftPackageReference "realm-swift" */; + productName = RealmSwift; + }; 7AF58D332402A91C00CE01A0 /* Kingfisher */ = { isa = XCSwiftPackageProductDependency; package = 7AF58D322402A91C00CE01A0 /* XCRemoteSwiftPackageReference "Kingfisher" */; diff --git a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 7238c22..d2d699c 100644 --- a/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AutoCat.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,13 @@ { + "originHash" : "ae480e95b5199c1834610abd8aa26a821694aa5167d41c0aceddc8513e267012", "pins" : [ { "identity" : "eureka", "kind" : "remoteSourceControl", "location" : "https://github.com/xmartlabs/Eureka", "state" : { - "revision" : "b6e35acf77a5551070afa6248935ec68e71f22af", - "version" : "5.4.0" + "revision" : "028ef8e3191a256b8f6b8bb6b9496efcb0762dbc", + "version" : "5.5.0" } }, { @@ -23,8 +24,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/onevcat/Kingfisher", "state" : { - "revision" : "1a0c2df04b31ed7aa318354f3583faea24f006fc", - "version" : "5.15.8" + "branch" : "8.0.0-alpha.1", + "revision" : "bb4e6ecf6c7a221dfc51c8e69f04fd3757fc519a" + } + }, + { + "identity" : "mockable", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Kolos65/Mockable", + "state" : { + "revision" : "81ccaead99a3c038c09345caa2888ae74b644ee9", + "version" : "0.0.9" } }, { @@ -41,8 +51,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-core.git", "state" : { - "revision" : "dd91f5f967c4ae89c37e24ab2a0315c31106648f", - "version" : "13.6.0" + "revision" : "f3d7ae5f9f31d90b327a64536bb7801cc69fd85b", + "version" : "14.9.0" } }, { @@ -50,17 +60,17 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/realm/realm-swift.git", "state" : { - "revision" : "8ac6fe1aa5d0fb0100062d80863416a4d70de8ca", - "version" : "10.37.0" + "revision" : "4c4413abd0cd2221f59318f800960fe5bddc1494", + "version" : "10.51.0" } }, { - "identity" : "rxswift", + "identity" : "swift-syntax", "kind" : "remoteSourceControl", - "location" : "https://github.com/ReactiveX/RxSwift.git", + "location" : "https://github.com/apple/swift-syntax.git", "state" : { - "revision" : "b4307ba0b6425c0ba4178e138799946c3da594f8", - "version" : "6.5.0" + "revision" : "303e5c5c36d6a558407d364878df131c3546fad8", + "version" : "510.0.2" } }, { @@ -80,7 +90,16 @@ "revision" : "5ad36cccf0c4b9fea32f4e9b17a8e38f07563ef0", "version" : "2.0.0" } + }, + { + "identity" : "swiftlocation", + "kind" : "remoteSourceControl", + "location" : "https://github.com/malcommac/SwiftLocation.git", + "state" : { + "revision" : "010073e62cea4daefea61042a51b8619d23cdc35", + "version" : "6.0.0" + } } ], - "version" : 2 + "version" : 3 } diff --git a/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme b/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme index 7a98d5f..579b984 100644 --- a/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme +++ b/AutoCat.xcodeproj/xcshareddata/xcschemes/AutoCat.xcscheme @@ -32,7 +32,7 @@ skipped = "NO"> @@ -42,7 +42,7 @@ skipped = "NO"> diff --git a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist index 897aeb7..a83a6d8 100644 --- a/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +++ b/AutoCat.xcodeproj/xcuserdata/selim.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist @@ -20,16 +20,16 @@ diff --git a/AutoCat/ACUIKit/Extensions/GestureRecognizers.swift b/AutoCat/ACUIKit/Extensions/GestureRecognizers.swift index 75e03f6..e495ff1 100644 --- a/AutoCat/ACUIKit/Extensions/GestureRecognizers.swift +++ b/AutoCat/ACUIKit/Extensions/GestureRecognizers.swift @@ -13,7 +13,7 @@ extension UIGestureRecognizer { typealias Action = ((UIGestureRecognizer) -> ()) private struct Keys { - static var actionKey = "ActionKey" + @MainActor static var actionKey = "ActionKey" } private var block: Action? { diff --git a/AutoCat/ACUIKit/Extensions/UISegmentedControl.swift b/AutoCat/ACUIKit/Extensions/UISegmentedControl.swift index 92826d7..4baf7ac 100644 --- a/AutoCat/ACUIKit/Extensions/UISegmentedControl.swift +++ b/AutoCat/ACUIKit/Extensions/UISegmentedControl.swift @@ -22,7 +22,7 @@ extension UISegmentedControl { return view } - func onValueChanged(_ closure: @escaping (Int) -> Void) -> UISegmentedControl { + func onValueChanged(_ closure: @MainActor @escaping (Int) -> Void) -> UISegmentedControl { addActionImpl(for: .valueChanged) { [weak self] in guard let index = self?.selectedSegmentIndex else { return diff --git a/AutoCat/ACUIKit/Views/ACButton.swift b/AutoCat/ACUIKit/Views/ACButton.swift index fd07940..6565e09 100644 --- a/AutoCat/ACUIKit/Views/ACButton.swift +++ b/AutoCat/ACUIKit/Views/ACButton.swift @@ -9,7 +9,7 @@ class ACButton: UIButton { private var style: ACButtonStyle = .generic - convenience init(style: ACButtonStyle = .roundedBlue, title: String, onTap: @escaping () -> Void) { + convenience init(style: ACButtonStyle = .roundedBlue, title: String, onTap: @MainActor @escaping () -> Void) { self.init() self.style(style) self.onTap(onTap) diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index 9d31720..ca5dcc7 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -1,24 +1,17 @@ import UIKit import RealmSwift -import RxSwift -import RxCocoa -import os.log import PKHUD import AutoCatCore -extension OSLog { - static let startup = OSLog(subsystem: "pro.aliencat.autocat.startup", category: "startup") -} - enum QuickAction { case none case check - case checkNumber(String, VehicleEvent?) + case checkNumber(String, VehicleEventDto?) case addVoiceRecord case openReport(String) } -@UIApplicationMain +@main class AppDelegate: UIResponder, UIApplicationDelegate { var quickAction: QuickAction = .none diff --git a/AutoCat/Base.lproj/Main.storyboard b/AutoCat/Base.lproj/Main.storyboard index 60bcfe9..00b7dc3 100644 --- a/AutoCat/Base.lproj/Main.storyboard +++ b/AutoCat/Base.lproj/Main.storyboard @@ -1,9 +1,9 @@ - + - + @@ -42,21 +42,6 @@ - - - - - - - - - - - - - - - @@ -72,94 +57,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -169,21 +66,21 @@ - + - + - + - + - + - + @@ -274,7 +171,7 @@ - + @@ -297,7 +194,7 @@ - + @@ -598,7 +495,7 @@ - + @@ -621,7 +518,7 @@ - + @@ -734,7 +631,7 @@ - + @@ -742,7 +639,7 @@ - + @@ -865,19 +762,6 @@ - - @@ -890,7 +774,6 @@ - @@ -975,7 +858,7 @@ - + @@ -994,7 +877,7 @@ - + @@ -1012,7 +895,7 @@ - + @@ -1031,7 +914,7 @@ - + @@ -1049,7 +932,7 @@ - + @@ -1063,13 +946,13 @@ - + - + - + @@ -1077,7 +960,7 @@ - + diff --git a/AutoCat/Cells/AudioRecordCell.swift b/AutoCat/Cells/AudioRecordCell.swift index c9575de..178447f 100644 --- a/AutoCat/Cells/AudioRecordCell.swift +++ b/AutoCat/Cells/AudioRecordCell.swift @@ -1,5 +1,4 @@ import UIKit -import RxSwift import PKHUD import AutoCatCore @@ -13,10 +12,8 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell { let dateFormatter = DateFormatter() let componentsFormatter = DateComponentsFormatter() - var stateDisposable: Disposable? - var progressDisposable: Disposable? - var record: AudioRecord? + var record: AudioRecordDto? override func awakeFromNib() { super.awakeFromNib() @@ -34,39 +31,38 @@ class AudioRecordCell: UITableViewCell, ConfigurableCell { override func prepareForReuse() { super.prepareForReuse() self.record = nil - self.stateDisposable?.dispose() - self.progressDisposable?.dispose() self.progressView.progress = 0 } - func configure(with record: AudioRecord) { + func configure(with record: AudioRecordDto) { self.record = record self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: record.getAddedDate())) 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 - }) + // TODO: Fix player +// 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) { diff --git a/AutoCat/Cells/ConfigurableCell.swift b/AutoCat/Cells/ConfigurableCell.swift index d2eb25a..413a68d 100644 --- a/AutoCat/Cells/ConfigurableCell.swift +++ b/AutoCat/Cells/ConfigurableCell.swift @@ -1,5 +1,6 @@ import UIKit +@MainActor protocol ConfigurableCell { associatedtype Item func configure(with item: Item) diff --git a/AutoCat/Cells/EventCell.swift b/AutoCat/Cells/EventCell.swift index 2a6afd3..db221fb 100644 --- a/AutoCat/Cells/EventCell.swift +++ b/AutoCat/Cells/EventCell.swift @@ -15,7 +15,7 @@ class EventCell: UITableViewCell { self.dateFormatter.timeStyle = .short } - func configure(with event: VehicleEvent) { + func configure(with event: VehicleEventDto) { if let addressString = event.address { self.address.text = addressString } else { diff --git a/AutoCat/Cells/VehicleCell.swift b/AutoCat/Cells/VehicleCell.swift index 1135941..634f6d3 100644 --- a/AutoCat/Cells/VehicleCell.swift +++ b/AutoCat/Cells/VehicleCell.swift @@ -20,7 +20,7 @@ class VehicleCell: UITableViewCell, ConfigurableCell { formatter.timeStyle = .short } - func configure(with vehicle: Vehicle) { + func configure(with vehicle: VehicleDto) { self.name.text = vehicle.brand?.name?.original ?? "" self.plate.number = PlateNumber(vehicle.getNumber()) self.plate.fontSize = 40 diff --git a/AutoCat/Cells/VehicleNoteCell.swift b/AutoCat/Cells/VehicleNoteCell.swift index e65be24..bc8d9e4 100644 --- a/AutoCat/Cells/VehicleNoteCell.swift +++ b/AutoCat/Cells/VehicleNoteCell.swift @@ -14,7 +14,7 @@ class VehicleNoteCell: UITableViewCell { self.dateFormatter.timeStyle = .medium } - func configure(with note: VehicleNote) { + func configure(with note: VehicleNoteDto) { self.noteText.text = note.text self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: note.date)) } diff --git a/AutoCat/Controllers/AdsController.swift b/AutoCat/Controllers/AdsController.swift index 9282cd7..a5c90db 100644 --- a/AutoCat/Controllers/AdsController.swift +++ b/AutoCat/Controllers/AdsController.swift @@ -6,8 +6,8 @@ import AutoCatCore class AdsController: FormViewController, MediaBrowserViewControllerDataSource { - var ads: [VehicleAd] = [] - private var currentAd: VehicleAd? + var ads: [VehicleAdDto] = [] + private var currentAd: VehicleAdDto? override func viewDidLoad() { super.viewDidLoad() @@ -52,27 +52,27 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource { } } - if let description = ad.adDescription, !description.isEmpty { - section <<< MultilineLabelRow() { row in - row.title = NSLocalizedString("Description", comment: "") - row.value = description - } - } - - if let urlStr = ad.url, let url = URL(string: urlStr) { - section <<< MultilineLinkRow() { row in - row.title = NSLocalizedString("Link", comment: "") - row.value = urlStr - } - .onCellSelection { _, _ in - let safari = SFSafariViewController(url: url) - self.present(safari, animated: true) - } - } +// if let description = ad.adDescription, !description.isEmpty { +// section <<< MultilineLabelRow() { row in +// row.title = NSLocalizedString("Description", comment: "") +// row.value = description +// } +// } +// +// if let urlStr = ad.url, let url = URL(string: urlStr) { +// section <<< MultilineLinkRow() { row in +// row.title = NSLocalizedString("Link", comment: "") +// row.value = urlStr +// } +// .onCellSelection { _, _ in +// let safari = SFSafariViewController(url: url) +// self.present(safari, animated: true) +// } +// } if !ad.photos.isEmpty { section <<< ImageGridRow() { row in - row.value = ad.photos.toArray() + row.value = ad.photos } .onDidSelected { index in self.currentAd = ad @@ -98,13 +98,16 @@ class AdsController: FormViewController, MediaBrowserViewControllerDataSource { } KingfisherManager.shared.retrieveImage(with: url) { result in - switch result { - case .success(let res): - completion(index, res.image, ZoomScale.default, nil) - break - case .failure(let error): - completion(index, nil, ZoomScale.default, error) - break + + Task { @MainActor in + switch result { + case .success(let res): + completion(index, res.image, ZoomScale.default, nil) + break + case .failure(let error): + completion(index, nil, ZoomScale.default, error) + break + } } } } diff --git a/AutoCat/Controllers/AuthController.swift b/AutoCat/Controllers/AuthController.swift index e6a0bb9..314532c 100644 --- a/AutoCat/Controllers/AuthController.swift +++ b/AutoCat/Controllers/AuthController.swift @@ -1,33 +1,27 @@ import UIKit -import RxSwift -import RxCocoa import RealmSwift import AuthenticationServices import PKHUD import AutoCatCore -class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAuthorizationControllerPresentationContextProviding { +class AuthController: UIViewController { @IBOutlet weak var username: UITextField! @IBOutlet weak var password: UITextField! @IBOutlet weak var login: UIButton! @IBOutlet weak var signup: UIButton! - @IBOutlet weak var appleSignIn: ASAuthorizationAppleIDButton! - - let bag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() - - self.appleSignIn.cornerRadius = 6 - let authValid = Observable.combineLatest(self.username.rx.text, self.password.rx.text) { name, pass -> Bool in - guard let name = name, let pass = pass else { return false } - return name.count >= 4 && pass.count >= 5 - } - - authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag) - authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag) + // FIX login/password lengt checking +// let authValid = Observable.combineLatest(self.username.rx.text, self.password.rx.text) { name, pass -> Bool in +// guard let name = name, let pass = pass else { return false } +// return name.count >= 4 && pass.count >= 5 +// } +// +// authValid.bind(to: self.login.rx.isEnabled).disposed(by: self.bag) +// authValid.bind(to: self.signup.rx.isEnabled).disposed(by: self.bag) if Settings.shared.user.email.count > 0 { self.username.text = Settings.shared.user.email @@ -37,32 +31,29 @@ class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAut @IBAction func loginTapped(_ sender: UIButton) { guard let email = self.username.text, let pass = self.password.text else { return } - HUD.show(.progress) - Api.login(email: email, password: pass) - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:)) - .disposed(by: self.bag) + Task { + do { + HUD.show(.progress) + let user = try await ApiService.shared.login(email: email, password: pass) + self.goToMainScreen(user: user) + } catch { + HUD.show(error: error) + } + } } @IBAction func signupTapped(_ sender: UIButton) { guard let email = self.username.text, let pass = self.password.text else { return } - HUD.show(.progress) - Api.signUp(email: email, password: pass) - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:)) - .disposed(by: self.bag) - } - - @IBAction func appleSignInTapped(_ sender: ASAuthorizationAppleIDButton) { - let appleIDProvider = ASAuthorizationAppleIDProvider() - let request = appleIDProvider.createRequest() - request.requestedScopes = [.email] - - let authorizationController = ASAuthorizationController(authorizationRequests: [request]) - authorizationController.delegate = self - authorizationController.presentationContextProvider = self - authorizationController.performRequests() + Task { + do { + HUD.show(.progress) + let user = try await ApiService.shared.signUp(email: email, password: pass) + self.goToMainScreen(user: user) + } catch { + HUD.show(error: error) + } + } } func goToMainScreen(user: AutoCatCore.User) { @@ -83,38 +74,4 @@ class AuthController: UIViewController, ASAuthorizationControllerDelegate, ASAut let storyboard = UIStoryboard(name: "Main", bundle: nil) self.view.window?.rootViewController = storyboard.instantiateViewController(identifier: "MainSplitController") } - - // MARK: - Apple SignIn - - func presentationAnchor(for controller: ASAuthorizationController) -> ASPresentationAnchor { - return self.view.window! - } - - func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) { - switch authorization.credential { - case let appleIDCredential as ASAuthorizationAppleIDCredential: - guard let email = appleIDCredential.email else { - HUD.flash(.labeledError(title: nil, subtitle: "Cannot get email")) - return - } - - HUD.show(.progress) - Api.signIn(email: email, password: appleIDCredential.user) - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: self.goToMainScreen(user:), onError: HUD.show(error:)) - .disposed(by: self.bag) - - if let tokenData = appleIDCredential.identityToken { - let token = String(data: tokenData, encoding: .utf8) ?? "" - _ = Api.fbVerifyAssertion(provider: "apple.com", idToken: token).subscribe(onSuccess: { _ in - print("") - }, onError: { error in - print(error) - }) - } - default: - HUD.flash(.labeledError(title: nil, subtitle: "Unsupported authorization credential")) - break - } - } } diff --git a/AutoCat/Controllers/CheckController.swift b/AutoCat/Controllers/CheckController.swift index b10ccbe..849c3ff 100644 --- a/AutoCat/Controllers/CheckController.swift +++ b/AutoCat/Controllers/CheckController.swift @@ -1,10 +1,10 @@ import UIKit import RealmSwift -import RxSwift import SwiftDate import PKHUD import CoreLocation import AutoCatCore +import SwiftLocation enum EventAction: Equatable { case doNotSend @@ -30,7 +30,6 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd @IBOutlet weak var history: UITableView! - private let bag = DisposeBag() private var historyDataSource: RealmSectionedDataSource! private var historyFilter: HistoryFilter = .all @@ -69,12 +68,12 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - self.handleQuickActions() + Task { await self.handleQuickActions() } } // MARK: - - func handleQuickActions() { + func handleQuickActions() async { guard let ad = UIApplication.shared.delegate as? AppDelegate else { return } switch ad.quickAction { @@ -87,24 +86,23 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd case .checkNumber(let number, let event): ad.quickAction = .none var action: EventAction = .receiveAndSend - var events: [VehicleEvent] = [] + var events: [VehicleEventDto] = [] if let event = event { events = [event] action = .doNotSend } - HUD.show(.progress) - self.check(number: number, action: action, notes: [], events: events).subscribe { (vehicle, errors) in + do { + HUD.show(.progress) + let (vehicle, errors) = try await self.check(number: number, action: action, notes: [], events: events) if !vehicle.unrecognized { self.updateDetailController(with: vehicle) } HUD.hide() self.showErrors(errors) - } onFailure: { error in + } catch { HUD.hide() self.show(error: error) - //HUD.show(error: error) } - .disposed(by: self.bag) break case .addVoiceRecord: self.tabBarController?.selectedIndex = 1 @@ -112,7 +110,7 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd case .openReport(let number): ad.quickAction = .none if let sd = self.view.window?.windowScene?.delegate as? SceneDelegate { - sd.openReport(with: number) + Task { await sd.openReport(with: number) } } break default: @@ -218,32 +216,36 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd func checkTapped(number: String) { let numberNormalized = number.filter { !$0.isWhitespace }.uppercased() - var events: [VehicleEvent] = [] + var events: [VehicleEventDto] = [] do { let realm = try Realm() if let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: numberNormalized) { - events.append(contentsOf: dbVehicle.events.map { $0.clone() }) + events.append(contentsOf: dbVehicle.events.map(\.dto)) } } catch { print(error) } - HUD.show(.progress) - self.check(number: numberNormalized, action: .receiveAndSend, notes: [], events: events).subscribe { (vehicle, errors) in - if !vehicle.unrecognized && errors.isEmpty { - self.updateDetailController(with: vehicle) + Task { + do { + HUD.show(.progress) + let (vehicle, errors) = try await self.check(number: numberNormalized, + action: .receiveAndSend, + notes: [], + events: events) + if !vehicle.unrecognized && errors.isEmpty { + self.updateDetailController(with: vehicle) + } + HUD.hide() + self.showErrors(errors) + } catch { + HUD.hide() + self.show(error: error) } - HUD.hide() - self.showErrors(errors) - } onFailure: { error in - HUD.hide() - self.show(error: error) } - .disposed(by: self.bag) - } - func updateDetailController(with vehicle: Vehicle) { + func updateDetailController(with vehicle: VehicleDto) { if let splitViewController = self.view.window?.rootViewController as? UISplitViewController { var detail: UINavigationController? @@ -330,22 +332,28 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd // MARK: - Contextual actions - func update(vehicle: Vehicle) { - HUD.show(.progress) - self.check(number: vehicle.getNumber(), action: .doNotSend, notes: Array(vehicle.notes), events: Array(vehicle.events), force: true).subscribe { (vehicle, errors) in - if !vehicle.unrecognized { - self.updateDetailController(with: vehicle) + func update(vehicle: VehicleDto) { + Task { + do { + HUD.show(.progress) + let (vehicle, errors) = try await self.check(number: vehicle.getNumber(), + action: .doNotSend, + notes: Array(vehicle.notes), + events: Array(vehicle.events), + force: true) + if !vehicle.unrecognized { + self.updateDetailController(with: vehicle) + } + HUD.hide() + self.showErrors(errors) + } catch { + HUD.hide() + self.show(error: error) } - HUD.hide() - self.showErrors(errors) - } onFailure: { error in - HUD.hide() - self.show(error: error) } - .disposed(by: self.bag) } - func remove(vehicle: Vehicle) { + func remove(vehicle: VehicleDto) { guard let realm = try? Realm() else { return } guard let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) else { return } @@ -360,107 +368,122 @@ class CheckController: UIViewController, UITableViewDelegate, UISearchResultsUpd // MARK: - Checking number - func save(vehicle: Vehicle) throws { + func save(vehicle: VehicleDto) throws { let realm = try Realm() try realm.write { - realm.add(vehicle, update: .all) + realm.add(Vehicle(dto: vehicle), update: .all) } } - func getEvent(for action: EventAction) -> Single { - if let event = RxLocationManager.lastEvent, (Date().timeIntervalSince1970 - event.date) < 100 { - return Single.just(event) + func getEvent(for action: EventAction) async throws -> VehicleEventDto { + if let event = await RxLocationManager.getLastEvent(), (Date().timeIntervalSince1970 - event.date) < 100 { + return event } else { - return RxLocationManager.requestCurrentLocation() + return try await RxLocationManager.requestCurrentLocation() } } - func check(number: String, action: EventAction, notes: [VehicleNote], events: [VehicleEvent], force: Bool = false) -> Single<(vehicle: Vehicle, errors: [Error])> { - var eventSingle: Single<(event: VehicleEvent?, error: Error?)> = .just((event: nil, error: nil)) - if action != .doNotSend { - eventSingle = self.getEvent(for: action) - .flatMap { event in event.findAddress().map{ event }.catchAndReturn(event) } - .map { event -> (event: VehicleEvent?, error: Error?) in (event: event, error: nil) } - .observe(on: MainScheduler.instance) - .catch { .just((event: nil, error: $0)) } + func prepareEvent(for action: EventAction) async -> (event: VehicleEventDto?, error: Error?) { + guard action != .doNotSend else { + return (event: nil, error: nil) } - let checkSingle = Api.checkVehicle(by: number, notes: notes, events: events, force: force) - .observe(on: MainScheduler.instance) - .map { (vehicle: Vehicle) -> (vehicle: Vehicle, error: Error?) in - try self.save(vehicle: vehicle) - return (vehicle: vehicle, error: nil) - } - .catch { error in - let realm = try Realm() - if let existingVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) { - return .just((vehicle: existingVehicle, error: error)) - } else { - let vehicle = Vehicle(number) - try realm.write { realm.add(vehicle, update: .all) } - return .just((vehicle: vehicle, error: error)) - } - } + do { + let event = try await getEvent(for: action) + try? await event.findAddress() + return (event: event, error: nil) + } catch { + return (event: nil, error: error) + } + } + + func checkVehicle(number: String, + notes: [VehicleNoteDto], + events: [VehicleEventDto], + force: Bool = false) async -> (vehicle: VehicleDto, error: Error?) { - return Single.zip(eventSingle, checkSingle).flatMap { eventResult, vehicleResult in - var errors = [eventResult.error, vehicleResult.error].map { error -> Error? in - if let clerror = error as? CLError { - if clerror.code != .denied { - return CocoaError.error(NSLocalizedString("Location error", comment: ""), reason: clerror.code.description) - } else { - return nil - } - } else { - return error - } - } - .compactMap { $0 } - - RxLocationManager.resetLastEvent() - - let realm = try Realm() - let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicleResult.vehicle.getNumber()) - if let event = eventResult.event, let vehicle = dbVehicle { - try realm.write { - vehicle.events.append(event) - vehicle.updatedDate = Date().timeIntervalSince1970 - vehicle.synchronized = false - } - } - - if vehicleResult.error != nil { - return .just((vehicle: vehicleResult.vehicle, errors: errors)) + do { + let vehicle = try await ApiService.shared.checkVehicle(by: number, notes: notes, events: events, force: force) + try self.save(vehicle: vehicle) + return (vehicle: vehicle, error: nil) + } catch { + let realm = try? await Realm() + if let existingVehicle = realm?.object(ofType: Vehicle.self, forPrimaryKey: number) { + return (vehicle: existingVehicle.dto, error: error) } else { - if let event = eventResult.event { - return Api.add(event: event, to: vehicleResult.vehicle.getNumber()) - .observe(on: MainScheduler.instance) - .map { - try self.save(vehicle: $0) - return (vehicle: $0, errors: errors) - } - .catch { error in - errors.append(error) - return .just((vehicle: vehicleResult.vehicle, errors: errors)) - } + let vehicle = Vehicle(number) + try? realm?.write { realm?.add(vehicle, update: .all) } + return (vehicle: vehicle.dto, error: error) + } + } + } + + func check(number: String, + action: EventAction, + notes: [VehicleNoteDto], + events: [VehicleEventDto], + force: Bool = false) async throws -> (vehicle: VehicleDto, errors: [Error]) { + + async let eventTask = prepareEvent(for: action) + async let vehicleTask = checkVehicle(number: number, notes: notes, events: events, force: force) + let (eventResult, vehicleResult) = await (eventTask, vehicleTask) + + var errors = [eventResult.error, vehicleResult.error].map { error -> Error? in + if let clerror = error as? CLError { + if clerror.code != .denied { + return CocoaError.error(NSLocalizedString("Location error", comment: ""), reason: clerror.code.description) } else { - return .just((vehicle: vehicleResult.vehicle, errors: errors)) + return nil } + } else { + return error + } + } + .compactMap { $0 } + + RxLocationManager.resetLastEvent() + + let realm = try await Realm() + let dbVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicleResult.vehicle.getNumber()) + if let event = eventResult.event, let vehicle = dbVehicle { + try realm.write { + vehicle.events.append(VehicleEvent(dto: event)) + vehicle.updatedDate = Date().timeIntervalSince1970 + vehicle.synchronized = false + } + } + + if vehicleResult.error != nil { + return (vehicle: vehicleResult.vehicle, errors: errors) + } else { + if let event = eventResult.event { + do { + let vehicle = try await ApiService.shared.add(event: event, to: vehicleResult.vehicle.getNumber()) + try self.save(vehicle: vehicle) + return (vehicle: vehicle, errors: errors) + } catch { + errors.append(error) + return (vehicle: vehicleResult.vehicle, errors: errors) + } + } else { + return (vehicle: vehicleResult.vehicle, errors: errors) } } } func showErrors(_ errors: [Error]) { - let observables = errors.map(rxShowError) - Observable.from(observables).concat().subscribe().disposed(by: self.bag) + Task { + for error in errors { + await asyncShowError(error) + } + } } - func rxShowError(_ error: Error) -> Observable { - return Observable.create { observer in + func asyncShowError(_ error: Error) async { + await withCheckedContinuation { continuation in self.show(error: error, animated: true) { - observer.on(.next(())) - observer.on(.completed) + continuation.resume() } - return Disposables.create() } } } diff --git a/AutoCat/Controllers/FiltersController.swift b/AutoCat/Controllers/FiltersController.swift index b07d78c..9b62d1f 100644 --- a/AutoCat/Controllers/FiltersController.swift +++ b/AutoCat/Controllers/FiltersController.swift @@ -1,6 +1,5 @@ import UIKit import Eureka -import RxSwift import AutoCatCore class FiltersController: FormViewController { @@ -9,186 +8,230 @@ class FiltersController: FormViewController { var filter: Filter! var onDone: (() -> Void)? var regions: [VehicleRegion] = [] - - let bag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() - form +++ Section(NSLocalizedString("Main filters", comment: "")) { $0.tag = "MainFilters" } - <<< PushRow("Brand") { row in - row.title = NSLocalizedString("Brand", comment: "") - row.value = self.filter.brand ?? "Any" - row.selectorTitle = NSLocalizedString("Brands", comment: "") - row.optionsProvider = .lazy({ form, completion in - Api.getBrands().observeOn(MainScheduler.instance).subscribe(onSuccess: { brands in - completion(["Any"] + brands) - }, onError: { error in - print("Get brands error: ", error) - }).disposed(by: self.bag) - }) - }.onPresent(removeSectionName(from:to:)) + addMainSection() + addRegionSection() + addAddedBySection() + addTimeSections() + addLocationTimeSection() + addSortSection() + addClearAllSection() + } + + func runAsync(_ completion: @escaping () async throws -> Void) { + Task { + do { + try await completion() + } catch { + print("Error: \(error.localizedDescription)") + } + } + } + + func addMainSection() { + + let brandRow = PushRow("Brand") { row in + row.title = NSLocalizedString("Brand", comment: "") + row.value = self.filter.brand ?? "Any" + row.selectorTitle = NSLocalizedString("Brands", comment: "") + row.optionsProvider = .lazy({ form, completion in + self.runAsync { + let brands = try await ApiService.shared.getBrands() + completion(["Any"] + brands) + } + }) + } + .onPresent(removeSectionName(from:to:)) .onChange { self.filter.brand = $0.value == "Any" ? nil : $0.value } .cellUpdate { $1.value = self.filter.brand ?? "Any" } - - <<< PushRow("Model") { row in - row.title = NSLocalizedString("Model", comment: "") - row.value = self.filter.model ?? "Any" - row.disabled = "$Brand == 'Any'" - row.optionsProvider = .lazy({ form, completion in - guard let brand = self.filter.brand else { - completion(["Any"]) - return - } - Api.getModels(of: brand).observeOn(MainScheduler.instance).subscribe(onSuccess: { models in - completion(["Any"] + models) - }, onError: { error in - print("Get models error: ", error) - }).disposed(by: self.bag) - }) - }.onPresent(removeSectionName(from:to:)) + + let modelRow = PushRow("Model") { row in + row.title = NSLocalizedString("Model", comment: "") + row.value = self.filter.model ?? "Any" + row.disabled = "$Brand == 'Any'" + row.optionsProvider = .lazy({ form, completion in + guard let brand = self.filter.brand else { + completion(["Any"]) + return + } + + self.runAsync { + let models = try await ApiService.shared.getModels(of: brand) + completion(["Any"] + models) + } + }) + } + .onPresent(removeSectionName(from:to:)) .onChange { self.filter.model = $0.value == "Any" ? nil : $0.value } .cellUpdate { $1.value = self.filter.model ?? "Any" } - - <<< PushRow("Color") { row in - row.title = NSLocalizedString("Color", comment: "") - row.value = self.filter.color ?? "Any" - row.optionsProvider = .lazy({ form, completion in - Api.getColors().observeOn(MainScheduler.instance).subscribe(onSuccess: { colors in - completion(["Any"] + colors) - }, onError: { error in - print("Get colors error: ", error) - }).disposed(by: self.bag) - }) - }.onPresent(removeSectionName(from:to:)) + + let colorRow = PushRow("Color") { row in + row.title = NSLocalizedString("Color", comment: "") + row.value = self.filter.color ?? "Any" + row.optionsProvider = .lazy({ form, completion in + self.runAsync { + let colors = try await ApiService.shared.getColors() + completion(["Any"] + colors) + } + }) + } + .onPresent(removeSectionName(from:to:)) .onChange { self.filter.color = $0.value == "Any" ? nil : $0.value } .cellUpdate { $1.value = self.filter.color ?? "Any" } - <<< PushRow("Year") { row in - row.title = NSLocalizedString("Year", comment: "Manufacturing year") - row.value = self.filter.year ?? "Any" - row.optionsProvider = .lazy({ form, completion in - Api.getYears().observeOn(MainScheduler.instance).subscribe { years in - completion(["Any"] + years.map(String.init)) - } onError: { error in - print("Get years error: \(error)") - }.disposed(by: self.bag) - }) - } + let yearRow = PushRow("Year") { row in + row.title = NSLocalizedString("Year", comment: "Manufacturing year") + row.value = self.filter.year ?? "Any" + row.optionsProvider = .lazy({ form, completion in + self.runAsync { + let years = try await ApiService.shared.getYears() + completion(["Any"] + years.map(String.init)) + } + }) + } .onChange { self.filter.year = $0.value == "Any" ? nil : $0.value } .cellUpdate { $1.value = self.filter.year ?? "Any" } + let mainSection = Section(NSLocalizedString("Main filters", comment: "")) { $0.tag = "MainFilters" } + + form +++ mainSection + <<< brandRow + <<< modelRow + <<< colorRow + <<< yearRow + } + + func addRegionSection() { + form +++ Section() { $0.tag = "Regions" } - <<< LabelRow("RegionsRow") { row in - row.title = NSLocalizedString("Regions", comment: "") + <<< LabelRow("RegionsRow") { row in + row.title = NSLocalizedString("Regions", comment: "") + row.value = self.filter.regions?.map(String.init).joined(separator: ",") ?? "Any" + row.cellUpdate { cell, _ in + cell.accessoryType = .disclosureIndicator row.value = self.filter.regions?.map(String.init).joined(separator: ",") ?? "Any" - row.cellUpdate { cell, _ in - cell.accessoryType = .disclosureIndicator - row.value = self.filter.regions?.map(String.init).joined(separator: ",") ?? "Any" - } } - .onCellSelection { cell, row in - let sb = UIStoryboard(name: "Main", bundle: nil) - let vc = sb.instantiateViewController(identifier: "RegionsController") as RegionsController - vc.regionCodes = self.filter.regions ?? [] - vc.onDone = { regions in - row.value = regions?.map(String.init).joined(separator: ",") ?? "Any" - self.filter.regions = regions - } - self.navigationController?.pushViewController(vc, animated: true) + } + .onCellSelection { cell, row in + let sb = UIStoryboard(name: "Main", bundle: nil) + let vc = sb.instantiateViewController(identifier: "RegionsController") as RegionsController + vc.regionCodes = self.filter.regions ?? [] + vc.onDone = { regions in + row.value = regions?.map(String.init).joined(separator: ",") ?? "Any" + self.filter.regions = regions } + self.navigationController?.pushViewController(vc, animated: true) + } + } + + func addAddedBySection() { form +++ Section() { $0.tag = "AddedByMe" } - <<< ActionSheetRow("AddedByMeRow") { row in - row.title = NSLocalizedString("Added by", comment: "") - row.selectorTitle = NSLocalizedString("Added by", comment: "") - row.options = AddedBy.allCases.map { $0.description } - row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description - } - .onChange { row in - if let index = row.options?.firstIndex(of: row.value ?? "") { - self.filter.addedBy = AddedBy.allCases[index] - } else { - self.filter.addedBy = .anyone - } - } - .cellUpdate { cell, row in - row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description + <<< ActionSheetRow("AddedByMeRow") { row in + row.title = NSLocalizedString("Added by", comment: "") + row.selectorTitle = NSLocalizedString("Added by", comment: "") + row.options = AddedBy.allCases.map { $0.description } + row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description + } + .onChange { row in + if let index = row.options?.firstIndex(of: row.value ?? "") { + self.filter.addedBy = AddedBy.allCases[index] + } else { + self.filter.addedBy = .anyone } + } + .cellUpdate { cell, row in + row.value = self.filter.addedBy?.description ?? AddedBy.anyone.description + } + } + + func addTimeSections() { form +++ Section(NSLocalizedString("Update time", comment: "")) - <<< DateInlineRow("FromDateUpdated") { row in - row.title = NSLocalizedString("From", comment: "") - row.noValueDisplayText = NSLocalizedString("Beginning", comment: "") - row.value = self.filter.fromDateUpdated - } - .onChange { self.filter.fromDateUpdated = self.nullifyTime(of: $0.value) } - .cellUpdate(self.update(cell:row:)) - <<< DateInlineRow("ToDateUpdated") { row in - row.title = NSLocalizedString("To", comment: "") - row.noValueDisplayText = NSLocalizedString("Now", comment: "") - row.value = self.filter.toDateUpdated - } - .onChange { self.filter.toDateUpdated = self.nullifyTime(of: $0.value) } - .cellUpdate(self.update(cell:row:)) + <<< DateInlineRow("FromDateUpdated") { row in + row.title = NSLocalizedString("From", comment: "") + row.noValueDisplayText = NSLocalizedString("Beginning", comment: "") + row.value = self.filter.fromDateUpdated + } + .onChange { self.filter.fromDateUpdated = self.nullifyTime(of: $0.value) } + .cellUpdate(self.update(cell:row:)) + <<< DateInlineRow("ToDateUpdated") { row in + row.title = NSLocalizedString("To", comment: "") + row.noValueDisplayText = NSLocalizedString("Now", comment: "") + row.value = self.filter.toDateUpdated + } + .onChange { self.filter.toDateUpdated = self.nullifyTime(of: $0.value) } + .cellUpdate(self.update(cell:row:)) form +++ Section(NSLocalizedString("Added time", comment: "")) - <<< DateInlineRow("FromDate") { row in - row.title = NSLocalizedString("From", comment: "") - row.noValueDisplayText = NSLocalizedString("Beginning", comment: "") - row.value = self.filter.fromDate - } - .onChange { self.filter.fromDate = self.nullifyTime(of: $0.value) } - .cellUpdate(self.update(cell:row:)) - <<< DateInlineRow("ToDate") { row in - row.title = NSLocalizedString("To", comment: "") - row.noValueDisplayText = NSLocalizedString("Now", comment: "") - row.value = self.filter.toDate - } - .onChange { self.filter.toDate = self.nullifyTime(of: $0.value) } - .cellUpdate(self.update(cell:row:)) + <<< DateInlineRow("FromDate") { row in + row.title = NSLocalizedString("From", comment: "") + row.noValueDisplayText = NSLocalizedString("Beginning", comment: "") + row.value = self.filter.fromDate + } + .onChange { self.filter.fromDate = self.nullifyTime(of: $0.value) } + .cellUpdate(self.update(cell:row:)) + <<< DateInlineRow("ToDate") { row in + row.title = NSLocalizedString("To", comment: "") + row.noValueDisplayText = NSLocalizedString("Now", comment: "") + row.value = self.filter.toDate + } + .onChange { self.filter.toDate = self.nullifyTime(of: $0.value) } + .cellUpdate(self.update(cell:row:)) + } + + func addLocationTimeSection() { form +++ Section(NSLocalizedString("Location adding time", comment: "")) - <<< DateInlineRow("FromLocationDate") { row in - row.title = NSLocalizedString("From", comment: "") - row.noValueDisplayText = NSLocalizedString("Beginning", comment: "") - row.value = self.filter.fromLocationDate - } - .onChange { self.filter.fromLocationDate = self.nullifyTime(of: $0.value) } - .cellUpdate(self.update(cell:row:)) - <<< DateInlineRow("ToLocationDate") { row in - row.title = NSLocalizedString("To", comment: "") - row.noValueDisplayText = NSLocalizedString("Now", comment: "") - row.value = self.filter.toLocationDate - } - .onChange { self.filter.toLocationDate = self.nullifyTime(of: $0.value) } - .cellUpdate(self.update(cell:row:)) + <<< DateInlineRow("FromLocationDate") { row in + row.title = NSLocalizedString("From", comment: "") + row.noValueDisplayText = NSLocalizedString("Beginning", comment: "") + row.value = self.filter.fromLocationDate + } + .onChange { self.filter.fromLocationDate = self.nullifyTime(of: $0.value) } + .cellUpdate(self.update(cell:row:)) + <<< DateInlineRow("ToLocationDate") { row in + row.title = NSLocalizedString("To", comment: "") + row.noValueDisplayText = NSLocalizedString("Now", comment: "") + row.value = self.filter.toLocationDate + } + .onChange { self.filter.toLocationDate = self.nullifyTime(of: $0.value) } + .cellUpdate(self.update(cell:row:)) + } + + func addSortSection() { form +++ Section(NSLocalizedString("Sort", comment: "Header section. Noun.")) - <<< PickerInlineRow("SortBy") { row in - row.title = NSLocalizedString("Sort by", comment: "") - row.value = self.filter.sortBy - row.options = SortParameter.allCases - } - .onChange { self.filter.sortBy = $0.value } - .cellUpdate { $1.value = self.filter.sortBy } - <<< SegmentedRow("SortOrder") { row in - row.title = NSLocalizedString("Order", comment: "sort order") - row.value = self.filter.sortOrder - row.options = AutoCatCore.SortOrder.allCases - } - .onChange { self.filter.sortOrder = $0.value } - .cellUpdate { $1.value = self.filter.sortOrder } + <<< PickerInlineRow("SortBy") { row in + row.title = NSLocalizedString("Sort by", comment: "") + row.value = self.filter.sortBy + row.options = SortParameter.allCases + } + .onChange { self.filter.sortBy = $0.value } + .cellUpdate { $1.value = self.filter.sortBy } + <<< SegmentedRow("SortOrder") { row in + row.title = NSLocalizedString("Order", comment: "sort order") + row.value = self.filter.sortOrder + row.options = AutoCatCore.SortOrder.allCases + } + .onChange { self.filter.sortOrder = $0.value } + .cellUpdate { $1.value = self.filter.sortOrder } + } + + func addClearAllSection() { form +++ Section() - <<< ButtonRow("ClearAll") { $0.title = NSLocalizedString("Clear all filters", comment: "") }.onCellSelection { cell, row in - self.filter.clear() - for section in self.form.allSections { - // For some reason certain cells do not redraw after first reload - section.reload() - section.reload() - } + <<< ButtonRow("ClearAll") { $0.title = NSLocalizedString("Clear all filters", comment: "") }.onCellSelection { cell, row in + self.filter.clear() + for section in self.form.allSections { + // For some reason certain cells do not redraw after first reload + section.reload() + section.reload() } + } } override func viewWillAppear(_ animated: Bool) { diff --git a/AutoCat/Controllers/GoogleSignInController.swift b/AutoCat/Controllers/GoogleSignInController.swift index c8b0403..62f8a7f 100644 --- a/AutoCat/Controllers/GoogleSignInController.swift +++ b/AutoCat/Controllers/GoogleSignInController.swift @@ -1,7 +1,6 @@ import UIKit import WebKit import CommonCrypto -import RxSwift import PKHUD import AutoCatCore @@ -17,7 +16,6 @@ struct TokenResponse: Codable { class GoogleSignInController: UIViewController, WKNavigationDelegate { @IBOutlet weak var webView: WKWebView! - private var bag = DisposeBag() private var codeVerifier: String = "" public var completion: (() -> Void)? @@ -48,22 +46,22 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { } } - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { if let url = navigationAction.request.url { if let components = URLComponents(url: url, resolvingAgainstBaseURL: false) { if let queryItems = components.queryItems { if let code = queryItems.first(where: { $0.name == "code" })?.value { decisionHandler(.cancel) - self.getToken(code: code) - .flatMap { Api.fbVerifyAssertion(provider: "google.com", idToken: $0.id_token, accessToken: $0.access_token) } - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: { _ in + + Task { @MainActor in + do { + let token = try await self.getToken(code: code) + await ApiService.shared.fbVerifyAssertion(provider: "google.com", idToken: token.id_token, accessToken: token.access_token) self.dismiss(animated: true, completion: self.completion) - }, onError: { error in + } catch { HUD.flash(.labeledError(title: nil, subtitle: error.localizedDescription)) - }) - .disposed(by: self.bag) - return + } + } } } } @@ -87,7 +85,7 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { return String(data: Data(Base64FS.encode(data: hash)), encoding: .utf8)?.trimmingCharacters(in: CharacterSet(charactersIn: "=")) } - func getToken(code: String) -> Single { + func getToken(code: String) async throws -> TokenResponse { let tokenUrlString = Constants.googleTokenURL + "?grant_type=authorization_code" + "&code=" + code @@ -98,12 +96,10 @@ class GoogleSignInController: UIViewController, WKNavigationDelegate { if let url = URL(string: tokenUrlString) { var request = URLRequest(url: url) request.httpMethod = "POST" - return URLSession.shared.rx.data(request: request).asSingle().map { data in - return try JSONDecoder().decode(TokenResponse.self, from: data) - } + let (data, _) = try await URLSession.shared.data(for: request) + return try JSONDecoder().decode(TokenResponse.self, from: data) } else { - let error = NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"]) - return Single.error(error) + throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Bad URL"]) } } } diff --git a/AutoCat/Controllers/Location/EventsController.swift b/AutoCat/Controllers/Location/EventsController.swift index 132b5bf..e8e9fb9 100644 --- a/AutoCat/Controllers/Location/EventsController.swift +++ b/AutoCat/Controllers/Location/EventsController.swift @@ -1,7 +1,5 @@ import UIKit import MapKit -import RxSwift -import Realm import RealmSwift import PKHUD import MobileCoreServices @@ -21,7 +19,7 @@ class EventPin: NSObject, MKAnnotation { self.id = id } - convenience init(event: VehicleEvent) { + convenience init(event: VehicleEventDto) { let coordinate = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude) let address = event.address ?? "\(event.latitude), \(event.longitude)" let date = Date(timeIntervalSince1970: event.date) @@ -48,13 +46,12 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele @IBOutlet weak var map: MKMapView! @IBOutlet weak var tableView: UITableView! - let bag = DisposeBag() var modeButton: UIBarButtonItem! var addButton: UIBarButtonItem! var pasteButton: UIBarButtonItem! var mode: EventsMode = .map - public var vehicle: Vehicle? { + public var vehicle: VehicleDto? { didSet { if self.isViewLoaded { self.updateInterface() @@ -241,41 +238,45 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele // MARK: - Event actions - func deleteEvent(event: VehicleEvent, completion: ((Bool) -> Void)? = nil) { - HUD.show(.progress) - Api.remove(event: event.id).observe(on: MainScheduler.instance).subscribe(onSuccess: { vehicle in - let result = self.update(vehicle: vehicle) - completion?(result) - }, onFailure: { error in - completion?(false) - HUD.show(error: error) - print(error) - }).disposed(by: self.bag) + func deleteEvent(event: VehicleEventDto, completion: ((Bool) -> Void)? = nil) { + Task { + do { + HUD.show(.progress) + let vehicle = try await ApiService.shared.remove(event: event.id) + let result = self.update(vehicle: vehicle) + completion?(result) + } catch { + completion?(false) + HUD.show(error: error) + } + } } - func editEvent(event: VehicleEvent) { + func editEvent(event: VehicleEventDto) { let sb = UIStoryboard(name: "Main", bundle: nil) let controller = sb.instantiateViewController(identifier: "LocationEditController") as LocationEditController controller.title = NSLocalizedString("Edit event", comment: "") controller.date = Date(timeIntervalSince1970: event.date) controller.placemark = Placemark(latitude: event.latitude, longitude: event.longitude, address: event.address) controller.onDone = { newEvent in - newEvent.id = event.id + var updatedEvent = newEvent + updatedEvent.id = event.id self.navigationController?.popViewController(animated: true, completion: { - HUD.show(.progress) - Api.edit(event: newEvent) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure: - { error in + Task { + do { + HUD.show(.progress) + let vehicle = try await ApiService.shared.edit(event: updatedEvent) + self.update(vehicle: vehicle) + } catch { HUD.show(error: error) - }) - .disposed(by: self.bag) + } + } }) } self.navigationController?.pushViewController(controller, animated: true) } - func copyEvent(event: VehicleEvent) { + func copyEvent(event: VehicleEventDto) { var items: [String: Any] = [:] if let url = event.getMapLink() { @@ -295,7 +296,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele self.setupBarButtonItems() } - func shareEvent(event: VehicleEvent) { + func shareEvent(event: VehicleEventDto) { guard let url = event.getMapLink() else { return } @@ -304,7 +305,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele self.present(controller, animated: true) } - func openInAppleMaps(event: VehicleEvent) { + func openInAppleMaps(event: VehicleEventDto) { let coordinates = CLLocationCoordinate2D(latitude: event.latitude, longitude: event.longitude) let placemark = MKPlacemark(coordinate: coordinates) @@ -312,7 +313,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele mapItem.openInMaps() } - func openInYandexMaps(event: VehicleEvent) { + func openInYandexMaps(event: VehicleEventDto) { guard let url = URL(string: "yandexmaps://maps.yandex.ru/?pt=\(event.longitude),\(event.latitude)&z=12") else { return } @@ -331,14 +332,15 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele controller.title = NSLocalizedString("Add new event", comment: "") controller.onDone = { newEvent in self.navigationController?.popViewController(animated: true, completion: { - HUD.show(.progress) - Api.add(event: newEvent, to: vehicle.getNumber()) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure: - { error in + Task { + do { + HUD.show(.progress) + let vehicle = try await ApiService.shared.add(event: newEvent, to: vehicle.getNumber()) + self.update(vehicle: vehicle) + } catch { HUD.show(error: error) - }) - .disposed(by: self.bag) + } + } }) } self.navigationController?.pushViewController(controller, animated: true) @@ -352,7 +354,7 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele guard let data = UIPasteboard.general.data(forPasteboardType: "pro.aliencat.vehicle.event") else { return } do { - let event = try JSONDecoder().decode(VehicleEvent.self, from: data) + let event = try JSONDecoder().decode(VehicleEventDto.self, from: data) let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .medium @@ -360,15 +362,17 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele let alert = UIAlertController(title: NSLocalizedString("Paste event", comment: "from clipboard"), message: msg, preferredStyle: .alert) alert.addAction(UIAlertAction(title: NSLocalizedString("Paste", comment: "from clipboard"), style: .default, handler: { action in - HUD.show(.progress) - event.id = UUID().uuidString - Api.add(event: event, to: vehicle.getNumber()) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { self.update(vehicle: $0) }, onFailure: - { error in + Task { + do { + HUD.show(.progress) + var newEvent = event + newEvent.id = UUID().uuidString + let vehicle = try await ApiService.shared.add(event: newEvent, to: vehicle.getNumber()) + self.update(vehicle: vehicle) + } catch { HUD.show(error: error) - }) - .disposed(by: self.bag) + } + } })) alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: nil)) self.present(alert, animated: true) @@ -383,17 +387,17 @@ class EventsController: UIViewController, UITableViewDataSource, UITableViewDele } @discardableResult - func update(vehicle: Vehicle) -> Bool { + func update(vehicle: VehicleDto) -> Bool { do { - if let v = self.vehicle, let realm = v.realm, !v.isFrozen { + let realm = try Realm() + if let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) { try ExceptionCatcher.catch { try realm.write { - realm.add(vehicle, update: .all) + realm.add(Vehicle(dto: vehicle), update: .all) } } } else { - self.vehicle?.events.removeAll() - self.vehicle?.events.append(objectsIn: vehicle.events) + self.vehicle?.events = vehicle.events } self.updateInterface() HUD.hide() diff --git a/AutoCat/Controllers/Location/GlobalEventsController.swift b/AutoCat/Controllers/Location/GlobalEventsController.swift index 091c255..5a3a249 100644 --- a/AutoCat/Controllers/Location/GlobalEventsController.swift +++ b/AutoCat/Controllers/Location/GlobalEventsController.swift @@ -1,6 +1,5 @@ import UIKit import MapKit -import RxSwift import PKHUD import AutoCatCore @@ -8,7 +7,6 @@ class GlobalEventsController: UIViewController { @IBOutlet weak var map: MKMapView! - let bag = DisposeBag() var filter: Filter! override func viewDidLoad() { @@ -23,20 +21,22 @@ class GlobalEventsController: UIViewController { #endif - HUD.show(.progress) - Api.events(with: self.filter) - .observe(on: MainScheduler.init()) - .subscribe(onSuccess: { events in - self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count) - let pins = events.map(EventPin.init(event:)) - self.map.removeAnnotations(self.map.annotations) - self.map.addAnnotations(pins) - self.map.centerOnPins() - HUD.hide() - }, onFailure: { error in - HUD.show(error: error) - }) - .disposed(by: self.bag) + Task { await loadEvents() } + } + + func loadEvents() async { + do { + HUD.show(.progress) + let events = try await ApiService.shared.events(with: self.filter) + self.title = String.localizedStringWithFormat(NSLocalizedString("events found", comment: ""), events.count) + let pins = events.map(EventPin.init(event:)) + self.map.removeAnnotations(self.map.annotations) + self.map.addAnnotations(pins) + self.map.centerOnPins() + HUD.hide() + } catch { + HUD.show(error: error) + } } @IBAction func close(_ sender: UIBarButtonItem) { diff --git a/AutoCat/Controllers/Location/LocationEditController.swift b/AutoCat/Controllers/Location/LocationEditController.swift index ca13660..96f6d7f 100644 --- a/AutoCat/Controllers/Location/LocationEditController.swift +++ b/AutoCat/Controllers/Location/LocationEditController.swift @@ -1,18 +1,16 @@ import UIKit import Eureka -import RxSwift import CoreLocation import AutoCatCore class LocationEditController: FormViewController { - private let bag = DisposeBag() private var doneButton: UIBarButtonItem! var date = Date() var placemark: Placemark? = nil - var onDone: ((VehicleEvent) -> Void)? + var onDone: ((VehicleEventDto) -> Void)? override func viewDidLoad() { super.viewDidLoad() @@ -34,12 +32,13 @@ class LocationEditController: FormViewController { } } - <<< LocationRow() { row in + // TODO: Use one of the standard rows (properly) + <<< /*LocationRow()*/LabelRow { row in row.title = NSLocalizedString("Location", comment: "") - row.value = self.placemark + row.value = self.placemark?.address }.onChange { row in if let newPlacemark = row.value { - self.placemark = newPlacemark + //self.placemark = newPlacemark self.doneButton.isEnabled = true } else { self.doneButton.isEnabled = false @@ -49,7 +48,7 @@ class LocationEditController: FormViewController { @objc func doneTapped(_ sender: UIBarButtonItem) { guard let placemark = self.placemark else { return } - let event = VehicleEvent(lat: placemark.latitude, lon: placemark.longitude) + var event = VehicleEventDto(lat: placemark.latitude, lon: placemark.longitude) event.date = self.date.timeIntervalSince1970 if let address = placemark.address { event.address = address diff --git a/AutoCat/Controllers/Location/LocationPickerController.swift b/AutoCat/Controllers/Location/LocationPickerController.swift index 37af5c2..65f1d32 100644 --- a/AutoCat/Controllers/Location/LocationPickerController.swift +++ b/AutoCat/Controllers/Location/LocationPickerController.swift @@ -1,7 +1,5 @@ import Foundation import MapKit -import Eureka -import RxSwift import Intents import AutoCatCore @@ -11,14 +9,13 @@ public struct Placemark: Equatable { var address: String? } -public class LocationPickerController : UIViewController, TypedRowControllerType, MKMapViewDelegate { +public class LocationPickerController : UIViewController, MKMapViewDelegate { - public var row: RowOf! + public var placemark: Placemark? public var onDismissCallback: ((UIViewController) -> ())? - private let bag = DisposeBag() - private var geocodingDisposable: Disposable? private var address: String? + private var geocodingTask: Task? lazy var mapView : MKMapView = { [unowned self] in let v = MKMapView(frame: self.view.bounds) @@ -89,7 +86,7 @@ public class LocationPickerController : UIViewController, TypedRowControllerType button.title = "Done" navigationItem.rightBarButtonItem = button - if let value = row.value { + if let value = placemark { let region = MKCoordinateRegion(center: CLLocationCoordinate2D(latitude: value.latitude, longitude: value.longitude), latitudinalMeters: 1000, longitudinalMeters: 1000) mapView.setRegion(region, animated: true) } @@ -111,11 +108,11 @@ public class LocationPickerController : UIViewController, TypedRowControllerType @objc func tappedDone(_ sender: UIBarButtonItem){ let target = mapView.convert(ellipsisLayer.position, toCoordinateFrom: mapView) - row.value = Placemark(latitude: target.latitude, longitude: target.longitude, address: self.address) + placemark = Placemark(latitude: target.latitude, longitude: target.longitude, address: self.address) onDismissCallback?(self) } - func updateTitle(){ + func updateTitle() { let fmt = NumberFormatter() fmt.maximumFractionDigits = 4 fmt.minimumFractionDigits = 4 @@ -124,14 +121,15 @@ public class LocationPickerController : UIViewController, TypedRowControllerType title = "\(latitude), \(longitude)" self.address = nil - self.geocodingDisposable?.dispose() - self.geocodingDisposable = RxLocationManager - .getAddressForLocation(latitude: mapView.centerCoordinate.latitude, longitude: mapView.centerCoordinate.longitude) - .observeOn(MainScheduler.instance) - .subscribe(onSuccess: { address in - self.title = address - self.address = address - }) + + geocodingTask?.cancel() + geocodingTask = Task { + address = try? await RxLocationManager.getAddressForLocation(latitude: mapView.centerCoordinate.latitude, + longitude: mapView.centerCoordinate.longitude) + title = address + geocodingTask = nil + return address + } } public func mapView(_ mapView: MKMapView, regionWillChangeAnimated animated: Bool) { diff --git a/AutoCat/Controllers/Location/LocationRow.swift b/AutoCat/Controllers/Location/LocationRow.swift index 0364e26..d6abad7 100644 --- a/AutoCat/Controllers/Location/LocationRow.swift +++ b/AutoCat/Controllers/Location/LocationRow.swift @@ -1,7 +1,10 @@ import UIKit -import Eureka +//import Eureka import CoreLocation +// TODO: Rewrite Eureka forms to native UIKit/SwiftUI + +/* public final class LocationRow: OptionsRow>, PresenterRowType, RowType { public typealias PresenterRow = LocationPickerController @@ -55,3 +58,4 @@ public final class LocationRow: OptionsRow>, Present rowVC.row = self } } +*/ diff --git a/AutoCat/Controllers/Location/ShowEventController.swift b/AutoCat/Controllers/Location/ShowEventController.swift index c59676b..f1063c7 100644 --- a/AutoCat/Controllers/Location/ShowEventController.swift +++ b/AutoCat/Controllers/Location/ShowEventController.swift @@ -5,7 +5,7 @@ import AutoCatCore class ShowEventController: UIViewController { private var map = MKMapView() - var event: VehicleEvent? + var event: VehicleEventDto? override func viewDidLoad() { super.viewDidLoad() diff --git a/AutoCat/Controllers/MainTabController.swift b/AutoCat/Controllers/MainTabController.swift index 3f5e687..20b67ce 100644 --- a/AutoCat/Controllers/MainTabController.swift +++ b/AutoCat/Controllers/MainTabController.swift @@ -1,12 +1,9 @@ import UIKit import SwiftEntryKit import AutoCatCore -import RxSwift class MainTabController: UITabBarController, UITabBarControllerDelegate { - private let bag = DisposeBag() - override func viewDidLoad() { super.viewDidLoad() self.delegate = self @@ -58,6 +55,6 @@ class MainTabController: UITabBarController, UITabBarControllerDelegate { // User probably just saw a vehicle and is about to start entering plate number // Requesting current location ASAP while we still close to initial location - RxLocationManager.requestCurrentLocation().subscribe().disposed(by: self.bag) + Task { try? await RxLocationManager.requestCurrentLocation() } } } diff --git a/AutoCat/Controllers/NotesController.swift b/AutoCat/Controllers/NotesController.swift deleted file mode 100644 index 2c88906..0000000 --- a/AutoCat/Controllers/NotesController.swift +++ /dev/null @@ -1,295 +0,0 @@ -import UIKit -import AutoCatCore -import MobileCoreServices -import PKHUD -import RxSwift -import ExceptionCatcher -import RealmSwift - -class NotesController: UIViewController, UITableViewDataSource, UITableViewDelegate { - - @IBOutlet weak var notesTable: UITableView! - - private var textView = UITextView() - private var bag = DisposeBag() - - var vehicle: Vehicle? { - didSet { - if self.isViewLoaded { - self.notesTable.reloadData() - } - } - } - - override func viewDidLoad() { - super.viewDidLoad() - - self.title = NSLocalizedString("Notes", comment: "") - self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(addNote(_:))) - self.notesTable.reloadData() - self.hideKeyboardWhenTappedAround() - } - - @discardableResult - func update(vehicle: Vehicle) -> Bool { - do { - if let v = self.vehicle, let realm = v.realm, !v.isFrozen { - try ExceptionCatcher.catch { - try realm.write { - realm.add(vehicle, update: .all) - } - } - } else { - self.vehicle?.notes.removeAll() - self.vehicle?.notes.append(objectsIn: vehicle.notes) - } - self.notesTable.reloadData() - return true - } catch { - self.show(error: error) - return false - } - } - - // MARK: - UITableViewDataSource - - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return self.vehicle?.notes.count ?? 0 - } - - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "VehicleNoteCell", for: indexPath) as? VehicleNoteCell else { - return UITableViewCell() - } - - if let note = self.vehicle?.notes[indexPath.row] { - cell.configure(with: note) - } - - return cell - } - - // MARK: - UITableViewDelegate - - func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in - let copy = UIAction(title: NSLocalizedString("Copy", comment: ""), image: UIImage(systemName: "doc.on.doc")) { action in - self.copyNote(index: indexPath.row) - } - - let edit = UIAction(title: NSLocalizedString("Edit", comment: ""), image: UIImage(systemName: "pencil")) { action in - self.editNote(index: indexPath.row) - } - - let delete = UIAction(title: NSLocalizedString("Delete", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { action in - self.deleteNote(index: indexPath.row) - } - - return UIMenu(title: NSLocalizedString("Actions", comment: ""), children: [copy, edit, delete]) - } - } - - func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { - let copy = UIContextualAction(style: .normal, title: NSLocalizedString("Copy", comment: "")) { action, view, completion in - self.copyNote(index: indexPath.row) - completion(true) - } - copy.image = UIImage(systemName: "doc.on.doc") - copy.backgroundColor = .systemBlue - - let delete = UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { action, view, completion in - self.deleteNote(index: indexPath.row, completion: completion) - } - delete.image = UIImage(systemName: "trash") - - let edit = UIContextualAction(style: .normal, title: NSLocalizedString("Edit", comment: "")) { action, view, completion in - self.editNote(index: indexPath.row) - completion(true) - } - edit.image = UIImage(systemName: "pencil") - edit.backgroundColor = .systemBlue - - let configuration = UISwipeActionsConfiguration(actions: [delete, edit, copy]) - configuration.performsFirstActionWithFullSwipe = false - return configuration - } - - // MARK: - Actions - - @objc func addNote(_ sender: UIBarButtonItem) { - guard let vehicle = self.vehicle else { - HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle")) - return - } - - self.showAddNoteAlert(text: nil) { noteText in - let note = VehicleNote(text: noteText) - - if vehicle.unrecognized { - if let realm = vehicle.realm { - try? realm.write { - vehicle.notes.append(note) - vehicle.updatedDate = Date().timeIntervalSince1970 - } - self.notesTable.reloadData() - } - return - } - - HUD.show(.progress) - Api.add(notes: [note], to: vehicle.getNumber()) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { - HUD.hide() - self.update(vehicle: $0) - }, onFailure: { error in - HUD.hide() - self.show(error: error) - }) - .disposed(by: self.bag) - } - } - - func copyNote(index: Int) { - guard let vehicle = self.vehicle else { - HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle")) - return - } - - UIPasteboard.general.setValue(vehicle.notes[index].text, forPasteboardType: kUTTypePlainText as String) - } - - func editNote(index: Int) { - guard let vehicle = self.vehicle else { - HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle")) - return - } - - let note = vehicle.notes[index] - self.showAddNoteAlert(text: note.text) { noteText in - - if vehicle.unrecognized { - if let realm = vehicle.realm { - try? realm.write { - note.text = noteText - vehicle.updatedDate = Date().timeIntervalSince1970 - } - self.notesTable.reloadData() - } - return - } - - HUD.show(.progress) - let newNote = note.clone() - newNote.text = noteText - Api.edit(note: newNote) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { - HUD.hide() - self.update(vehicle: $0) - }, onFailure: { error in - HUD.hide() - self.show(error: error) - }) - .disposed(by: self.bag) - } - } - - func deleteNote(index: Int, completion: ((Bool) -> Void)? = nil) { - guard let vehicle = self.vehicle else { - HUD.flash(.labeledError(title: nil, subtitle: "Unknown vehicle")) - return - } - - let note = vehicle.notes[index] - - if vehicle.unrecognized { - if let realm = vehicle.realm { - try? realm.write { - vehicle.notes.remove(at: index) - vehicle.updatedDate = Date().timeIntervalSince1970 - realm.delete(note) - } - self.notesTable.reloadData() - } - return - } - - HUD.show(.progress) - Api.remove(note: note.id) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { vehicle in - HUD.hide() - let result = self.update(vehicle: vehicle) - completion?(result) - }, onFailure: { error in - completion?(false) - HUD.hide() - self.show(error: error) - print(error) - }).disposed(by: self.bag) - } - - // MARK: - Utils - - func showAddNoteAlert(text: String?, completion: @escaping (String) -> Void) { - #if targetEnvironment(macCatalyst) - showAddNoteAlertCatalyst(text: text, completion: completion) - #else - showAddNoteAlertIos(text: text, completion: completion) - #endif - } - - func showAddNoteAlertIos(text: String?, completion: @escaping (String) -> Void) { - let alertController = UIAlertController(title: NSLocalizedString("New note", comment: ""), message: nil, preferredStyle: .alert) - - let cancelAction = UIAlertAction.init(title: NSLocalizedString("Cancel", comment: ""), style: .default) - alertController.addAction(cancelAction) - - let saveAction = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { (action) in - let enteredText = self.textView.text ?? "" - completion(enteredText) - } - alertController.addAction(saveAction) - self.textView = UITextView() - self.textView.text = text - self.textView.addDoneButton(title: NSLocalizedString("Done", comment: ""), target: self, selector: #selector(tapDone(sender:))) - self.textView.translatesAutoresizingMaskIntoConstraints = false - alertController.view.addSubview(self.textView) - - NSLayoutConstraint.activate([ - self.textView.topAnchor.constraint(equalTo: alertController.view.topAnchor, constant: 60), - self.textView.bottomAnchor.constraint(equalTo: alertController.view.bottomAnchor, constant: -60), - self.textView.leadingAnchor.constraint(equalTo: alertController.view.leadingAnchor, constant: 8), - self.textView.trailingAnchor.constraint(equalTo: alertController.view.trailingAnchor, constant: -8), - self.textView.heightAnchor.constraint(greaterThanOrEqualToConstant: 100) - ]) - - self.present(alertController, animated: true) { - self.textView.becomeFirstResponder() - } - } - - @objc func tapDone(sender: Any) { - self.textView.endEditing(true) - } - - func showAddNoteAlertCatalyst(text: String?, completion: @escaping (String) -> Void) { - let alertController = UIAlertController(title: NSLocalizedString("New note", comment: ""), message: nil, preferredStyle: .alert) - - let cancelAction = UIAlertAction.init(title: NSLocalizedString("Cancel", comment: ""), style: .default) - alertController.addAction(cancelAction) - - let saveAction = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { (action) in - let enteredText = alertController.textFields?.first?.text ?? "" - completion(enteredText) - } - alertController.addAction(saveAction) - - alertController.addTextField { textField in - textField.text = text - } - - self.present(alertController, animated: true) - } -} diff --git a/AutoCat/Controllers/Osago/DkbmController.swift b/AutoCat/Controllers/Osago/DkbmController.swift deleted file mode 100644 index 3c33df6..0000000 --- a/AutoCat/Controllers/Osago/DkbmController.swift +++ /dev/null @@ -1,65 +0,0 @@ -import UIKit -import WebKit -import PKHUD - -class DkbmController: UIViewController, WKScriptMessageHandlerWithReply { - - private var webView: WKWebView! - private var captchaAdded = false - - var onDone: ((String) -> Void)? - var checkSource: OsagoCheckSource? - - override func viewDidLoad() { - super.viewDidLoad() - - let config = WKWebViewConfiguration() - if let jsPath = Bundle.main.path(forResource: "dkbm", ofType: "js") { - let js = try? String(contentsOfFile: jsPath) - let contentController = WKUserContentController() - let script = WKUserScript(source: js!, injectionTime: .atDocumentEnd, forMainFrameOnly: false) - contentController.addUserScript(script) - if #available(iOS 14.0, *) { - contentController.addScriptMessageHandler(self, contentWorld: .page, name: "dkbmHandler") - } - config.userContentController = contentController - } - - self.webView = WKWebView(frame: .zero, configuration: config) - self.webView.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(self.webView) - NSLayoutConstraint.activate([ - self.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor), - self.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor), - self.webView.topAnchor.constraint(equalTo: self.view.topAnchor), - self.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor) - ]) - - //self.webView.isHidden = true - //HUD.show(.progress) - - let url = URL(string: "https://dkbm-web.autoins.ru/dkbm-web-1.0/policyInfo.htm")! - let request = URLRequest(url: url) - self.webView.load(request) - } - - // MARK: - WKScriptMessageHandler - - func userContentController(_ userContentController: WKUserContentController, - didReceive message: WKScriptMessage, - replyHandler: @escaping (Any?, String?) -> Void) { - - guard let msg = message.body as? [String:String], let checkSource else { return } - - if msg.contains(where: { $0.key == "loaded" }) { - switch checkSource { - case .plateNumber(let number): - replyHandler(["plateNumber": number], nil) - case .vin(let number): - replyHandler(["vin", number], nil) - } - } else if let urlString = msg["url"], let url = URL(string: urlString) { - - } - } -} diff --git a/AutoCat/Controllers/Osago/OsagoAddController.swift b/AutoCat/Controllers/Osago/OsagoAddController.swift deleted file mode 100644 index 557f18d..0000000 --- a/AutoCat/Controllers/Osago/OsagoAddController.swift +++ /dev/null @@ -1,78 +0,0 @@ -import UIKit -import Eureka -import PKHUD -import RxSwift -import RxCocoa -import AutoCatCore - -enum OsagoCheckSource: Equatable, CustomStringConvertible { - case plateNumber(number: String) - case vin(number: String) - - var description: String { - switch self { - case .plateNumber(let number): - return NSLocalizedString("plate number", comment: "Check by") + " (\(number))" - case .vin(let number): - return "VIN (\(number))" - } - } -} - -class OsagoAddController: FormViewController { - - private let bag = DisposeBag() - var checkSources: [OsagoCheckSource] = [] - var onDone: ((Vehicle) -> Void)? - - override func viewDidLoad() { - super.viewDidLoad() - self.title = NSLocalizedString("OSAGO check", comment: "") - - form +++ Section(NSLocalizedString("Check parameters", comment: "")) - <<< DateTimeInlineRow("date") { row in - row.title = NSLocalizedString("Check date", comment: "") - row.value = Date() - } - <<< PickerInlineRow("SourcePicker") { row in - row.title = NSLocalizedString("Check by", comment: "") - row.value = self.checkSources.first - row.options = self.checkSources - } - - form +++ Section() - <<< ButtonRow() { $0.title = NSLocalizedString("Check", comment: "verb") }.onCellSelection { _, _ in - guard let source = (self.form.rowBy(tag: "SourcePicker") as? PickerInlineRow)?.value, - let date = (self.form.rowBy(tag: "date") as? DateTimeInlineRow)?.value - else { return } - - let controller = DkbmController() - controller.checkSource = source - controller.onDone = { token in - self.navigationController?.popViewController(animated: true, completion: { - - var number, vin: String? - switch source { - case .plateNumber(let n): - number = n - case .vin(let v): - vin = v - } - HUD.show(.progress) - Api.checkOsago(number: number, vin: vin, date: date, token: token) - .observe(on: MainScheduler.instance) - .subscribe { vehicle in - HUD.hide() - self.onDone?(vehicle) - } onFailure: { err in - HUD.show(error: err) - } - .disposed(by: self.bag) - - }) - } - self.navigationController?.pushViewController(controller, animated: true) - //self.present(controller, animated: true) - } - } -} diff --git a/AutoCat/Controllers/Osago/OsagoController.swift b/AutoCat/Controllers/Osago/OsagoController.swift deleted file mode 100644 index ebf970e..0000000 --- a/AutoCat/Controllers/Osago/OsagoController.swift +++ /dev/null @@ -1,108 +0,0 @@ -import UIKit -import Eureka -import PKHUD -import AutoCatCore - -class OsagoController: FormViewController { - - var vehicle: Vehicle? { - didSet { - self.updateInterface() - } - } - - override func viewDidLoad() { - super.viewDidLoad() - self.title = NSLocalizedString("OSAGO contracts", comment: "") - self.navigationItem.rightBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(checkNewDate(_:))) - - self.tableView.rowHeight = UITableView.automaticDimension - } - - @objc func checkNewDate(_ sender: UIBarButtonItem) { - guard let vehicle = self.vehicle else { return } - - let sb = UIStoryboard(name: "Main", bundle: nil) - let controller = sb.instantiateViewController(identifier: "OsagoAddController") as OsagoAddController - controller.checkSources = [.plateNumber(number: vehicle.getNumber())] - if let vin = vehicle.vin1, !vin.contains("*") { - controller.checkSources.append(.vin(number: vin)) - } - - controller.onDone = { vehicle in - self.navigationController?.popViewController(animated: true, completion: { - self.update(vehicle: vehicle) - }) - } - - self.navigationController?.pushViewController(controller, animated: true) - } - - func updateInterface() { - guard let vehicle = self.vehicle else { return } - - let formatter = DateFormatter() - formatter.dateStyle = .medium - formatter.timeStyle = .none - - self.form.removeAll() - for osago in vehicle.osagoContracts.sorted(by: { $0.date < $1.date }) { - self.form +++ Section(formatter.string(from: Date(timeIntervalSince1970: osago.date))) - <<< self.multilineRow(NSLocalizedString("Contract series and number", comment: ""), value: osago.number) - <<< self.multilineRow(NSLocalizedString("Insurance organization name", comment: ""), value: osago.name) - <<< self.multilineRow(NSLocalizedString("OSAGO contract status", comment: ""), value: osago.status) - <<< self.multilineRow(NSLocalizedString("Insurant", comment: ""), value: osago.insurant) - <<< self.multilineRow(NSLocalizedString("Owner", comment: ""), value: osago.owner) - <<< self.multilineRow(NSLocalizedString("Birthday", comment: ""), value: osago.birthday) - <<< self.multilineRow(NSLocalizedString("Vehicle usage region", comment: ""), value: osago.usageRegion) - <<< self.multilineRow(NSLocalizedString("Contract restrictions", comment: ""), value: osago.restrictions) - <<< self.row(NSLocalizedString("Plate number", comment: ""), value: osago.plateNumber) - <<< self.row(NSLocalizedString("VIN", comment: ""), value: osago.vin) - } - } - - func row(_ title: String, value: String?) -> LabelRow { - LabelRow() { row in - if let cell = row.cell, let label = cell.detailTextLabel, let titleLabel = cell.textLabel { - titleLabel.translatesAutoresizingMaskIntoConstraints = false - titleLabel.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8).isActive = true - titleLabel.leadingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.leadingAnchor).isActive = true - - label.translatesAutoresizingMaskIntoConstraints = false - label.topAnchor.constraint(equalTo: cell.contentView.topAnchor, constant: 8).isActive = true - label.bottomAnchor.constraint(equalTo: cell.contentView.bottomAnchor, constant: -8).isActive = true - label.leadingAnchor.constraint(equalTo: titleLabel.trailingAnchor, constant: 8).isActive = true - label.trailingAnchor.constraint(equalTo: cell.contentView.layoutMarginsGuide.trailingAnchor).isActive = true - label.numberOfLines = 0 - label.font = UIFont.preferredFont(forTextStyle: .subheadline) - } - - row.title = title - row.value = value - } - } - - func multilineRow(_ title: String, value: String?) -> MultilineLabelRow { - MultilineLabelRow() { row in - row.title = title - row.value = value - } - } - - func update(vehicle: Vehicle) { - do { - if let realm = self.vehicle?.realm { - try realm.write { - realm.add(vehicle, update: .all) - } - } else { - self.vehicle?.osagoContracts.removeAll() - self.vehicle?.osagoContracts.append(objectsIn: vehicle.osagoContracts) - } - self.updateInterface() - } catch { - HUD.show(error: error) - print(error) - } - } -} diff --git a/AutoCat/Controllers/OwnersController.swift b/AutoCat/Controllers/OwnersController.swift deleted file mode 100644 index 4efb716..0000000 --- a/AutoCat/Controllers/OwnersController.swift +++ /dev/null @@ -1,70 +0,0 @@ -import UIKit -import Eureka -import AutoCatCore - -class OwnersController: FormViewController { - - public var owners: [VehicleOwnershipPeriod] = [] - - private var formatter = DateFormatter() - - override func viewDidLoad() { - super.viewDidLoad() - - self.tableView.rowHeight = UITableView.automaticDimension - - self.formatter.dateStyle = .long - self.formatter.timeStyle = .none - - self.title = String.localizedStringWithFormat(NSLocalizedString("owners count", comment: ""), self.owners.count) - - for (index, owner) in self.owners.enumerated() { - - let fromDate = Date(timeIntervalSince1970: TimeInterval(owner.from/1000)) - let from = self.formatter.string(from: fromDate) - var to = NSLocalizedString("now", comment: "") - if owner.to > 0 { - let toDate = Date(timeIntervalSince1970: TimeInterval(owner.to/1000)) - to = self.formatter.string(from: toDate) - } - - let section = Section(header: from + " - " + to, footer: owner.lastOperation) - form +++ section - <<< LabelRow("Owner\(index)") { row in - row.title = NSLocalizedString("Owner type", comment: "") - row.value = NSLocalizedString(owner.ownerType, comment: "") - } - - if let vehicleRegistrationRegion = owner.region { - section <<< MultilineLabelRow("VehicleRegion\(index)") { row in - row.title = NSLocalizedString("Vehicle region", comment: "") - row.value = vehicleRegistrationRegion - } - } - - - if let driverLocality = owner.locality { - var dRegion = driverLocality - if let driverRegion = owner.registrationRegion { - dRegion += " (\(driverRegion))" - } - section <<< MultilineLabelRow("DriverRegion\(index)") { row in - row.title = NSLocalizedString("Driver region", comment: "") - row.value = dRegion - } - } - - if let code = owner.code { - section <<< MultilineLabelRow("Code\(index)") { row in - row.title = NSLocalizedString("ZIP (or OKTMO) code", comment: "") - row.value = code - } - } - } - } - - override func viewWillAppear(_ animated: Bool) { - super.viewWillAppear(animated) - self.navigationController?.setNavigationBarHidden(false, animated: animated) - } -} diff --git a/AutoCat/Controllers/RecordsController.swift b/AutoCat/Controllers/RecordsController.swift index 4979dd7..c45aaec 100644 --- a/AutoCat/Controllers/RecordsController.swift +++ b/AutoCat/Controllers/RecordsController.swift @@ -1,7 +1,6 @@ import UIKit import AVFoundation import RealmSwift -import RxSwift import Intents import CoreSpotlight import MobileCoreServices @@ -15,8 +14,6 @@ class RecordsController: UIViewController, UITableViewDelegate { var recorder: Recorder? var addButton: UIBarButtonItem! - let bag = DisposeBag() - var recordDisposable: Disposable? var audioSessionObserver: NSObjectProtocol? var recordsDataSource: RealmSectionedDataSource! @@ -94,67 +91,69 @@ class RecordsController: UIViewController, UITableViewDelegate { var alert: UIAlertController? var url: URL! - let locationObservable = RxLocationManager.requestCurrentLocation() - .map(Optional.init) - .catchAndReturn(nil) - - let recordObservable: Single = recorder.requestPermissions() - .observe(on: MainScheduler.instance) - .flatMap(self.makeStartSoundIfNeeded) - .flatMap { - #if targetEnvironment(macCatalyst) || targetEnvironment(simulator) - DispatchQueue.main.async { + Task { + do { + async let locationTask = RxLocationManager.requestCurrentLocation() + async let permissionTask: () = recorder.requestPermissions() + let (event, _) = try await (locationTask, permissionTask) + + await makeStartSoundIfNeeded() + +#if targetEnvironment(macCatalyst) || targetEnvironment(simulator) + DispatchQueue.main.async { + alert = self.showRecordingAlert() + } +#else + if let observer = self.audioSessionObserver { + NotificationCenter.default.removeObserver(observer, name: AVAudioSession.routeChangeNotification, object: nil) + } + self.audioSessionObserver = NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { notification in + guard let dict = notification.userInfo as? [String: Any], + let reasonInt = dict["AVAudioSessionRouteChangeReasonKey"] as? NSNumber, + let reason = AVAudioSession.RouteChangeReason(rawValue: reasonInt.uintValue), + let session = notification.object as? AVAudioSession else { return } + + if reason == .categoryChange && session.category == .playAndRecord { alert = self.showRecordingAlert() } - #else - if let observer = self.audioSessionObserver { - NotificationCenter.default.removeObserver(observer, name: AVAudioSession.routeChangeNotification, object: nil) - } - self.audioSessionObserver = NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: nil, queue: .main) { notification in - guard let dict = notification.userInfo as? [String: Any], - let reasonInt = dict["AVAudioSessionRouteChangeReasonKey"] as? NSNumber, - let reason = AVAudioSession.RouteChangeReason(rawValue: reasonInt.uintValue), - let session = notification.object as? AVAudioSession else { return } - - if reason == .categoryChange && session.category == .playAndRecord { - alert = self.showRecordingAlert() - } - } - #endif + } +#endif let date = Date() let fileName = "recording-\(date.timeIntervalSince1970).m4a" url = try FileManager.default.url(for: fileName, in: "recordings") - return recorder.startRecording(to: url) - } - - self.recordDisposable = Single.zip(locationObservable, recordObservable) { event, text -> AudioRecord in - let asset = AVURLAsset(url: url) - let duration = TimeInterval(CMTimeGetSeconds(asset.duration)) - return AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: text), raw: text, duration: duration, event: event) - } - .subscribe(onSuccess: { record in - let realm = try? Realm() - try? realm?.write { - realm?.add(record) - } - alert?.dismiss(animated: true) - self.addButton.isEnabled = true - }, onFailure: { error in - if let alert = alert { - alert.dismiss(animated: true) { + let text = try await recorder.startRecording(to: url) + + let asset = AVURLAsset(url: url) + let duration = TimeInterval(CMTimeGetSeconds(asset.duration)) + let record = AudioRecordDto(path: url.lastPathComponent, + number: self.getPlateNumber(from: text), + raw: text, + duration: duration, + event: event) + + let realm = try await Realm() + try realm.write { + realm.add(AudioRecord(dto: record)) + } + alert?.dismiss(animated: true) + self.addButton.isEnabled = true + } catch { + if let alert = alert { + alert.dismiss(animated: true) { + HUD.show(error: error) + } + } else { HUD.show(error: error) } - } else { - HUD.show(error: error) + self.addButton.isEnabled = true } - self.addButton.isEnabled = true - }) + } } func showRecordingAlert() -> UIAlertController { let alert = UIAlertController(title: NSLocalizedString("Recording...", comment: ""), message: nil, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { _ in self.recordDisposable?.dispose() })) + alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { _ in self.recorder?.cancelRecording() })) alert.addAction(UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default, handler: { _ in self.recorder?.stopRecording() })) self.present(alert, animated: true) return alert @@ -207,18 +206,17 @@ class RecordsController: UIViewController, UITableViewDelegate { && region! < 1000 } - func makeStartSoundIfNeeded() -> Single { - if !Settings.shared.recordBeep { - return .just(()) - } else { - return Single.create { observer in - var soundId = SystemSoundID() - let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf") - AudioServicesCreateSystemSoundID(url as CFURL, &soundId) - AudioServicesPlaySystemSoundWithCompletion(soundId) { - observer(.success(())) - } - return Disposables.create() + func makeStartSoundIfNeeded() async { + guard Settings.shared.recordBeep else { + return + } + + return await withCheckedContinuation { continuation in + var soundId = SystemSoundID() + let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf") + AudioServicesCreateSystemSoundID(url as CFURL, &soundId) + AudioServicesPlaySystemSoundWithCompletion(soundId) { + continuation.resume() } } } @@ -297,7 +295,7 @@ class RecordsController: UIViewController, UITableViewDelegate { return configuration } - func moreActions(for record: AudioRecord, cell: UITableViewCell) { + func moreActions(for record: AudioRecordDto, cell: UITableViewCell) { let sheet = UIAlertController(title: NSLocalizedString("More actions", comment: ""), message: nil, preferredStyle: .actionSheet) let cancel = UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel) { _ in sheet.dismiss(animated: true, completion: nil) } let share = UIAlertAction(title: NSLocalizedString("Share", comment: ""), style: .default) { _ in @@ -325,19 +323,19 @@ class RecordsController: UIViewController, UITableViewDelegate { self.present(sheet, animated: true, completion: nil) } - func check(number: String, event: VehicleEvent?) { + func check(number: String, event: VehicleEventDto?) { guard let ad = UIApplication.shared.delegate as? AppDelegate else { return } ad.quickAction = .checkNumber(number, event) self.tabBarController?.selectedIndex = 0 } - func edit(record: AudioRecord) { + func edit(record: AudioRecordDto) { let alert = UIAlertController(title: NSLocalizedString("Edit plate number", comment: ""), message: nil, preferredStyle: .alert) let done = UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default) { action in guard let tf = alert.textFields?.first else { return } - if let realm = try? Realm() { + if let realm = try? Realm(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) { try? realm.write { - record.number = tf.text?.uppercased() + realmRecord.number = tf.text?.uppercased() } } } @@ -347,18 +345,20 @@ class RecordsController: UIViewController, UITableViewDelegate { })) alert.addTextField { tf in tf.text = record.number ?? record.rawText.replacingOccurrences(of: " ", with: "") - NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: tf, queue: OperationQueue.main) { _ in - done.isEnabled = self.valid(number: tf.text?.uppercased() ?? "") + NotificationCenter.default.addObserver(forName: UITextField.textDidChangeNotification, object: tf, queue: nil) { _ in + DispatchQueue.main.async { + done.isEnabled = self.valid(number: tf.text?.uppercased() ?? "") + } } } self.present(alert, animated: true) } - func delete(record: AudioRecord) { + func delete(record: AudioRecordDto) { do { - if let realm = record.realm { + if let realm = try? Realm(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) { try realm.write { - realm.delete(record) + realm.delete(realmRecord) } } } catch { @@ -366,7 +366,7 @@ class RecordsController: UIViewController, UITableViewDelegate { } } - func share(record: AudioRecord) { + func share(record: AudioRecordDto) { do { let url = try FileManager.default.url(for: record.path, in: "recordings") let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil) @@ -377,7 +377,7 @@ class RecordsController: UIViewController, UITableViewDelegate { } } - func showOnMap(_ record: AudioRecord) { + func showOnMap(_ record: AudioRecordDto) { let controller = ShowEventController() controller.event = record.event controller.hidesBottomBarWhenPushed = true diff --git a/AutoCat/Controllers/RegionsController.swift b/AutoCat/Controllers/RegionsController.swift index 9e6ccbf..075fdf1 100644 --- a/AutoCat/Controllers/RegionsController.swift +++ b/AutoCat/Controllers/RegionsController.swift @@ -1,5 +1,4 @@ import UIKit -import RxSwift import AutoCatCore class RegionsDataSourse: UITableViewDiffableDataSource { @@ -20,7 +19,6 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD var datasource: RegionsDataSourse! let searchController = UISearchController(searchResultsController: nil) - let bag = DisposeBag() override func viewDidLoad() { super.viewDidLoad() @@ -47,14 +45,16 @@ class RegionsController: UIViewController, UISearchResultsUpdating, UITableViewD return cell } - Api.getRegions().observeOn(MainScheduler.instance).subscribe(onSuccess: { regions in - self.regions = regions - self.regionsFiltered = regions - self.updateTableView() - self.applySelection() - }, onError: { error in - print("Get regions error: ", error) - }).disposed(by: self.bag) + Task { + do { + self.regions = try await ApiService.shared.getRegions() + self.regionsFiltered = regions + self.updateTableView() + self.applySelection() + } catch { + print("Get regions error: ", error) + } + } } override func viewWillDisappear(_ animated: Bool) { diff --git a/AutoCat/Controllers/ReportController.swift b/AutoCat/Controllers/ReportController.swift index 1ae284c..50c1050 100644 --- a/AutoCat/Controllers/ReportController.swift +++ b/AutoCat/Controllers/ReportController.swift @@ -7,21 +7,19 @@ import AutoCatCore import SwiftEntryKit import MobileCoreServices import PKHUD -import RxSwift -class ReportController: FormViewController, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate, UIActivityItemSource { +class ReportController: FormViewController, MediaBrowserViewControllerDataSource, MediaBrowserViewControllerDelegate { @IBOutlet weak var actionBarItem: UIBarButtonItem! @IBOutlet weak var copyBarItem: UIBarButtonItem! - private var reportImageUrl: URL? private let logoPlaceholder = UIImage(named: "SteeringWheel") private let copyableTags = ["Model", "Year", "Color", "Category", "STP", "Japanese", "PlateNumber", "VIN", "STS", "PTS", "EngineNumber", "FuelType", "Volume", "PowerHP", "PowerKw"]; - var vehicle: Vehicle? { + var vehicle: VehicleDto? { didSet { if isViewLoaded && self.view.window != nil { self.updateReport() @@ -37,15 +35,13 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource if let realm = try? Realm(), let num = number { let vehicles = realm.objects(Vehicle.self).filter("number = %@", num) self.notificationToken?.invalidate() - self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first } + self.notificationToken = vehicles.observe { _ in self.vehicle = vehicles.first?.dto } } else { self.vehicle = nil } } } - let bag = DisposeBag() - // MARK: - Lifecycle override func viewDidLoad() { @@ -93,10 +89,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource <<< LabelRow("OSAGO") { $0.title = NSLocalizedString("OSAGO", comment: "") } .cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator } .onCellSelection { _, _ in - let sb = UIStoryboard(name: "Main", bundle: nil) - let controller = sb.instantiateViewController(identifier: "OsagoController") as OsagoController - controller.vehicle = self.vehicle - self.navigationController?.pushViewController(controller, animated: true) + if let contracts = self.vehicle?.osagoContracts, let navController = self.navigationController { + let coordinator = OsagoCoordinator(navController: navController, contracts: contracts) + Task { try await coordinator.start() } + } } <<< LabelRow("Owners") { row in @@ -106,10 +102,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource .cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator } .onCellSelection { _, row in if row.value != "0" { - let sb = UIStoryboard(name: "Main", bundle: nil) - let controller = sb.instantiateViewController(identifier: "OwnersController") as OwnersController - controller.owners = self.vehicle?.ownershipPeriods.toArray() ?? [] - self.navigationController?.pushViewController(controller, animated: true) + if let ownerships = self.vehicle?.ownershipPeriods, let navController = self.navigationController { + let coordinator = OwnersCoordinator(navController: navController, ownerships: ownerships) + Task { try await coordinator.start() } + } } } @@ -134,7 +130,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource .cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator } .onCellSelection { _, row in let controller = AdsController() - controller.ads = self.vehicle?.ads.toArray() ?? [] + controller.ads = self.vehicle?.ads ?? [] self.navigationController?.pushViewController(controller, animated: true) } @@ -143,10 +139,10 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource } .cellUpdate { cell, _ in cell.accessoryType = .disclosureIndicator } .onCellSelection { _, row in - let sb = UIStoryboard(name: "Main", bundle: nil) - let controller = sb.instantiateViewController(identifier: "NotesController") as NotesController - controller.vehicle = self.vehicle - self.navigationController?.pushViewController(controller, animated: true) + if let vehicle = self.vehicle, let navController = self.navigationController { + let coordinator = NotesCoordinator(navController: navController, vehicle: vehicle) + Task { try await coordinator.start() } + } } if Settings.shared.showDebugInfo { @@ -160,7 +156,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource form +++ Section("") <<< ButtonRow("CheckGB") { $0.title = NSLocalizedString("Check GB", comment: "") }.onCellSelection { cell, row in - self.checkGB() + Task { await self.checkGB() } } setupCopyBehaviour() @@ -213,7 +209,7 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource } } - func update(sourceStatusRow tag: String, with value: DebugInfoEntry) { + func update(sourceStatusRow tag: String, with value: DebugInfoEntryDto) { if let row = self.form.rowBy(tag: tag) as? SourceStatusRow { row.value = value row.reload() @@ -279,27 +275,31 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource self.updateReport() } - func checkGB() { + func checkGB() async { guard let vehicle = self.vehicle else { return } - HUD.show(.progress) - Api.checkVehicleGb(by: vehicle.getNumber()).observe(on: MainScheduler.instance).subscribe(onSuccess: { newVehicle in - if let realm = vehicle.realm, !vehicle.isFrozen { + do { + HUD.show(.progress) + let newVehicle = try await ApiService.shared.checkVehicleGb(by: vehicle.getNumber()) + + let realm = try await Realm() + if let realmVehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) { try? realm.write { - realm.add(newVehicle, update: .all) + realm.add(Vehicle(dto: newVehicle), update: .all) } } else { self.vehicle?.vin1 = newVehicle.vin1 self.vehicle?.color = newVehicle.color self.vehicle?.sts = newVehicle.sts } + self.updateReport() self.form.allSections.forEach { $0.reload() } HUD.hide() - }, onFailure: { error in + } catch { HUD.hide() self.show(error: error) - }).disposed(by: self.bag) + } } // MARK: - MediaBrowserViewControllerDataSource & MediaBrowserViewControllerDelegate @@ -316,13 +316,15 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource } KingfisherManager.shared.retrieveImage(with: url) { result in - switch result { - case .success(let res): - completion(index, res.image, ZoomScale.default, nil) - break - case .failure(let error): - completion(index, nil, ZoomScale.default, error) - break + Task { @MainActor in + switch result { + case .success(let res): + completion(index, res.image, ZoomScale.default, nil) + break + case .failure(let error): + completion(index, nil, ZoomScale.default, error) + break + } } } } @@ -350,10 +352,11 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource let fileURL = documentDirectory.appendingPathComponent("report.png") if let imageData = image.pngData() { try imageData.write(to: fileURL) - self.reportImageUrl = fileURL } - let controller = UIActivityViewController(activityItems: [self], applicationActivities: nil) + let item = ActivityItemSource(url: fileURL, title: vehicle.getNumber()) + + let controller = UIActivityViewController(activityItems: [item], applicationActivities: nil) controller.popoverPresentationController?.barButtonItem = sender self.present(controller, animated: true) } catch { @@ -365,12 +368,13 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource guard let vehicle = self.vehicle else { return } var items: [Any] = [vehicle.reportText()] for photo in vehicle.photos { - if let url = URL(string: photo.url) { - if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { - items.append(image) - } - - } + // TODO: Fix sharing +// if let url = URL(string: photo.url) { +// if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { +// items.append(image) +// } +// +// } } let controller = UIActivityViewController(activityItems: items, applicationActivities: nil) @@ -408,26 +412,6 @@ class ReportController: FormViewController, MediaBrowserViewControllerDataSource self.present(sheet, animated: true, completion: nil) } - func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { - return UIImage() - } - - func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { - return self.reportImageUrl - } - - func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { - guard let url = self.reportImageUrl else { return nil } - - let metadata = LPLinkMetadata() - metadata.title = self.vehicle?.getNumber() - metadata.originalURL = url - metadata.url = url - metadata.imageProvider = NSItemProvider.init(contentsOf: url) - metadata.iconProvider = NSItemProvider.init(contentsOf: url) - return metadata - } - // MARK: - Copy @IBAction func onCopy(_ sender: UIBarButtonItem) { diff --git a/AutoCat/Controllers/SearchController.swift b/AutoCat/Controllers/SearchController.swift index ff7f80c..538ee67 100644 --- a/AutoCat/Controllers/SearchController.swift +++ b/AutoCat/Controllers/SearchController.swift @@ -1,6 +1,4 @@ import UIKit -import RxSwift -import RxCocoa import RealmSwift import PKHUD import ExceptionCatcher @@ -15,8 +13,6 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe private var refreshIndicator: UIBarButtonItem! private var moreActionsButton: UIBarButtonItem? - private let bag = DisposeBag() - private lazy var searchController: UISearchController = .default .placeholder(NSLocalizedString("Search plate numbers", comment: "")) .resultsUpdater(self) @@ -25,11 +21,10 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe .scopeButtons(SearchScope.allCases.map(\.title)) private var refreshControl = UIRefreshControl() - private var datasource: RxSectionedDataSource! + private var datasource: SectionedDataSource! private var isLoadingPage = false private var pageLoadingIndicator = UIActivityIndicatorView(style: .medium) - var filterRelay = BehaviorRelay(value: Filter()) var filter = Filter() override func viewDidLoad() { @@ -58,37 +53,31 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe self.refreshControl.addTarget(self, action: #selector(self.refresh(_:)), for: .valueChanged) self.tableView.addSubview(self.refreshControl) - self.datasource = RxSectionedDataSource(table: self.tableView) + self.datasource = SectionedDataSource(table: self.tableView) self.tableView.delegate = self self.tableView.keyboardDismissMode = .onDrag - - DispatchQueue.main.async { - self.filterRelay - .debounce(.milliseconds(500), scheduler: MainScheduler.instance) - .do(onNext: { _ in - self.showProgress() - }) - .flatMapLatest { filter in - if filter.needReset { - self.datasource.reset() - } - return Api.getVehicles(with: filter, pageToken: self.datasource.pageToken) - .do(onError: { print($0) }) - .catchAndReturn(PagedResponse()) - } - .observe(on: MainScheduler.instance) - .do(onNext: { - if let count = $0.count { - self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count) - self.showMapButton?.isEnabled = count > 0 - } - self.refreshControl.endRefreshing() - self.isLoadingPage = false - self.pageLoadingIndicator.stopAnimating() - self.hideProgress() - }) - .bind(to: self.datasource.data) - .disposed(by: self.bag) + } + + func updateSearchResults(with filter: Filter) { + Task { + showProgress() + + if filter.needReset { + self.datasource.reset() + } + + let vehicles = (try? await ApiService.shared.getVehicles(with: filter, pageToken: self.datasource.pageToken, pageSize: 50)) ?? PagedResponse() + + if let count = vehicles.count { + self.navigationItem.title = String.localizedStringWithFormat(NSLocalizedString("vehicles found", comment: ""), count) + self.showMapButton?.isEnabled = count > 0 + } + + refreshControl.endRefreshing() + isLoadingPage = false + pageLoadingIndicator.stopAnimating() + hideProgress() + datasource.update(with: vehicles) } } @@ -109,7 +98,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe } // FIXME: Code duplication - func updateDetailController(with vehicle: Vehicle) { + func updateDetailController(with vehicle: VehicleDto) { if let splitViewController = self.view.window?.rootViewController as? UISplitViewController { var detail: UINavigationController? @@ -140,7 +129,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe self.filter.needReset = true self.filter.scope = SearchScope(rawValue: searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber - self.filterRelay.accept(self.filter) + updateSearchResults(with: filter) } func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) { @@ -149,7 +138,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe } filter.scope = scope - filterRelay.accept(filter) + updateSearchResults(with: filter) } // MARK: NavigationBar actions @@ -185,7 +174,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe @objc func refresh(_ sender: AnyObject) { self.showMapButton?.isEnabled = false self.filter.needReset = true - self.filterRelay.accept(self.filter) + updateSearchResults(with: filter) } func showFilter() { @@ -197,7 +186,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe self.datasource.setSortParameter(self.filter.sortBy ?? .updatedDate) self.filter.needReset = true self.filter.scope = SearchScope(rawValue: self.searchController.searchBar.selectedScopeButtonIndex) ?? .plateNumber - self.filterRelay.accept(self.filter) + self.updateSearchResults(with: self.filter) } self.navigationController?.pushViewController(controller, animated: true) } @@ -214,37 +203,33 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe } func exportSearchResults() { - showProgress() - - Api.getVehicles(with: filter, pageSize: 0) - .observe(on: MainScheduler.instance) - .subscribe(onSuccess: { resp in - self.hideProgress() + Task { + do { + showProgress() + let resp = try await ApiService.shared.getVehicles(with: filter, pageSize: 0) let newLine = "\r\n" - var csvString = Vehicle.csvHeader + newLine + var csvString = VehicleDto.csvHeader + newLine for vehicle in resp.items { csvString.append(vehicle.csvLine) csvString.append(newLine) } - do { - let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv") - try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8) + let tmpUrl = FileManager.default.tmpUrl(name: "search", ext: "csv") + try csvString.write(to: tmpUrl, atomically: true, encoding: .utf8) #if targetEnvironment(macCatalyst) - self.save(file: tmpUrl) + self.save(file: tmpUrl) #else - self.share(file: tmpUrl) + self.share(file: tmpUrl) #endif - } catch { - HUD.show(error: error) - } - }, onFailure: { error in - self.hideProgress() + + hideProgress() + } catch { + hideProgress() HUD.show(error: error) - }) - .disposed(by: bag) + } + } } func share(file url: URL) { @@ -290,28 +275,26 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe } } - func update(vehicle: Vehicle, at indexPath: IndexPath) { - HUD.show(.progress) - Api.checkVehicle(by: vehicle.getNumber(), notes: Array(vehicle.notes), events: [], force: true).observe(on: MainScheduler.instance).subscribe { newVehicle in - HUD.hide() + func update(vehicle: VehicleDto, at indexPath: IndexPath) { + + Task { do { - let realm = try Realm() + HUD.show(.progress) + let newVehicle = try await ApiService.shared.checkVehicle(by: vehicle.getNumber(), notes: vehicle.notes, events: [], force: true) + HUD.hide() + let realm = try await Realm() if realm.object(ofType: Vehicle.self, forPrimaryKey: vehicle.getNumber()) != nil { try realm.write { - realm.add(newVehicle, update: .all) + realm.add(Vehicle(dto: newVehicle), update: .all) } } + datasource.set(item: newVehicle, at: indexPath) + updateDetailController(with: newVehicle) } catch { - print(error) - self.show(error: error) + HUD.hide() + show(error: error) } - - let frozenVehicle = newVehicle.realm != nil ? newVehicle.clone() : newVehicle - self.datasource.set(item: frozenVehicle, at: indexPath) - self.updateDetailController(with: frozenVehicle) - } onFailure: { err in - HUD.show(error: err) - }.disposed(by: self.bag) + } } func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { @@ -326,7 +309,7 @@ class SearchController: UIViewController, UISearchResultsUpdating, UITableViewDe if toBottom < 100 && !self.isLoadingPage && self.datasource.needMoreData() { self.isLoadingPage = true self.filter.needReset = false - self.filterRelay.accept(self.filter) + updateSearchResults(with: filter) } } } diff --git a/AutoCat/Extensions/AudioEngine.swift b/AutoCat/Extensions/AudioEngine.swift deleted file mode 100644 index f780362..0000000 --- a/AutoCat/Extensions/AudioEngine.swift +++ /dev/null @@ -1,25 +0,0 @@ -import Foundation -import AVFoundation -import RxSwift - -extension AVAudioSession { - func setCategoryAsync(_ category: AVAudioSession.Category) -> Single { - if self.category == category { - return .just(()) - } else { - return Single.create { observer in - NotificationCenter.default.addObserver(forName: AVAudioSession.routeChangeNotification, object: self, queue: .main) { notification in - print("") - } - - do { - try self.setCategory(category, mode: .default, options: []) - } catch { - observer(.failure(error)) - } - - return Disposables.create() - } - } - } -} diff --git a/AutoCat/Extensions/Dated.swift b/AutoCat/Extensions/Dated.swift index d0c6313..577da40 100644 --- a/AutoCat/Extensions/Dated.swift +++ b/AutoCat/Extensions/Dated.swift @@ -9,7 +9,7 @@ protocol Dated { var updatedTimestamp: TimeInterval { get } } -extension Vehicle: Dated { +extension VehicleDto: Dated { var updated: Date { Date(timeIntervalSince1970: self.updatedDate) @@ -28,7 +28,7 @@ extension Vehicle: Dated { } } -extension AudioRecord: Dated { +extension AudioRecordDto: Dated { var updated: Date { Date(timeIntervalSince1970: self.getAddedDate()) } @@ -53,7 +53,10 @@ extension RandomAccessCollection where Element: Dated & Identifiable { var key: TimeInterval = 0 var keyNext: TimeInterval = 0 var index = self.index(before: endIndex) - let currentMonthStart = DateCache.shared.monthStart.timeIntervalSince1970 + + let now = DateInRegion(Date(), region: Region.current) + let monthStart = now.dateAtStartOf(.month) + let weekStart = now.dateAtStartOf(.weekOfMonth) while index >= startIndex { @@ -65,12 +68,16 @@ extension RandomAccessCollection where Element: Dated & Identifiable { if keyNext == 0 || timestamp > keyNext { - let component: Calendar.Component = timestamp >= currentMonthStart ? .day : .month + let component: Calendar.Component = timestamp >= monthStart.timeIntervalSince1970 ? .day : .month let dateInRegion = DateInRegion(seconds: timestamp, region: .current) let startOfPeriod = dateInRegion.dateAtStartOf(component) key = startOfPeriod.timeIntervalSince1970 - sectionsArray.insert(DateSection(timestamp: key, items: []), at: 0) + let section = DateSection(timestamp: key, + items: [], + monthStart: monthStart, + weekStart: weekStart) + sectionsArray.insert(section, at: 0) keyNext = startOfPeriod.dateByAdding(1, component).timeIntervalSince1970 } diff --git a/AutoCat/Extensions/VehicleReportImage.swift b/AutoCat/Extensions/VehicleReportImage.swift index e6a27e2..f51fd17 100644 --- a/AutoCat/Extensions/VehicleReportImage.swift +++ b/AutoCat/Extensions/VehicleReportImage.swift @@ -2,7 +2,9 @@ import UIKit import Kingfisher import AutoCatCore -extension Vehicle { +extension VehicleDto { + + @MainActor func drawLine(y: CGFloat, width: CGFloat, margin: CGFloat = 15,context: CGContext) { let lineWidth = 1/UIScreen.main.scale context.move(to: CGPoint(x: margin, y: y + lineWidth/2)) @@ -13,6 +15,7 @@ extension Vehicle { context.strokePath() } + @MainActor func drawCell(y: CGFloat, width: CGFloat, height: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) { let fontSize: CGFloat = 17 let font = UIFont.systemFont(ofSize: fontSize) @@ -29,6 +32,7 @@ extension Vehicle { self.drawLine(y: y + height - 1/UIScreen.main.scale, width: width, margin: lineMargin, context: context) } + @MainActor func drawBigTextCell(y: CGFloat, width: CGFloat, title: String, value: String, lineMargin: CGFloat = 15, context: CGContext) -> CGFloat { let pStyle = NSMutableParagraphStyle() pStyle.alignment = .right @@ -50,6 +54,7 @@ extension Vehicle { return height } + @MainActor func reportImage(width: CGFloat) -> UIImage { var realHeight: CGFloat = 0 let rect = CGRect(origin: .zero, size: CGSize(width: width, height: CGFloat(10000))) @@ -177,28 +182,29 @@ extension Vehicle { y += 24 self.drawLine(y: y, width: w, margin: 0, context: ctx) y += 1/UIScreen.main.scale - - for photo in self.photos { - let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000)) - var name = "" - if let brand = photo.brand, let model = photo.model { - name = "\(brand) \(model)" - } - - if let url = URL(string: photo.url) { - if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { - let imgHeight = image.size.height*w/image.size.width - let rect = CGRect(x: 0, y: y, width: w, height: imgHeight) - image.draw(in: rect) - y += imgHeight - } - } - - self.drawCell(y: y, width: w, height: cellHeight, title: name, value: formatter.string(from: date), lineMargin: 0, context: ctx) - y += cellHeight - - y += 16 - } + + // TODO: Fix drawing photos in report image +// for photo in self.photos { +// let date = Date(timeIntervalSince1970: TimeInterval(photo.date/1000)) +// var name = "" +// if let brand = photo.brand, let model = photo.model { +// name = "\(brand) \(model)" +// } +// +// if let url = URL(string: photo.url) { +// if let image = ImageCache.default.retrieveImageInDiskCache(forKey: url.cacheKey) { +// let imgHeight = image.size.height*w/image.size.width +// let rect = CGRect(x: 0, y: y, width: w, height: imgHeight) +// image.draw(in: rect) +// y += imgHeight +// } +// } +// +// self.drawCell(y: y, width: w, height: cellHeight, title: name, value: formatter.string(from: date), lineMargin: 0, context: ctx) +// y += cellHeight +// +// y += 16 +// } realHeight = y } diff --git a/AutoCat/JS/dkbm.js b/AutoCat/JS/dkbm.js deleted file mode 100644 index 6f9f725..0000000 --- a/AutoCat/JS/dkbm.js +++ /dev/null @@ -1,37 +0,0 @@ -//const sitekey = '6Lf2uycUAAAAALo3u8D10FqNuSpUvUXlfP7BzHOk'; - -let verifyCallback = (response) => { - console.log('verifyCallback: ', response); - window.webkit.messageHandlers.dkbmHandler.postMessage({ token: response }); -}; - -var timerId; - -function getImages() { - let nodes = document.querySelectorAll(".policies-tbl img"); - let urls = Array(...nodes).map(img => img.src); - if(urls.length > 1) { - window.webkit.messageHandlers.dkbmHandler.postMessage({ "url": urls[1] }); - clearInterval(timerId); - } -} - -timerId = setInterval(getImages, 1000); - -window.addEventListener('load', async (event) => { - if(window.top == window.self) { - - let { plateNumber, vin } = await window.webkit.messageHandlers.dkbmHandler.postMessage({ "loaded": "true" }); - switchTab('tsBlock'); - - if(plateNumber) { - let licencePlateInput = document.getElementById('licensePlate'); - licencePlateInput.value = plateNumber; - } - - if(vin) { - let vinInput = document.getElementById('vin'); - vinInput.value = vin; - } - } -}) diff --git a/AutoCat/Preview/ApiServiceStub.swift b/AutoCat/Preview/ApiServiceStub.swift new file mode 100644 index 0000000..410009f --- /dev/null +++ b/AutoCat/Preview/ApiServiceStub.swift @@ -0,0 +1,26 @@ +// +// ApiServiceStub.swift +// AutoCat +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import AutoCatCore + +actor ApiServiceStub: ApiServiceProtocol { + + let vehicle = VehicleDto() + + func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto { + vehicle + } + + func edit(note: VehicleNoteDto) async throws -> VehicleDto { + vehicle + } + + func remove(note id: String) async throws -> VehicleDto { + vehicle + } +} diff --git a/AutoCat/Preview/StorageServiceStub.swift b/AutoCat/Preview/StorageServiceStub.swift new file mode 100644 index 0000000..5f3113b --- /dev/null +++ b/AutoCat/Preview/StorageServiceStub.swift @@ -0,0 +1,28 @@ +// +// StorageServiceStub.swift +// AutoCat +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import AutoCatCore + +actor StorageServiceStub: StorageServiceProtocol { + + let vehicle = VehicleDto() + + func addNote(text: String, to number: String) async throws -> AutoCatCore.VehicleDto { + vehicle + } + + func deleteNote(id: String, for number: String) async throws -> AutoCatCore.VehicleDto { + vehicle + } + + func editNote(id: String, text: String, for number: String) async throws -> AutoCatCore.VehicleDto { + vehicle + } + + func updateVehicleIfExists(dto: AutoCatCore.VehicleDto) async throws { } +} diff --git a/AutoCat/SceneDelegate.swift b/AutoCat/SceneDelegate.swift index a69d494..4c9dea3 100644 --- a/AutoCat/SceneDelegate.swift +++ b/AutoCat/SceneDelegate.swift @@ -1,7 +1,6 @@ import UIKit import os.log import AVFoundation -import RxSwift import PKHUD import AutoCatCore @@ -112,7 +111,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if tabvc.selectedIndex == 0 { if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController { if let check = child as? CheckController { - check.handleQuickActions() + Task { await check.handleQuickActions() } } else { nav.popToRootViewController(animated: false) } @@ -134,7 +133,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let url = userActivity.webpageURL { if let param = URLComponents(url: url, resolvingAgainstBaseURL: false)?.queryItems?.first, let token = param.value { if let jwt = JWT(string: token) { - self.openReport(with: jwt.payload.plateNumber) + Task { await self.openReport(with: jwt.payload.plateNumber) } } } } @@ -159,11 +158,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - func openReport(with number: String) { + func openReport(with number: String) async { guard let rootController = self.window?.rootViewController else { return } - HUD.show(.progress) - _ = Api.getReport(for: number).observe(on: MainScheduler.instance).subscribe { vehicle in + do { + HUD.show(.progress) + let vehicle = try await ApiService.shared.getReport(for: number) let sb = UIStoryboard(name: "Main", bundle: nil) let controller = sb.instantiateViewController(identifier: "ReportController") as ReportController controller.vehicle = vehicle @@ -172,7 +172,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { controller.navigationItem.leftBarButtonItem = BlockBarButtonItem(barButtonSystemItem: .close) { _ in nav.dismiss(animated: true) } rootController.present(nav, animated: true) HUD.hide() - } onFailure: { error in + } catch { HUD.show(error: error) } } diff --git a/AutoCat/Screens/NotesScreen/NoteAlertModifier.swift b/AutoCat/Screens/NotesScreen/NoteAlertModifier.swift new file mode 100644 index 0000000..96f10e5 --- /dev/null +++ b/AutoCat/Screens/NotesScreen/NoteAlertModifier.swift @@ -0,0 +1,44 @@ +// +// NoteAlertModifier.swift +// AutoCat +// +// Created by Selim Mustafaev on 07.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct NoteAlertModifier: ViewModifier { + + let title: String + let isPresented: Binding + let action: ((String) -> Void)? + + @Binding var text: String + + func body(content: Content) -> some View { + + content + .alert(title, isPresented: isPresented) { + TextField("", text: $text) + Button("Cancel") {} + Button("Done") { + action?(text) + } + } + } +} + +extension View { + + func noteAlert(title: String, + body: Binding, + isPresented: Binding, + action: ((String) -> Void)?) -> some View { + + modifier(NoteAlertModifier(title: title, + isPresented: isPresented, + action: action, + text: body)) + } +} diff --git a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift new file mode 100644 index 0000000..9d7aeec --- /dev/null +++ b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift @@ -0,0 +1,32 @@ +// +// NotesCoordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 24.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import SwiftUI +import AutoCatCore + +class NotesCoordinator: Coordinator { + + let viewController: UINavigationController? + let vehicle: VehicleDto + + init(navController: UINavigationController, vehicle: VehicleDto) { + + self.viewController = navController + self.vehicle = vehicle + } + + func start() async throws { + + let viewModel = await NotesViewModel(vehicle: vehicle, + storageService: try await StorageService.shared, + apiService: ApiService.shared) + let controller = await UIHostingController(rootView: NotesScreen(viewModel: viewModel)) + await viewController?.pushViewController(controller, animated: true) + } +} diff --git a/AutoCat/Screens/NotesScreen/NotesScreen.swift b/AutoCat/Screens/NotesScreen/NotesScreen.swift new file mode 100644 index 0000000..1f4ed29 --- /dev/null +++ b/AutoCat/Screens/NotesScreen/NotesScreen.swift @@ -0,0 +1,104 @@ +// +// NotesScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 24.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct NotesScreen: View { + + @StateObject var viewModel: NotesViewModel + + @State var showNewNoteAlert = false + @State var showEditNoteAlert = false + @State var selectedNoteId = "" + @State var noteText = "" + + var body: some View { + + List(viewModel.vehicle.notes) { note in + VStack(alignment: .leading) { + Text(note.text) + HStack { + Spacer() + Text(Formatters.standard.string(from: Date(timeIntervalSince1970: note.date))) + .font(.footnote) + .foregroundColor(.secondary) + } + } + .swipeActions(allowsFullSwipe: false) { + makeActions(for: note) + } + .contextMenu { + makeActions(for: note, useLabels: true) + } + } + .listStyle(.inset) + .navigationTitle("Notes") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .primaryAction) { + Button { + noteText = "" + showNewNoteAlert = true + } label: { + Image(systemName: "plus") + } + .noteAlert(title: "New note", body: $noteText, isPresented: $showNewNoteAlert) { text in + Task { await viewModel.addNote(text: text) } + } + } + } + .hud($viewModel.hud) + .noteAlert(title: "Edit note", + body: $noteText, + isPresented: $showEditNoteAlert) { text in + Task { await viewModel.editNote(id: selectedNoteId, text: text) } + } + } + + @ViewBuilder + func makeActions(for note: VehicleNoteDto, useLabels: Bool = false) -> some View { + Button() { + Task { await viewModel.deleteNote(id: note.id) } + } label: { + Label(useLabels ? "Delete" : "", systemImage: "trash") + } + .tint(.red) + + Button() { + selectedNoteId = note.id + noteText = note.text + showEditNoteAlert = true + } label: { + Label(useLabels ? "Edit" : "", systemImage: "pencil") + } + + Button() { + viewModel.copyNote(note) + } label: { + Label(useLabels ? "Copy" : "", systemImage: "doc.on.doc") + } + } +} + +#Preview { + + var vehicle = VehicleDto() + + vehicle.notes = [ + .init(text: "qwe"), + .init(text: "asdf"), + .init(text: "zxcv") + ] + + let vm = NotesViewModel(vehicle: vehicle, + storageService: StorageServiceStub(), + apiService: ApiServiceStub()) + + return NotesScreen(viewModel: vm) +} diff --git a/AutoCat/Screens/NotesScreen/NotesViewModel.swift b/AutoCat/Screens/NotesScreen/NotesViewModel.swift new file mode 100644 index 0000000..8aa60d6 --- /dev/null +++ b/AutoCat/Screens/NotesScreen/NotesViewModel.swift @@ -0,0 +1,91 @@ +// +// NotesViewModel.swift +// AutoCat +// +// Created by Selim Mustafaev on 24.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import AutoCatCore +import UIKit +import UniformTypeIdentifiers + +@MainActor +class NotesViewModel: ObservableObject, ACHudContainer { + + let storageService: StorageServiceProtocol + let apiService: ApiServiceProtocol + + @Published var vehicle: VehicleDto + @Published var hud: ACHud? + + init(vehicle: VehicleDto, storageService: StorageServiceProtocol, apiService: ApiServiceProtocol) { + + self.vehicle = vehicle + self.storageService = storageService + self.apiService = apiService + } + + func addNote(text: String) async { + if vehicle.unrecognized { + await wrapWithToast(showProgress: false) { [weak self] in + guard let self else { return } + vehicle = try await storageService.addNote(text: text, to: vehicle.getNumber()) + } + return + } + + let note = VehicleNoteDto(text: text) + + await wrapWithToast { [weak self] in + guard let self else { return } + let vehicle = try await apiService.add(notes: [note], to: vehicle.getNumber()) + try await storageService.updateVehicleIfExists(dto: vehicle) + self.vehicle = vehicle + } + } + + func deleteNote(id: String) async { + if vehicle.unrecognized { + await wrapWithToast(showProgress: false) { [weak self] in + guard let self else { return } + vehicle = try await storageService.deleteNote(id: id, for: vehicle.getNumber()) + } + return + } + + await wrapWithToast { [weak self] in + guard let self else { return } + let vehicle = try await apiService.remove(note: id) + try await storageService.updateVehicleIfExists(dto: vehicle) + self.vehicle = vehicle + } + } + + func editNote(id: String, text: String) async { + if vehicle.unrecognized { + await wrapWithToast(showProgress: false) { [weak self] in + guard let self else { return } + vehicle = try await storageService.editNote(id: id, text: text, for: vehicle.getNumber()) + } + return + } + + var note = VehicleNoteDto(text: text) + note.id = id + + await wrapWithToast { [weak self] in + guard let self else { return } + let vehicle = try await apiService.edit(note: note) + try await storageService.updateVehicleIfExists(dto: vehicle) + self.vehicle = vehicle + } + } + + func copyNote(_ note: VehicleNoteDto) { + + UIPasteboard.general.setValue(note.text, + forPasteboardType: UTType.plainText.identifier) + } +} diff --git a/AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift b/AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift new file mode 100644 index 0000000..85c7459 --- /dev/null +++ b/AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift @@ -0,0 +1,29 @@ +// +// OsagoCoordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 14.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import UIKit +import SwiftUI +import AutoCatCore + +class OsagoCoordinator: Coordinator { + + let viewController: UINavigationController? + let contracts: [OsagoDto] + + init(navController: UINavigationController, contracts: [OsagoDto]) { + + self.viewController = navController + self.contracts = contracts + } + + func start() async throws { + + let controller = await UIHostingController(rootView: OsagoScreen(contracts: contracts)) + await viewController?.pushViewController(controller, animated: true) + } +} diff --git a/AutoCat/Screens/OsagoScreen/OsagoScreen.swift b/AutoCat/Screens/OsagoScreen/OsagoScreen.swift new file mode 100644 index 0000000..5bd7938 --- /dev/null +++ b/AutoCat/Screens/OsagoScreen/OsagoScreen.swift @@ -0,0 +1,84 @@ +// +// OsagoScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 14.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct OsagoScreen: View { + + let contracts: [OsagoDto] + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + var body: some View { + List(contracts) { contract in + Section(header: Text(makeHeader(from: contract))) { + OsagoPropertyView(title: "Contract series and number", value: contract.number) + OsagoPropertyView(title: "Insurance organization name", value: contract.name) + OsagoPropertyView(title: "OSAGO contract status", value: contract.status) + OsagoPropertyView(title: "Insurant", value: contract.insurant) + OsagoPropertyView(title: "Owner", value: contract.owner) + OsagoPropertyView(title: "Birthday", value: contract.birthday) + OsagoPropertyView(title: "Vehicle usage region", value: contract.usageRegion) + OsagoPropertyView(title: "Contract restrictions", value: contract.restrictions) + OsagoPropertyView(title: "Plate number", value: contract.plateNumber) + OsagoPropertyView(title: "VIN", value: contract.vin) + } + } + } + + func makeHeader(from contract: OsagoDto) -> String { + + let date = Date(timeIntervalSince1970: contract.date) + return formatter.string(from: date) + } +} + +struct OsagoPropertyView: View { + + let title: String + let value: String? + + var body: some View { + VStack(spacing: 4) { + HStack { + Text(title) + .multilineTextAlignment(.leading) + .font(.caption) + Spacer(minLength: 0) + } + HStack { + Spacer(minLength: 0) + Text(value ?? "") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + } + } + } +} + +#Preview { + + let contracts: [OsagoDto] = [ + .init(date: 1720963133, + number: "XXX 12345678", + name: "Some organization", + restrictions: "Restrictions"), + .init(date: 1720964133, + number: "XXX 12345678", + name: "Some organization", + restrictions: "Restrictions") + ] + + return OsagoScreen(contracts: contracts) +} diff --git a/AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift b/AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift new file mode 100644 index 0000000..9a3b7d8 --- /dev/null +++ b/AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift @@ -0,0 +1,29 @@ +// +// OwnersCoordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 14.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import UIKit +import SwiftUI +import AutoCatCore + +class OwnersCoordinator: Coordinator { + + let viewController: UINavigationController? + let ownerships: [VehicleOwnershipPeriodDto] + + init(navController: UINavigationController, ownerships: [VehicleOwnershipPeriodDto]) { + + self.viewController = navController + self.ownerships = ownerships + } + + func start() async throws { + + let controller = await UIHostingController(rootView: OwnersScreen(ownerships: ownerships)) + await viewController?.pushViewController(controller, animated: true) + } +} diff --git a/AutoCat/Screens/OwnersScreen/OwnersScreen.swift b/AutoCat/Screens/OwnersScreen/OwnersScreen.swift new file mode 100644 index 0000000..630a991 --- /dev/null +++ b/AutoCat/Screens/OwnersScreen/OwnersScreen.swift @@ -0,0 +1,75 @@ +// +// OwnersScreen.swift +// AutoCat +// +// Created by Selim Mustafaev on 14.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct OwnersScreen: View { + + let formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + let ownerships: [VehicleOwnershipPeriodDto] + + var body: some View { + + List(ownerships) { ownership in + Section(header: Text(makeHeader(for: ownership))) { + + HStack { + Text("Owner type") + Spacer() + Text(ownership.ownerType) + .foregroundStyle(.secondary) + } + + Text(ownership.lastOperation) + .foregroundStyle(.secondary) + .font(.callout) + } + } + .navigationTitle(String.localizedStringWithFormat(NSLocalizedString("owners count", comment: ""), ownerships.count)) + } + + func makeHeader(for owner: VehicleOwnershipPeriodDto) -> String { + + let fromDate = Date(timeIntervalSince1970: TimeInterval(owner.from/1000)) + let from = self.formatter.string(from: fromDate) + var to = NSLocalizedString("now", comment: "") + if owner.to > 0 { + let toDate = Date(timeIntervalSince1970: TimeInterval(owner.to/1000)) + to = self.formatter.string(from: toDate) + } + + return "\(from) - \(to)" + } +} + +#Preview { + + let ownerships: [VehicleOwnershipPeriodDto] = [ + .init(lastOperation: "в связи с прекращением права собственности (отчуждение, конфискация ТС)", + ownerType: "individual", + from: 1018051200000, + to: 1094515200000), + .init(lastOperation: "в связи с прекращением права собственности (отчуждение, конфискация ТС)", + ownerType: "individual", + from: 1099440000000, + to: 1105488000000), + .init(lastOperation: "регистрация снятых с учета", + ownerType: "individual", + from: 1105747200000, + to: 1323129600000) + ] + + return OwnersScreen(ownerships: ownerships) +} diff --git a/AutoCat/SwiftUI/ACProgressHud/ACHud.swift b/AutoCat/SwiftUI/ACProgressHud/ACHud.swift new file mode 100644 index 0000000..416ce54 --- /dev/null +++ b/AutoCat/SwiftUI/ACProgressHud/ACHud.swift @@ -0,0 +1,33 @@ +// +// ACHud.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +enum ACHud: Equatable, Identifiable, Sendable { + + case progress + case error(Error) + + var id: String { + switch self { + case .progress: + "progress" + case .error(let error): + error.localizedDescription + } + } + + static func == (lhs: ACHud, rhs: ACHud) -> Bool { + switch (lhs, rhs) { + case (.progress, .progress): + true + case (let .error(lerr), let .error(rerr)): + lerr.localizedDescription == rerr.localizedDescription + default: + false + } + } +} diff --git a/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift b/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift new file mode 100644 index 0000000..07e3aa7 --- /dev/null +++ b/AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift @@ -0,0 +1,34 @@ +// +// ACHudContainer.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +@MainActor +protocol ACHudContainer: AnyObject { + + var hud: ACHud? { get set } +} + +extension ACHudContainer where Self: AnyObject { + + func wrapWithToast(showProgress: Bool = true, closure: @escaping () async throws -> Void) async { + do { + if showProgress { + hud = .progress + } + + try await closure() + + if showProgress { + hud = nil + } + } catch { + hud = .error(error) + } + } +} diff --git a/AutoCat/SwiftUI/ACProgressHud/ACMessageView.swift b/AutoCat/SwiftUI/ACProgressHud/ACMessageView.swift new file mode 100644 index 0000000..1d6c4c2 --- /dev/null +++ b/AutoCat/SwiftUI/ACProgressHud/ACMessageView.swift @@ -0,0 +1,84 @@ +// +// ACMessageView.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct ACMessageView: View { + + enum MessageType { + + case success + case warning + case error + case info + } + + let message: String + let type: MessageType + let action: (() -> Void)? + + var body: some View { + ZStack(alignment: .center) { + Rectangle() + .fill(.black.opacity(0.7)) + .ignoresSafeArea() + HStack(spacing: 0) { + Spacer(minLength: 40) + VStack(spacing: 0) { + VStack(spacing: 16) { + Image(systemName: iconName) + .resizable() + .frame(width: 48, height: 48) + .foregroundColor(iconColor) + Text(message) + .multilineTextAlignment(.center) + } + .padding(20) + + Divider() + Button("OK") { + action?() + } + .padding() + } + .background(.thickMaterial, + in: RoundedRectangle(cornerRadius: 16)) + Spacer(minLength: 40) + } + } + } + + var iconName: String { + switch type { + case .success: "checkmark.circle.fill" + case .warning: "exclamationmark.circle.fill" + case .error: "xmark.circle.fill" + case .info: "info.circle.fill" + } + } + + var iconColor: Color { + switch type { + case .success: .green + case .warning: .orange + case .error: .red + case .info: .blue + } + } +} + +#Preview { + VStack { + ACMessageView(message: "Some error with long text", + type: .error, + action: nil) + ACMessageView(message: "Some error with very very very very very long text", + type: .error, + action: nil) + } +} diff --git a/AutoCat/SwiftUI/ACProgressHud/ACProgressHud+Modifiers.swift b/AutoCat/SwiftUI/ACProgressHud/ACProgressHud+Modifiers.swift new file mode 100644 index 0000000..a300546 --- /dev/null +++ b/AutoCat/SwiftUI/ACProgressHud/ACProgressHud+Modifiers.swift @@ -0,0 +1,50 @@ +// +// ACProgressHud+Modifiers.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct ACProgressHudModifier: ViewModifier { + + @Binding var item: ACHud? + + func body(content: Content) -> some View { + + content + .fullScreenCover(item: $item) { item in + if #available(iOS 16.4, *) { + makeHud(for: item) + .presentationBackground(.clear) + } else { + makeHud(for: item) + } + } + .transaction { transaction in + transaction.disablesAnimations = true + } + } + + @ViewBuilder + func makeHud(for item: ACHud) -> some View { + switch item { + case .progress: + ACProgressView() + case .error(let error): + ACMessageView(message: error.localizedDescription, + type: .error) { + self.item = nil + } + } + } +} + +extension View { + + func hud(_ item: Binding) -> some View { + modifier(ACProgressHudModifier(item: item)) + } +} diff --git a/AutoCat/SwiftUI/ACProgressHud/ACProgressHud.swift b/AutoCat/SwiftUI/ACProgressHud/ACProgressHud.swift new file mode 100644 index 0000000..3bf0b52 --- /dev/null +++ b/AutoCat/SwiftUI/ACProgressHud/ACProgressHud.swift @@ -0,0 +1,37 @@ +// +// ACProgressHud.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI +import AutoCatCore + +struct ACProgressHud: View { + + @State var toast: ACHud? + + var body: some View { + VStack(spacing: 16) { + Rectangle() + .fill(.orange) + .frame(width: 200, height: 200) + Button("Show progress") { + toast = .progress + } + Button("Show error") { + toast = .error(ApiError.generic) + } + Rectangle() + .fill(.orange) + .frame(width: 200, height: 200) + } + .hud($toast) + } +} + +#Preview { + ACProgressHud() +} diff --git a/AutoCat/SwiftUI/ACProgressHud/ACProgressView.swift b/AutoCat/SwiftUI/ACProgressHud/ACProgressView.swift new file mode 100644 index 0000000..fc86533 --- /dev/null +++ b/AutoCat/SwiftUI/ACProgressHud/ACProgressView.swift @@ -0,0 +1,31 @@ +// +// ACProgressView.swift +// AutoCat +// +// Created by Selim Mustafaev on 29.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import SwiftUI + +struct ACProgressView: View { + var body: some View { + ZStack(alignment: .center) { + Rectangle() + .fill(.black.opacity(0.7)) + .ignoresSafeArea() + VStack { + ProgressView() + .controlSize(.large) + .padding(20) + .background(.thickMaterial, + in: RoundedRectangle(cornerRadius: 16)) + + } + } + } +} + +#Preview { + ACProgressView() +} diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift b/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift index db63a50..a76cfc7 100644 --- a/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift +++ b/AutoCat/ThirdParty/ATGMediaBrowser/ContentTransformers.swift @@ -48,6 +48,7 @@ public typealias ContentTransformer = (_ contentView: UIView, _ position: CGFloa // MARK: - Default Transitions /// An enumeration to hold default content transformers +@MainActor public enum DefaultContentTransformers { /** diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift b/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift index 0ab44fa..1abbac7 100644 --- a/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift +++ b/AutoCat/ThirdParty/ATGMediaBrowser/DismissAnimationController.swift @@ -23,6 +23,7 @@ import UIKit +@MainActor internal class DismissAnimationController: NSObject { private enum Constants { diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift b/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift index 208806f..ea0f59e 100644 --- a/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift +++ b/AutoCat/ThirdParty/ATGMediaBrowser/MediaBrowserViewController.swift @@ -25,6 +25,7 @@ import UIKit // MARK: - MediaBrowserViewControllerDataSource protocol /// Protocol to supply media browser contents. +@MainActor public protocol MediaBrowserViewControllerDataSource: AnyObject { /** @@ -38,7 +39,7 @@ public protocol MediaBrowserViewControllerDataSource: AnyObject { Remember to pass the index received in the datasource method back. This index is used to set the image to the correct image view. */ - typealias CompletionBlock = (_ index: Int, _ image: UIImage?, _ zoomScale: ZoomScale?, _ error: Error?) -> Void + typealias CompletionBlock = @MainActor @Sendable (_ index: Int, _ image: UIImage?, _ zoomScale: ZoomScale?, _ error: Error?) -> Void /** Method to supply number of items to be shown in media browser. @@ -88,6 +89,7 @@ extension MediaBrowserViewControllerDataSource { // MARK: - MediaBrowserViewControllerDelegate protocol +@MainActor public protocol MediaBrowserViewControllerDelegate: AnyObject { /** @@ -767,6 +769,7 @@ extension MediaBrowserViewController { // MARK: - Updating View Positions +@MainActor extension MediaBrowserViewController { @objc private func update(_ timeInterval: TimeInterval) { diff --git a/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift b/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift index 032dd94..9dcb5a1 100644 --- a/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift +++ b/AutoCat/ThirdParty/ATGMediaBrowser/MediaContentView.swift @@ -24,7 +24,7 @@ import UIKit /// Holds the value of minimumZoomScale and maximumZoomScale of the image. -public struct ZoomScale { +public struct ZoomScale: Sendable { /// Minimum zoom level, the image can be zoomed out to. public var minimumZoomScale: CGFloat diff --git a/AutoCat/ThirdParty/SwiftMaskTextfield.swift b/AutoCat/ThirdParty/SwiftMaskTextfield.swift deleted file mode 100644 index 706d47d..0000000 --- a/AutoCat/ThirdParty/SwiftMaskTextfield.swift +++ /dev/null @@ -1,269 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2016 Gabriel Maccori Kozma - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -import UIKit - -//********************************************************************************************************** -// -// MARK: - Constants - -// -//********************************************************************************************************** - -//********************************************************************************************************** -// -// MARK: - Definitions - -// -//********************************************************************************************************** - -//********************************************************************************************************** -// -// MARK: - Class - -// -//********************************************************************************************************** - -open class SwiftMaskTextfield : UITextField { - -//************************************************** -// MARK: - Properties -//************************************************** - - public let lettersAndDigitsReplacementChar: String = "*" - public let anyLetterReplecementChar: String = "@" - public let lowerCaseLetterReplecementChar: String = "a" - public let upperCaseLetterReplecementChar: String = "A" - public let digitsReplecementChar: String = "#" - - /** - Var that holds the format pattern that you wish to apply - to some text - - If the pattern is set to "" no mask would be applied and - the textfield would behave like a normal one - */ - @IBInspectable open var formatPattern: String = "" - - /** - Var that holds the prefix to be added to the textfield - - If the prefix is set to "" no string will be added to the beggining - of the text - */ - @IBInspectable open var prefix: String = "" - - /** - Var that have the maximum length, based on the mask set - */ - open var maxLength: Int { - get { - return formatPattern.count + prefix.count - } - } - - /** - Overriding the var text from UITextField so if any text - is applied programmatically by calling formatText - */ - override open var text: String? { - set { - super.text = newValue - self.formatText() - } - - get { - return super.text - } - } - -//************************************************** -// MARK: - Constructors -//************************************************** - - public required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - self.setup() - } - - public override init(frame: CGRect) { - super.init(frame: frame) - self.setup() - } - - deinit { - self.deRegisterForNotifications() - } - -//************************************************** -// MARK: - Private Methods -//************************************************** - - fileprivate func setup() { - self.registerForNotifications() - } - - fileprivate func registerForNotifications() { - NotificationCenter.default.addObserver(self, - selector: #selector(textDidChange), - name: NSNotification.Name(rawValue: "UITextFieldTextDidChangeNotification"), - object: self) - } - - fileprivate func deRegisterForNotifications() { - NotificationCenter.default.removeObserver(self) - } - - @objc fileprivate func textDidChange() { - self.undoManager?.removeAllActions() - self.formatText() - } - - fileprivate func getOnlyDigitsString(_ string: String) -> String { - let charactersArray = string.components(separatedBy: CharacterSet.decimalDigits.inverted) - return charactersArray.joined(separator: "") - } - - fileprivate func getOnlyLettersString(_ string: String) -> String { - let charactersArray = string.components(separatedBy: CharacterSet.letters.inverted) - return charactersArray.joined(separator: "") - } - - fileprivate func getUppercaseLettersString(_ string: String) -> String { - let charactersArray = string.components(separatedBy: CharacterSet.uppercaseLetters.inverted) - return charactersArray.joined(separator: "") - } - - fileprivate func getLowercaseLettersString(_ string: String) -> String { - let charactersArray = string.components(separatedBy: CharacterSet.lowercaseLetters.inverted) - return charactersArray.joined(separator: "") - } - - fileprivate func getFilteredString(_ string: String) -> String { - let charactersArray = string.components(separatedBy: CharacterSet.alphanumerics.inverted) - return charactersArray.joined(separator: "") - } - - fileprivate func getStringWithoutPrefix(_ string: String) -> String { - if string.range(of: self.prefix) != nil { - if string.count > self.prefix.count { - let prefixIndex = string.index(string.endIndex, offsetBy: (string.count - self.prefix.count) * -1) - return String(string[prefixIndex...]) - } else if string.count == self.prefix.count { - return "" - } - - } - return string - } - -//************************************************** -// MARK: - Self Public Methods -//************************************************** - - /** - Func that formats the text based on formatPattern - - Override this function if you want to customize the behaviour of - the class - */ - open func formatText() { - var currentTextForFormatting = "" - - if let text = super.text { - if text.count > 0 { - currentTextForFormatting = self.getStringWithoutPrefix(text) - } - } - - if self.maxLength > 0 { - var formatterIndex = self.formatPattern.startIndex, currentTextForFormattingIndex = currentTextForFormatting.startIndex - var finalText = "" - - currentTextForFormatting = self.getFilteredString(currentTextForFormatting) - - if currentTextForFormatting.count > 0 { - while true { - let formatPatternRange = formatterIndex ..< formatPattern.index(after: formatterIndex) - let currentFormatCharacter = String(self.formatPattern[formatPatternRange]) - - let currentTextForFormattingPatterRange = currentTextForFormattingIndex ..< currentTextForFormatting.index(after: currentTextForFormattingIndex) - let currentTextForFormattingCharacter = String(currentTextForFormatting[currentTextForFormattingPatterRange]) - - switch currentFormatCharacter { - case self.lettersAndDigitsReplacementChar: - finalText += currentTextForFormattingCharacter - currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex) - formatterIndex = formatPattern.index(after: formatterIndex) - case self.anyLetterReplecementChar: - let filteredChar = self.getOnlyLettersString(currentTextForFormattingCharacter) - if !filteredChar.isEmpty { - finalText += filteredChar - formatterIndex = formatPattern.index(after: formatterIndex) - } - currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex) - case self.lowerCaseLetterReplecementChar: - let filteredChar = self.getLowercaseLettersString(currentTextForFormattingCharacter) - if !filteredChar.isEmpty { - finalText += filteredChar - formatterIndex = formatPattern.index(after: formatterIndex) - } - currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex) - case self.upperCaseLetterReplecementChar: - let filteredChar = self.getUppercaseLettersString(currentTextForFormattingCharacter) - if !filteredChar.isEmpty { - finalText += filteredChar - formatterIndex = formatPattern.index(after: formatterIndex) - } - currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex) - case self.digitsReplecementChar: - let filteredChar = self.getOnlyDigitsString(currentTextForFormattingCharacter) - if !filteredChar.isEmpty { - finalText += filteredChar - formatterIndex = formatPattern.index(after: formatterIndex) - } - currentTextForFormattingIndex = currentTextForFormatting.index(after: currentTextForFormattingIndex) - default: - finalText += currentFormatCharacter - formatterIndex = formatPattern.index(after: formatterIndex) - } - - if formatterIndex >= self.formatPattern.endIndex || - currentTextForFormattingIndex >= currentTextForFormatting.endIndex { - break - } - } - } - - if finalText.count > 0 { - super.text = "\(self.prefix)\(finalText)" - } else { - super.text = finalText - } - - if let text = self.text { - if text.count > self.maxLength { - super.text = String(text[text.index(text.startIndex, offsetBy: self.maxLength)]) - } - } - } - } -} diff --git a/AutoCat/Utils/ActivityItemSource.swift b/AutoCat/Utils/ActivityItemSource.swift new file mode 100644 index 0000000..90fecd7 --- /dev/null +++ b/AutoCat/Utils/ActivityItemSource.swift @@ -0,0 +1,40 @@ +// +// ActivityItemSource.swift +// AutoCat +// +// Created by Selim Mustafaev on 11.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import UIKit +import LinkPresentation + +class ActivityItemSource: NSObject, UIActivityItemSource { + + let url: URL + let title: String + + init(url: URL, title: String) { + self.url = url + self.title = title + } + + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { + return UIImage() + } + + func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { + return url + } + + func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { + + let metadata = LPLinkMetadata() + metadata.title = title + metadata.originalURL = url + metadata.url = url + metadata.imageProvider = NSItemProvider(contentsOf: url) + metadata.iconProvider = NSItemProvider(contentsOf: url) + return metadata + } +} diff --git a/AutoCat/Utils/AudioPlayer.swift b/AutoCat/Utils/AudioPlayer.swift index 6566757..e3bf69f 100644 --- a/AutoCat/Utils/AudioPlayer.swift +++ b/AutoCat/Utils/AudioPlayer.swift @@ -1,7 +1,5 @@ import Foundation import AVFoundation -import RxSwift -import RxRelay enum PlayerState { case stopped @@ -11,12 +9,12 @@ enum PlayerState { class AudioPlayer: NSObject, AVAudioPlayerDelegate { - static let shared = AudioPlayer() + @MainActor static let shared = AudioPlayer() private var player: AVAudioPlayer? private var url: URL? - private var state = BehaviorRelay(value: .stopped) - private var progress = BehaviorRelay(value: 0) + private var state: PlayerState = .stopped + private var progress: Double = 0 private var progressTimer: Timer? func set(url: URL) throws { @@ -35,12 +33,12 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { if player.isPlaying { player.pause() try self.deactivateSession() - self.state.accept(.paused) + self.state = .paused } else { try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers]) try AVAudioSession.sharedInstance().setActive(true) player.play() - self.state.accept(.playing) + self.state = .playing if self.progressTimer == nil { self.progressTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(progressTick), userInfo: nil, repeats: true) } @@ -57,7 +55,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { if let player = self.player { player.pause() try? self.deactivateSession() - self.state.accept(.paused) + self.state = .paused } } @@ -65,18 +63,18 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { if let player = self.player { player.stop() try? self.deactivateSession() - self.state.accept(.stopped) + self.state = .stopped self.progressTimer?.invalidate() self.progressTimer = nil } } func getState() -> PlayerState { - return self.state.value + return self.state } func getProgress() -> Double { - return self.progress.value + return self.progress } func getUrl() -> URL? { @@ -87,14 +85,6 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { return self.player?.duration ?? 0 } - func stateObservable() -> Observable { - return self.state.asObservable() - } - - func progressObservable() -> Observable { - return self.progress.asObservable() - } - func deactivateSession() throws { try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation) } @@ -103,15 +93,15 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate { func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { try? self.deactivateSession() - self.progress.accept(1) + self.progress = 1 self.stop() - self.state.accept(.stopped) + self.state = .stopped } @objc func progressTick() { if let player = self.player { let progress = player.currentTime/player.duration - self.progress.accept(progress) + self.progress = progress } } } diff --git a/AutoCat/Utils/Coordinator.swift b/AutoCat/Utils/Coordinator.swift new file mode 100644 index 0000000..10b216d --- /dev/null +++ b/AutoCat/Utils/Coordinator.swift @@ -0,0 +1,27 @@ +// +// Coordinator.swift +// AutoCat +// +// Created by Selim Mustafaev on 24.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import UIKit + +protocol Coordinator { + + associatedtype ResultType + //associatedtype Controller: UIViewController + + var viewController: UIViewController? { get } + + @discardableResult + func start() async throws -> ResultType +} + +extension Coordinator { + + var viewController: UIViewController? { + nil + } +} diff --git a/AutoCat/Utils/Formatters.swift b/AutoCat/Utils/Formatters.swift new file mode 100644 index 0000000..58fca72 --- /dev/null +++ b/AutoCat/Utils/Formatters.swift @@ -0,0 +1,19 @@ +// +// Formatters.swift +// AutoCat +// +// Created by Selim Mustafaev on 24.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +struct Formatters { + + static let standard: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .medium + return formatter + }() +} diff --git a/AutoCat/Utils/Recorder.swift b/AutoCat/Utils/Recorder.swift index 76fbbb1..c7c014d 100644 --- a/AutoCat/Utils/Recorder.swift +++ b/AutoCat/Utils/Recorder.swift @@ -2,7 +2,6 @@ import Foundation import Speech import AVFoundation import AudioToolbox -import RxSwift import os.log import ExceptionCatcher @@ -34,58 +33,53 @@ class Recorder { return true } - func requestPermissions() -> Single { - return Single.create { observer in + func requestPermissions() async throws { + + try await withCheckedThrowingContinuation { continuation in AVAudioSession.sharedInstance().requestRecordPermission { allowed in if allowed { SFSpeechRecognizer.requestAuthorization { status in switch status { case .authorized: - observer(.success(())) + continuation.resume() break case .denied: let error = CocoaError.error("Access error", reason: "Access to speech recognition is denied", suggestion: "Please give permission to use speech recognition in system settings") - observer(.failure(error)) - break + continuation.resume(throwing: error) case .restricted: let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device") - observer(.failure(error)) - break + continuation.resume(throwing: error) case .notDetermined: let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined") - observer(.failure(error)) - break + continuation.resume(throwing: error) @unknown default: let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer") - observer(.failure(error)) - break + continuation.resume(throwing: error) } } } else { let error = CocoaError.error("Access error", reason: "Access to microphone is denied", suggestion: "Please give permission to use microphone in system settings") - observer(.failure(error)) + continuation.resume(throwing: error) } } - - return Disposables.create() } } - func startRecording(to file: URL) -> Single { + func startRecording(to file: URL) async throws -> String { guard self.microphoneAvailable() else { - return Single.error(CocoaError.error("Recording error", reason: "Microphone not found")) + throw CocoaError.error("Recording error", reason: "Microphone not found") } - return Single.create { observer in + return try await withCheckedThrowingContinuation { continuation in guard let aac = AVAudioFormat(settings: self.recordingSettings) else { - observer(.failure(CocoaError.error("Recording error", reason: "Format not supported"))) - return Disposables.create() + continuation.resume(throwing: CocoaError.error("Recording error", reason: "Format not supported")) + return } ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef) guard let fileRef = self.fileRef else { - observer(.failure(CocoaError.error(CocoaError.Code.fileWriteUnknown))) - return Disposables.create() + continuation.resume(throwing: CocoaError.error(CocoaError.Code.fileWriteUnknown)) + return } do { @@ -106,26 +100,24 @@ class Recorder { if let transcription = result?.bestTranscription { self.result = transcription.formattedString self.endRecognitionTimer?.invalidate() - self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in + + // TODO: Check if it actually works + RunLoop.current.schedule(after: .init(Date()).advanced(by: .seconds(5)), tolerance: .seconds(1), options: nil) { self.finishRecording() - observer(.success(self.result)) + continuation.resume(returning: self.result) } } } - self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in + RunLoop.current.schedule(after: .init(Date()).advanced(by: .seconds(5)), tolerance: .seconds(1), options: nil) { self.finishRecording() - observer(.success(self.result)) + continuation.resume(returning: self.result) } self.engine.prepare() try self.engine.start() } catch { - observer(.failure(error)) - } - - return Disposables.create { - self.cancelRecording() + continuation.resume(throwing: error) } } } diff --git a/AutoCat/Utils/RxRealmDataSource.swift b/AutoCat/Utils/RxRealmDataSource.swift index 5fef017..1cbd2b1 100644 --- a/AutoCat/Utils/RxRealmDataSource.swift +++ b/AutoCat/Utils/RxRealmDataSource.swift @@ -6,20 +6,21 @@ import AutoCatCore typealias FilterPredicate = (T) -> Bool class RealmSectionedDataSource: NSObject, UITableViewDataSource - where Item: Object & Identifiable & Dated & Cloneable, - Cell: UITableViewCell & ConfigurableCell, - Cell.Item == Item { + where Item: Object & DtoConvertible, + Item.Dto: Identifiable & Dated, + Cell: UITableViewCell & ConfigurableCell, + Cell.Item == Item.Dto { private var tv: UITableView private var data: Results private var notificationToken: NotificationToken? - private var sections: [DateSection] = [] + private var sections: [DateSection] = [] private var cellIdentifier: String - private var filterPredicate: FilterPredicate? - private var searchPredicate: FilterPredicate? + private var filterPredicate: FilterPredicate? + private var searchPredicate: FilterPredicate? private let groupQueue = DispatchQueue(label: "group") - private var objects: [Item] = [] + private var objects: [Item.Dto] = [] private let onSizeChanged: ((Int) -> Void)? @@ -39,13 +40,13 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource self.notificationToken = self.data.observe { changes in switch changes { case .initial: - self.objects = self.data.toArray().map { $0.shallowClone() } + self.objects = self.data.toArray().map(\.shallowDto) self.reload(animated: false) case .update(_, let deletions, let insertions, let modifications): deletions.forEach { self.objects.remove(at: $0) } - insertions.forEach { self.objects.insert(self.data[$0].shallowClone(), at: $0) } - modifications.forEach { self.objects[$0] = self.data[$0].shallowClone() } + insertions.forEach { self.objects.insert(self.data[$0].shallowDto, at: $0) } + modifications.forEach { self.objects[$0] = self.data[$0].shallowDto } self.reload() // if deletions.isEmpty && modifications.isEmpty && insertions.count == 1 && insertions.first == 0 { @@ -93,7 +94,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource // MARK: - Public - func item(at indexPath: IndexPath) -> Item { + func item(at indexPath: IndexPath) -> Item.Dto { return self.sections[indexPath.section].elements[indexPath.row] } @@ -114,7 +115,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource } func insertFirst() { - guard let item = data.first?.shallowClone() else { + guard let item = data.first?.shallowDto else { reload(animated: false) return } @@ -129,7 +130,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource } func reloadFirst() { - guard !sections.isEmpty, let item = data.first?.clone() else { + guard !sections.isEmpty, let item = data.first?.dto else { reload(animated: false) return } @@ -147,7 +148,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource } sections[index].remove(at: itemIndex) - sections[0].insert(data[0].clone(), at: 0) + sections[0].insert(data[0].dto, at: 0) let fromIndexPath = IndexPath(row: itemIndex, section: index) let toIndexPath = IndexPath(row: 0, section: 0) tv.moveRow(at: fromIndexPath, to: toIndexPath) @@ -161,7 +162,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource return nil } - func setFilterPredicate(_ predicate: FilterPredicate?) { + func setFilterPredicate(_ predicate: FilterPredicate?) { guard self.filterPredicate != nil || predicate != nil else { return } @@ -170,7 +171,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource self.reload() } - func setSearchPredicate(_ predicate: FilterPredicate?) { + func setSearchPredicate(_ predicate: FilterPredicate?) { guard self.searchPredicate != nil || predicate != nil else { return } @@ -184,7 +185,7 @@ class RealmSectionedDataSource: NSObject, UITableViewDataSource throw CocoaError.error("Type \(Item.self) is not exportable") } - var items = self.data.toArray().map { $0.clone() } + var items = self.data.toArray().map(\.dto) if let predicate = self.filterPredicate { items = items.filter(predicate) } diff --git a/AutoCat/Utils/RxSectionedDataSource.swift b/AutoCat/Utils/RxSectionedDataSource.swift index 834354c..7ac38c3 100644 --- a/AutoCat/Utils/RxSectionedDataSource.swift +++ b/AutoCat/Utils/RxSectionedDataSource.swift @@ -1,9 +1,7 @@ import UIKit -import RxSwift -import RxCocoa import AutoCatCore -class RxSectionedDataSource: NSObject, UITableViewDataSource where Item: Dated & Decodable & Identifiable, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item { +class SectionedDataSource: NSObject, UITableViewDataSource where Item: Dated & Decodable & Identifiable & Sendable, Cell: UITableViewCell & ConfigurableCell, Cell.Item == Item { private var tv: UITableView private var cellIdentifier: String private var sections: [DateSection] = [] @@ -53,24 +51,23 @@ class RxSectionedDataSource: NSObject, UITableViewDataSource where I self.sections[indexPath.section].elements[indexPath.row] = item } - var data: Binder> { - return Binder(self) { datasource, data in - if let count = data.count { - self.count = count - self.items = data.items - } else { - self.items.append(contentsOf: data.items) - } - self.pageToken = data.pageToken - - DispatchQueue.global().async { - let newSections = self.items.groupedByDate(type: self.sortParam) - DispatchQueue.main.async { - self.sections = newSections - self.tv.reloadData() - } - } + func update(with data: PagedResponse) { + if let count = data.count { + self.count = count + self.items = data.items + } else { + self.items.append(contentsOf: data.items) } + self.pageToken = data.pageToken + + // TODO: Grouping on background thread + // DispatchQueue.global().async { + let newSections = self.items.groupedByDate(type: self.sortParam) + // DispatchQueue.main.async { + self.sections = newSections + self.tv.reloadData() + // } + // } } func needMoreData() -> Bool { diff --git a/AutoCat/Views/FlagLayer.swift b/AutoCat/Views/FlagLayer.swift index 2b87d06..32ac4f2 100644 --- a/AutoCat/Views/FlagLayer.swift +++ b/AutoCat/Views/FlagLayer.swift @@ -1,12 +1,22 @@ import UIKit class FlagLayer: CALayer { - let pixelWidth = 1/UIScreen.main.scale + let pixelWidth: CGFloat // Flag colors - https://ru.wikipedia.org/wiki/%D0%A4%D0%BB%D0%B0%D0%B3_%D0%A0%D0%BE%D1%81%D1%81%D0%B8%D0%B8 let blue = CGColor(srgbRed: 0, green: 57/256.0, blue: 166/256.0, alpha: 1) let red = CGColor(srgbRed: 213/256.0, green: 43/256.0, blue: 30/256.0, alpha: 1) - + + init(scale: CGFloat) { + + self.pixelWidth = 1/scale + super.init() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func draw(in ctx: CGContext) { ctx.saveGState() super.draw(in: ctx) diff --git a/AutoCat/Views/PNKeyboard.swift b/AutoCat/Views/PNKeyboard.swift index a77f876..1612bc5 100644 --- a/AutoCat/Views/PNKeyboard.swift +++ b/AutoCat/Views/PNKeyboard.swift @@ -1,10 +1,12 @@ import UIKit import AutoCatCore +@MainActor protocol PNKeyboardDelegate: AnyObject { func returnClicked() } +@MainActor protocol PNButtonDelegate: AnyObject { func buttonTapped(_ button: PNButton) } diff --git a/AutoCat/Views/PlateView.swift b/AutoCat/Views/PlateView.swift index 0af4e2c..08707c7 100644 --- a/AutoCat/Views/PlateView.swift +++ b/AutoCat/Views/PlateView.swift @@ -2,7 +2,7 @@ import UIKit import AutoCatCore private class CALayerAnimationsDisablingDelegate: NSObject, CALayerDelegate { - static let shared = CALayerAnimationsDisablingDelegate() + @MainActor static let shared = CALayerAnimationsDisablingDelegate() private let null = NSNull() func action(for layer: CALayer, forKey event: String) -> CAAction? { @@ -22,7 +22,7 @@ class PlateView: UIView { private var numberLayer = CenterTextLayer(coeff: fontHeightCoeff) private var regionLayer = CenterTextLayer(coeff: fontHeightCoeff) private var countryLayer = CenterTextLayer() - private var flagLayer = FlagLayer() + private var flagLayer = FlagLayer(scale: UIScreen.main.scale) var number: PlateNumber? { didSet { @@ -42,7 +42,7 @@ class PlateView: UIView { } } - var onChange: (() -> Void)? + var onChange: (@MainActor () -> Void)? override init(frame: CGRect) { super.init(frame: frame) diff --git a/AutoCat/Views/eureka/MultilineLabelRow.swift b/AutoCat/Views/eureka/MultilineLabelRow.swift deleted file mode 100644 index e8c0af8..0000000 --- a/AutoCat/Views/eureka/MultilineLabelRow.swift +++ /dev/null @@ -1,57 +0,0 @@ -import UIKit -import Eureka - -class MultilineLabelCell: Cell, CellType { - private(set) var title: UILabel! - private(set) var value: UILabel! - - required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - override func setup() { - super.setup() - - self.selectionStyle = .none - - self.title = UILabel() - self.contentView.addSubview(self.title) - self.title.translatesAutoresizingMaskIntoConstraints = false - self.title.font = UIFont.preferredFont(forTextStyle: .caption1) - self.title.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor).isActive = true - self.title.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor).isActive = true - self.title.topAnchor.constraint(equalTo: self.contentView.topAnchor, constant: 8).isActive = true - - self.value = UILabel() - self.contentView.addSubview(self.value) - self.value.translatesAutoresizingMaskIntoConstraints = false - self.value.textColor = .secondaryLabel - self.value.numberOfLines = 0 - self.value.textAlignment = .right - self.value.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor).isActive = true - self.value.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor).isActive = true - self.value.topAnchor.constraint(equalTo: self.title.bottomAnchor, constant: 8).isActive = true - self.value.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor, constant: -8).isActive = true - } - - override func update() { - super.update() - - self.textLabel?.text = nil - self.detailTextLabel?.text = nil - - self.title.text = row.title - self.value.text = row.value - } -} - -final class MultilineLabelRow: Row, RowType { - required init(tag: String?) { - super.init(tag: tag) - cellProvider = CellProvider() - } -} diff --git a/AutoCat/Views/eureka/MultilineLinkRow.swift b/AutoCat/Views/eureka/MultilineLinkRow.swift deleted file mode 100644 index 438cf59..0000000 --- a/AutoCat/Views/eureka/MultilineLinkRow.swift +++ /dev/null @@ -1,36 +0,0 @@ -import UIKit -import Eureka - -class MultilineLinkCell: MultilineLabelCell { - required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { - super.init(style: style, reuseIdentifier: reuseIdentifier) - } - - required init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - } - - override func update() { - super.update() - - self.textLabel?.text = nil - self.detailTextLabel?.text = nil - - self.title.text = row.title - - if let url = row.value { - self.value.attributedText = NSAttributedString(string: url, attributes: [ - .link: url - ]) - } else { - self.title.attributedText = NSAttributedString(string: "") - } - } -} - -final class MultilineLinkRow: Row, RowType { - required init(tag: String?) { - super.init(tag: tag) - cellProvider = CellProvider() - } -} diff --git a/AutoCat/Views/eureka/SourceStatusRow.swift b/AutoCat/Views/eureka/SourceStatusRow.swift index 2fb95d4..46d248d 100644 --- a/AutoCat/Views/eureka/SourceStatusRow.swift +++ b/AutoCat/Views/eureka/SourceStatusRow.swift @@ -12,7 +12,7 @@ extension DebugInfoStatus { } } -class SourceStatusCell: Cell, CellType { +class SourceStatusCell: Cell, CellType { private var circle: UIImageView! required init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { @@ -43,7 +43,7 @@ class SourceStatusCell: Cell, CellType { self.detailTextLabel?.text = nil if let value = row.value { - self.circle.tintColor = value.statusEnum.color + self.circle.tintColor = value.status.color //self.accessoryType = value.error == nil ? .none : .disclosureIndicator } else { self.circle.tintColor = .systemGray @@ -57,11 +57,12 @@ final class SourceStatusRow: Row, RowType { super.init(tag: tag) cellProvider = CellProvider() self.onCellSelection { cell, row in - if let error = row.value?.error, let controller = cell.parentViewController { - let alert = UIAlertController(title: row.title, message: error, preferredStyle: .alert) - alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) - controller.present(alert, animated: true) - } + // TODO: Fix isolation problems +// if let error = row.value?.error, let controller = cell.parentViewController { +// let alert = UIAlertController(title: row.title, message: error, preferredStyle: .alert) +// alert.addAction(UIAlertAction(title: "OK", style: .default, handler: nil)) +// controller.present(alert, animated: true) +// } } } } diff --git a/AutoCatCore/Extensions/CocoaError.swift b/AutoCatCore/Extensions/CocoaError.swift index 91a5c9e..102886f 100644 --- a/AutoCatCore/Extensions/CocoaError.swift +++ b/AutoCatCore/Extensions/CocoaError.swift @@ -11,7 +11,7 @@ extension NSError { return (title: failure, body: reason) } } else { - return (title: "Error", body: "") + return (title: "Error", body: localizedDescription) } } } diff --git a/AutoCatCore/Models/Cloneable.swift b/AutoCatCore/Models/Cloneable.swift deleted file mode 100644 index 728ddb3..0000000 --- a/AutoCatCore/Models/Cloneable.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation - -public protocol Cloneable { - init(copy: Self) - func shallowClone() -> Self -} - -extension Cloneable { - public func clone() -> Self { - return Self.init(copy: self) - } - - public func shallowClone() -> Self { - return clone() - } -} diff --git a/AutoCatCore/Models/DTO/AudioRecordDto.swift b/AutoCatCore/Models/DTO/AudioRecordDto.swift new file mode 100644 index 0000000..5fccb03 --- /dev/null +++ b/AutoCatCore/Models/DTO/AudioRecordDto.swift @@ -0,0 +1,37 @@ +// +// AudioRecordDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct AudioRecordDto: Decodable { + + public var path: String = "" + public var number: String? + public var rawText: String = "" + public var addedDate: TimeInterval = 0 + public var duration: TimeInterval = 0 + public var event: VehicleEventDto? + + public init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEventDto?) { + self.path = path + self.number = number + self.duration = duration + self.rawText = raw + self.event = event + self.addedDate = Date().timeIntervalSince1970 + } + + public func getAddedDate() -> TimeInterval { + addedDate + } +} + +extension AudioRecordDto: Identifiable { + + public var id: TimeInterval { addedDate } +} diff --git a/AutoCatCore/Models/DTO/DebugInfoDto.swift b/AutoCatCore/Models/DTO/DebugInfoDto.swift new file mode 100644 index 0000000..d1ea2ad --- /dev/null +++ b/AutoCatCore/Models/DTO/DebugInfoDto.swift @@ -0,0 +1,31 @@ +// +// DebugInfoDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public enum DebugInfoStatus: Int, Sendable, Decodable { + case success = 0 + case error = 1 + case warning = 2 +} + +public struct DebugInfoDto: Decodable, Sendable { + + public var autocod: DebugInfoEntryDto + public var vin01vin: DebugInfoEntryDto + public var vin01base: DebugInfoEntryDto + public var vin01history: DebugInfoEntryDto + public var nomerogram: DebugInfoEntryDto +} + +public struct DebugInfoEntryDto: Decodable, Sendable, Equatable { + + public var fields: Int64 = 0 + public var error: String? + public var status: DebugInfoStatus = .success +} diff --git a/AutoCatCore/Models/DTO/OsagoDto.swift b/AutoCatCore/Models/DTO/OsagoDto.swift new file mode 100644 index 0000000..f2d889a --- /dev/null +++ b/AutoCatCore/Models/DTO/OsagoDto.swift @@ -0,0 +1,54 @@ +// +// OsagoDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct OsagoDto: Decodable, Sendable { + + public var date: TimeInterval = 0 + public var number: String = "" + public var vin: String? + public var plateNumber: String? + public var name: String = "" + public var status: String? + public var restrictions: String = "" + public var insurant: String? + public var owner: String? + public var usageRegion: String? + public var birthday: String? + + public init(date: TimeInterval, + number: String, + vin: String? = nil, + plateNumber: String? = nil, + name: String, + status: String? = nil, + restrictions: String, + insurant: String? = nil, + owner: String? = nil, + usageRegion: String? = nil, + birthday: String? = nil) { + + self.date = date + self.number = number + self.vin = vin + self.plateNumber = plateNumber + self.name = name + self.status = status + self.restrictions = restrictions + self.insurant = insurant + self.owner = owner + self.usageRegion = usageRegion + self.birthday = birthday + } +} + +extension OsagoDto: Identifiable { + + public var id: TimeInterval { date } +} diff --git a/AutoCatCore/Models/DTO/VehicleAdDto.swift b/AutoCatCore/Models/DTO/VehicleAdDto.swift new file mode 100644 index 0000000..b7b655a --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleAdDto.swift @@ -0,0 +1,22 @@ +// +// VehicleAdDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleAdDto: Decodable, Sendable { + + public var id: Int = 0 + public var url: String? + public var price: String? + public var date: TimeInterval = Date().timeIntervalSince1970 + public var mileage: String? + public var region: String? + public var city: String? + public var adDescription: String? + public var photos: [String] +} diff --git a/AutoCatCore/Models/DTO/VehicleBrandDto.swift b/AutoCatCore/Models/DTO/VehicleBrandDto.swift new file mode 100644 index 0000000..56fd351 --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleBrandDto.swift @@ -0,0 +1,23 @@ +// +// VehicleBrandDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleBrandDto: Decodable, Sendable { + + public var name: VehicleNameDto? + public var logo: String? + + public init() { } + + public init(name: VehicleNameDto?, logo: String?) { + + self.name = name + self.logo = logo + } +} diff --git a/AutoCatCore/Models/DTO/VehicleDto.swift b/AutoCatCore/Models/DTO/VehicleDto.swift new file mode 100644 index 0000000..690742c --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleDto.swift @@ -0,0 +1,170 @@ +// +// VehicleDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleDto: Sendable { + + public var brand: VehicleBrandDto? + public var model: VehicleModelDto? + public var color: String? + public var year: Int = 0 + public var category: String? + public var engine: VehicleEngineDto? + public var number: String = "" + public var currentNumber: String? + public var vin1: String? + public var vin2: String? + public var sts: String? + public var pts: String? + public var isRightWheel: Bool? + public var isJapanese: Bool? + public var addedDate: TimeInterval = 0 + public var updatedDate: TimeInterval = 0 + public var addedBy: String = "" + public var photos: [VehiclePhotoDto] = [] + public var ownershipPeriods: [VehicleOwnershipPeriodDto] = [] + public var events: [VehicleEventDto] = [] + public var osagoContracts: [OsagoDto] = [] + public var ads: [VehicleAdDto] = [] + public var notes: [VehicleNoteDto] = [] + public var debugInfo: DebugInfoDto? + public var synchronized: Bool = true + + public init() { } +} + +extension VehicleDto: Identifiable { + + public var id: String { number } +} + +extension VehicleDto: Decodable { + + enum CodingKeys: String, CodingKey { + case brand + case model + case color + case year + case category + case engine + case number + case currentNumber + case vin1 + case vin2 + case sts + case pts + case isRightWheel + case isJapanese + case addedDate + case updatedDate + case addedBy + case photos + case ownershipPeriods + case events + case osagoContracts + case ads + case notes + case debugInfo + } + + public init(from decoder: Decoder) throws { + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.brand = try container.decodeIfPresent(VehicleBrandDto.self, forKey: .brand) + self.model = try container.decodeIfPresent(VehicleModelDto.self, forKey: .model) + self.color = try container.decodeIfPresent(String.self, forKey: .color) + self.year = try container.decodeIfPresent(Int.self, forKey: .year) ?? 0 + self.category = try container.decodeIfPresent(String.self, forKey: .category) + self.number = try container.decode(String.self, forKey: .number) + self.engine = try container.decodeIfPresent(VehicleEngineDto.self, forKey: .engine) + self.currentNumber = try container.decodeIfPresent(String.self, forKey: .currentNumber) + self.vin1 = try container.decodeIfPresent(String.self, forKey: .vin1) + self.vin2 = try container.decodeIfPresent(String.self, forKey: .vin2) + self.sts = try container.decodeIfPresent(String.self, forKey: .sts) + self.pts = try container.decodeIfPresent(String.self, forKey: .pts) + self.isRightWheel = try container.decodeIfPresent(Bool.self, forKey: .isRightWheel) + self.isJapanese = try container.decodeIfPresent(Bool.self, forKey: .isJapanese) + self.addedDate = (try container.decode(TimeInterval.self, forKey: .addedDate))/1000 + self.addedBy = try container.decode(String.self, forKey: .addedBy) + self.debugInfo = try container.decodeIfPresent(DebugInfoDto.self, forKey: .debugInfo) + self.updatedDate = (try container.decode(TimeInterval.self, forKey: .updatedDate))/1000 + + self.photos = try container.decodeIfPresent([VehiclePhotoDto].self, forKey: .photos) ?? [] + self.ownershipPeriods = try container.decodeIfPresent([VehicleOwnershipPeriodDto].self, forKey: .ownershipPeriods) ?? [] + self.events = try container.decodeIfPresent([VehicleEventDto].self, forKey: .events) ?? [] + self.osagoContracts = try container.decodeIfPresent([OsagoDto].self, forKey: .osagoContracts) ?? [] + self.ads = try container.decodeIfPresent([VehicleAdDto].self, forKey: .ads) ?? [] + self.notes = try container.decodeIfPresent([VehicleNoteDto].self, forKey: .notes) ?? [] + + // All vehicles received from API are synchronized by definition + self.synchronized = true + } +} + +extension VehicleDto: Exportable { + + public static var csvHeader: String { + return "Plate Number, Model, Color, Year, Power (HP), Events, Owners, VIN, STS, PTS, Engine number, Added Date, Updated date, Locations, Notes" + } + + public var csvLine: String { + let model = self.brand?.name?.original ?? "" + let added = Formatters.standard.string(from: Date(timeIntervalSince1970: self.addedDate)) + let updated = Formatters.standard.string(from: Date(timeIntervalSince1970: self.updatedDate)) + + var eventsString = "" + for event in events { + let location = event.address ?? "lat: \(event.latitude), lon: \(event.longitude)" + let date = Formatters.standard.string(from: Date(timeIntervalSince1970: event.date)) + eventsString.append(location + "; " + date + "\r\n") + } + + let notesString = self.notes.reduce("") { partialResult, note in + partialResult + note.text + "\r\n" + } + + let number = self.currentNumber == nil ? self.number : "\(self.number) (\(self.currentNumber ?? ""))" + + return String(format: "%@, \"%@\", %@, %d, %f, %d, %d, %@, %@, %@, %@, \"%@\", \"%@\", \"%@\", \"%@\"", + number, + model, + self.color ?? "", + self.year, + self.engine?.powerHp ?? 0.0, + self.events.count, + self.ownershipPeriods.count, + self.vin1 ?? "", + self.sts ?? "", + self.pts ?? "", + self.engine?.number ?? "", + added, + updated, + eventsString, + notesString) + } +} + +extension VehicleDto { + + public func getNumber() -> String { + return self.number + } + + public var unrecognized: Bool { + return self.brand == nil + } + + public var outdated: Bool { + if let current = self.currentNumber { + return current != self.number + } else { + return false + } + } +} diff --git a/AutoCatCore/Models/DTO/VehicleEngineDto.swift b/AutoCatCore/Models/DTO/VehicleEngineDto.swift new file mode 100644 index 0000000..e8a066d --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleEngineDto.swift @@ -0,0 +1,18 @@ +// +// VehicleEngineDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleEngineDto: Decodable, Sendable { + + public var number: String? + public var volume: Int? = 0 + public var powerHp: Float? = 0 + public var powerKw: Float? = 0 + public var fuelType: String? +} diff --git a/AutoCatCore/Models/DTO/VehicleEventDto.swift b/AutoCatCore/Models/DTO/VehicleEventDto.swift new file mode 100644 index 0000000..6d8b878 --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleEventDto.swift @@ -0,0 +1,61 @@ +// +// VehicleEventDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleEventDto: Codable, Sendable { + + public var id: String = UUID().uuidString + public var date: TimeInterval = Date().timeIntervalSince1970 + public var latitude: Double = 0 + public var longitude: Double = 0 + public var address: String? = nil + public var addedBy: String? = nil + public var number: String? + + public init(lat: Double, lon: Double) { + self.latitude = lat + self.longitude = lon + self.addedBy = Settings.shared.user.email + } + + public func getMapLink() -> URL? { + var urlString = "http://maps.apple.com/?sll=\(self.latitude),\(self.longitude)" + if let address = self.address?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + urlString = urlString + "&address=" + address + } + return URL(string: urlString) + } + + public func getLocationString() -> String { + let coordinates = "Lat: \(self.latitude), Lon: \(self.longitude)" + if let addressString = self.address { + return "\(addressString) (\(coordinates)" + } else { + return coordinates + } + } + + public func findAddress() async throws { + guard address == nil else { + return + } + + /* + return RxLocationManager + .getAddressForLocation(latitude: self.latitude, longitude: self.longitude) + .map { addr in + if let realm = self.realm { + try realm.write { self.address = addr } + } else { + self.address = addr + } + } + */ + } +} diff --git a/AutoCatCore/Models/DTO/VehicleModelDto.swift b/AutoCatCore/Models/DTO/VehicleModelDto.swift new file mode 100644 index 0000000..65f0431 --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleModelDto.swift @@ -0,0 +1,14 @@ +// +// VehicleModelDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleModelDto: Decodable, Sendable { + + let name: VehicleNameDto? +} diff --git a/AutoCatCore/Models/DTO/VehicleNameDto.swift b/AutoCatCore/Models/DTO/VehicleNameDto.swift new file mode 100644 index 0000000..8de2509 --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleNameDto.swift @@ -0,0 +1,15 @@ +// +// VehicleNameDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleNameDto: Decodable, Sendable { + + public var original: String? + public var normalized: String? +} diff --git a/AutoCatCore/Models/DTO/VehicleNoteDto.swift b/AutoCatCore/Models/DTO/VehicleNoteDto.swift new file mode 100644 index 0000000..bbeaccc --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleNoteDto.swift @@ -0,0 +1,22 @@ +// +// VehicleNoteDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleNoteDto: Codable, Sendable, Identifiable { + + public var id: String = UUID().uuidString + public var user: String = "" + public var date: TimeInterval = Date().timeIntervalSince1970 + public var text: String = "" + + public init(text: String) { + self.text = text + self.user = Settings.shared.user.email + } +} diff --git a/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift b/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift new file mode 100644 index 0000000..b018d80 --- /dev/null +++ b/AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift @@ -0,0 +1,54 @@ +// +// VehicleOwnershipPeriodDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehicleOwnershipPeriodDto: Decodable, Sendable { + + public var lastOperation: String = "" + public var ownerType: String = "" + public var from: Int64 = 0 + public var to: Int64 = 0 + public var region: String? + public var registrationRegion: String? + public var locality: String? + public var code: String? + public var street: String? + public var building: String? + public var inn: String? + + public init(lastOperation: String, + ownerType: String, + from: Int64, + to: Int64, + region: String? = nil, + registrationRegion: String? = nil, + locality: String? = nil, + code: String? = nil, + street: String? = nil, + building: String? = nil, + inn: String? = nil) { + + self.lastOperation = lastOperation + self.ownerType = ownerType + self.from = from + self.to = to + self.region = region + self.registrationRegion = registrationRegion + self.locality = locality + self.code = code + self.street = street + self.building = building + self.inn = inn + } +} + +extension VehicleOwnershipPeriodDto: Identifiable { + + public var id: Int64 { from } +} diff --git a/AutoCatCore/Models/DTO/VehiclePhotoDto.swift b/AutoCatCore/Models/DTO/VehiclePhotoDto.swift new file mode 100644 index 0000000..a4431bf --- /dev/null +++ b/AutoCatCore/Models/DTO/VehiclePhotoDto.swift @@ -0,0 +1,27 @@ +// +// VehiclePhotoDto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct VehiclePhotoDto: Decodable, Sendable { + + public var brand: String? + public var model: String? + public var date: TimeInterval = 0 + public var url: String = "" + + public var description: String { + let formatter = DateFormatter() + formatter.timeZone = TimeZone(identifier:"GMT") + formatter.dateStyle = .medium + formatter.timeStyle = .none + let date = Date(timeIntervalSince1970: self.date/1000) + let dateStr = formatter.string(from: date) + return "\(self.brand ?? "") \(self.model ?? "") (\(dateStr))" + } +} diff --git a/AutoCatCore/Models/DateSection.swift b/AutoCatCore/Models/DateSection.swift index b824ba5..fb8e166 100644 --- a/AutoCatCore/Models/DateSection.swift +++ b/AutoCatCore/Models/DateSection.swift @@ -22,14 +22,11 @@ public struct DateSection { self.elements = items } - public init(timestamp: Double, items: [T]) { + public init(timestamp: Double, items: [T], monthStart: DateInRegion, weekStart: DateInRegion) { let formatter = DateFormatter() formatter.dateStyle = .medium formatter.timeStyle = .medium - let monthStart = DateCache.shared.monthStart - let weekStart = DateCache.shared.weekStart - let date = Date(timeIntervalSince1970: timestamp) let dateInRegion = DateInRegion(date, region: Region.current) if dateInRegion.isToday { diff --git a/AutoCatCore/Models/DebugInfo.swift b/AutoCatCore/Models/DebugInfo.swift deleted file mode 100644 index d1fd0d3..0000000 --- a/AutoCatCore/Models/DebugInfo.swift +++ /dev/null @@ -1,27 +0,0 @@ -import Foundation -import RealmSwift - -public enum DebugInfoStatus: Int { - case success = 0 - case error = 1 - case warning = 2 -} - -public class DebugInfo: Object, Decodable { - @Persisted public var autocod: DebugInfoEntry! - @Persisted public var vin01vin: DebugInfoEntry! - @Persisted public var vin01base: DebugInfoEntry! - @Persisted public var vin01history: DebugInfoEntry! - @Persisted public var nomerogram: DebugInfoEntry! -} - -public class DebugInfoEntry: Object, Decodable { - @Persisted public var fields: Int64 = 0 - @Persisted public var error: String? - @Persisted public var status: Int = 0 - - public var statusEnum: DebugInfoStatus { - get { DebugInfoStatus(rawValue: self.status)! } - set { self.status = newValue.rawValue } - } -} diff --git a/AutoCatCore/Models/Filter.swift b/AutoCatCore/Models/Filter.swift index 0c47b9d..4f0c2e2 100644 --- a/AutoCatCore/Models/Filter.swift +++ b/AutoCatCore/Models/Filter.swift @@ -1,6 +1,6 @@ import Foundation -public enum AddedBy: String, CustomStringConvertible, CaseIterable { +public enum AddedBy: String, CustomStringConvertible, CaseIterable, Sendable { case anyone case me case anyoneButMe @@ -14,7 +14,7 @@ public enum AddedBy: String, CustomStringConvertible, CaseIterable { } } -public enum SortParameter: String, CustomStringConvertible, CaseIterable { +public enum SortParameter: String, CustomStringConvertible, CaseIterable, Sendable { case addedDate case updatedDate @@ -26,7 +26,7 @@ public enum SortParameter: String, CustomStringConvertible, CaseIterable { } } -public enum SortOrder: String, CustomStringConvertible, CaseIterable { +public enum SortOrder: String, CustomStringConvertible, CaseIterable, Sendable { case ascending case descending @@ -38,7 +38,7 @@ public enum SortOrder: String, CustomStringConvertible, CaseIterable { } } -public enum SearchScope: Int, CaseIterable { +public enum SearchScope: Int, CaseIterable, Sendable { case plateNumber = 0 case vin = 1 @@ -61,7 +61,7 @@ public enum SearchScope: Int, CaseIterable { } } -public struct Filter { +public struct Filter: Sendable { public var searchString = "" public var brand: String? public var model: String? diff --git a/AutoCatCore/Models/Firebase/FbRefreshTokenModel.swift b/AutoCatCore/Models/Firebase/FbRefreshTokenModel.swift new file mode 100644 index 0000000..7ca0549 --- /dev/null +++ b/AutoCatCore/Models/Firebase/FbRefreshTokenModel.swift @@ -0,0 +1,15 @@ +// +// FbRefreshTokenModel.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 11.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +struct FbRefreshTokenModel: Decodable { + + let id_token: String? + let refresh_token: String? +} diff --git a/AutoCatCore/Models/Firebase/FbVerifyTokenModel.swift b/AutoCatCore/Models/Firebase/FbVerifyTokenModel.swift new file mode 100644 index 0000000..e0e87e1 --- /dev/null +++ b/AutoCatCore/Models/Firebase/FbVerifyTokenModel.swift @@ -0,0 +1,15 @@ +// +// FbVerifyTokenModel.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 11.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +struct FbVerifyTokenModel: Decodable { + + let idToken: String? + let refreshToken: String? +} diff --git a/AutoCatCore/Models/Osago.swift b/AutoCatCore/Models/Osago.swift deleted file mode 100644 index 2591153..0000000 --- a/AutoCatCore/Models/Osago.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import RealmSwift - -public class Osago: Object, Decodable, Cloneable { - @Persisted public var date: TimeInterval = 0 - @Persisted public var number: String = "" - @Persisted public var vin: String? - @Persisted public var plateNumber: String? - @Persisted public var name: String = "" - @Persisted public var status: String? - @Persisted public var restrictions: String = "" - @Persisted public var insurant: String? - @Persisted public var owner: String? - @Persisted public var usageRegion: String? - @Persisted public var birthday: String? - - public required init(copy: Osago) { - self.date = copy.date - self.number = copy.number - self.vin = copy.vin - self.plateNumber = copy.plateNumber - self.name = copy.name - self.status = copy.status - self.restrictions = copy.restrictions - self.insurant = copy.insurant - self.owner = copy.owner - self.usageRegion = copy.usageRegion - } - - required override init() { - super.init() - } -} diff --git a/AutoCatCore/Models/PagedResponse.swift b/AutoCatCore/Models/PagedResponse.swift index b2a72d5..e3df2aa 100644 --- a/AutoCatCore/Models/PagedResponse.swift +++ b/AutoCatCore/Models/PagedResponse.swift @@ -1,6 +1,6 @@ import Foundation -public class PagedResponse: Decodable where T: Decodable { +public final class PagedResponse: Decodable, Sendable where T: Decodable & Sendable { public let count: Int? public let pageToken: String? public let items: [T] diff --git a/AutoCatCore/Models/Protocols/DtoConvertible.swift b/AutoCatCore/Models/Protocols/DtoConvertible.swift new file mode 100644 index 0000000..4d5bc28 --- /dev/null +++ b/AutoCatCore/Models/Protocols/DtoConvertible.swift @@ -0,0 +1,33 @@ +// +// DtoConvertible.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public protocol DtoConvertible { + + associatedtype Dto + + var dto: Dto { get } + var shallowDto: Dto { get } + + init(dto: Dto) + init?(dto: Dto?) +} + +extension DtoConvertible { + + public var shallowDto: Dto { dto } + + public init?(dto: Dto?) { + guard let dto else { + return nil + } + + self.init(dto: dto) + } +} diff --git a/AutoCatCore/Models/AudioRecord.swift b/AutoCatCore/Models/Realm/AudioRecord.swift similarity index 50% rename from AutoCatCore/Models/AudioRecord.swift rename to AutoCatCore/Models/Realm/AudioRecord.swift index cf81e26..818da52 100644 --- a/AutoCatCore/Models/AudioRecord.swift +++ b/AutoCatCore/Models/Realm/AudioRecord.swift @@ -1,7 +1,7 @@ import Foundation import RealmSwift -public class AudioRecord: Object, Identifiable, Cloneable { +public final class AudioRecord: Object { @Persisted public var path: String = "" @Persisted public var number: String? @@ -10,21 +10,12 @@ public class AudioRecord: Object, Identifiable, Cloneable { @Persisted public var duration: TimeInterval = 0 @Persisted public var event: VehicleEvent? - public var identifier: TimeInterval = 0 - public var id: TimeInterval { - if self.identifier == 0 { - self.identifier = self.addedDate - } - return self.identifier - } - - public var differenceIdentifier: TimeInterval { id } - public func isContentEqual(to source: AudioRecord) -> Bool { return self == source } - public init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEvent?) { + public convenience init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEvent?) { + self.init() self.path = path self.number = number self.duration = duration @@ -33,10 +24,6 @@ public class AudioRecord: Object, Identifiable, Cloneable { self.addedDate = Date().timeIntervalSince1970 } - required override init() { - super.init() - } - public override static func primaryKey() -> String? { return "path" } @@ -44,23 +31,29 @@ public class AudioRecord: Object, Identifiable, Cloneable { public override class func ignoredProperties() -> [String] { return ["id", "identifier", "differenceIdentifier"] } +} + +extension AudioRecord: DtoConvertible { - public func getAddedDate() -> TimeInterval { - if self.identifier == 0 { - self.identifier = self.addedDate - } + public var dto: AudioRecordDto { - return self.addedDate + var record = AudioRecordDto(path: path, + number: number, + raw: rawText, + duration: duration, + event: event?.dto) + record.addedDate = addedDate + return record } - // MARK: - Cloneable - - required public init(copy: AudioRecord) { - self.path = copy.path - self.number = copy.number - self.rawText = copy.rawText - self.addedDate = copy.addedDate - self.duration = copy.duration - self.event = copy.event?.clone() + public convenience init(dto: AudioRecordDto) { + + self.init(path: dto.path, + number: dto.number, + raw: dto.rawText, + duration: dto.duration, + event: VehicleEvent(dto: dto.event)) + + self.addedDate = dto.addedDate } } diff --git a/AutoCatCore/Models/Realm/DebugInfo.swift b/AutoCatCore/Models/Realm/DebugInfo.swift new file mode 100644 index 0000000..0035b0d --- /dev/null +++ b/AutoCatCore/Models/Realm/DebugInfo.swift @@ -0,0 +1,60 @@ +import Foundation +import RealmSwift + +public final class DebugInfo: Object { + + @Persisted public var autocod: DebugInfoEntry! + @Persisted public var vin01vin: DebugInfoEntry! + @Persisted public var vin01base: DebugInfoEntry! + @Persisted public var vin01history: DebugInfoEntry! + @Persisted public var nomerogram: DebugInfoEntry! +} + +extension DebugInfo: DtoConvertible { + + public var dto: DebugInfoDto { + + DebugInfoDto(autocod: autocod.dto, + vin01vin: vin01vin.dto, + vin01base: vin01base.dto, + vin01history: vin01history.dto, + nomerogram: nomerogram.dto) + } + + public convenience init(dto: DebugInfoDto) { + + self.init() + + autocod = DebugInfoEntry(dto: dto.autocod) + vin01vin = DebugInfoEntry(dto: dto.vin01vin) + vin01base = DebugInfoEntry(dto: dto.vin01base) + vin01history = DebugInfoEntry(dto: dto.vin01history) + nomerogram = DebugInfoEntry(dto: dto.nomerogram) + } +} + +public final class DebugInfoEntry: Object, Decodable { + + @Persisted public var fields: Int64 = 0 + @Persisted public var error: String? + @Persisted public var status: Int = 0 +} + +extension DebugInfoEntry: DtoConvertible { + + public var dto: DebugInfoEntryDto { + + DebugInfoEntryDto(fields: fields, + error: error, + status: DebugInfoStatus(rawValue: status) ?? .success) + } + + public convenience init(dto: DebugInfoEntryDto) { + + self.init() + + fields = dto.fields + error = dto.error + status = dto.status.rawValue + } +} diff --git a/AutoCatCore/Models/Realm/Osago.swift b/AutoCatCore/Models/Realm/Osago.swift new file mode 100644 index 0000000..7c98af6 --- /dev/null +++ b/AutoCatCore/Models/Realm/Osago.swift @@ -0,0 +1,51 @@ +import Foundation +import RealmSwift + +public final class Osago: Object { + + @Persisted public var date: TimeInterval = 0 + @Persisted public var number: String = "" + @Persisted public var vin: String? + @Persisted public var plateNumber: String? + @Persisted public var name: String = "" + @Persisted public var status: String? + @Persisted public var restrictions: String = "" + @Persisted public var insurant: String? + @Persisted public var owner: String? + @Persisted public var usageRegion: String? + @Persisted public var birthday: String? +} + +extension Osago: DtoConvertible { + + public var dto: OsagoDto { + + OsagoDto(date: date, + number: number, + vin: vin, + plateNumber: plateNumber, + name: name, + status: status, + restrictions: restrictions, + insurant: insurant, + owner: owner, + usageRegion: usageRegion, + birthday: birthday) + } + + public convenience init(dto: OsagoDto) { + + self.init() + + self.date = dto.date + self.number = dto.number + self.vin = dto.vin + self.plateNumber = dto.plateNumber + self.name = dto.name + self.status = dto.status + self.restrictions = dto.restrictions + self.insurant = dto.insurant + self.owner = dto.owner + self.usageRegion = dto.usageRegion + } +} diff --git a/AutoCatCore/Models/Realm/Vehicle.swift b/AutoCatCore/Models/Realm/Vehicle.swift new file mode 100644 index 0000000..5842e58 --- /dev/null +++ b/AutoCatCore/Models/Realm/Vehicle.swift @@ -0,0 +1,170 @@ +import Foundation +import RealmSwift + +public enum OwnerType: String, CustomStringConvertible { + case legal + case individual + + public var description: String { + switch self { + case .legal: return NSLocalizedString("legal", comment: "Owner type") + case .individual: return NSLocalizedString("individual", comment: "Owner type") + } + } +} + +public enum SteeringWheelPosition: CustomStringConvertible { + case left + case right + case unknown + + public var description: String { + switch self { + case .left: return "Left" + case .right: return "Right" + case .unknown: return "Unknown" + } + } +} + +public final class Vehicle: Object, Identifiable { + @Persisted public var brand: VehicleBrand? + @Persisted public var model: VehicleModel? + @Persisted public var color: String? + @Persisted public var year: Int = 0 + @Persisted public var category: String? + @Persisted public var engine: VehicleEngine? + @Persisted private var number: String = "" + @Persisted public var currentNumber: String? + @Persisted public var vin1: String? + @Persisted public var vin2: String? + @Persisted public var sts: String? + @Persisted public var pts: String? + @Persisted public var isRightWheel: Bool? + @Persisted public var isJapanese: Bool? + @Persisted public var addedDate: TimeInterval = 0 + @Persisted public var updatedDate: TimeInterval = 0 + @Persisted public var addedBy: String = "" + @Persisted public var photos: List + @Persisted public var ownershipPeriods: List + @Persisted public var events: List + @Persisted public var osagoContracts: List + @Persisted public var ads: List + @Persisted public var notes: List + @Persisted public var debugInfo: DebugInfo? + @Persisted public var synchronized: Bool = true + + public convenience init(_ number: String) { + self.init() + self.number = number + self.addedDate = Date().timeIntervalSince1970 + self.updatedDate = self.addedDate + self.synchronized = false + } + + public override static func primaryKey() -> String? { + return "number" + } + + public override class func ignoredProperties() -> [String] { + return ["id", "identifier", "differenceIdentifier", "formatter"] + } +} + +extension Vehicle: DtoConvertible { + + public var shallowDto: VehicleDto { + var vehicle = VehicleDto() + vehicle.number = self.number + vehicle.currentNumber = self.currentNumber + vehicle.addedDate = self.addedDate + vehicle.updatedDate = self.updatedDate + vehicle.brand = self.brand?.dto + vehicle.synchronized = self.synchronized + + vehicle.notes = Array(notes.map(\.dto)) + vehicle.events = Array(events.map(\.dto).sorted { $0.date > $1.date }) + + return vehicle + } + + public var dto: VehicleDto { + + var vehicle = VehicleDto() + vehicle.brand = brand?.dto + vehicle.model = model?.dto + vehicle.color = color + vehicle.year = year + vehicle.category = category + vehicle.engine = engine?.dto + vehicle.number = number + vehicle.currentNumber = currentNumber + vehicle.vin1 = vin1 + vehicle.vin2 = vin2 + vehicle.sts = sts + vehicle.pts = pts + vehicle.isRightWheel = isRightWheel + vehicle.isJapanese = isJapanese + vehicle.addedDate = addedDate + vehicle.updatedDate = updatedDate + vehicle.addedBy = addedBy + vehicle.photos = photos.map(\.dto) + vehicle.ownershipPeriods = ownershipPeriods.map(\.dto) + vehicle.events = events.map(\.dto) + vehicle.osagoContracts = osagoContracts.map(\.dto) + vehicle.ads = ads.map(\.dto) + vehicle.notes = notes.map(\.dto) + vehicle.debugInfo = debugInfo?.dto + vehicle.synchronized = synchronized + return vehicle + } + + public convenience init(dto: VehicleDto) { + + self.init(dto.number) + + self.brand = VehicleBrand(dto: dto.brand) + self.model = VehicleModel(dto: dto.model) + self.color = dto.color + self.year = dto.year + self.category = dto.category + self.engine = VehicleEngine(dto: dto.engine) + self.number = dto.number + self.currentNumber = dto.currentNumber + self.vin1 = dto.vin1 + self.vin2 = dto.vin2 + self.sts = dto.sts + self.pts = dto.pts + self.isRightWheel = dto.isRightWheel + self.isJapanese = dto.isJapanese + self.addedDate = dto.addedDate + self.addedBy = dto.addedBy + self.updatedDate = dto.updatedDate + + let photos = List() + photos.append(objectsIn: dto.photos.map(VehiclePhoto.init)) + self.photos = photos + + let ownerships = List() + ownerships.append(objectsIn: dto.ownershipPeriods.map(VehicleOwnershipPeriod.init)) + self.ownershipPeriods = ownerships + + let events = List() + events.append(objectsIn: dto.events.map(VehicleEvent.init).sorted { $0.date > $1.date }) + self.events = events + + let osago = List() + osago.append(objectsIn: dto.osagoContracts.map(Osago.init)) + self.osagoContracts = osago + + let ads = List() + ads.append(objectsIn: dto.ads.map(VehicleAd.init)) + self.ads = ads + + let notes = List() + notes.append(objectsIn: dto.notes.map(VehicleNote.init)) + self.notes = notes + + self.synchronized = dto.synchronized + } +} diff --git a/AutoCatCore/Models/Realm/VehicleAd.swift b/AutoCatCore/Models/Realm/VehicleAd.swift new file mode 100644 index 0000000..38db005 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleAd.swift @@ -0,0 +1,49 @@ +import Foundation +import RealmSwift + +public final class VehicleAd: Object { + + @Persisted public var id: Int = 0 + @Persisted public var url: String? + @Persisted public var price: String? + @Persisted public var date: TimeInterval = Date().timeIntervalSince1970 + @Persisted public var mileage: String? + @Persisted public var region: String? + @Persisted public var city: String? + @Persisted public var adDescription: String? + @Persisted public var photos: List +} + +extension VehicleAd: DtoConvertible { + + public var dto: VehicleAdDto { + + VehicleAdDto(id: id, + url: url, + price: price, + date: date, + mileage: mileage, + region: region, + city: city, + adDescription: adDescription, + photos: Array(photos)) + } + + public convenience init(dto: VehicleAdDto) { + + self.init() + + self.id = dto.id + self.url = dto.url + self.price = dto.price + self.date = dto.date + self.mileage = dto.mileage + self.region = dto.region + self.city = dto.city + self.adDescription = dto.adDescription + + let photos = List() + photos.append(objectsIn: dto.photos) + self.photos = photos + } +} diff --git a/AutoCatCore/Models/Realm/VehicleBrand.swift b/AutoCatCore/Models/Realm/VehicleBrand.swift new file mode 100644 index 0000000..19d8758 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleBrand.swift @@ -0,0 +1,32 @@ +// +// VehicleBrand.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public final class VehicleBrand: Object{ + + @Persisted public var name: VehicleName? + @Persisted public var logo: String? +} + +extension VehicleBrand: DtoConvertible { + + public var dto: VehicleBrandDto { + + VehicleBrandDto(name: name?.dto, logo: logo) + } + + public convenience init(dto: VehicleBrandDto) { + + self.init() + + self.name = VehicleName(dto: dto.name) + self.logo = dto.logo + } +} diff --git a/AutoCatCore/Models/Realm/VehicleEngine.swift b/AutoCatCore/Models/Realm/VehicleEngine.swift new file mode 100644 index 0000000..82729b8 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleEngine.swift @@ -0,0 +1,42 @@ +// +// VehicleEngine.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public final class VehicleEngine: Object { + + @Persisted public var number: String? + @Persisted public var volume: Int? = 0 + @Persisted public var powerHp: Float? = 0 + @Persisted public var powerKw: Float? = 0 + @Persisted public var fuelType: String? +} + +extension VehicleEngine: DtoConvertible { + + public var dto: VehicleEngineDto { + + VehicleEngineDto(number: number, + volume: volume, + powerHp: powerHp, + powerKw: powerKw, + fuelType: fuelType) + } + + public convenience init(dto: VehicleEngineDto) { + + self.init() + + self.number = dto.number + self.volume = dto.volume + self.powerHp = dto.powerHp + self.powerKw = dto.powerKw + self.fuelType = dto.fuelType + } +} diff --git a/AutoCatCore/Models/Realm/VehicleEvent.swift b/AutoCatCore/Models/Realm/VehicleEvent.swift new file mode 100644 index 0000000..7a7c371 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleEvent.swift @@ -0,0 +1,52 @@ +import Foundation +import RealmSwift +import CoreLocation + +public final class VehicleEvent: Object { + + @Persisted public var id: String = UUID().uuidString + @Persisted public var date: TimeInterval = Date().timeIntervalSince1970 + @Persisted public var latitude: Double = 0 + @Persisted public var longitude: Double = 0 + @Persisted public var address: String? = nil + @Persisted public var addedBy: String? = nil + + public var number: String? + public var coordinate: CLLocationCoordinate2D { + return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) + } + + public override static func primaryKey() -> String? { + return "id" + } + + public override static func ignoredProperties() -> [String] { + return ["plateNumber"] + } +} + +extension VehicleEvent: DtoConvertible { + + public var dto: VehicleEventDto { + + var dto = VehicleEventDto(lat: latitude, lon: longitude) + dto.id = id + dto.date = date + dto.address = address + dto.addedBy = addedBy + return dto + } + + public convenience init(dto: VehicleEventDto) { + + self.init() + + self.id = dto.id + self.date = dto.date + self.address = dto.address + self.number = dto.number + self.addedBy = dto.addedBy + self.latitude = dto.latitude + self.longitude = dto.longitude + } +} diff --git a/AutoCatCore/Models/Realm/VehicleModel.swift b/AutoCatCore/Models/Realm/VehicleModel.swift new file mode 100644 index 0000000..9ea7248 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleModel.swift @@ -0,0 +1,29 @@ +// +// VehicleModel.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public final class VehicleModel: Object { + + @Persisted var name: VehicleName? +} + +extension VehicleModel: DtoConvertible { + + public var dto: VehicleModelDto { + + VehicleModelDto(name: name?.dto) + } + + public convenience init(dto: VehicleModelDto) { + + self.init() + self.name = VehicleName(dto: dto.name) + } +} diff --git a/AutoCatCore/Models/Realm/VehicleName.swift b/AutoCatCore/Models/Realm/VehicleName.swift new file mode 100644 index 0000000..3646851 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleName.swift @@ -0,0 +1,32 @@ +// +// VehicleName.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public final class VehicleName: Object { + + @Persisted public var original: String? + @Persisted public var normalized: String? +} + +extension VehicleName: DtoConvertible { + + public var dto: VehicleNameDto { + + VehicleNameDto(original: original, normalized: normalized) + } + + public convenience init(dto: VehicleNameDto) { + + self.init() + + self.original = dto.original + self.normalized = dto.normalized + } +} diff --git a/AutoCatCore/Models/Realm/VehicleNote.swift b/AutoCatCore/Models/Realm/VehicleNote.swift new file mode 100644 index 0000000..e9e5655 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleNote.swift @@ -0,0 +1,40 @@ +import Foundation +import RealmSwift + +public final class VehicleNote: Object { + + @Persisted public var id: String = UUID().uuidString + @Persisted public var user: String = "" + @Persisted public var date: TimeInterval = Date().timeIntervalSince1970 + @Persisted public var text: String = "" + + public convenience init(text: String) { + self.init() + self.text = text + self.user = Settings.shared.user.email + } + + public override static func primaryKey() -> String? { + return "id" + } +} + +extension VehicleNote: DtoConvertible { + + public var dto: VehicleNoteDto { + var dto = VehicleNoteDto(text: text) + dto.id = id + dto.user = user + dto.date = date + return dto + } + + public convenience init(dto: VehicleNoteDto) { + + self.init(text: dto.text) + + self.id = dto.id + self.user = dto.user + self.date = dto.date + } +} diff --git a/AutoCatCore/Models/Realm/VehicleOwnershipPeriod.swift b/AutoCatCore/Models/Realm/VehicleOwnershipPeriod.swift new file mode 100644 index 0000000..28eda9f --- /dev/null +++ b/AutoCatCore/Models/Realm/VehicleOwnershipPeriod.swift @@ -0,0 +1,59 @@ +// +// VehicleOwnershipPeriod.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public final class VehicleOwnershipPeriod: Object, Decodable { + @Persisted public var lastOperation: String = "" + @Persisted public var ownerType: String = "" + @Persisted public var from: Int64 = 0 + @Persisted public var to: Int64 = 0 + @Persisted public var region: String? + @Persisted public var registrationRegion: String? + @Persisted public var locality: String? + @Persisted public var code: String? + @Persisted public var street: String? + @Persisted public var building: String? + @Persisted public var inn: String? +} + +extension VehicleOwnershipPeriod: DtoConvertible { + + public var dto: VehicleOwnershipPeriodDto { + + VehicleOwnershipPeriodDto(lastOperation: lastOperation, + ownerType: ownerType, + from: from, + to: to, + region: region, + registrationRegion: registrationRegion, + locality: locality, + code: code, + street: street, + building: building, + inn: inn) + } + + convenience public init(dto: VehicleOwnershipPeriodDto) { + + self.init() + + self.lastOperation = dto.lastOperation + self.ownerType = dto.ownerType + self.from = dto.from + self.to = dto.to + self.region = dto.region + self.registrationRegion = dto.registrationRegion + self.locality = dto.locality + self.code = dto.code + self.street = dto.street + self.building = dto.building + self.inn = dto.inn + } +} diff --git a/AutoCatCore/Models/Realm/VehiclePhoto.swift b/AutoCatCore/Models/Realm/VehiclePhoto.swift new file mode 100644 index 0000000..2502581 --- /dev/null +++ b/AutoCatCore/Models/Realm/VehiclePhoto.swift @@ -0,0 +1,35 @@ +// +// VehiclePhoto.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public final class VehiclePhoto: Object { + @Persisted public var brand: String? + @Persisted public var model: String? + @Persisted public var date: TimeInterval = 0 + @Persisted public var url: String = "" +} + +extension VehiclePhoto: DtoConvertible { + + public var dto: VehiclePhotoDto { + + VehiclePhotoDto(brand: brand, model: model, date: date, url: url) + } + + public convenience init(dto: VehiclePhotoDto) { + + self.init() + + self.brand = dto.brand + self.model = dto.model + self.date = dto.date + self.url = dto.url + } +} diff --git a/AutoCatCore/Models/Settings.swift b/AutoCatCore/Models/Settings.swift index 5b149c8..23f8b07 100644 --- a/AutoCatCore/Models/Settings.swift +++ b/AutoCatCore/Models/Settings.swift @@ -1,6 +1,6 @@ import Foundation -public class Settings { +final public class Settings: Sendable { private static let defaults = UserDefaults.standard public static let shared = Settings() diff --git a/AutoCatCore/Models/User.swift b/AutoCatCore/Models/User.swift index 68f4508..b00306f 100644 --- a/AutoCatCore/Models/User.swift +++ b/AutoCatCore/Models/User.swift @@ -1,6 +1,6 @@ import Foundation -public struct User: Codable { +public struct User: Codable, Sendable { public let email: String public var token: String public var firebaseIdToken: String? diff --git a/AutoCatCore/Models/Vehicle.swift b/AutoCatCore/Models/Vehicle.swift deleted file mode 100644 index 658a2c0..0000000 --- a/AutoCatCore/Models/Vehicle.swift +++ /dev/null @@ -1,419 +0,0 @@ -import Foundation -import RealmSwift - -public class VehicleName: Object, Decodable, Cloneable { - @Persisted public var original: String? - @Persisted public var normalized: String? - - public required init(copy: VehicleName) { - self.original = copy.original - self.normalized = copy.normalized - } - - required override init() { - } -} - -public class VehicleBrand: Object, Decodable, Cloneable { - @Persisted public var name: VehicleName? - @Persisted public var logo: String? - - public required init(copy: VehicleBrand) { - self.name = copy.name?.clone() - self.logo = copy.logo - } - - required override init() { - super.init() - } -} - -public class VehicleModel: Object, Decodable, Cloneable { - @Persisted var name: VehicleName? - - public required init(copy: VehicleModel) { - self.name = copy.name?.clone() - } - - required override init() { - super.init() - } -} - -public class VehicleEngine: Object, Decodable, Cloneable { - @Persisted public var number: String? - @Persisted public var volume: Int? = 0 - @Persisted public var powerHp: Float? = 0 - @Persisted public var powerKw: Float? = 0 - @Persisted public var fuelType: String? - - enum CodingKeys: String, CodingKey { - case number, volume, powerHp, powerKw, fuelType - } - - required public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.number = try container.decodeIfPresent(String.self, forKey: .number) - self.volume = try container.decodeIfPresent(Int.self, forKey: .volume) - self.powerHp = try container.decodeIfPresent(Float.self, forKey: .powerHp) - self.powerKw = try container.decodeIfPresent(Float.self, forKey: .powerKw) - self.fuelType = try container.decodeIfPresent(String.self, forKey: .fuelType) - } - - required override init() { - super.init() - } - - public required init(copy: VehicleEngine) { - self.number = copy.number - self.volume = copy.volume - self.powerHp = copy.powerHp - self.powerKw = copy.powerKw - self.fuelType = copy.fuelType - } -} - -public class VehiclePhoto: Object, Decodable, Cloneable { - @Persisted public var brand: String? - @Persisted public var model: String? - @Persisted public var date: TimeInterval = 0 - @Persisted public var url: String = "" - - public override var description: String { - let formatter = DateFormatter() - formatter.timeZone = TimeZone(identifier:"GMT") - formatter.dateStyle = .medium - formatter.timeStyle = .none - let date = Date(timeIntervalSince1970: self.date/1000) - let dateStr = formatter.string(from: date) - return "\(self.brand ?? "") \(self.model ?? "") (\(dateStr))" - } - - public required init(copy: VehiclePhoto) { - self.brand = copy.brand - self.model = copy.model - self.date = copy.date - self.url = copy.url - } - - required override init() { - super.init() - } -} - -public enum OwnerType: String, CustomStringConvertible { - case legal - case individual - - public var description: String { - switch self { - case .legal: return NSLocalizedString("legal", comment: "Owner type") - case .individual: return NSLocalizedString("individual", comment: "Owner type") - } - } -} - -public enum SteeringWheelPosition: CustomStringConvertible { - case left - case right - case unknown - - public var description: String { - switch self { - case .left: return "Left" - case .right: return "Right" - case .unknown: return "Unknown" - } - } -} - -public class VehicleOwnershipPeriod: Object, Decodable, Cloneable { - @Persisted public var lastOperation: String = "" - @Persisted public var ownerType: String = "" - @Persisted public var from: Int64 = 0 - @Persisted public var to: Int64 = 0 - @Persisted public var region: String? - @Persisted public var registrationRegion: String? - @Persisted public var locality: String? - @Persisted public var code: String? - @Persisted public var street: String? - @Persisted public var building: String? - @Persisted public var inn: String? - - required public init(copy: VehicleOwnershipPeriod) { - self.lastOperation = copy.lastOperation - self.ownerType = copy.ownerType - self.from = copy.from - self.to = copy.to - self.region = copy.region - self.registrationRegion = copy.registrationRegion - self.locality = copy.locality - self.code = copy.code - self.street = copy.street - self.building = copy.building - self.inn = copy.inn - } - - required override init() { - super.init() - } -} - -public final class Vehicle: Object, Decodable, Identifiable, Cloneable, Exportable { - @Persisted public var brand: VehicleBrand? - @Persisted public var model: VehicleModel? - @Persisted public var color: String? - @Persisted public var year: Int = 0 - @Persisted public var category: String? - @Persisted public var engine: VehicleEngine? - @Persisted private var number: String = "" - @Persisted public var currentNumber: String? - @Persisted public var vin1: String? - @Persisted public var vin2: String? - @Persisted public var sts: String? - @Persisted public var pts: String? - @Persisted public var isRightWheel: Bool? - @Persisted public var isJapanese: Bool? - @Persisted public var addedDate: TimeInterval = 0 - @Persisted public var updatedDate: TimeInterval = 0 - @Persisted public var addedBy: String = "" - @Persisted public var photos: List - @Persisted public var ownershipPeriods: List - @Persisted public var events: List - @Persisted public var osagoContracts: List - @Persisted public var ads: List - @Persisted public var notes: List - @Persisted public var debugInfo: DebugInfo? - @Persisted public var synchronized: Bool = true - - lazy var formatter: DateFormatter = { - let f = DateFormatter() - f.dateStyle = .medium - f.timeStyle = .medium - return f - }() - - enum CodingKeys: String, CodingKey { - case brand - case model - case color - case year - case category - case engine - case number - case currentNumber - case vin1 - case vin2 - case sts - case pts - case isRightWheel - case isJapanese - case addedDate - case updatedDate - case addedBy - case photos - case ownershipPeriods - case events - case osagoContracts - case ads - case notes - case debugInfo - } - - required public init(from decoder: Decoder) throws { - super.init() - - let container = try decoder.container(keyedBy: CodingKeys.self) - self.brand = try container.decodeIfPresent(VehicleBrand.self, forKey: .brand) - self.model = try container.decodeIfPresent(VehicleModel.self, forKey: .model) - self.color = try container.decodeIfPresent(String.self, forKey: .color) - self.year = try container.decodeIfPresent(Int.self, forKey: .year) ?? 0 - self.category = try container.decodeIfPresent(String.self, forKey: .category) - self.number = try container.decode(String.self, forKey: .number) - self.engine = try container.decodeIfPresent(VehicleEngine.self, forKey: .engine) - self.currentNumber = try container.decodeIfPresent(String.self, forKey: .currentNumber) - self.vin1 = try container.decodeIfPresent(String.self, forKey: .vin1) - self.vin2 = try container.decodeIfPresent(String.self, forKey: .vin2) - self.sts = try container.decodeIfPresent(String.self, forKey: .sts) - self.pts = try container.decodeIfPresent(String.self, forKey: .pts) - self.isRightWheel = try container.decodeIfPresent(Bool.self, forKey: .isRightWheel) - self.isJapanese = try container.decodeIfPresent(Bool.self, forKey: .isJapanese) - self.addedDate = (try container.decode(TimeInterval.self, forKey: .addedDate))/1000 - self.addedBy = try container.decode(String.self, forKey: .addedBy) - self.debugInfo = try container.decodeIfPresent(DebugInfo.self, forKey: .debugInfo) - self.updatedDate = (try container.decode(TimeInterval.self, forKey: .updatedDate))/1000 - - if let photosArray = try container.decodeIfPresent([VehiclePhoto].self, forKey: .photos) { - self.photos.append(objectsIn: photosArray) - } - - if let ownersipsArray = try container.decodeIfPresent([VehicleOwnershipPeriod].self, forKey: .ownershipPeriods) { - self.ownershipPeriods.append(objectsIn: ownersipsArray) - } - - if let eventsArray = try container.decodeIfPresent([VehicleEvent].self, forKey: .events) { - self.events.append(objectsIn: eventsArray.sorted { $0.date > $1.date }) - } - - if let osago = try container.decodeIfPresent([Osago].self, forKey: .osagoContracts) { - self.osagoContracts.append(objectsIn: osago) - } - - if let ads = try container.decodeIfPresent([VehicleAd].self, forKey: .ads) { - self.ads.append(objectsIn: ads) - } - - if let notes = try container.decodeIfPresent([VehicleNote].self, forKey: .notes) { - self.notes.append(objectsIn: notes) - } - - // All vehicles received from API are synchronized by definition - self.synchronized = true - } - - required override init() { - super.init() - } - - public init(_ number: String) { - super.init() - self.number = number - self.addedDate = Date().timeIntervalSince1970 - self.updatedDate = self.addedDate - self.synchronized = false - } - - public func getNumber() -> String { - return self.number - } - - public override static func primaryKey() -> String? { - return "number" - } - - public override class func ignoredProperties() -> [String] { - return ["id", "identifier", "differenceIdentifier", "formatter"] - } - - public var unrecognized: Bool { - return self.brand == nil - } - - public var outdated: Bool { - if let current = self.currentNumber { - return current != self.number - } else { - return false - } - } - - // MARK: - Cloneable - - required public init(copy: Vehicle) { - self.brand = copy.brand - self.model = copy.model - self.color = copy.color - self.year = copy.year - self.category = copy.category - self.engine = copy.engine - self.number = copy.number - self.currentNumber = copy.currentNumber - self.vin1 = copy.vin1 - self.vin2 = copy.vin2 - self.sts = copy.sts - self.pts = copy.pts - self.isRightWheel = copy.isRightWheel - self.isJapanese = copy.isJapanese - self.addedDate = copy.addedDate - self.addedBy = copy.addedBy - self.updatedDate = copy.updatedDate - - let photos = List() - photos.append(objectsIn: copy.photos.map { $0.clone() }) - self.photos = photos - - let ownerships = List() - ownerships.append(objectsIn: copy.ownershipPeriods.map { $0.clone() }) - self.ownershipPeriods = ownerships - - let events = List() - events.append(objectsIn: copy.events.map { $0.clone() }.sorted { $0.date > $1.date }) - self.events = events - - let osago = List() - osago.append(objectsIn: copy.osagoContracts.map { $0.clone() }) - self.osagoContracts = osago - - let ads = List() - ads.append(objectsIn: copy.ads.map { $0.clone() }) - self.ads = ads - - let notes = List() - notes.append(objectsIn: copy.notes.map { $0.clone() }) - self.notes = notes - - self.synchronized = copy.synchronized - } - - public func shallowClone() -> Vehicle { - let vehicle = Vehicle() - vehicle.number = self.number - vehicle.currentNumber = self.currentNumber - vehicle.addedDate = self.addedDate - vehicle.updatedDate = self.updatedDate - vehicle.brand = self.brand - vehicle.synchronized = self.synchronized - - let notes = List() - notes.append(objectsIn: self.notes.map { $0.clone() }) - vehicle.notes = notes - - let events = List() - events.append(objectsIn: self.events.map { $0.clone() }.sorted { $0.date > $1.date }) - vehicle.events = events - - return vehicle - } - - // MARK: - Exportable - - public static var csvHeader: String { - return "Plate Number, Model, Color, Year, Power (HP), Events, Owners, VIN, STS, PTS, Engine number, Added Date, Updated date, Locations, Notes" - } - - public var csvLine: String { - let model = self.brand?.name?.original ?? "" - let added = self.formatter.string(from: Date(timeIntervalSince1970: self.addedDate)) - let updated = self.formatter.string(from: Date(timeIntervalSince1970: self.updatedDate)) - - var eventsString = "" - for event in events { - let location = event.address ?? "lat: \(event.latitude), lon: \(event.longitude)" - let date = formatter.string(from: Date(timeIntervalSince1970: event.date)) - eventsString.append(location + "; " + date + "\r\n") - } - - let notesString = self.notes.reduce("") { partialResult, note in - partialResult + note.text + "\r\n" - } - - let number = self.currentNumber == nil ? self.number : "\(self.number) (\(self.currentNumber ?? ""))" - - return String(format: "%@, \"%@\", %@, %d, %f, %d, %d, %@, %@, %@, %@, \"%@\", \"%@\", \"%@\", \"%@\"", - number, - model, - self.color ?? "", - self.year, - self.engine?.powerHp ?? 0.0, - self.events.count, - self.ownershipPeriods.count, - self.vin1 ?? "", - self.sts ?? "", - self.pts ?? "", - self.engine?.number ?? "", - added, - updated, - eventsString, - notesString) - } -} diff --git a/AutoCatCore/Models/VehicleAd.swift b/AutoCatCore/Models/VehicleAd.swift deleted file mode 100644 index 135c74b..0000000 --- a/AutoCatCore/Models/VehicleAd.swift +++ /dev/null @@ -1,33 +0,0 @@ -import Foundation -import RealmSwift - -public class VehicleAd: Object, Decodable, Cloneable { - @Persisted public var id: Int = 0 - @Persisted public var url: String? - @Persisted public var price: String? - @Persisted public var date: TimeInterval = Date().timeIntervalSince1970 - @Persisted public var mileage: String? - @Persisted public var region: String? - @Persisted public var city: String? - @Persisted public var adDescription: String? - @Persisted public var photos: List - - public required init(copy: VehicleAd) { - self.id = copy.id - self.url = copy.url - self.price = copy.price - self.date = copy.date - self.mileage = copy.mileage - self.region = copy.region - self.city = copy.city - self.adDescription = copy.adDescription - - let photos = List() - photos.append(objectsIn: copy.photos) - self.photos = photos - } - - required override init() { - super.init() - } -} diff --git a/AutoCatCore/Models/VehicleEvent.swift b/AutoCatCore/Models/VehicleEvent.swift deleted file mode 100644 index dfa422e..0000000 --- a/AutoCatCore/Models/VehicleEvent.swift +++ /dev/null @@ -1,97 +0,0 @@ -import Foundation -import RealmSwift -import RxSwift -import CoreLocation - -public class VehicleEvent: Object, Codable, Cloneable { - @Persisted public var id: String = UUID().uuidString - @Persisted public var date: TimeInterval = Date().timeIntervalSince1970 - @Persisted public var latitude: Double = 0 - @Persisted public var longitude: Double = 0 - @Persisted public var address: String? = nil - @Persisted public var addedBy: String? = nil - - public var number: String? - public var coordinate: CLLocationCoordinate2D { - return CLLocationCoordinate2D(latitude: self.latitude, longitude: self.longitude) - } - - public init(lat: Double, lon: Double) { - self.latitude = lat - self.longitude = lon - self.addedBy = Settings.shared.user.email - } - - required override init() { - super.init() - } - - public func findAddress() -> Single { - if address != nil { - return Single.just(()) - } else { - return RxLocationManager - .getAddressForLocation(latitude: self.latitude, longitude: self.longitude) - .map { addr in - if let realm = self.realm { - try realm.write { self.address = addr } - } else { - self.address = addr - } - } - } - } - - public override static func primaryKey() -> String? { - return "id" - } - - public override static func ignoredProperties() -> [String] { - return ["plateNumber"] - } - - public func getMapLink() -> URL? { - var urlString = "http://maps.apple.com/?sll=\(self.latitude),\(self.longitude)" - if let address = self.address?.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { - urlString = urlString + "&address=" + address - } - return URL(string: urlString) - } - - public func getLocationString() -> String { - let coordinates = "Lat: \(self.latitude), Lon: \(self.longitude)" - if let addressString = self.address { - return "\(addressString) (\(coordinates)" - } else { - return coordinates - } - } - - // MARK: - Cloneable - - public required init(copy: VehicleEvent) { - self.id = copy.id - self.date = copy.date - self.latitude = copy.latitude - self.longitude = copy.longitude - self.address = copy.address - self.number = copy.number - self.addedBy = copy.addedBy - } - - // MARK: - Codable - - enum CodingKeys: String, CodingKey { - case id, date, latitude, longitude, address, addedBy - } - - public func encode(to encoder: Encoder) throws { - var container = encoder.container(keyedBy: CodingKeys.self) - try container.encode(id, forKey: .id) - try container.encode(date, forKey: .date) - try container.encode(latitude, forKey: .latitude) - try container.encode(longitude, forKey: .longitude) - try container.encodeIfPresent(address, forKey: .address) - try container.encodeIfPresent(addedBy, forKey: .addedBy) - } -} diff --git a/AutoCatCore/Models/VehicleNote.swift b/AutoCatCore/Models/VehicleNote.swift deleted file mode 100644 index feb2104..0000000 --- a/AutoCatCore/Models/VehicleNote.swift +++ /dev/null @@ -1,31 +0,0 @@ -import Foundation -import RealmSwift - -public class VehicleNote: Object, Codable, Cloneable { - @Persisted public var id: String = UUID().uuidString - @Persisted public var user: String = "" - @Persisted public var date: TimeInterval = Date().timeIntervalSince1970 - @Persisted public var text: String = "" - - // MARK: - Cloneable - - public required init(copy: VehicleNote) { - self.id = copy.id - self.user = copy.user - self.date = copy.date - self.text = copy.text - } - - required override init() { - super.init() - } - - public init(text: String) { - self.text = text - self.user = Settings.shared.user.email - } - - public override static func primaryKey() -> String? { - return "id" - } -} diff --git a/AutoCatCore/Models/VehicleRegion.swift b/AutoCatCore/Models/VehicleRegion.swift index d7c7eb1..0153f62 100644 --- a/AutoCatCore/Models/VehicleRegion.swift +++ b/AutoCatCore/Models/VehicleRegion.swift @@ -1,6 +1,6 @@ import Foundation -public struct VehicleRegion: Codable, Hashable { +public struct VehicleRegion: Codable, Hashable, Sendable { public var name: String public var codes: [Int] diff --git a/AutoCatCore/Services/ApiService/ApiError.swift b/AutoCatCore/Services/ApiService/ApiError.swift new file mode 100644 index 0000000..6766378 --- /dev/null +++ b/AutoCatCore/Services/ApiService/ApiError.swift @@ -0,0 +1,30 @@ +// +// ApiError.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 11.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public enum ApiError: LocalizedError, Equatable { + + case generic + case emptyResponse + case unauthorized + case threadSafety + case message(String) + case httpError(Int) + + public var errorDescription: String? { + switch self { + case .generic: "Something went wrong" + case .emptyResponse: "Empty response" + case .unauthorized: "Unauthorized" + case .threadSafety: "Thread safety error" + case .message(let message): message + case .httpError(let status): "General http error (status \(status))" + } + } +} diff --git a/AutoCatCore/Services/ApiService/ApiService.swift b/AutoCatCore/Services/ApiService/ApiService.swift new file mode 100644 index 0000000..21f54d3 --- /dev/null +++ b/AutoCatCore/Services/ApiService/ApiService.swift @@ -0,0 +1,329 @@ +import Foundation + +public actor ApiService: ApiServiceProtocol { + + public static let shared = ApiService() + + private let session: URLSession = { + let sessionConfig = URLSessionConfiguration.default + sessionConfig.timeoutIntervalForRequest = 40.0 + sessionConfig.timeoutIntervalForResource = 40.0 + return URLSession(configuration: sessionConfig) + }() + + // MARK: - Private wrappres + + private func genError(_ msg: String, suggestion: String, code: Int = 0) -> Error { + return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion]) + } + + private func createRequest(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible { + guard var urlComponents = URLComponents(string: Constants.baseUrl + api) else { return nil } + + if let params = params, method.uppercased() == "GET" { + urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: String($1)) } + } + + var request = URLRequest(url: urlComponents.url!) + request.httpMethod = method + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue("application/json", forHTTPHeaderField: "Accept") + request.addValue("Bearer " + Settings.shared.user.token, forHTTPHeaderField: "Authorization") + + if let body = body, method.uppercased() != "GET" { + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + if let data = try? encoder.encode(body) { + request.httpBody = data + } + } + + return request + } + + private func makeRequest(api: String, + method: String = "GET", + body: B?, + params: [String:P]? = nil) async throws -> T where T: Decodable, B: Encodable, P: LosslessStringConvertible { + + guard let request = self.createRequest(api: api, method: method, body: body, params: params) else { + throw ApiError.generic + } + + let (data, resp) = try await session.data(for: request) + + guard data.count > 0 else { + throw ApiError.emptyResponse + } + + guard let httpResp = resp as? HTTPURLResponse else { + throw ApiError.generic + } + + if httpResp.statusCode == 401 { + throw ApiError.unauthorized + } + + if httpResp.statusCode < 200 || httpResp.statusCode >= 300 { + throw ApiError.httpError(httpResp.statusCode) + } + + do { + let resp = try JSONDecoder().decode(Response.self, from: data) + if resp.success { + return resp.data! + } else { + throw ApiError.message(resp.error!) + } + } catch let error as Swift.DecodingError { + throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription) + } catch { + throw error + } + } + + private func makeGetRequest(api: String, params:[String: P]? = nil) async throws -> T where T: Decodable, P: LosslessStringConvertible { + // Kind of hack to satisfy compiler + try await makeRequest(api: api, method: "GET", body: nil as Int?, params: params) + } + + private func makeEmptyGetRequest(api: String) async throws -> T where T: Decodable { + + try await makeRequest(api: api, method: "GET", body: nil as Int?, params: nil as [String:Int]?) + } + + private func makeEmptyBodyRequest(api: String, method: String = "POST") async throws -> T where T: Decodable { + + try await makeRequest(api: api, method: method, body: nil as Int?, params: nil as [String:Int]?) + } + + private func makeBodyRequest(api: String, body: B?, method: String = "POST") async throws -> T where T: Decodable, B: Encodable { + + try await makeRequest(api: api, method: method, body: body, params: nil as [String:Int]?) + } + + // MARK: - Firebase API + + public func refreshFbToken() async throws { + guard let token = Settings.shared.user.firebaseIdToken, + let refreshToken = Settings.shared.user.firebaseRefreshToken, + let jwt = JWT(string: token), jwt.expired else { + return + } + + let refreshUrlString = Constants.fbRefreshToken + "?key=" + Constants.fbApiKey + + guard let url = URL(string: refreshUrlString) else { + return + } + + let body = [ + "grantType": "refresh_token", + "refreshToken": refreshToken + ] + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version") + request.addValue(Constants.secondProviderBundleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") + request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") + + let (data, _) = try await session.data(for: request) + let model = try JSONDecoder().decode(FbRefreshTokenModel.self, from: data) + + if let idToken = model.id_token { + Settings.shared.user.firebaseIdToken = idToken + } + + if let refreshToken = model.refresh_token { + Settings.shared.user.firebaseRefreshToken = refreshToken + } + } + + public func fbVerifyAssertion(provider: String, idToken: String, accessToken: String? = nil) async { + let signupUrl = Constants.fbVerifyAssertion + "?key=" + (Constants.fbApiKey.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? Constants.fbApiKey) + + guard let url = URL(string: signupUrl) else { + return + } + + var innerBody = "providerId=" + provider + + "&id_token=" + idToken + + if let accessToken = accessToken { + innerBody += "&access_token=" + accessToken + } + + let body: [String:Any] = [ + "returnIdpCredential": true, + "returnSecureToken": true, + "autoCreate": true, + "requestUri": "http://localhost", + "postBody": innerBody + ] + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version") + request.addValue(Constants.secondProviderBundleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") + request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") + + do { + let (data, _) = try await session.data(for: request) + let model = try JSONDecoder().decode(FbVerifyTokenModel.self, from: data) + + if let idToken = model.idToken { + Settings.shared.user.firebaseIdToken = idToken + } + + if let refreshToken = model.refreshToken { + Settings.shared.user.firebaseRefreshToken = refreshToken + } + + } catch { + print("Firebase verify token error: \(error.localizedDescription)") + } + } + + // MARK: - AutoCat public API + + public func login(email: String, password: String) async throws -> User { + let body = [ + "email": email, + "password": password + ] + return try await makeBodyRequest(api: "user/login", body: body) + } + + public func signIn(email: String, password: String) async throws -> User { + let body = [ + "email": email, + "password": password + ] + return try await makeBodyRequest(api: "user/signIn", body: body) + } + + public func signUp(email: String, password: String) async throws -> User { + let body = [ + "email": email, + "password": password + ] + return try await makeBodyRequest(api: "user/signup", body: body) + } + + public func getVehicles(with filter: Filter, pageToken: String? = nil, pageSize: Int = 50) async throws -> PagedResponse { + var params = filter.queryDictionary() + params["pageSize"] = String(pageSize) + if let token = pageToken { + params["pageToken"] = token; + } + return try await makeGetRequest(api: "vehicles", params: params) + } + + public func checkVehicle(by number: String, notes: [VehicleNoteDto], events: [VehicleEventDto], force: Bool = false) async throws -> VehicleDto { + + try await refreshFbToken() + + var body = [ + "number": AnyEncodable(number), + "forceUpdate": AnyEncodable(force) + ] + + if let token = Settings.shared.user.firebaseIdToken { + body["googleIdToken"] = AnyEncodable(token) + } + + if !notes.isEmpty { + body["notes"] = AnyEncodable(notes) + } + + if !events.isEmpty { + body["events"] = AnyEncodable(events) + } + + return try await makeBodyRequest(api: "vehicles/check", body: body) + } + + public func getReport(for number: String) async throws -> VehicleDto { + try await makeGetRequest(api: "vehicles/report", params: ["number": number]) + } + + public func getBrands() async throws -> [String] { + try await makeEmptyGetRequest(api: "vehicles/brands") + } + + public func getModels(of brand: String) async throws -> [String] { + try await makeGetRequest(api: "vehicles/models", params: ["brand": brand]) + } + + public func getColors() async throws -> [String] { + try await makeEmptyGetRequest(api: "vehicles/colors") + } + + public func getRegions() async throws -> [VehicleRegion] { + try await makeEmptyGetRequest(api: "vehicles/regions") + } + + public func getYears() async throws -> [Int] { + try await makeEmptyGetRequest(api: "vehicles/years") + } + + public func add(event: VehicleEventDto, to number: String) async throws -> VehicleDto { + let body = ["number": AnyEncodable(number), "event": AnyEncodable(event)] + return try await makeBodyRequest(api: "events", body: body) + } + + public func remove(event id: String) async throws -> VehicleDto { + let body = ["eventId": id] + return try await makeBodyRequest(api: "events", body: body, method: "DELETE") + } + + public func edit(event: VehicleEventDto) async throws -> VehicleDto { + let body = ["event": event] + return try await makeBodyRequest(api: "events", body: body, method: "PUT") + } + + public func events(with filter: Filter) async throws -> [VehicleEventDto] { + try await makeGetRequest(api: "events", params: filter.queryDictionary()) + } + + public func checkOsago(number: String?, vin: String?, date: Date, token: String) async throws -> VehicleDto { + let body = [ + "date": AnyEncodable(date.timeIntervalSince1970), + "number": AnyEncodable(number), + "vin": AnyEncodable(vin), + "token": AnyEncodable(token) + ] + return try await makeBodyRequest(api: "vehicles/checkOsago", body: body) + } + + public func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto { + let body = ["number": AnyEncodable(number), "notes": AnyEncodable(notes)] + return try await makeBodyRequest(api: "notes", body: body) + } + + public func edit(note: VehicleNoteDto) async throws -> VehicleDto { + try await makeBodyRequest(api: "notes", body: ["note": note], method: "PUT") + } + + public func remove(note id: String) async throws -> VehicleDto { + try await makeBodyRequest(api: "notes", body: ["noteId": id], method: "DELETE") + } + + public func checkVehicleGb(by number: String) async throws -> VehicleDto { + + try await refreshFbToken() + + var body = ["number": number] + + if let token = Settings.shared.user.firebaseIdToken { + body["token"] = token + } + + return try await makeBodyRequest(api: "vehicles/checkGbTg", body: body) + } +} diff --git a/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift new file mode 100644 index 0000000..652aa57 --- /dev/null +++ b/AutoCatCore/Services/ApiService/ApiServiceProtocol.swift @@ -0,0 +1,17 @@ +// +// ApiServiceProtocol.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Mockable + +@Mockable +public protocol ApiServiceProtocol: Sendable { + + func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto + func edit(note: VehicleNoteDto) async throws -> VehicleDto + func remove(note id: String) async throws -> VehicleDto +} diff --git a/AutoCatCore/Services/StorageService/StorageService+Notes.swift b/AutoCatCore/Services/StorageService/StorageService+Notes.swift new file mode 100644 index 0000000..13baf7f --- /dev/null +++ b/AutoCatCore/Services/StorageService/StorageService+Notes.swift @@ -0,0 +1,65 @@ +// +// StorageService+Notes.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +extension StorageService { + + public func addNote(text: String, to number: String) async throws -> VehicleDto { + + guard let vehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) else { + throw StorageError.vehicleNotFound + } + + let note = VehicleNote(text: text) + + try realm.write { + vehicle.notes.append(note) + vehicle.updatedDate = Date().timeIntervalSince1970 + } + + return vehicle.dto + } + + public func deleteNote(id: String, for number: String) async throws -> VehicleDto { + + guard let vehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) else { + throw StorageError.vehicleNotFound + } + + guard let realmNote = realm.object(ofType: VehicleNote.self, forPrimaryKey: id) else { + throw StorageError.noteNotFound + } + + try realm.write { + realm.delete(realmNote) + vehicle.updatedDate = Date().timeIntervalSince1970 + } + + return vehicle.dto + } + + public func editNote(id: String, text: String, for number: String) async throws -> VehicleDto { + + guard let vehicle = realm.object(ofType: Vehicle.self, forPrimaryKey: number) else { + throw StorageError.vehicleNotFound + } + + guard let note = realm.object(ofType: VehicleNote.self, forPrimaryKey: id) else { + throw StorageError.noteNotFound + } + + try realm.write { + note.text = text + vehicle.updatedDate = Date().timeIntervalSince1970 + + } + + return vehicle.dto + } +} diff --git a/AutoCatCore/Services/StorageService/StorageService.swift b/AutoCatCore/Services/StorageService/StorageService.swift new file mode 100644 index 0000000..4e5e13e --- /dev/null +++ b/AutoCatCore/Services/StorageService/StorageService.swift @@ -0,0 +1,58 @@ +// +// StorageService.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 22.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import RealmSwift + +public enum StorageError: LocalizedError { + + case vehicleNotFound + case noteNotFound + + public var errorDescription: String? { + switch self { + case .vehicleNotFound: "Vehicle not found in realm database" + case .noteNotFound: "Vehicle note not found in realm database" + } + } +} + +public actor StorageService: StorageServiceProtocol { + + private static var instance: StorageService? + + public static var shared: StorageService { + get async throws { + if let instance { + return instance + } else { + let service = try await StorageService() + instance = service + return service + } + } + } + + var realm: Realm! + + public init(config: Realm.Configuration = .defaultConfiguration) async throws { + + realm = try await Realm(configuration: config, actor: self) + } + + public func updateVehicleIfExists(dto: VehicleDto) async throws { + + guard realm.object(ofType: Vehicle.self, forPrimaryKey: dto.getNumber()) != nil else { + return + } + + try realm.write { + realm.add(Vehicle(dto: dto), update: .all) + } + } +} diff --git a/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift new file mode 100644 index 0000000..83d2084 --- /dev/null +++ b/AutoCatCore/Services/StorageService/StorageServiceProtocol.swift @@ -0,0 +1,18 @@ +// +// StorageServiceProtocol.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Mockable + +@Mockable +public protocol StorageServiceProtocol: Sendable { + + func addNote(text: String, to number: String) async throws -> VehicleDto + func deleteNote(id: String, for number: String) async throws -> VehicleDto + func editNote(id: String, text: String, for number: String) async throws -> VehicleDto + func updateVehicleIfExists(dto: VehicleDto) async throws +} diff --git a/AutoCatCore/Utils/Api.swift b/AutoCatCore/Utils/Api.swift deleted file mode 100644 index 89298a8..0000000 --- a/AutoCatCore/Utils/Api.swift +++ /dev/null @@ -1,320 +0,0 @@ -import Foundation -import RxSwift -import RxCocoa - -public class Api { - - private static let session: URLSession = { - let sessionConfig = URLSessionConfiguration.default - sessionConfig.timeoutIntervalForRequest = 40.0 - sessionConfig.timeoutIntervalForResource = 40.0 - return URLSession(configuration: sessionConfig) - }() - - // MARK: - Private wrappres - - private static func genError(_ msg: String, suggestion: String, code: Int = 0) -> Error { - return NSError(domain: "", code: code, userInfo: [NSLocalizedDescriptionKey: msg, NSLocalizedRecoverySuggestionErrorKey: suggestion]) - } - - private static func createRequest(api: String, method: String, body: B? = nil, params: [String:P]? = nil) -> URLRequest? where B: Encodable, P: LosslessStringConvertible { - guard var urlComponents = URLComponents(string: Constants.baseUrl + api) else { return nil } - - if let params = params, method.uppercased() == "GET" { - urlComponents.queryItems = params.map { URLQueryItem(name: $0, value: String($1)) } - } - - var request = URLRequest(url: urlComponents.url!) - request.httpMethod = method - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue("application/json", forHTTPHeaderField: "Accept") - request.addValue("Bearer " + Settings.shared.user.token, forHTTPHeaderField: "Authorization") - - if let body = body, method.uppercased() != "GET" { - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - if let data = try? encoder.encode(body) { - request.httpBody = data - } - } - - return request - } - - private static func makeRequest(api: String, method: String = "GET", body: B?, params: [String:P]? = nil) -> Single where T: Decodable, B: Encodable, P: LosslessStringConvertible { - guard let request = self.createRequest(api: api, method: method, body: body, params: params) else { - return Single.error(self.genError("Error creating request", suggestion: "")) - } - - return self.session.rx.response(request: request).asSingle().map { httpResp, data in -// let str = String(data: data, encoding: .utf8) -// print("================================") -// if let string = str?.replacingOccurrences(of: "\\\"", with: "\"") -// .replacingOccurrences(of: "\\'", with: "'") -// .replacingOccurrences(of: "\\n", with: "") { -// print(string) -// } -// print("================================") - - if httpResp.statusCode == 401 { - throw genError("Unauthorized", suggestion: "", code: httpResp.statusCode) - } - - if httpResp.statusCode < 200 || httpResp.statusCode >= 300 { - throw genError("HTTP error \(httpResp.statusCode)", suggestion: "", code: httpResp.statusCode) - } - - do { - let resp = try JSONDecoder().decode(Response.self, from: data) - if resp.success { - return resp.data! - } else { - throw self.genError(resp.error!, suggestion: "") - } - } catch let error as Swift.DecodingError { - throw CocoaError.error((error as CustomDebugStringConvertible).debugDescription) - } catch { - throw error - } - } - } - - private static func makeGetRequest(api: String, params:[String: P]? = nil) -> Single where T: Decodable, P: LosslessStringConvertible { - // Kind of hack to satisfy compiler - return self.makeRequest(api: api, method: "GET", body: nil as Int?, params: params) - } - - private static func makeEmptyGetRequest(api: String) -> Single where T: Decodable { - // Same hack as before - return self.makeRequest(api: api, method: "GET", body: nil as Int?, params: nil as [String:Int]?) - } - - private static func makeEmptyBodyRequest(api: String, method: String = "POST") -> Single where T: Decodable { - // Same hack as before - return self.makeRequest(api: api, method: method, body: nil as Int?, params: nil as [String:Int]?) - } - - private static func makeBodyRequest(api: String, body: B?, method: String = "POST") -> Single where T: Decodable, B: Encodable { - // Same hack as before - return self.makeRequest(api: api, method: method, body: body, params: nil as [String:Int]?) - } - - // MARK: - Firebase API - - public static func refreshFbToken() -> Single { - guard let token = Settings.shared.user.firebaseIdToken, let refreshToken = Settings.shared.user.firebaseRefreshToken, let jwt = JWT(string: token), jwt.expired else { - return .just(()) - } - - let refreshUrlString = Constants.fbRefreshToken + "?key=" + Constants.fbApiKey - - let body = [ - "grantType": "refresh_token", - "refreshToken": refreshToken - ] - - if let url = URL(string: refreshUrlString) { - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version") - request.addValue(Constants.secondProviderBundleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") - request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") - return self.session.rx.json(request: request).asSingle().map { resp in - guard let json = resp as? [String: Any] else { return } - if let newToken = json["id_token"] as? String { - Settings.shared.user.firebaseIdToken = newToken - //print("Token was successfully refresh to: \(newToken)") - } - if let newRefreshToken = json["refresh_token"] as? String { - Settings.shared.user.firebaseRefreshToken = newRefreshToken - } - } - } else { - return .just(()) - } - } - - public static func fbVerifyAssertion(provider: String, idToken: String, accessToken: String? = nil) -> Single { - let signupUrl = Constants.fbVerifyAssertion + "?key=" + (Constants.fbApiKey.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? Constants.fbApiKey) - if let url = URL(string: signupUrl) { - var innerBody = "providerId=" + provider - + "&id_token=" + idToken - - if let accessToken = accessToken { - innerBody += "&access_token=" + accessToken - } - - let body: [String:Any] = [ - "returnIdpCredential": true, - "returnSecureToken": true, - "autoCreate": true, - "requestUri": "http://localhost", - "postBody": innerBody - ] - - var request = URLRequest(url: url) - request.httpMethod = "POST" - request.httpBody = try? JSONSerialization.data(withJSONObject: body, options: .prettyPrinted) - request.addValue("application/json", forHTTPHeaderField: "Content-Type") - request.addValue(Constants.fbClientVersion, forHTTPHeaderField: "X-Client-Version") - request.addValue(Constants.secondProviderBundleId, forHTTPHeaderField: "X-Ios-Bundle-Identifier") - request.addValue(Constants.fbUserAgent, forHTTPHeaderField: "User-Agent") - - return self.session.rx.json(request: request).asSingle().map { response in - guard let json = response as? [String: Any] else { return } - if let newToken = json["idToken"] as? String { - Settings.shared.user.firebaseIdToken = newToken - print("Token: \(newToken)") - } - if let newRefreshToken = json["refreshToken"] as? String { - Settings.shared.user.firebaseRefreshToken = newRefreshToken - print("Refresh token: \(newRefreshToken)") - } - }.catch { err in - print(err) - return .just(()) - } - } else { - return .just(()) - } - } - - // MARK: - AutoCat public API - - public static func login(email: String, password: String) -> Single { - let body = [ - "email": email, - "password": password - ] - return self.makeBodyRequest(api: "user/login", body: body) - } - - public static func signIn(email: String, password: String) -> Single { - let body = [ - "email": email, - "password": password - ] - return self.makeBodyRequest(api: "user/signIn", body: body) - } - - public static func signUp(email: String, password: String) -> Single { - let body = [ - "email": email, - "password": password - ] - return self.makeBodyRequest(api: "user/signup", body: body) - } - - public static func getVehicles(with filter: Filter, pageToken: String? = nil, pageSize: Int = 50) -> Single> { - var params = filter.queryDictionary() - params["pageSize"] = String(pageSize) - if let token = pageToken { - params["pageToken"] = token; - } - return self.makeGetRequest(api: "vehicles", params: params) - } - - public static func checkVehicle(by number: String, notes: [VehicleNote], events: [VehicleEvent], force: Bool = false) -> Single { - return self.refreshFbToken().flatMap { () -> Single in - var body = [ - "number": AnyEncodable(number), - "forceUpdate": AnyEncodable(force) - ] - - if let token = Settings.shared.user.firebaseIdToken { - body["googleIdToken"] = AnyEncodable(token) - } - - if !notes.isEmpty { - body["notes"] = AnyEncodable(notes) - } - - if !events.isEmpty { - body["events"] = AnyEncodable(events) - } - - return self.makeBodyRequest(api: "vehicles/check", body: body) - } - } - - public static func getReport(for number: String) -> Single { - return self.makeGetRequest(api: "vehicles/report", params: ["number": number]) - } - - public static func getBrands() -> Single<[String]> { - return self.makeEmptyGetRequest(api: "vehicles/brands") - } - - public static func getModels(of brand: String) -> Single<[String]> { - return self.makeGetRequest(api: "vehicles/models", params: ["brand": brand]) - } - - public static func getColors() -> Single<[String]> { - return self.makeEmptyGetRequest(api: "vehicles/colors") - } - - public static func getRegions() -> Single<[VehicleRegion]> { - return self.makeEmptyGetRequest(api: "vehicles/regions") - } - - public static func getYears() -> Single<[Int]> { - return self.makeEmptyGetRequest(api: "vehicles/years") - } - - public static func add(event: VehicleEvent, to number: String) -> Single { - let body = ["number": AnyEncodable(number), "event": AnyEncodable(event)] - return self.makeBodyRequest(api: "events", body: body) - } - - public static func remove(event id: String) -> Single { - let body = ["eventId": id] - return self.makeBodyRequest(api: "events", body: body, method: "DELETE") - } - - public static func edit(event: VehicleEvent) -> Single { - let body = ["event": event] - return self.makeBodyRequest(api: "events", body: body, method: "PUT") - } - - public static func events(with filter: Filter) -> Single<[VehicleEvent]> { - return self.makeGetRequest(api: "events", params: filter.queryDictionary()) - } - - public static func checkOsago(number: String?, vin: String?, date: Date, token: String) -> Single { - let body = [ - "date": AnyEncodable(date.timeIntervalSince1970), - "number": AnyEncodable(number), - "vin": AnyEncodable(vin), - "token": AnyEncodable(token) - ] - return self.makeBodyRequest(api: "vehicles/checkOsago", body: body) - } - - public static func add(notes: [VehicleNote], to number: String) -> Single { - let body = ["number": AnyEncodable(number), "notes": AnyEncodable(notes)] - return self.makeBodyRequest(api: "notes", body: body) - } - - public static func edit(note: VehicleNote) -> Single { - return self.makeBodyRequest(api: "notes", body: ["note": note], method: "PUT") - } - - public static func remove(note id: String) -> Single { - return self.makeBodyRequest(api: "notes", body: ["noteId": id], method: "DELETE") - } - - public static func checkVehicleGb(by number: String) -> Single { - - return self.refreshFbToken().flatMap { () -> Single in - var body = ["number": number] - - if let token = Settings.shared.user.firebaseIdToken { - body["token"] = token - } - - return self.makeBodyRequest(api: "vehicles/checkGbTg", body: body) - } - } -} diff --git a/AutoCatCore/Utils/DateCache.swift b/AutoCatCore/Utils/DateCache.swift deleted file mode 100644 index 52233fb..0000000 --- a/AutoCatCore/Utils/DateCache.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// DateCache.swift -// AutoCatCore -// -// Created by Selim Mustafaev on 12.02.2023. -// Copyright © 2023 Selim Mustafaev. All rights reserved. -// - -import Foundation -import SwiftDate - -public class DateCache { - - public static let shared = DateCache() - - public var monthStart: DateInRegion - public var weekStart: DateInRegion - - public init() { - - let now = DateInRegion(Date(), region: Region.current) - self.monthStart = now.dateAtStartOf(.month) - self.weekStart = now.dateAtStartOf(.weekOfMonth) - - NotificationCenter.default.addObserver(self, - selector: #selector(dayChanged), - name: .NSCalendarDayChanged, - object: nil) - } - - @objc func dayChanged(_ notification: Notification) { - let now = DateInRegion(Date(), region: Region.current) - self.monthStart = now.dateAtStartOf(.month) - self.weekStart = now.dateAtStartOf(.weekOfMonth) - } -} diff --git a/AutoCatCore/Utils/Formatters.swift b/AutoCatCore/Utils/Formatters.swift new file mode 100644 index 0000000..66d3bd7 --- /dev/null +++ b/AutoCatCore/Utils/Formatters.swift @@ -0,0 +1,19 @@ +// +// Formatters.swift +// AutoCatCore +// +// Created by Selim Mustafaev on 12.06.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation + +public struct Formatters { + + public static let standard: DateFormatter = { + let f = DateFormatter() + f.dateStyle = .medium + f.timeStyle = .medium + return f + }() +} diff --git a/AutoCatCore/Utils/Location.swift b/AutoCatCore/Utils/Location.swift index 39f3f92..5d2e601 100644 --- a/AutoCatCore/Utils/Location.swift +++ b/AutoCatCore/Utils/Location.swift @@ -1,159 +1,112 @@ import Foundation -import RxSwift -import RxCocoa import CoreLocation +import SwiftLocation -class RxLocationManagerDelegateProxy: DelegateProxy, DelegateProxyType, CLLocationManagerDelegate { +enum LocationError: LocalizedError { - private let generalErrors: [CLError.Code] = [.locationUnknown, .denied, .network, .headingFailure, .rangingUnavailable, .rangingFailure] - private let geocodingErrors: [CLError.Code] = [.geocodeCanceled, .geocodeFoundNoResult, .geocodeFoundPartialResult] + case generic + case permission + case reverseGeocode - private(set) var authSubject = PublishSubject() - private(set) var locationSubject = PublishSubject() - - init(locationManager: ParentObject) { - super.init(parentObject: locationManager, delegateProxy: RxLocationManagerDelegateProxy.self) - } - - deinit { - print("deinit") - } - - // MARK: - DelegateProxyType - - static func registerKnownImplementations() { - self.register { RxLocationManagerDelegateProxy(locationManager: $0) } - } - - static func currentDelegate(for object: CLLocationManager) -> CLLocationManagerDelegate? { - return object.delegate - } - - static func setCurrentDelegate(_ delegate: CLLocationManagerDelegate?, to object: CLLocationManager) { - object.delegate = delegate - } - - // MARK: - CLLocationManagerDelegate - - func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) { - self.authSubject.onNext(status) - } - - func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) { - if let location = locations.first { - self.locationSubject.onNext(location) + var errorDescription: String? { + switch self { + case .generic: "Location error" + case .permission: "Location permission error" + case .reverseGeocode: "Reverse geocode error" } } - - func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) { - guard let err = error as? CLError else { return } - - if self.generalErrors.contains(err.code) { - // Pass general errors to all existing subjects - self.authSubject.onError(error) - self.locationSubject.onError(error) - } else if self.geocodingErrors.contains(err.code) { - // TODO: pass error to geocoding subject - } else { - print("Unexpected CoreLocation error: \(error)") - } - } - - func getNewLocationSubject() -> PublishSubject { - self.locationSubject = PublishSubject() - return self.locationSubject - } } public class RxLocationManager { - private static let manager: CLLocationManager = { - let mgr = CLLocationManager() - mgr.desiredAccuracy = kCLLocationAccuracyBest - return mgr + + private let generalErrors: [CLError.Code] = [.locationUnknown, .denied, .network, .headingFailure, .rangingUnavailable, .rangingFailure] + private let geocodingErrors: [CLError.Code] = [.geocodeCanceled, .geocodeFoundNoResult, .geocodeFoundPartialResult] + + private static let locationManager: Location = { + let manger = CLLocationManager() + manger.desiredAccuracy = kCLLocationAccuracyBest + return Location(locationManager: manger) }() - private static let bag = DisposeBag() - private static var eventObservable: Single? - public private(set) static var lastEvent: VehicleEvent? + private static var eventTask: Task? + public private(set) static var lastEvent: VehicleEventDto? - private static func checkPermissions() -> Single { - return Single.create { observer in - switch CLLocationManager.authorizationStatus() { - case .authorizedWhenInUse, .authorizedAlways: - observer(.success(())) - break - case .notDetermined: - self.manager.requestAlwaysAuthorization() - let proxy = RxLocationManagerDelegateProxy.proxy(for: self.manager) - _ = proxy.authSubject.filter{ $0 != .notDetermined }.first().subscribe(onSuccess: { result in - if let status = result, [.authorizedWhenInUse, .authorizedAlways].contains(status) { - observer(.success(())) - } else { - observer(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"]))) - } - }, onFailure: { observer(.failure($0)) }) - case .denied: - observer(.failure(CLError(.denied))) - break - default: - observer(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"]))) - break + private static func checkPermissions() async throws { + + switch locationManager.authorizationStatus { + case .authorizedWhenInUse, .authorizedAlways: + break + case .notDetermined: + let status = try await locationManager.requestPermission(.always) + if [.authorizedWhenInUse, .authorizedAlways].contains(status) { + return + } else { + throw LocationError.permission } - - return Disposables.create() + case .denied: + throw CLError(.denied) + default: + throw LocationError.permission } } - private static func requestLocation() -> Single { - let proxy = RxLocationManagerDelegateProxy.proxy(for: self.manager) - let single = proxy.getNewLocationSubject().take(1).asSingle().map { location -> VehicleEvent in - let event = VehicleEvent(lat: location.coordinate.latitude, lon: location.coordinate.longitude) - self.lastEvent = event - return event + private static func requestLocation() async throws -> VehicleEventDto { + let locationEvent = try await locationManager.requestLocation(timeout: 20) + + guard let coordinate = locationEvent.location?.coordinate else { + throw LocationError.generic } - self.manager.requestLocation() - return single + + let event = VehicleEventDto(lat: coordinate.latitude, lon: coordinate.longitude) + self.lastEvent = event + return event } - public static func requestCurrentLocation() -> Single { - if let result = self.eventObservable { - return result + @discardableResult + public static func requestCurrentLocation() async throws -> VehicleEventDto { + + if let eventTask { + return try await eventTask.value } else { - self.eventObservable = self.checkPermissions().flatMap(self.requestLocation).do(onError: { _ in - self.eventObservable = nil - }, onDispose: { - self.eventObservable = nil - self.manager.stopUpdatingLocation() - }) - return self.eventObservable! + let task = Task { + let location = try await requestLocation() + eventTask = nil + return location + } + eventTask = task + return try await task.value } } public static func locationRequestInProgress() -> Bool { - return self.eventObservable != nil + return self.eventTask != nil } - public static func getAddressForLocation(latitude: Double, longitude: Double) -> Single { - return Single.create { observer in + public static func getAddressForLocation(latitude: Double, longitude: Double) async throws -> String { + + try await withCheckedThrowingContinuation { continuation in let geocoder = CLGeocoder() let location = CLLocation(latitude: latitude, longitude: longitude) geocoder.reverseGeocodeLocation(location) { placemarks, error in if let error = error { - observer(.failure(error)) + continuation.resume(throwing: error) } else if let placemark = placemarks?.first, let name = placemark.name { - observer(.success(name)) + continuation.resume(returning: name) } else { - observer(.failure(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Reverse geolocation error"]))) + continuation.resume(throwing: LocationError.reverseGeocode) } } - return Disposables.create { - geocoder.cancelGeocode() - } - }.timeout(.seconds(20), scheduler: MainScheduler.instance) + // TODO: Add cancellation after timeout (20 seconds) + //geocoder.cancelGeocode() + } } public static func resetLastEvent() { self.lastEvent = nil } + + public static func getLastEvent() async -> VehicleEventDto? { + lastEvent + } } diff --git a/AutoCatCoreTests/StorageServiceTests.swift b/AutoCatCoreTests/StorageServiceTests.swift new file mode 100644 index 0000000..939f834 --- /dev/null +++ b/AutoCatCoreTests/StorageServiceTests.swift @@ -0,0 +1,115 @@ +// +// StorageServiceTests.swift +// AutoCatCoreTests +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Testing +import RealmSwift +import Foundation + +@testable import AutoCatCore + +struct StorageServiceTests { + + let existingVehicleNumber = "А123АА761" + let nonExistingVehicleNumber = "А999АА761" + let noteText = "Test note text" + + let storageService: StorageService + + init() async throws { + + var config: Realm.Configuration = .defaultConfiguration + config.inMemoryIdentifier = UUID().uuidString + + self.storageService = try await StorageService(config: config) + + try addTestVehicle(config: config) + } + + func addTestVehicle(config: Realm.Configuration) throws { + let realm = try Realm(configuration: config) + try realm.write { + realm.add(Vehicle(existingVehicleNumber)) + } + } + + @Test("Add note to vehicle") + func addNote() async throws { + + let vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) + + #expect(vehicle.number == existingVehicleNumber) + #expect(vehicle.notes.contains { $0.text == noteText }) + } + + @Test("Adding note to non-existent vehicle") + func addNoteError() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.addNote(text: noteText, to: nonExistingVehicleNumber) + } + } + + @Test("Edit note") + func editNote() async throws { + + let newNoteText = "New test text" + + var vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) + let note = try #require(vehicle.notes.first { $0.text == noteText }) + + vehicle = try await storageService.editNote(id: note.id, text: newNoteText, for: existingVehicleNumber) + + #expect(vehicle.number == existingVehicleNumber) + #expect(vehicle.notes.contains { $0.text == newNoteText }) + #expect(!vehicle.notes.contains { $0.text == noteText }) + } + + @Test("Edit note of non-existent vehicle") + func addNoteNonExistentVehicle() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.editNote(id: "", text: "", for: nonExistingVehicleNumber) + } + } + + @Test("Edit non-existent note") + func editNonExistentNote() async throws { + + await #expect(throws: StorageError.noteNotFound) { + _ = try await storageService.editNote(id: "", text: "", for: existingVehicleNumber) + } + } + + @Test("Delete note") + func deleteNote() async throws { + + var vehicle = try await storageService.addNote(text: noteText, to: existingVehicleNumber) + let note = try #require(vehicle.notes.first { $0.text == noteText }) + + vehicle = try await storageService.deleteNote(id: note.id, for: existingVehicleNumber) + + #expect(vehicle.number == existingVehicleNumber) + #expect(!vehicle.notes.contains { $0.text == noteText }) + } + + @Test("Delete note from non-existent vehicle") + func deleteNoteNonExistentVehicle() async throws { + + await #expect(throws: StorageError.vehicleNotFound) { + _ = try await storageService.deleteNote(id: "", for: nonExistingVehicleNumber) + } + } + + @Test("Delete non-existent note") + func deleteNonExistentNote() async throws { + + await #expect(throws: StorageError.noteNotFound) { + _ = try await storageService.deleteNote(id: "", for: existingVehicleNumber) + } + } +} diff --git a/AutoCatTests/AutoCatTests.swift b/AutoCatTests/AutoCatTests.swift deleted file mode 100644 index c0b0e42..0000000 --- a/AutoCatTests/AutoCatTests.swift +++ /dev/null @@ -1,19 +0,0 @@ -import XCTest -import AutoCatCore - -class AutoCatTests: XCTestCase { - - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } - - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. - } - - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. - } - -} diff --git a/AutoCatTests/Extensions/VehicleDto+Presets.swift b/AutoCatTests/Extensions/VehicleDto+Presets.swift new file mode 100644 index 0000000..6c83bc3 --- /dev/null +++ b/AutoCatTests/Extensions/VehicleDto+Presets.swift @@ -0,0 +1,47 @@ +// +// VehicleDto+Presets.swift +// AutoCatTests +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import AutoCatCore + +extension VehicleDto { + + static var normal: VehicleDto { + var vehicle = VehicleDto() + vehicle.number = "А123АА761" + vehicle.brand = VehicleBrandDto() + return vehicle + } + + static var unrecognized: VehicleDto { + var vehicle = VehicleDto() + vehicle.number = "А123АА761" + return vehicle + } +} + +// Fluent + +extension VehicleDto { + + func addNote(text: String) -> Self { + + var vehicle = self + vehicle.notes.append(VehicleNoteDto(text: text)) + return vehicle + } + + func addNote(text: String, id: String) -> Self { + + var note = VehicleNoteDto(text: text) + note.id = id + + var vehicle = self + vehicle.notes.append(note) + return vehicle + } +} diff --git a/AutoCatTests/Info.plist b/AutoCatTests/Info.plist deleted file mode 100644 index 64d65ca..0000000 --- a/AutoCatTests/Info.plist +++ /dev/null @@ -1,22 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - 1.0 - CFBundleVersion - 1 - - diff --git a/AutoCatTests/Mocks/ApiServiceMock.swift b/AutoCatTests/Mocks/ApiServiceMock.swift new file mode 100644 index 0000000..4202822 --- /dev/null +++ b/AutoCatTests/Mocks/ApiServiceMock.swift @@ -0,0 +1,37 @@ +// +// ApiServiceMock.swift +// AutoCatTests +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import AutoCatCore + +actor ApiServiceMock { + + var vehicle = VehicleDto() + + func setVehicle(_ vehicle: VehicleDto) async { + self.vehicle = vehicle + } + + func addNote(text: String) async { + vehicle.notes.append(VehicleNoteDto(text: text)) + } +} + +extension ApiServiceMock: ApiServiceProtocol { + + func add(notes: [VehicleNoteDto], to number: String) async throws -> VehicleDto { + vehicle + } + + func edit(note: VehicleNoteDto) async throws -> VehicleDto { + vehicle + } + + func remove(note id: String) async throws -> VehicleDto { + vehicle + } +} diff --git a/AutoCatTests/Mocks/FakeLocationManager.swift b/AutoCatTests/Mocks/FakeLocationManager.swift deleted file mode 100644 index 561115b..0000000 --- a/AutoCatTests/Mocks/FakeLocationManager.swift +++ /dev/null @@ -1,16 +0,0 @@ -import Foundation -import CoreLocation - -class FakeLocationManager: CLLocationManager { - override func requestAlwaysAuthorization() { - - } - - override func requestLocation() { - - } - - override func stopUpdatingLocation() { - - } -} diff --git a/AutoCatTests/Mocks/StorageServiceMock.swift b/AutoCatTests/Mocks/StorageServiceMock.swift new file mode 100644 index 0000000..30355ec --- /dev/null +++ b/AutoCatTests/Mocks/StorageServiceMock.swift @@ -0,0 +1,36 @@ +// +// StorageServiceMock.swift +// AutoCatTests +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Foundation +import AutoCatCore + +actor StorageServiceMock { + + var vehicle = VehicleDto() + + func setVehicle(_ vehicle: VehicleDto) async { + self.vehicle = vehicle + } +} + +extension StorageServiceMock: StorageServiceProtocol { + + func addNote(text: String, to number: String) async throws -> AutoCatCore.VehicleDto { + vehicle + } + + func deleteNote(id: String, for number: String) async throws -> AutoCatCore.VehicleDto { + vehicle + } + + func editNote(id: String, text: String, for number: String) async throws -> AutoCatCore.VehicleDto { + vehicle + } + + func updateVehicleIfExists(dto: AutoCatCore.VehicleDto) async throws { } +} diff --git a/AutoCatTests/NotesTests.swift b/AutoCatTests/NotesTests.swift new file mode 100644 index 0000000..80ac1a5 --- /dev/null +++ b/AutoCatTests/NotesTests.swift @@ -0,0 +1,116 @@ +// +// NotesTests.swift +// AutoCatTests +// +// Created by Selim Mustafaev on 13.07.2024. +// Copyright © 2024 Selim Mustafaev. All rights reserved. +// + +import Testing +import AutoCatCore +import MockableTest +@testable import AutoCat + +@MainActor +final class NotesTests { + + var storageServiceMock = StorageServiceMock() + var apiServiceMock = ApiServiceMock() + + lazy var viewModel = NotesViewModel(vehicle: VehicleDto(), + storageService: storageServiceMock, + apiService: apiServiceMock) + + let noteText = "Test note text" + let noteTextModified = "Test note text modified" + + lazy var vehicleWithNote: VehicleDto = .normal.addNote(text: noteText) + lazy var unrecognizedVehicleWithNote: VehicleDto = .unrecognized.addNote(text: noteText) + + @Test("Add note (normal vehicle)") + func addNote() async throws { + +// given(apiServiceMock) +// .add(notes: .any, to: .any).willReturn(vehicleWithNote) + + await apiServiceMock.setVehicle(vehicleWithNote) + viewModel.vehicle = .normal + + await viewModel.addNote(text: noteText) + +// verify(apiServiceMock) +// .add(notes: .any, to: .any).called(.once) +// +// verify(storageServiceMock) +// .addNote(text: .any, to: .any).called(.never) +// .updateVehicleIfExists(dto: .any).called(.once) + + #expect(viewModel.vehicle.notes.contains { $0.text == noteText }) + #expect(viewModel.hud == nil) + } + + @Test("Add note (unrecognized vehicle)") + func addNoteUnrecognized() async throws { + + await storageServiceMock.setVehicle(vehicleWithNote) + viewModel.vehicle = .unrecognized + + await viewModel.addNote(text: noteText) + + #expect(viewModel.vehicle.notes.contains { $0.text == noteText }) + #expect(viewModel.hud == nil) + } + + @Test("Edit note (normal vehicle)") + func editNote() async throws { + + let noteId = try #require(vehicleWithNote.notes.first?.id) + await apiServiceMock.setVehicle(.normal.addNote(text: noteTextModified, id: noteId)) + viewModel.vehicle = vehicleWithNote + + await viewModel.editNote(id: noteId, text: noteTextModified) + + #expect(viewModel.vehicle.notes.contains { $0.text == noteTextModified }) + #expect(viewModel.hud == nil) + } + + @Test("Edit note (unrecognized vehicle)") + func editNoteUnrecognized() async throws { + + let vehicle: VehicleDto = .unrecognized.addNote(text: noteText) + let noteId = try #require(vehicle.notes.first?.id) + await storageServiceMock.setVehicle(.unrecognized.addNote(text: noteTextModified, id: noteId)) + viewModel.vehicle = vehicle + + await viewModel.editNote(id: noteId, text: noteTextModified) + + #expect(viewModel.vehicle.notes.contains { $0.text == noteTextModified }) + #expect(viewModel.hud == nil) + } + + @Test("Delete note (normal vehicle)") + func deleteNote() async throws { + + await apiServiceMock.setVehicle(.normal) + viewModel.vehicle = vehicleWithNote + let noteId = try #require(vehicleWithNote.notes.first?.id) + + await viewModel.deleteNote(id: noteId) + + #expect(!viewModel.vehicle.notes.contains { $0.text == noteText }) + #expect(viewModel.hud == nil) + } + + @Test("Delete note (unrecognized vehicle)") + func deleteNoteUnrecognized() async throws { + + await storageServiceMock.setVehicle(.unrecognized) + viewModel.vehicle = unrecognizedVehicleWithNote + let noteId = try #require(unrecognizedVehicleWithNote.notes.first?.id) + + await viewModel.deleteNote(id: noteId) + + #expect(!viewModel.vehicle.notes.contains { $0.text == noteText }) + #expect(viewModel.hud == nil) + } +} diff --git a/dmg/AppIcon.icns b/dmg/AppIcon.icns deleted file mode 100644 index 722b660..0000000 Binary files a/dmg/AppIcon.icns and /dev/null differ diff --git a/dmg/dmgbg.png b/dmg/dmgbg.png deleted file mode 100644 index c19c277..0000000 Binary files a/dmg/dmgbg.png and /dev/null differ diff --git a/dmg/sign_dmg.sh b/dmg/sign_dmg.sh deleted file mode 100755 index 29a5fe8..0000000 --- a/dmg/sign_dmg.sh +++ /dev/null @@ -1,62 +0,0 @@ -#!/usr/bin/env bash - -APP=$1 -VERSION=$2 - -CODESIGNID="Developer ID Application: Selim Mustafaev" -NOTARY="--username selim@fastmail.fm --password ejgb-eqzw-knin-wocz" -APPNAME="AutoCat" -BUNDLEID="pro.aliencat.AutoCat" - -function msg { - printf "\\n${fBold}-- %s${fNormal}\\n" "${@}" -} - -srcDir="$(mktemp -dt $$)" - -msg "Preparing disk image sources at ${srcDir}:" -cp -R "${APP}" "${srcDir}" -ln -s /Applications "${srcDir}" - -# Disk image name -dmg_name="${APPNAME}-${VERSION}" - -msg "Creating disk image:" -hdiutil create -format UDBZ -fs HFS+ -srcdir "${srcDir}" -volname "${dmg_name}" "${dmg_name}.dmg" - -# Sign disk image -codesign --deep --force -v -s "${CODESIGNID}" --timestamp "${dmg_name}.dmg" - -# Notarize the dmg -if ! test -z "$NOTARY" ; then - zip "${dmg_name}.dmg.zip" "${dmg_name}.dmg" - uuid=`xcrun altool --notarize-app --primary-bundle-id "${BUNDLEID}" ${NOTARY} --file "${dmg_name}.dmg.zip" 2>&1 | grep 'RequestUUID' | awk '{ print $3 }'` - echo "dmg Result= $uuid" # Display identifier string - sleep 15 - while : - do - fullstatus=`xcrun altool --notarization-info "$uuid" ${NOTARY} 2>&1` # get the status - status1=`echo "$fullstatus" | grep 'Status\:' | awk '{ print $2 }'` - if [ "$status1" = "success" ]; then - xcrun stapler staple "${dmg_name}.dmg" # staple the ticket - xcrun stapler validate -v "${dmg_name}.dmg" - echo "dmg Notarization success" - break - elif [ "$status1" = "in" ]; then - echo "dmg Notarization still in progress, sleeping for 15 seconds and trying again" - sleep 15 - else - echo "dmg Notarization failed fullstatus below" - echo "$fullstatus" - exit 1 - fi - done -fi - -# Zip disk image for redistribution -#zip "${dmg_name}.zip" "${dmg_name}.dmg" -#rm "${dmg_name}.dmg" - -msg "Removing disk image caches:" -rm -rf "${srcDir}" -rm "${dmg_name}.dmg.zip" diff --git a/dmg/test.sh b/dmg/test.sh deleted file mode 100755 index bbac04a..0000000 --- a/dmg/test.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env bash - -create-dmg \ - --volname "AutoCat" \ - --volicon "AppIcon.icns" \ - --background "dmgbg.png" \ - --window-pos 800 600 \ - --window-size 800 600 \ - --icon-size 128 \ - --icon "AutoCat.app" 160 290 \ - --hide-extension "AutoCat.app" \ - --app-drop-link 635 290 \ - "AutoCat.dmg" \ - "app/"