From 01bd7b2f87719c66799373b32ff78ad9beb092dc Mon Sep 17 00:00:00 2001 From: Selim Mustafaev Date: Sun, 14 Jul 2024 17:15:38 +0300 Subject: [PATCH] Refactoring whole project to support Swift 6. RxSwift entirely removed. Starting migration of some screens to SwiftUI (preferably ones built with Eureka) --- AutoCat.xcodeproj/project.pbxproj | 770 +++++++++++++----- .../xcshareddata/swiftpm/Package.resolved | 45 +- .../xcshareddata/xcschemes/AutoCat.xcscheme | 4 +- .../xcdebugger/Breakpoints_v2.xcbkptlist | 10 +- .../Extensions/GestureRecognizers.swift | 2 +- .../Extensions/UISegmentedControl.swift | 2 +- AutoCat/ACUIKit/Views/ACButton.swift | 2 +- AutoCat/AppDelegate.swift | 11 +- AutoCat/Base.lproj/Main.storyboard | 165 +--- AutoCat/Cells/AudioRecordCell.swift | 54 +- AutoCat/Cells/ConfigurableCell.swift | 1 + AutoCat/Cells/EventCell.swift | 2 +- AutoCat/Cells/VehicleCell.swift | 2 +- AutoCat/Cells/VehicleNoteCell.swift | 2 +- AutoCat/Controllers/AdsController.swift | 57 +- AutoCat/Controllers/AuthController.swift | 97 +-- AutoCat/Controllers/CheckController.swift | 251 +++--- AutoCat/Controllers/FiltersController.swift | 341 ++++---- .../Controllers/GoogleSignInController.swift | 30 +- .../Location/EventsController.swift | 102 +-- .../Location/GlobalEventsController.swift | 32 +- .../Location/LocationEditController.swift | 13 +- .../Location/LocationPickerController.swift | 32 +- .../Controllers/Location/LocationRow.swift | 6 +- .../Location/ShowEventController.swift | 2 +- AutoCat/Controllers/MainTabController.swift | 5 +- AutoCat/Controllers/NotesController.swift | 295 ------- .../Controllers/Osago/DkbmController.swift | 65 -- .../Osago/OsagoAddController.swift | 78 -- .../Controllers/Osago/OsagoController.swift | 108 --- AutoCat/Controllers/OwnersController.swift | 70 -- AutoCat/Controllers/RecordsController.swift | 150 ++-- AutoCat/Controllers/RegionsController.swift | 20 +- AutoCat/Controllers/ReportController.swift | 112 ++- AutoCat/Controllers/SearchController.swift | 133 ++- AutoCat/Extensions/AudioEngine.swift | 25 - AutoCat/Extensions/Dated.swift | 17 +- AutoCat/Extensions/VehicleReportImage.swift | 52 +- AutoCat/JS/dkbm.js | 37 - AutoCat/Preview/ApiServiceStub.swift | 26 + AutoCat/Preview/StorageServiceStub.swift | 28 + AutoCat/SceneDelegate.swift | 14 +- .../NotesScreen/NoteAlertModifier.swift | 44 + .../NotesScreen/NotesCoordinator.swift | 32 + AutoCat/Screens/NotesScreen/NotesScreen.swift | 104 +++ .../Screens/NotesScreen/NotesViewModel.swift | 91 +++ .../OsagoScreen/OsagoCoordinator.swift | 29 + AutoCat/Screens/OsagoScreen/OsagoScreen.swift | 84 ++ .../OwnersScreen/OwnersCoordinator.swift | 29 + .../Screens/OwnersScreen/OwnersScreen.swift | 75 ++ AutoCat/SwiftUI/ACProgressHud/ACHud.swift | 33 + .../ACProgressHud/ACHudContainer.swift | 34 + .../SwiftUI/ACProgressHud/ACMessageView.swift | 84 ++ .../ACProgressHud+Modifiers.swift | 50 ++ .../SwiftUI/ACProgressHud/ACProgressHud.swift | 37 + .../ACProgressHud/ACProgressView.swift | 31 + .../ATGMediaBrowser/ContentTransformers.swift | 1 + .../DismissAnimationController.swift | 1 + .../MediaBrowserViewController.swift | 5 +- .../ATGMediaBrowser/MediaContentView.swift | 2 +- AutoCat/ThirdParty/SwiftMaskTextfield.swift | 269 ------ AutoCat/Utils/ActivityItemSource.swift | 40 + AutoCat/Utils/AudioPlayer.swift | 34 +- AutoCat/Utils/Coordinator.swift | 27 + AutoCat/Utils/Formatters.swift | 19 + AutoCat/Utils/Recorder.swift | 54 +- AutoCat/Utils/RxRealmDataSource.swift | 35 +- AutoCat/Utils/RxSectionedDataSource.swift | 37 +- AutoCat/Views/FlagLayer.swift | 14 +- AutoCat/Views/PNKeyboard.swift | 2 + AutoCat/Views/PlateView.swift | 6 +- AutoCat/Views/eureka/MultilineLabelRow.swift | 57 -- AutoCat/Views/eureka/MultilineLinkRow.swift | 36 - AutoCat/Views/eureka/SourceStatusRow.swift | 15 +- AutoCatCore/Extensions/CocoaError.swift | 2 +- AutoCatCore/Models/Cloneable.swift | 16 - AutoCatCore/Models/DTO/AudioRecordDto.swift | 37 + AutoCatCore/Models/DTO/DebugInfoDto.swift | 31 + AutoCatCore/Models/DTO/OsagoDto.swift | 54 ++ AutoCatCore/Models/DTO/VehicleAdDto.swift | 22 + AutoCatCore/Models/DTO/VehicleBrandDto.swift | 23 + AutoCatCore/Models/DTO/VehicleDto.swift | 170 ++++ AutoCatCore/Models/DTO/VehicleEngineDto.swift | 18 + AutoCatCore/Models/DTO/VehicleEventDto.swift | 61 ++ AutoCatCore/Models/DTO/VehicleModelDto.swift | 14 + AutoCatCore/Models/DTO/VehicleNameDto.swift | 15 + AutoCatCore/Models/DTO/VehicleNoteDto.swift | 22 + .../DTO/VehicleOwnershipPeriodDto.swift | 54 ++ AutoCatCore/Models/DTO/VehiclePhotoDto.swift | 27 + AutoCatCore/Models/DateSection.swift | 5 +- AutoCatCore/Models/DebugInfo.swift | 27 - AutoCatCore/Models/Filter.swift | 10 +- .../Models/Firebase/FbRefreshTokenModel.swift | 15 + .../Models/Firebase/FbVerifyTokenModel.swift | 15 + AutoCatCore/Models/Osago.swift | 33 - AutoCatCore/Models/PagedResponse.swift | 2 +- .../Models/Protocols/DtoConvertible.swift | 33 + .../Models/{ => Realm}/AudioRecord.swift | 53 +- AutoCatCore/Models/Realm/DebugInfo.swift | 60 ++ AutoCatCore/Models/Realm/Osago.swift | 51 ++ AutoCatCore/Models/Realm/Vehicle.swift | 170 ++++ AutoCatCore/Models/Realm/VehicleAd.swift | 49 ++ AutoCatCore/Models/Realm/VehicleBrand.swift | 32 + AutoCatCore/Models/Realm/VehicleEngine.swift | 42 + AutoCatCore/Models/Realm/VehicleEvent.swift | 52 ++ AutoCatCore/Models/Realm/VehicleModel.swift | 29 + AutoCatCore/Models/Realm/VehicleName.swift | 32 + AutoCatCore/Models/Realm/VehicleNote.swift | 40 + .../Models/Realm/VehicleOwnershipPeriod.swift | 59 ++ AutoCatCore/Models/Realm/VehiclePhoto.swift | 35 + AutoCatCore/Models/Settings.swift | 2 +- AutoCatCore/Models/User.swift | 2 +- AutoCatCore/Models/Vehicle.swift | 419 ---------- AutoCatCore/Models/VehicleAd.swift | 33 - AutoCatCore/Models/VehicleEvent.swift | 97 --- AutoCatCore/Models/VehicleNote.swift | 31 - AutoCatCore/Models/VehicleRegion.swift | 2 +- .../Services/ApiService/ApiError.swift | 30 + .../Services/ApiService/ApiService.swift | 329 ++++++++ .../ApiService/ApiServiceProtocol.swift | 17 + .../StorageService/StorageService+Notes.swift | 65 ++ .../StorageService/StorageService.swift | 58 ++ .../StorageServiceProtocol.swift | 18 + AutoCatCore/Utils/Api.swift | 320 -------- AutoCatCore/Utils/DateCache.swift | 36 - AutoCatCore/Utils/Formatters.swift | 19 + AutoCatCore/Utils/Location.swift | 187 ++--- AutoCatCoreTests/StorageServiceTests.swift | 115 +++ AutoCatTests/AutoCatTests.swift | 19 - .../Extensions/VehicleDto+Presets.swift | 47 ++ AutoCatTests/Info.plist | 22 - AutoCatTests/Mocks/ApiServiceMock.swift | 37 + AutoCatTests/Mocks/FakeLocationManager.swift | 16 - AutoCatTests/Mocks/StorageServiceMock.swift | 36 + AutoCatTests/NotesTests.swift | 116 +++ dmg/AppIcon.icns | Bin 37903 -> 0 bytes dmg/dmgbg.png | Bin 13463 -> 0 bytes dmg/sign_dmg.sh | 62 -- dmg/test.sh | 14 - 139 files changed, 4654 insertions(+), 3586 deletions(-) delete mode 100644 AutoCat/Controllers/NotesController.swift delete mode 100644 AutoCat/Controllers/Osago/DkbmController.swift delete mode 100644 AutoCat/Controllers/Osago/OsagoAddController.swift delete mode 100644 AutoCat/Controllers/Osago/OsagoController.swift delete mode 100644 AutoCat/Controllers/OwnersController.swift delete mode 100644 AutoCat/Extensions/AudioEngine.swift delete mode 100644 AutoCat/JS/dkbm.js create mode 100644 AutoCat/Preview/ApiServiceStub.swift create mode 100644 AutoCat/Preview/StorageServiceStub.swift create mode 100644 AutoCat/Screens/NotesScreen/NoteAlertModifier.swift create mode 100644 AutoCat/Screens/NotesScreen/NotesCoordinator.swift create mode 100644 AutoCat/Screens/NotesScreen/NotesScreen.swift create mode 100644 AutoCat/Screens/NotesScreen/NotesViewModel.swift create mode 100644 AutoCat/Screens/OsagoScreen/OsagoCoordinator.swift create mode 100644 AutoCat/Screens/OsagoScreen/OsagoScreen.swift create mode 100644 AutoCat/Screens/OwnersScreen/OwnersCoordinator.swift create mode 100644 AutoCat/Screens/OwnersScreen/OwnersScreen.swift create mode 100644 AutoCat/SwiftUI/ACProgressHud/ACHud.swift create mode 100644 AutoCat/SwiftUI/ACProgressHud/ACHudContainer.swift create mode 100644 AutoCat/SwiftUI/ACProgressHud/ACMessageView.swift create mode 100644 AutoCat/SwiftUI/ACProgressHud/ACProgressHud+Modifiers.swift create mode 100644 AutoCat/SwiftUI/ACProgressHud/ACProgressHud.swift create mode 100644 AutoCat/SwiftUI/ACProgressHud/ACProgressView.swift delete mode 100644 AutoCat/ThirdParty/SwiftMaskTextfield.swift create mode 100644 AutoCat/Utils/ActivityItemSource.swift create mode 100644 AutoCat/Utils/Coordinator.swift create mode 100644 AutoCat/Utils/Formatters.swift delete mode 100644 AutoCat/Views/eureka/MultilineLabelRow.swift delete mode 100644 AutoCat/Views/eureka/MultilineLinkRow.swift delete mode 100644 AutoCatCore/Models/Cloneable.swift create mode 100644 AutoCatCore/Models/DTO/AudioRecordDto.swift create mode 100644 AutoCatCore/Models/DTO/DebugInfoDto.swift create mode 100644 AutoCatCore/Models/DTO/OsagoDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleAdDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleBrandDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleEngineDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleEventDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleModelDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleNameDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleNoteDto.swift create mode 100644 AutoCatCore/Models/DTO/VehicleOwnershipPeriodDto.swift create mode 100644 AutoCatCore/Models/DTO/VehiclePhotoDto.swift delete mode 100644 AutoCatCore/Models/DebugInfo.swift create mode 100644 AutoCatCore/Models/Firebase/FbRefreshTokenModel.swift create mode 100644 AutoCatCore/Models/Firebase/FbVerifyTokenModel.swift delete mode 100644 AutoCatCore/Models/Osago.swift create mode 100644 AutoCatCore/Models/Protocols/DtoConvertible.swift rename AutoCatCore/Models/{ => Realm}/AudioRecord.swift (50%) create mode 100644 AutoCatCore/Models/Realm/DebugInfo.swift create mode 100644 AutoCatCore/Models/Realm/Osago.swift create mode 100644 AutoCatCore/Models/Realm/Vehicle.swift create mode 100644 AutoCatCore/Models/Realm/VehicleAd.swift create mode 100644 AutoCatCore/Models/Realm/VehicleBrand.swift create mode 100644 AutoCatCore/Models/Realm/VehicleEngine.swift create mode 100644 AutoCatCore/Models/Realm/VehicleEvent.swift create mode 100644 AutoCatCore/Models/Realm/VehicleModel.swift create mode 100644 AutoCatCore/Models/Realm/VehicleName.swift create mode 100644 AutoCatCore/Models/Realm/VehicleNote.swift create mode 100644 AutoCatCore/Models/Realm/VehicleOwnershipPeriod.swift create mode 100644 AutoCatCore/Models/Realm/VehiclePhoto.swift delete mode 100644 AutoCatCore/Models/Vehicle.swift delete mode 100644 AutoCatCore/Models/VehicleAd.swift delete mode 100644 AutoCatCore/Models/VehicleEvent.swift delete mode 100644 AutoCatCore/Models/VehicleNote.swift create mode 100644 AutoCatCore/Services/ApiService/ApiError.swift create mode 100644 AutoCatCore/Services/ApiService/ApiService.swift create mode 100644 AutoCatCore/Services/ApiService/ApiServiceProtocol.swift create mode 100644 AutoCatCore/Services/StorageService/StorageService+Notes.swift create mode 100644 AutoCatCore/Services/StorageService/StorageService.swift create mode 100644 AutoCatCore/Services/StorageService/StorageServiceProtocol.swift delete mode 100644 AutoCatCore/Utils/Api.swift delete mode 100644 AutoCatCore/Utils/DateCache.swift create mode 100644 AutoCatCore/Utils/Formatters.swift create mode 100644 AutoCatCoreTests/StorageServiceTests.swift delete mode 100644 AutoCatTests/AutoCatTests.swift create mode 100644 AutoCatTests/Extensions/VehicleDto+Presets.swift delete mode 100644 AutoCatTests/Info.plist create mode 100644 AutoCatTests/Mocks/ApiServiceMock.swift delete mode 100644 AutoCatTests/Mocks/FakeLocationManager.swift create mode 100644 AutoCatTests/Mocks/StorageServiceMock.swift create mode 100644 AutoCatTests/NotesTests.swift delete mode 100644 dmg/AppIcon.icns delete mode 100644 dmg/dmgbg.png delete mode 100755 dmg/sign_dmg.sh delete mode 100755 dmg/test.sh 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 722b6601f60de662a23e9d58c7742a4e54914cf0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 37903 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 DoRXEb 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/"