Remove some old code

This commit is contained in:
Selim Mustafaev 2025-04-05 20:32:36 +03:00
parent 6ce8c31635
commit a9bbbfa35b
18 changed files with 1 additions and 3121 deletions

View File

@ -20,8 +20,6 @@
7A1022772C557EC400B84627 /* LocationPickerScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1022762C557EC400B84627 /* LocationPickerScreen.swift */; };
7A1022792C557ED600B84627 /* LocationPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */; };
7A10227B2C557EE900B84627 /* LocationPickerCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */; };
7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */; };
7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */; };
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */; };
7A11470123FDE7E500B424AF /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470023FDE7E500B424AF /* AppDelegate.swift */; };
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A11470223FDE7E500B424AF /* SceneDelegate.swift */; };
@ -47,14 +45,11 @@
7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F72CE900440004B740 /* ReportViewModel.swift */; };
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */; };
7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78FE2CE91A740004B740 /* Vehicle.swift */; };
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; };
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF6249FEF690035F39E /* Recorder.swift */; };
7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */; };
7A2E11292CCE395300E5CA17 /* OptionalDatePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A2E11282CCE395300E5CA17 /* OptionalDatePicker.swift */; };
7A2E6FA72C42B3AD00C40DA7 /* AutoCatCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7AF6D1EF2677C03B0086EA64 /* AutoCatCore.framework */; };
7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3399AA299063370087DF98 /* SearchControllerExt.swift */; };
7A3E12D72C7B42B700EE710D /* UserDefaults+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */; };
7A3E30F32C18840600567704 /* ActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3E30F22C18840600567704 /* ActivityItemSource.swift */; };
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A3F07AA24360DC800E59687 /* Dated.swift */; };
7A4322912CB2CC8A00085CF6 /* FiltersScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */; };
7A4322932CB2CCAA00085CF6 /* FiltersViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */; };
@ -62,7 +57,6 @@
7A45FB382C27073700618694 /* StorageService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A45FB372C27073700618694 /* StorageService.swift */; };
7A4927D52CCE438600851C01 /* OptionalBinding.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4927D42CCE438600851C01 /* OptionalBinding.swift */; };
7A4955822D58CCF900912E66 /* HistoryFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A4955812D58CCF900912E66 /* HistoryFilter.swift */; };
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */; };
7A54BFD32D43B95E00176D6D /* DbUpdatePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */; };
7A589E0F2D6B6E8E00EF3FBE /* NumberEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A589E0E2D6B6E8E00EF3FBE /* NumberEditView.swift */; };
7A5911EE2D63226F00EC51BA /* SearchScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A5911ED2D63226F00EC51BA /* SearchScreen.swift */; };
@ -99,10 +93,6 @@
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 */; };
7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A64AE722469DFB600ABE48E /* ContentTransformers.swift */; };
7A6B65B32CFB0DB500AABA6B /* NullifyDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */; };
7A6C4D9E2C56BCA600982597 /* SwiftLocation in Frameworks */ = {isa = PBXBuildFile; productRef = 7A6C4D9D2C56BCA600982597 /* SwiftLocation */; };
7A6C65222D999325001240C2 /* AudioRecordViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A6C65212D999325001240C2 /* AudioRecordViewModel.swift */; };
@ -129,7 +119,6 @@
7A761C0B267E8FF90005F28F /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A761C0A267E8FF90005F28F /* Error.swift */; };
7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A7DADAB2D99738300F52F6C /* AudioRecordView.swift */; };
7A809F392D66755B00CF1B3C /* Error+Canceled.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A809F382D66755B00CF1B3C /* Error+Canceled.swift */; };
7A813DC32508EE4F00CC93B9 /* EventCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A813DC22508EE4F00CC93B9 /* EventCell.swift */; };
7A8A2209248D10EC0073DFD9 /* ResizeImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */; };
7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */; };
7A912F372D381B7400002938 /* LicensePlateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A912F362D381B7400002938 /* LicensePlateView.swift */; };
@ -144,8 +133,6 @@
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A961C6D2C4C3C9E00CE2211 /* LinkRowView.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 */; };
7AA514E02D0B75B3001CAC50 /* StorageService+Events.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */; };
7AA515D02D9ABCC800EB3418 /* RecordPlayerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA515CF2D9ABCC800EB3418 /* RecordPlayerService.swift */; };
7AA515D22D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AA515D12D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift */; };
@ -158,7 +145,6 @@
7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */; };
7AABB1F2267E9CC800D7AB32 /* SwiftDate in Frameworks */ = {isa = PBXBuildFile; productRef = 7AABB1F1267E9CC800D7AB32 /* SwiftDate */; };
7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABBE3A2CF9F85600346588 /* Binding+Map.swift */; };
7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */; };
7AB0EF812C5CC0FE00291EE6 /* SwiftLocationProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */; };
7AB490292D6B1217002F39C6 /* ACKeyboardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */; };
7AB4902B2D6B1446002F39C6 /* ACKeyboardButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB4902A2D6B1446002F39C6 /* ACKeyboardButton.swift */; };
@ -208,7 +194,6 @@
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 */; };
7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.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 */; };
@ -303,8 +288,6 @@
7A1022762C557EC400B84627 /* LocationPickerScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerScreen.swift; sourceTree = "<group>"; };
7A1022782C557ED600B84627 /* LocationPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerViewModel.swift; sourceTree = "<group>"; };
7A10227A2C557EE900B84627 /* LocationPickerCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationPickerCoordinator.swift; sourceTree = "<group>"; };
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordCell.swift; sourceTree = "<group>"; };
7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioPlayer.swift; sourceTree = "<group>"; };
7A1090EB24A4E3E100B4F0B2 /* CellProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CellProgressView.swift; sourceTree = "<group>"; };
7A1146FD23FDE7E500B424AF /* AutoCat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AutoCat.app; sourceTree = BUILT_PRODUCTS_DIR; };
7A11470023FDE7E500B424AF /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -336,8 +319,6 @@
7A1E78F72CE900440004B740 /* ReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = "<group>"; };
7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCoordinator.swift; sourceTree = "<group>"; };
7A1E78FE2CE91A740004B740 /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = "<group>"; };
7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = "<group>"; };
7A27ADF824A09CAD0035F39E /* CocoaError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CocoaError.swift; sourceTree = "<group>"; };
7A2C96112C3B155B00AE46B5 /* NoteAlertModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoteAlertModifier.swift; sourceTree = "<group>"; };
7A2DE69725868AC800A113FC /* VehicleAd.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleAd.swift; sourceTree = "<group>"; };
@ -346,7 +327,6 @@
7A333813249A532400D878F1 /* Filter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Filter.swift; sourceTree = "<group>"; };
7A3399AA299063370087DF98 /* SearchControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchControllerExt.swift; sourceTree = "<group>"; };
7A3E12D62C7B42B700EE710D /* UserDefaults+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+Settings.swift"; sourceTree = "<group>"; };
7A3E30F22C18840600567704 /* ActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityItemSource.swift; sourceTree = "<group>"; };
7A3F07AA24360DC800E59687 /* Dated.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Dated.swift; sourceTree = "<group>"; };
7A4322902CB2CC8A00085CF6 /* FiltersScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersScreen.swift; sourceTree = "<group>"; };
7A4322922CB2CCAA00085CF6 /* FiltersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FiltersViewModel.swift; sourceTree = "<group>"; };
@ -356,7 +336,6 @@
7A4927D42CCE438600851C01 /* OptionalBinding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OptionalBinding.swift; sourceTree = "<group>"; };
7A4955812D58CCF900912E66 /* HistoryFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryFilter.swift; sourceTree = "<group>"; };
7A52AB292580112E002CD910 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleCell.swift; sourceTree = "<group>"; };
7A530B7F2401803A00CBFE6E /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
7A54BFD22D43B95E00176D6D /* DbUpdatePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DbUpdatePolicy.swift; sourceTree = "<group>"; };
7A589E0E2D6B6E8E00EF3FBE /* NumberEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberEditView.swift; sourceTree = "<group>"; };
@ -400,10 +379,6 @@
7A64A2232C1A07EA00284124 /* Formatters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Formatters.swift; sourceTree = "<group>"; };
7A64A2252C1A32C800284124 /* AudioRecordDto.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordDto.swift; sourceTree = "<group>"; };
7A64AE6B2469DC6900ABE48E /* AutoCat.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AutoCat.entitlements; sourceTree = "<group>"; };
7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DismissAnimationController.swift; sourceTree = "<group>"; };
7A64AE702469DFB600ABE48E /* MediaContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaContentView.swift; sourceTree = "<group>"; };
7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaBrowserViewController.swift; sourceTree = "<group>"; };
7A64AE722469DFB600ABE48E /* ContentTransformers.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentTransformers.swift; sourceTree = "<group>"; };
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecord.swift; sourceTree = "<group>"; };
7A6B65B22CFB0DB500AABA6B /* NullifyDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NullifyDate.swift; sourceTree = "<group>"; };
7A6C65212D999325001240C2 /* AudioRecordViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordViewModel.swift; sourceTree = "<group>"; };
@ -427,7 +402,6 @@
7A7DADAB2D99738300F52F6C /* AudioRecordView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioRecordView.swift; sourceTree = "<group>"; };
7A809F382D66755B00CF1B3C /* Error+Canceled.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Canceled.swift"; sourceTree = "<group>"; };
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; };
7A813DC22508EE4F00CC93B9 /* EventCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventCell.swift; sourceTree = "<group>"; };
7A8A2208248D10EC0073DFD9 /* ResizeImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResizeImage.swift; sourceTree = "<group>"; };
7A8AB76425A0DB8F00ECF2C1 /* BundleVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BundleVersion.swift; sourceTree = "<group>"; };
7A8AB76725A0DC8200ECF2C1 /* DebugInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebugInfo.swift; sourceTree = "<group>"; };
@ -446,8 +420,6 @@
7A96AE2E246B2BCD00297C33 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
7A96AE30246B2FE400297C33 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = "<group>"; };
7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleNoteCell.swift; sourceTree = "<group>"; };
7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxRealmDataSource.swift; sourceTree = "<group>"; };
7AA514DF2D0B75B3001CAC50 /* StorageService+Events.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StorageService+Events.swift"; sourceTree = "<group>"; };
7AA515CF2D9ABCC800EB3418 /* RecordPlayerService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordPlayerService.swift; sourceTree = "<group>"; };
7AA515D12D9ABCE600EB3418 /* RecordPlayerServiceProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordPlayerServiceProtocol.swift; sourceTree = "<group>"; };
@ -457,7 +429,6 @@
7AAAFADB2C4D1E130050410D /* ACImageSliderView+Modifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ACImageSliderView+Modifier.swift"; sourceTree = "<group>"; };
7AAAFADD2C4D23620050410D /* ACImageSliderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACImageSliderModel.swift; sourceTree = "<group>"; };
7AABBE3A2CF9F85600346588 /* Binding+Map.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Binding+Map.swift"; sourceTree = "<group>"; };
7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RxSectionedDataSource.swift; sourceTree = "<group>"; };
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
7AB0EF802C5CC0FE00291EE6 /* SwiftLocationProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationProtocol.swift; sourceTree = "<group>"; };
7AB490282D6B1217002F39C6 /* ACKeyboardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ACKeyboardView.swift; sourceTree = "<group>"; };
@ -506,7 +477,6 @@
7AE24C5E251F1B4E00758E39 /* Buttons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Buttons.swift; sourceTree = "<group>"; };
7AE26A3224EEF9EC00625033 /* UIViewControllerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewControllerExt.swift; sourceTree = "<group>"; };
7AE8424D26109F78002F6B31 /* Exportable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Exportable.swift; sourceTree = "<group>"; };
7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigurableCell.swift; sourceTree = "<group>"; };
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 = "<group>"; };
7AF6D1F22677C03B0086EA64 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@ -652,7 +622,6 @@
7AB587352C42E3BF00FA7B66 /* Preview */,
7AFBE8C52C30812E003C491D /* SwiftUI */,
7AC355552969742800889457 /* ACUIKit */,
7A530B7C24017FBE00CBFE6E /* Cells */,
7A11471423FDEAF800B424AF /* Controllers */,
7A1441632C297E9800E79018 /* Screens */,
7A3F07A924360D9100E59687 /* Extensions */,
@ -681,7 +650,6 @@
7A11471923FE839000B424AF /* AuthController.swift */,
7A96AE2C246B2B7400297C33 /* GoogleSignInController.swift */,
7A11471523FDEB2A00B424AF /* MainSplitController.swift */,
7A27ADF2249F8B650035F39E /* RecordsController.swift */,
7AC3554B29696A1C00889457 /* MainTabController.swift */,
7AC3554D29696C4500889457 /* DummyNewController.swift */,
7AC3554F29696D5A00889457 /* NewNumberController.swift */,
@ -692,7 +660,6 @@
7A11472C23FECA3E00B424AF /* ThirdParty */ = {
isa = PBXGroup;
children = (
7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */,
7A6DD90724329144009DE740 /* CenterTextLayer.swift */,
);
path = ThirdParty;
@ -701,11 +668,6 @@
7A11474223FF06B600B424AF /* Utils */ = {
isa = PBXGroup;
children = (
7A27ADF6249FEF690035F39E /* Recorder.swift */,
7A1090E924A3A26300B4F0B2 /* AudioPlayer.swift */,
7A9FEEC72529AB23001CA50E /* RxRealmDataSource.swift */,
7AABDE25253350C30041AFC6 /* RxSectionedDataSource.swift */,
7A3E30F22C18840600567704 /* ActivityItemSource.swift */,
7A14416D2C297F7C00E79018 /* Coordinator.swift */,
7AB4E4652D58A16C0006D052 /* GenericError.swift */,
);
@ -844,18 +806,6 @@
path = Services;
sourceTree = "<group>";
};
7A530B7C24017FBE00CBFE6E /* Cells */ = {
isa = PBXGroup;
children = (
7A530B7D24017FEE00CBFE6E /* VehicleCell.swift */,
7A1090E724A394F100B4F0B2 /* AudioRecordCell.swift */,
7A813DC22508EE4F00CC93B9 /* EventCell.swift */,
7AEFC3BD2529D3CC00BADFB2 /* ConfigurableCell.swift */,
7A99406326E4BFAE002E9CB6 /* VehicleNoteCell.swift */,
);
path = Cells;
sourceTree = "<group>";
};
7A5911EC2D63225500EC51BA /* SearchScreen */ = {
isa = PBXGroup;
children = (
@ -934,17 +884,6 @@
path = Protocols;
sourceTree = "<group>";
};
7A64AE6E2469DFB600ABE48E /* ATGMediaBrowser */ = {
isa = PBXGroup;
children = (
7A64AE6F2469DFB600ABE48E /* DismissAnimationController.swift */,
7A64AE702469DFB600ABE48E /* MediaContentView.swift */,
7A64AE712469DFB600ABE48E /* MediaBrowserViewController.swift */,
7A64AE722469DFB600ABE48E /* ContentTransformers.swift */,
);
path = ATGMediaBrowser;
sourceTree = "<group>";
};
7A6C65202D99930C001240C2 /* AudioRecordView */ = {
isa = PBXGroup;
children = (
@ -1468,7 +1407,6 @@
buildActionMask = 2147483647;
files = (
7A961C6C2C4C3C8600CE2211 /* TextRowView.swift in Sources */,
7AEFC3BE2529D3CC00BADFB2 /* ConfigurableCell.swift in Sources */,
7A1022772C557EC400B84627 /* LocationPickerScreen.swift in Sources */,
7A4322932CB2CCAA00085CF6 /* FiltersViewModel.swift in Sources */,
7A5D7E0C2C71EB25002C17E7 /* ToggleRowView.swift in Sources */,
@ -1479,13 +1417,10 @@
7A3399AB299063370087DF98 /* SearchControllerExt.swift in Sources */,
7A14416E2C297F7C00E79018 /* Coordinator.swift in Sources */,
7A6DD90824329144009DE740 /* CenterTextLayer.swift in Sources */,
7A99406426E4BFAE002E9CB6 /* VehicleNoteCell.swift in Sources */,
7AC3554C29696A1C00889457 /* MainTabController.swift in Sources */,
7A813DC32508EE4F00CC93B9 /* EventCell.swift in Sources */,
7A1441682C297EFD00E79018 /* NotesViewModel.swift in Sources */,
7AFBE8C02C3024E5003C491D /* ACHud.swift in Sources */,
7A9519842D80B72B00E69883 /* RecordsCoordinator.swift in Sources */,
7AABDE26253350C30041AFC6 /* RxSectionedDataSource.swift in Sources */,
7AAAFADA2C4D1AFE0050410D /* Zoomable.swift in Sources */,
7AC8B2762D6A01C700190706 /* UISearchTextField+Dumb.swift in Sources */,
7A6DD90C24335A6D009DE740 /* FlagLayer.swift in Sources */,
@ -1505,8 +1440,6 @@
7A06E0AE2C7065C7005731AC /* SettingsViewModel.swift in Sources */,
7AB4E42C2D397D8E0006D052 /* VehicleCellView.swift in Sources */,
7A961C6E2C4C3C9E00CE2211 /* LinkRowView.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 */,
@ -1517,7 +1450,6 @@
7AFBE8CA2C3081C7003C491D /* ACProgressHud+Modifiers.swift in Sources */,
7A71EF572D0A26B200943129 /* EventModel.swift in Sources */,
7AABBE3B2CF9F85600346588 /* Binding+Map.swift in Sources */,
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */,
7A1E78F62CE900330004B740 /* ReportScreen.swift in Sources */,
7A10226C2C551EC500B84627 /* LocationEditScreen.swift in Sources */,
7A7158072C44085600852088 /* OsagoScreen.swift in Sources */,
@ -1526,12 +1458,9 @@
7A912F372D381B7400002938 /* LicensePlateView.swift in Sources */,
7A3F07AB24360DC800E59687 /* Dated.swift in Sources */,
7AC76D7B270083AE0084DB27 /* TextView.swift in Sources */,
7A1090E824A394F100B4F0B2 /* AudioRecordCell.swift in Sources */,
7A2C96122C3B155B00AE46B5 /* NoteAlertModifier.swift in Sources */,
7A64AE762469DFB600ABE48E /* ContentTransformers.swift in Sources */,
7AE24C5F251F1B4E00758E39 /* Buttons.swift in Sources */,
7A11471A23FE839000B424AF /* AuthController.swift in Sources */,
7A64AE742469DFB600ABE48E /* MediaContentView.swift in Sources */,
7A5911F22D63268400EC51BA /* SearchCoordinator.swift in Sources */,
7A7DADAC2D99738300F52F6C /* AudioRecordView.swift in Sources */,
7A1090EC24A4E3E100B4F0B2 /* CellProgressView.swift in Sources */,
@ -1539,20 +1468,17 @@
7A96AE2D246B2B7400297C33 /* GoogleSignInController.swift in Sources */,
7A10227B2C557EE900B84627 /* LocationPickerCoordinator.swift in Sources */,
7AB490292D6B1217002F39C6 /* ACKeyboardView.swift in Sources */,
7A1090EA24A3A26300B4F0B2 /* AudioPlayer.swift in Sources */,
7A11471623FDEB2A00B424AF /* MainSplitController.swift in Sources */,
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
7A1022722C554A1300B84627 /* CustomHostingController.swift in Sources */,
7ADF6C9F251201D200F237B2 /* GlobalEventsController.swift in Sources */,
7A1022792C557ED600B84627 /* LocationPickerViewModel.swift in Sources */,
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */,
7A10226E2C551EE000B84627 /* LocationEditViewModel.swift in Sources */,
7AB4902B2D6B1446002F39C6 /* ACKeyboardButton.swift in Sources */,
7AFBE8CE2C308B53003C491D /* ACMessageView.swift in Sources */,
7A14416C2C297F2100E79018 /* NotesCoordinator.swift in Sources */,
7A9FEEC82529AB23001CA50E /* RxRealmDataSource.swift in Sources */,
7AAAFADE2C4D23620050410D /* ACImageSliderModel.swift in Sources */,
7A8AB76525A0DB8F00ECF2C1 /* BundleVersion.swift in Sources */,
7AC3555229696E3F00889457 /* UIView+layout.swift in Sources */,
@ -1579,10 +1505,8 @@
7AB9FE222D08C2A5005DE374 /* EventsScreen.swift in Sources */,
7ADF6C93250B954900F237B2 /* Navigation.swift in Sources */,
7A5911F02D63266B00EC51BA /* SearchViewModel.swift in Sources */,
7A64AE752469DFB600ABE48E /* MediaBrowserViewController.swift in Sources */,
7ABD1B4B2D044A7D00B43213 /* GalleryCoordinator.swift in Sources */,
7A589E0F2D6B6E8E00EF3FBE /* NumberEditView.swift in Sources */,
7A64AE732469DFB600ABE48E /* DismissAnimationController.swift in Sources */,
7ADF6C97250F41B000F237B2 /* PNKeyboard.swift in Sources */,
7A1022702C551EFD00B84627 /* LocationEditCoordinator.swift in Sources */,
7A7158042C43EAA200852088 /* OwnersCoordinator.swift in Sources */,

View File

@ -1,81 +0,0 @@
import UIKit
import PKHUD
import AutoCatCore
class AudioRecordCell: UITableViewCell, ConfigurableCell {
@IBOutlet weak var playButton: UIButton!
@IBOutlet weak var duration: UILabel!
@IBOutlet weak var number: UILabel!
@IBOutlet weak var date: UILabel!
@IBOutlet weak var progressView: CellProgressView!
let dateFormatter = DateFormatter()
let componentsFormatter = DateComponentsFormatter()
var record: AudioRecordDto?
override func awakeFromNib() {
super.awakeFromNib()
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeStyle = .short
self.componentsFormatter.unitsStyle = .abbreviated
self.componentsFormatter.allowedUnits = [.minute, .second]
self.componentsFormatter.zeroFormattingBehavior = .pad
DispatchQueue.main.async {
self.progressView.progress = 0
}
}
override func prepareForReuse() {
super.prepareForReuse()
self.record = nil
self.progressView.progress = 0
}
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)
// 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) {
if let record = self.record {
do {
let url = try FileManager.default.url(for: record.path, in: "recordings")
try AudioPlayer.shared.play(url: url)
} catch {
print("Error playing audio record: \(error.localizedDescription)")
HUD.show(error: error)
}
}
}
}

View File

@ -1,7 +0,0 @@
import UIKit
@MainActor
protocol ConfigurableCell {
associatedtype Item
func configure(with item: Item)
}

View File

@ -1,39 +0,0 @@
import UIKit
import AutoCatCore
class EventCell: UITableViewCell {
@IBOutlet weak var address: UILabel!
@IBOutlet weak var date: UILabel!
@IBOutlet weak var userImageView: UIImageView!
@Service var settingsService: SettingsServiceProtocol
let dateFormatter = DateFormatter()
override func awakeFromNib() {
super.awakeFromNib()
self.dateFormatter.dateStyle = .short
self.dateFormatter.timeStyle = .short
}
func configure(with event: VehicleEventDto) {
if let addressString = event.address {
self.address.text = addressString
} else {
self.address.text = "Lat: \(event.latitude), Lon: \(event.longitude)"
}
let date = Date(timeIntervalSince1970: event.date)
self.date.text = self.dateFormatter.string(from: date)
if let addedBy = event.addedBy {
let isMe = addedBy == settingsService.user.email
userImageView.image = UIImage(systemName: isMe ? "person.fill" : "person")
userImageView.tintColor = isMe ? self.tintColor : .secondaryLabel
userImageView.isHidden = false
} else {
userImageView.isHidden = true
}
}
}

View File

@ -1,53 +0,0 @@
import UIKit
import AutoCatCore
class VehicleCell: UITableViewCell, ConfigurableCell {
@IBOutlet weak var name: UILabel!
@IBOutlet weak var plate: PlateView!
@IBOutlet weak var addedDate: UILabel!
@IBOutlet weak var updatedDate: UILabel!
@IBOutlet weak var bubbleImage: UIImageView!
@IBOutlet weak var notesCount: UILabel!
@IBOutlet weak var syncImage: UIImageView!
let formatter = DateFormatter()
override func awakeFromNib() {
super.awakeFromNib()
formatter.dateStyle = .short
formatter.timeStyle = .short
}
func configure(with vehicle: VehicleDto) {
self.name.text = vehicle.brand?.name?.original ?? "<unknown>"
self.plate.number = PlateNumber(vehicle.getNumber())
self.plate.fontSize = 40
if vehicle.unrecognized {
self.plate.foreground = .systemRed
} else if vehicle.outdated {
self.plate.foreground = .systemGray3
} else {
self.plate.foreground = nil
}
if vehicle.updatedDate - vehicle.addedDate > 60 {
self.addedDate.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.addedDate))
self.updatedDate.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.updatedDate))
self.addedDate.isHidden = false
} else {
self.addedDate.isHidden = true
self.updatedDate.text = formatter.string(from: Date(timeIntervalSince1970: vehicle.updatedDate))
}
self.syncImage.isHidden = vehicle.synchronized || vehicle.unrecognized
self.bubbleImage.isHidden = vehicle.notes.isEmpty
self.notesCount.isHidden = vehicle.notes.isEmpty
if !vehicle.notes.isEmpty {
self.notesCount.text = String(vehicle.notes.count)
}
}
}

View File

@ -1,24 +0,0 @@
import UIKit
import AutoCatCore
class VehicleNoteCell: UITableViewCell {
@IBOutlet weak var noteText: UILabel!
@IBOutlet weak var date: UILabel!
private var dateFormatter = DateFormatter()
override func awakeFromNib() {
super.awakeFromNib()
DispatchQueue.main.async {
self.dateFormatter.dateStyle = .medium
self.dateFormatter.timeStyle = .medium
}
}
func configure(with note: VehicleNoteDto) {
self.noteText.text = note.text
self.date.text = self.dateFormatter.string(from: Date(timeIntervalSince1970: note.date))
}
}

View File

@ -1,368 +0,0 @@
import UIKit
import AVFoundation
import RealmSwift
import Intents
import CoreSpotlight
import MobileCoreServices
import os.log
import PKHUD
import AutoCatCore
class RecordsController: UIViewController, UITableViewDelegate {
@IBOutlet weak var tableView: UITableView!
@Service var settingsService: SettingsServiceProtocol
var recorder: Recorder?
var addButton: UIBarButtonItem!
var audioSessionObserver: NSObjectProtocol?
var recordsDataSource: RealmSectionedDataSource<AudioRecord, AudioRecordCell>!
let validLetters = Constants.pnLettersMap.keys.map(String.init)
override func viewDidLoad() {
super.viewDidLoad()
guard let realm = try? Realm() else { return }
self.addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(onAddVoiceRecord(_:)))
self.navigationItem.rightBarButtonItem = self.addButton
self.recorder = Recorder()
DispatchQueue.main.async {
self.recordsDataSource = RealmSectionedDataSource(table: self.tableView, data: realm.objects(AudioRecord.self)
.sorted(byKeyPath: "addedDate", ascending: false))
}
self.tableView.delegate = self
}
func donateUserActivity() {
let activityId = "pro.aliencat.autocat.addVoiceRecord"
let activity = NSUserActivity(activityType: activityId)
activity.persistentIdentifier = activityId
activity.isEligibleForSearch = true
activity.isEligibleForPrediction = true
activity.title = NSLocalizedString("Add new audio record", comment: "")
activity.suggestedInvocationPhrase = "Запиши номер"
let attributes = CSSearchableItemAttributeSet()
attributes.contentType = kUTTypeItem as String
attributes.contentDescription = NSLocalizedString("Add new plate number via audio recording", comment: "")
activity.contentAttributeSet = attributes
self.userActivity = activity
activity.becomeCurrent()
}
func stopRecording() {
self.recorder?.stopRecording()
}
// MARK: - Bar button handlers
@objc func onAddVoiceRecord(_ sender: UIBarButtonItem) {
guard let recorder = self.recorder else {
HUD.flash(.labeledError(title: nil, subtitle: "Audio recorder is not available"))
return
}
self.addButton.isEnabled = false
var alert: UIAlertController?
var url: URL!
Task { @MainActor in
do {
let event = try await RxLocationManager.requestCurrentLocation()
try await recorder.requestPermissions()
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 {
DispatchQueue.main.async {
alert = self.showRecordingAlert()
}
}
}
#endif
let date = Date()
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
url = try FileManager.default.url(for: fileName, in: "recordings")
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)
}
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.recorder?.cancelRecording() }))
alert.addAction(UIAlertAction(title: NSLocalizedString("Done", comment: ""), style: .default, handler: { _ in self.recorder?.stopRecording() }))
self.present(alert, animated: true)
return alert
}
// MARK: - Processing
func getPlateNumber(from recognizedText: String) -> String? {
let trimmed = recognizedText
.replacingOccurrences(of: " ", with: "")
.uppercased()
.replacingOccurrences(of: "Ф", with: "В")
.replacingOccurrences(of: "НОЛЬ", with: "0")
.replacingOccurrences(of: "Э", with: "")
var result = ""
if let range = trimmed.range(of: #"\S\d\d\d\S\S\d\d\d?"#, options: .regularExpression) {
result = String(trimmed[range])
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d\d\d\d?"#, options: .regularExpression), settingsService.recognizeAlternativeOrder {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + n.substring(from: 6)
} else if let range = trimmed.range(of: #"\S\d\d\d\S\S"#, options: .regularExpression), settingsService.recognizeShortenedNumbers {
result = String(trimmed[range]) + settingsService.defaultRegion
} else if let range = trimmed.range(of: #"\S\S\S\d\d\d"#, options: .regularExpression), settingsService.recognizeAlternativeOrder && settingsService.recognizeShortenedNumbers {
let n = String(trimmed[range])
result = String(n.prefix(1)) + n.substring(with: 3..<6) + n.substring(with: 1..<3) + settingsService.defaultRegion
}
if !result.isEmpty && valid(number: result) {
return result
} else {
return nil
}
}
func valid(number: String) -> Bool {
guard number.count >= 8 else { return false }
let first = String(number.prefix(1))
let second = number.substring(with: 4..<5)
let third = number.substring(with: 5..<6)
let digits = Int(number.substring(with: 1..<4))
let region = Int(number.substring(from: 6))
return self.validLetters.contains(first)
&& self.validLetters.contains(second)
&& self.validLetters.contains(third)
&& digits != nil
&& region != nil
&& region! < 1000
}
func makeStartSoundIfNeeded() async {
guard settingsService.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()
}
}
}
// MARK: - UITableViewDelegate
func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
let record = self.recordsDataSource.item(at: indexPath)
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { _ in
let check = UIAction(title: NSLocalizedString("Check", comment: ""), image: UIImage(systemName: "eye")) { action in
if let number = record.number {
self.check(number: number, event: record.event)
}
}
let delete = UIAction(title: NSLocalizedString("Delete", comment: ""), image: UIImage(systemName: "trash"), attributes: .destructive) { action in
self.delete(record: record)
}
let share = UIAction(title: NSLocalizedString("Share", comment: ""), image: UIImage(systemName: "square.and.arrow.up")) { action in
self.share(record: record)
}
let showText = UIAction(title: NSLocalizedString("Show recognized text", comment: ""), image: UIImage(systemName: "textformat")) { action in
self.showAlert(title: NSLocalizedString("Recognized text", comment: ""), message: record.rawText)
}
let showMap = UIAction(title: NSLocalizedString("Show on map", comment: ""), image: UIImage(systemName: "mappin.and.ellipse")) { action in
self.showOnMap(record)
}
let edit = UIAction(title: NSLocalizedString("Edit plate number", comment: ""), image: UIImage(systemName: "pencil")) { action in
self.edit(record: record)
}
var actions = [edit, showText, showMap, share, delete]
if record.number != nil {
actions.insert(check, at: 0)
}
return UIMenu(title: NSLocalizedString("Actions", comment: ""), children: actions)
}
}
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let cell = tableView.cellForRow(at: indexPath) else { return nil }
let record = self.recordsDataSource.item(at: indexPath)
let check = UIContextualAction(style: .normal, title: NSLocalizedString("Check", comment: "")) { action, view, completion in
if let number = record.number {
self.check(number: number, event: record.event)
}
completion(true)
}
check.backgroundColor = .systemGray2
check.image = UIImage(systemName: "eye")
let action = UIContextualAction(style: .normal, title: NSLocalizedString("Action", comment: "")) { action, view, completion in
self.moreActions(for: record, cell: cell)
completion(true)
}
action.backgroundColor = .systemGray2
action.image = UIImage(systemName: "ellipsis" /*"square.and.arrow.up"*/)
let delete = UIContextualAction(style: .destructive, title: NSLocalizedString("Delete", comment: "")) { action, view, completion in
self.delete(record: record)
completion(true)
}
delete.image = UIImage(systemName: "trash")
let actions = record.number == nil ? [delete, action] : [delete, check, action]
let configuration = UISwipeActionsConfiguration(actions: actions)
configuration.performsFirstActionWithFullSwipe = false
return configuration
}
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
self.share(record: record)
}
let showText = UIAlertAction(title: NSLocalizedString("Show recognized text", comment: ""), style: .default) { action in
self.showAlert(title: NSLocalizedString("Recognized text", comment: ""), message: record.rawText)
}
let editNumber = UIAlertAction(title: NSLocalizedString("Edit plate number", comment: ""), style: .default) { action in
self.edit(record: record)
}
let showOnMap = UIAlertAction(title: NSLocalizedString("Show on map", comment: ""), style: .default) { action in
self.showOnMap(record)
}
sheet.addAction(editNumber)
sheet.addAction(showText)
if record.event != nil {
sheet.addAction(showOnMap)
}
sheet.addAction(share)
sheet.addAction(cancel)
sheet.popoverPresentationController?.sourceView = cell
sheet.popoverPresentationController?.sourceRect = cell.frame
self.present(sheet, animated: true, completion: nil)
}
func check(number: String, event: VehicleEventDto?) {
// TODO: Implement checking number without quick action
self.tabBarController?.selectedIndex = 0
}
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(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) {
try? realm.write {
realmRecord.number = tf.text?.uppercased()
}
}
}
alert.addAction(done)
alert.addAction(UIAlertAction(title: NSLocalizedString("Cancel", comment: ""), style: .cancel, handler: { action in
alert.dismiss(animated: true)
}))
alert.addTextField { tf in
tf.text = record.number ?? record.rawText.replacingOccurrences(of: " ", with: "")
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: AudioRecordDto) {
do {
if let realm = try? Realm(), let realmRecord = realm.object(ofType: AudioRecord.self, forPrimaryKey: record.path) {
try realm.write {
realm.delete(realmRecord)
}
}
} catch {
print(error)
}
}
func share(record: AudioRecordDto) {
do {
let url = try FileManager.default.url(for: record.path, in: "recordings")
let controller = UIActivityViewController(activityItems: [url], applicationActivities: nil)
self.present(controller, animated: true)
} catch {
print("Error sharing audio record: \(error.localizedDescription)")
HUD.show(error: error)
}
}
func showOnMap(_ record: AudioRecordDto) {
let controller = ShowEventController()
controller.event = record.event
controller.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(controller, animated: true)
}
}

View File

@ -136,23 +136,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
// to restore the scene back to its current state.
if let split = self.window?.rootViewController as? MainSplitController, let tabvc = split.viewControllers.first as? UITabBarController {
if tabvc.selectedIndex == 1 {
if let nav = tabvc.selectedViewController as? UINavigationController, let child = nav.topViewController {
print(type(of: child))
if let record = child as? RecordsController {
record.stopRecording()
}
}
}
}
do {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
} catch {
print("sceneDidEnterBackground failed to deactivate audio session: \(error.localizedDescription)")
}
}
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {

View File

@ -71,5 +71,5 @@ struct VehicleCellView: View {
}
#Preview {
VehicleCell()
VehicleCellView(vehicle: .preview)
}

View File

@ -1,237 +0,0 @@
//
// ContentTransformers.swift
// ATGMediaBrowser
//
// Created by Suraj Thomas K on 7/17/18.
// Copyright © 2018 Al Tayer Group LLC.
//
// 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
/**
Content transformer used for transition between media item views.
- parameter contentView: The content view on which transform corresponding to the position has to be applied.
- parameter position: Current position for the passed content view.
- note:
The trasnform to be applied on the contentView has to be dependent on the position passed.
The position value can be -ve, 0.0 or positive.
Try to visualize content views at -1.0[previous]=>0.0[current]=>1.0[next].
1. When position is -1.0, the content view should be at the place meant for previous view.
2. When the position is 0.0, the transform applied on the content view should make it visible full screen at origin.
3. When position is 1.0, the content view should be at the place meant for next view.
Be mindful of the drawing order, when designing new transitions.
*/
public typealias ContentTransformer = (_ contentView: UIView, _ position: CGFloat) -> Void
// MARK: - Default Transitions
/// An enumeration to hold default content transformers
@MainActor
public enum DefaultContentTransformers {
/**
Horizontal move-in-out content transformer.
- Requires:
* GestureDirection: Horizontal
*/
public static let horizontalMoveInOut: ContentTransformer = { contentView, position in
let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing
contentView.transform = CGAffineTransform(translationX: widthIncludingGap * position, y: 0.0)
}
/**
Vertical move-in-out content transformer.
- Requires:
* GestureDirection: Vertical
*/
public static let verticalMoveInOut: ContentTransformer = { contentView, position in
let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing
contentView.transform = CGAffineTransform(translationX: 0.0, y: heightIncludingGap * position)
}
/**
Horizontal slide-out content transformer.
- Requires:
* GestureDirection: Horizontal
* DrawOrder: PreviousToNext
*/
public static let horizontalSlideOut: ContentTransformer = { contentView, position in
var scale: CGFloat = 1.0
if position < -0.5 {
scale = 0.9
} else if -0.5...0.0 ~= Double(position) {
scale = 1.0 + (position * 0.2)
}
var transform = CGAffineTransform(scaleX: scale, y: scale)
let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing
let x = position >= 0.0 ? widthIncludingGap * position : 0.0
transform = transform.translatedBy(x: x, y: 0.0)
contentView.transform = transform
let margin: CGFloat = 0.0000001
contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
}
/**
Vertical slide-out content transformer.
- Requires:
* GestureDirection: Vertical
* DrawOrder: PreviousToNext
*/
public static let verticalSlideOut: ContentTransformer = { contentView, position in
var scale: CGFloat = 1.0
if position < -0.5 {
scale = 0.9
} else if -0.5...0.0 ~= Double(position) {
scale = 1.0 + (position * 0.2)
}
var transform = CGAffineTransform(scaleX: scale, y: scale)
let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing
let y = position >= 0.0 ? heightIncludingGap * position : 0.0
transform = transform.translatedBy(x: 0.0, y: y)
contentView.transform = transform
let margin: CGFloat = 0.0000001
contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
}
/**
Horizontal slide-in content transformer.
- Requires:
* GestureDirection: Horizontal
* DrawOrder: NextToPrevious
*/
public static let horizontalSlideIn: ContentTransformer = { contentView, position in
var scale: CGFloat = 1.0
if position > 0.5 {
scale = 0.9
} else if 0.0...0.5 ~= Double(position) {
scale = 1.0 - (position * 0.2)
}
var transform = CGAffineTransform(scaleX: scale, y: scale)
let widthIncludingGap = contentView.bounds.size.width + MediaContentView.interItemSpacing
let x = position > 0.0 ? 0.0 : widthIncludingGap * position
transform = transform.translatedBy(x: x, y: 0.0)
contentView.transform = transform
let margin: CGFloat = 0.0000001
contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
}
/**
Vertical slide-in content transformer.
- Requires:
* GestureDirection: Vertical
* DrawOrder: NextToPrevious
*/
public static let verticalSlideIn: ContentTransformer = { contentView, position in
var scale: CGFloat = 1.0
if position > 0.5 {
scale = 0.9
} else if 0.0...0.5 ~= Double(position) {
scale = 1.0 - (position * 0.2)
}
var transform = CGAffineTransform(scaleX: scale, y: scale)
let heightIncludingGap = contentView.bounds.size.height + MediaContentView.interItemSpacing
let y = position > 0.0 ? 0.0 : heightIncludingGap * position
transform = transform.translatedBy(x: 0.0, y: y)
contentView.transform = transform
let margin: CGFloat = 0.0000001
contentView.isHidden = ((1.0-margin)...(1.0+margin) ~= abs(position))
}
/**
Horizontal zoom-in-out content transformer.
- Requires:
* GestureDirection: Horizontal
*/
public static let horizontalZoomInOut: ContentTransformer = { contentView, position in
let minScale: CGFloat = 0.5
// Scale factor is used to reduce the scale animation speed.
let scaleFactor: CGFloat = 0.5
var scale: CGFloat = CGFloat.maximum(minScale, 1.0 - abs(position * scaleFactor))
// Actual gap will be scaleFactor * 0.5 times of contentView.bounds.size.width.
let actualGap = contentView.bounds.size.width * scaleFactor * 0.5
let gapCorrector = MediaContentView.interItemSpacing - actualGap
let widthIncludingGap = contentView.bounds.size.width + gapCorrector
let translation = (widthIncludingGap * position)/scale
var transform = CGAffineTransform(scaleX: scale, y: scale)
transform = transform.translatedBy(x: translation, y: 0.0)
contentView.transform = transform
}
/**
Vertical zoom-in-out content transformer.
- Requires:
* GestureDirection: Vertical
*/
public static let verticalZoomInOut: ContentTransformer = { contentView, position in
let minScale: CGFloat = 0.5
// Scale factor is used to reduce the scale animation speed.
let scaleFactor: CGFloat = 0.5
let scale: CGFloat = CGFloat.maximum(minScale, 1.0 - abs(position * scaleFactor))
// Actual gap will be scaleFactor * 0.5 times of contentView.bounds.size.height.
let actualGap = contentView.bounds.size.height * scaleFactor * 0.5
let gapCorrector = MediaContentView.interItemSpacing - actualGap
let heightIncludingGap = contentView.bounds.size.height + gapCorrector
let translation = (heightIncludingGap * position)/scale
var transform = CGAffineTransform(scaleX: scale, y: scale)
transform = transform.translatedBy(x: 0.0, y: translation)
contentView.transform = transform
}
}

View File

@ -1,289 +0,0 @@
//
// DismissAnimationController.swift
// ATGMediaBrowser
//
// Created by Suraj Thomas K on 7/19/18.
// Copyright © 2018 Al Tayer Group LLC.
//
// 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
@MainActor
internal class DismissAnimationController: NSObject {
private enum Constants {
static let minimumVelocity: CGFloat = 15.0
static let minimumTranslation: CGFloat = 0.25
static let transitionDuration = 0.3
static let updateFrameRate: CGFloat = 60.0
static let transitionSpeedFactor: CGFloat = 0.15
static let minimumZoomDuringInteraction: CGFloat = 0.9
}
internal var image: UIImage?
internal let gestureDirection: MediaBrowserViewController.GestureDirection
internal weak var viewController: MediaBrowserViewController?
internal var interactionInProgress = false
private lazy var imageView = UIImageView()
private var backgroundView: UIView?
private var timer: Timer?
private var distanceToMove: CGPoint = .zero
private var relativePosition: CGPoint = .zero
private var progressValue: CGFloat {
return (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x
}
private var shouldZoomOutOnInteraction = false
init(
image: UIImage? = nil,
gestureDirection: MediaBrowserViewController.GestureDirection,
viewController: MediaBrowserViewController
) {
self.image = image
self.gestureDirection = gestureDirection
self.viewController = viewController
}
internal func handleInteractiveTransition(_ recognizer: UIPanGestureRecognizer) {
let translation = recognizer.translation(in: recognizer.view)
let progress = CGPoint(
x: translation.x / UIScreen.main.bounds.size.width,
y: translation.y / UIScreen.main.bounds.size.height
)
switch recognizer.state {
case .began:
beginTransition()
fallthrough
case .changed:
relativePosition = progress
updateTransition()
case .ended, .cancelled, .failed:
var toMove: CGFloat = 0.0
if abs(progressValue) > Constants.minimumTranslation {
if let viewController = viewController,
let targetFrame = viewController.dataSource?.targetFrameForDismissal(viewController) {
animateToTargetFrame(targetFrame)
return
} else {
toMove = (progressValue / abs(progressValue))
}
} else {
toMove = -progressValue
}
if gestureDirection == .horizontal {
distanceToMove.x = -relativePosition.x
distanceToMove.y = toMove
} else {
distanceToMove.x = toMove
distanceToMove.y = -relativePosition.y
}
if timer == nil {
timer = Timer.scheduledTimer(
timeInterval: 1.0/Double(Constants.updateFrameRate),
target: self,
selector: #selector(update(_:)),
userInfo: nil,
repeats: true
)
}
default:
break
}
}
internal func animateToTargetFrame(_ target: CGRect) {
let frame = imageViewFrame(for: imageView.bounds.size, in: target, mode: .scaleAspectFill)
UIView.animate(withDuration: Constants.transitionDuration, animations: {
self.imageView.frame = frame
self.backgroundView?.alpha = 0.0
}) { finished in
if finished {
self.interactionInProgress = false
if self.gestureDirection == .horizontal {
self.relativePosition.y = -1.0
} else {
self.relativePosition.x = -1.0
}
self.finishTransition()
}
}
}
@objc private func update(_ timeInterval: TimeInterval) {
let speed = (Constants.updateFrameRate * Constants.transitionSpeedFactor)
let xDistance = distanceToMove.x / speed
let yDistance = distanceToMove.y / speed
distanceToMove.x -= xDistance
distanceToMove.y -= yDistance
relativePosition.x += xDistance
relativePosition.y += yDistance
updateTransition()
let translation = CGPoint(
x: xDistance * (UIScreen.main.bounds.size.width),
y: yDistance * (UIScreen.main.bounds.size.height)
)
let directionalTranslation = (gestureDirection == .horizontal) ? translation.y : translation.x
if abs(directionalTranslation) < 1.0 {
relativePosition.x += distanceToMove.x
relativePosition.y += distanceToMove.y
updateTransition()
interactionInProgress = false
finishTransition()
}
}
internal func beginTransition() {
shouldZoomOutOnInteraction = false
if let viewController = viewController {
shouldZoomOutOnInteraction = viewController.dataSource?.targetFrameForDismissal(viewController) != nil
}
createTransitionViews()
viewController?.mediaContainerView.isHidden = true
viewController?.hideControls = true
viewController?.visualEffectContainer.isHidden = true
}
private func finishTransition() {
distanceToMove = .zero
timer?.invalidate()
timer = nil
imageView.removeFromSuperview()
backgroundView?.removeFromSuperview()
backgroundView = nil
let directionalPosition = (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x
if directionalPosition != 0.0 {
viewController?.dismiss(animated: false, completion: nil)
} else {
viewController?.mediaContainerView.isHidden = false
viewController?.hideControls = false
viewController?.visualEffectContainer.isHidden = false
}
}
private func createTransitionViews() {
backgroundView?.removeFromSuperview()
backgroundView = nil
if let viewController = viewController,
let bg = viewController.visualEffectContainer.snapshotView(afterScreenUpdates: true) {
backgroundView = bg
viewController.view.addSubview(bg)
NSLayoutConstraint.activate([
bg.leadingAnchor.constraint(equalTo: viewController.view.leadingAnchor),
bg.trailingAnchor.constraint(equalTo: viewController.view.trailingAnchor),
bg.topAnchor.constraint(equalTo: viewController.view.topAnchor),
bg.bottomAnchor.constraint(equalTo: viewController.view.bottomAnchor)
])
}
imageView.image = image
imageView.frame = imageViewFrame(
for: image?.size ?? .zero,
in: viewController?.view.bounds ?? .zero
)
viewController?.view.addSubview(imageView)
imageView.transform = CGAffineTransform.identity
}
private func updateTransition() {
var transform = CGAffineTransform.identity
let directionalPosition = (gestureDirection == .horizontal) ? relativePosition.y : relativePosition.x
if shouldZoomOutOnInteraction {
let scale = CGFloat.maximum(Constants.minimumZoomDuringInteraction, 1.0 - abs(directionalPosition))
transform = transform.scaledBy(x: scale, y: scale)
}
if gestureDirection == .horizontal {
transform = transform.translatedBy(
x: shouldZoomOutOnInteraction ? relativePosition.x * UIScreen.main.bounds.size.width : 0.0,
y: relativePosition.y * UIScreen.main.bounds.size.height
)
} else {
transform = transform.translatedBy(
x: relativePosition.x * UIScreen.main.bounds.size.width,
y: shouldZoomOutOnInteraction ? relativePosition.y * UIScreen.main.bounds.size.height : 0.0
)
}
imageView.transform = transform
let alpha = (directionalPosition < 0.0) ? directionalPosition + 1.0 : 1.0 - directionalPosition
backgroundView?.alpha = alpha
}
private func imageViewFrame(for imageSize: CGSize, in frame: CGRect, mode: UIView.ContentMode = .scaleAspectFit) -> CGRect {
guard imageSize != .zero,
mode == .scaleAspectFit || mode == .scaleAspectFill else {
return frame
}
var targetImageSize = frame.size
let aspectHeight = frame.size.width / imageSize.width * imageSize.height
let aspectWidth = frame.size.height / imageSize.height * imageSize.width
if imageSize.width / imageSize.height > frame.size.width / frame.size.height {
if mode == .scaleAspectFit {
targetImageSize.height = aspectHeight
} else {
targetImageSize.width = aspectWidth
}
} else {
if mode == .scaleAspectFit {
targetImageSize.width = aspectWidth
} else {
targetImageSize.height = aspectHeight
}
}
let x = frame.minX + (frame.size.width - targetImageSize.width) / 2.0
let y = frame.minY + (frame.size.height - targetImageSize.height) / 2.0
return CGRect(origin: CGPoint(x: x, y: y), size: targetImageSize)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,315 +0,0 @@
//
// MediaContentView.swift
// ATGMediaBrowser
//
// Created by Suraj Thomas K on 7/10/18.
// Copyright © 2018 Al Tayer Group LLC.
//
// 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
/// Holds the value of minimumZoomScale and maximumZoomScale of the image.
public struct ZoomScale: Sendable {
/// Minimum zoom level, the image can be zoomed out to.
public var minimumZoomScale: CGFloat
/// Maximum zoom level, the image can be zoomed into.
public var maximumZoomScale: CGFloat
/// Default zoom scale. minimum is 1.0 and maximum is 3.0
public static let `default` = ZoomScale(
minimum: 1.0,
maximum: 3.0
)
/// Identity zoom scale. Pass this to disable zoom.
public static let identity = ZoomScale(
minimum: 1.0,
maximum: 1.0
)
/**
Initializer.
- parameter minimum: The minimum zoom level.
- parameter maximum: The maximum zoom level.
*/
public init(minimum: CGFloat, maximum: CGFloat) {
minimumZoomScale = minimum
maximumZoomScale = maximum
}
}
internal class MediaContentView: UIScrollView {
// MARK: - Exposed variables
internal static var interItemSpacing: CGFloat = 0.0
internal var index: Int {
didSet {
resetZoom()
}
}
internal static var contentTransformer: ContentTransformer = DefaultContentTransformers.horizontalMoveInOut
internal var position: CGFloat {
didSet {
updateTransform()
}
}
internal var image: UIImage? {
didSet {
updateImageView()
}
}
internal var isLoading: Bool = false {
didSet {
indicatorContainer.isHidden = !isLoading
if isLoading {
indicator.startAnimating()
} else {
indicator.stopAnimating()
}
}
}
internal var zoomLevels: ZoomScale? {
didSet {
zoomScale = ZoomScale.default.minimumZoomScale
minimumZoomScale = zoomLevels?.minimumZoomScale ?? ZoomScale.default.minimumZoomScale
maximumZoomScale = zoomLevels?.maximumZoomScale ?? ZoomScale.default.maximumZoomScale
}
}
// MARK: - Private enumerations
private enum Constants {
static let indicatorViewSize: CGFloat = 60.0
}
// MARK: - Private variables
private lazy var imageView: UIImageView = {
let imageView = UIImageView()
imageView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
imageView.contentMode = .scaleAspectFit
imageView.clipsToBounds = true
return imageView
}()
private lazy var indicator: UIActivityIndicatorView = {
let indicatorView = UIActivityIndicatorView()
indicatorView.style = UIActivityIndicatorView.Style.large
indicatorView.hidesWhenStopped = true
return indicatorView
}()
private lazy var indicatorContainer: UIView = {
let container = UIView()
container.backgroundColor = .darkGray
container.layer.cornerRadius = Constants.indicatorViewSize * 0.5
container.layer.masksToBounds = true
return container
}()
private lazy var doubleTapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
let gesture = UITapGestureRecognizer(target: self, action: #selector(didDoubleTap(_:)))
gesture.numberOfTapsRequired = 2
gesture.numberOfTouchesRequired = 1
return gesture
}()
init(index itemIndex: Int, position: CGFloat, frame: CGRect) {
self.index = itemIndex
self.position = position
super.init(frame: frame)
initializeViewComponents()
}
required init?(coder aDecoder: NSCoder) {
fatalError("Do nto use `init?(coder:)`")
}
}
// MARK: - View Composition and Events
extension MediaContentView {
private func initializeViewComponents() {
addSubview(imageView)
imageView.frame = frame
setupIndicatorView()
configureScrollView()
addGestureRecognizer(doubleTapGestureRecognizer)
updateTransform()
}
private func configureScrollView() {
isMultipleTouchEnabled = true
showsHorizontalScrollIndicator = false
showsVerticalScrollIndicator = false
contentSize = imageView.bounds.size
canCancelContentTouches = false
zoomLevels = ZoomScale.default
delegate = self
bouncesZoom = false
}
private func resetZoom() {
setZoomScale(1.0, animated: false)
imageView.transform = CGAffineTransform.identity
contentSize = imageView.frame.size
contentOffset = .zero
}
private func setupIndicatorView() {
addSubview(indicatorContainer)
indicatorContainer.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
indicatorContainer.widthAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
indicatorContainer.heightAnchor.constraint(equalToConstant: Constants.indicatorViewSize),
indicatorContainer.centerXAnchor.constraint(equalTo: centerXAnchor),
indicatorContainer.centerYAnchor.constraint(equalTo: centerYAnchor)
])
indicatorContainer.addSubview(indicator)
indicator.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
indicator.leadingAnchor.constraint(equalTo: indicatorContainer.leadingAnchor),
indicator.trailingAnchor.constraint(equalTo: indicatorContainer.trailingAnchor),
indicator.topAnchor.constraint(equalTo: indicatorContainer.topAnchor),
indicator.bottomAnchor.constraint(equalTo: indicatorContainer.bottomAnchor)
])
indicatorContainer.setNeedsLayout()
indicatorContainer.layoutIfNeeded()
indicatorContainer.isHidden = true
}
internal func updateTransform() {
MediaContentView.contentTransformer(self, position)
}
internal func handleChangeInViewSize(to size: CGSize) {
let oldScale = zoomScale
zoomScale = 1.0
imageView.frame = CGRect(origin: .zero, size: size)
updateImageView()
updateTransform()
setZoomScale(oldScale, animated: false)
contentSize = imageView.frame.size
}
@objc private func didDoubleTap(_ recognizer: UITapGestureRecognizer) {
let locationInImage = recognizer.location(in: imageView)
let isImageCoveringScreen = imageView.frame.size.width > bounds.size.width &&
imageView.frame.size.height > bounds.size.height
let zoomTo = (isImageCoveringScreen || zoomScale == maximumZoomScale) ? minimumZoomScale : maximumZoomScale
guard zoomTo != zoomScale else {
return
}
let width = bounds.size.width / zoomTo
let height = bounds.size.height / zoomTo
let zoomRect = CGRect(
x: locationInImage.x - width * 0.5,
y: locationInImage.y - height * 0.5,
width: width,
height: height
)
zoom(to: zoomRect, animated: true)
}
}
// MARK: - UIScrollViewDelegate
extension MediaContentView: UIScrollViewDelegate {
internal func viewForZooming(in scrollView: UIScrollView) -> UIView? {
let shouldAllowZoom = (image != nil && position == 0.0)
return shouldAllowZoom ? imageView : nil
}
internal func scrollViewDidZoom(_ scrollView: UIScrollView) {
centerImageView()
}
private func centerImageView() {
var imageViewFrame = imageView.frame
if imageViewFrame.size.width < bounds.size.width {
imageViewFrame.origin.x = (bounds.size.width - imageViewFrame.size.width) / 2.0
} else {
imageViewFrame.origin.x = 0.0
}
if imageViewFrame.size.height < bounds.size.height {
imageViewFrame.origin.y = (bounds.size.height - imageViewFrame.size.height) / 2.0
} else {
imageViewFrame.origin.y = 0.0
}
imageView.frame = imageViewFrame
}
private func updateImageView() {
imageView.image = image
if let contentImage = image {
let imageViewSize = bounds.size
let imageSize = contentImage.size
var targetImageSize = imageViewSize
if imageSize.width / imageSize.height > imageViewSize.width / imageViewSize.height {
targetImageSize.height = imageViewSize.width / imageSize.width * imageSize.height
} else {
targetImageSize.width = imageViewSize.height / imageSize.height * imageSize.width
}
imageView.frame = CGRect(origin: .zero, size: targetImageSize)
}
centerImageView()
}
}

View File

@ -1,40 +0,0 @@
//
// 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
}
}

View File

@ -1,107 +0,0 @@
import Foundation
import AVFoundation
enum PlayerState {
case stopped
case paused
case playing
}
class AudioPlayer: NSObject, AVAudioPlayerDelegate {
@MainActor static let shared = AudioPlayer()
private var player: AVAudioPlayer?
private var url: URL?
private var state: PlayerState = .stopped
private var progress: Double = 0
private var progressTimer: Timer?
func set(url: URL) throws {
if let curUrl = self.url, curUrl == url {
return
}
self.stop()
self.url = url
self.player = try AVAudioPlayer(contentsOf: url)
self.player?.delegate = self
}
func play() throws {
if let player = self.player {
if player.isPlaying {
player.pause()
try self.deactivateSession()
self.state = .paused
} else {
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .default, options: [.duckOthers])
try AVAudioSession.sharedInstance().setActive(true)
player.play()
self.state = .playing
if self.progressTimer == nil {
self.progressTimer = Timer.scheduledTimer(timeInterval: 0.2, target: self, selector: #selector(progressTick), userInfo: nil, repeats: true)
}
}
}
}
func play(url: URL) throws {
try self.set(url: url)
try self.play()
}
func pause() {
if let player = self.player {
player.pause()
try? self.deactivateSession()
self.state = .paused
}
}
func stop() {
if let player = self.player {
player.stop()
try? self.deactivateSession()
self.state = .stopped
self.progressTimer?.invalidate()
self.progressTimer = nil
}
}
func getState() -> PlayerState {
return self.state
}
func getProgress() -> Double {
return self.progress
}
func getUrl() -> URL? {
return self.url
}
func duration() -> TimeInterval {
return self.player?.duration ?? 0
}
func deactivateSession() throws {
try AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
}
// MARK: - AVAudioPlayerDelegate
func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) {
try? self.deactivateSession()
self.progress = 1
self.stop()
self.state = .stopped
}
@objc func progressTick() {
if let player = self.player {
let progress = player.currentTime/player.duration
self.progress = progress
}
}
}

View File

@ -1,151 +0,0 @@
import Foundation
import Speech
import AVFoundation
import AudioToolbox
import os.log
import ExceptionCatcher
final class Recorder {
let engine = AVAudioEngine()
var fileRef: ExtAudioFileRef? = nil
let recognizer = SFSpeechRecognizer(locale: Locale(identifier: "ru_RU"))
let request = SFSpeechAudioBufferRecognitionRequest()
var recognitionTask: SFSpeechRecognitionTask?
var endRecognitionTimer: Timer?
var result: String = ""
let recordingSettings: [String:Any] = [
AVFormatIDKey:kAudioFormatMPEG4AAC_HE,
AVSampleRateKey:44100.0,
AVNumberOfChannelsKey:2,
//AVEncoderBitRateKey:320*1024,
//AVLinearPCMBitDepthKey:16,
AVEncoderAudioQualityKey:AVAudioQuality.max.rawValue
]
init() {
self.request.contextualStrings = ["61", "161", "761", "123", "750", "777", "799"]
}
func microphoneAvailable() -> Bool {
// FIXME:
return true
}
func requestPermissions() async throws {
try await withCheckedThrowingContinuation { continuation in
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
if allowed {
SFSpeechRecognizer.requestAuthorization { status in
switch status {
case .authorized:
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")
continuation.resume(throwing: error)
case .restricted:
let error = CocoaError.error("Access error", reason: "Speech recognition is restricted on this device")
continuation.resume(throwing: error)
case .notDetermined:
let error = CocoaError.error("Access error", reason: "Speech recognition status is not yet determined")
continuation.resume(throwing: error)
@unknown default:
let error = CocoaError.error("Access error", reason: "Unknown error accessing speech recognizer")
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")
continuation.resume(throwing: error)
}
}
}
}
func startRecording(to file: URL) async throws -> String {
guard self.microphoneAvailable() else {
throw CocoaError.error("Recording error", reason: "Microphone not found")
}
return try await withCheckedThrowingContinuation { continuation in
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
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 {
continuation.resume(throwing: CocoaError.error(CocoaError.Code.fileWriteUnknown))
return
}
do {
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [])
try AVAudioSession.sharedInstance().setActive(true)
let inFormat = self.engine.inputNode.outputFormat(forBus: 0)
ExtAudioFileSetProperty(fileRef, kExtAudioFileProperty_ClientDataFormat, UInt32(MemoryLayout<AudioStreamBasicDescription>.size), inFormat.streamDescription)
try ExceptionCatcher.catch {
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
self.request.append(buffer)
ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
}
}
self.recognitionTask = self.recognizer!.recognitionTask(with: self.request) { result, error in
if let transcription = result?.bestTranscription {
self.result = transcription.formattedString
self.endRecognitionTimer?.invalidate()
// TODO: Check if it actually works
RunLoop.current.schedule(after: .init(Date()).advanced(by: .seconds(5)), tolerance: .seconds(1), options: nil) {
self.finishRecording()
continuation.resume(returning: self.result)
}
}
}
RunLoop.current.schedule(after: .init(Date()).advanced(by: .seconds(5)), tolerance: .seconds(1), options: nil) {
self.finishRecording()
continuation.resume(returning: self.result)
}
self.engine.prepare()
try self.engine.start()
} catch {
continuation.resume(throwing: error)
}
}
}
func cancelRecording() {
self.finishRecording()
self.endRecognitionTimer?.invalidate()
self.endRecognitionTimer = nil
}
func finishRecording() {
guard self.engine.isRunning else { return }
self.engine.stop()
self.engine.inputNode.removeTap(onBus: 0)
self.request.endAudio()
self.recognitionTask?.cancel()
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
try? AVAudioSession.sharedInstance().setCategory(.soloAmbient)
if let fileRef = self.fileRef {
ExtAudioFileDispose(fileRef)
}
}
func stopRecording() {
self.finishRecording()
self.endRecognitionTimer?.fire()
self.endRecognitionTimer = nil
}
}

View File

@ -1,207 +0,0 @@
import UIKit
import RealmSwift
import ExceptionCatcher
import AutoCatCore
typealias FilterPredicate<T> = (T) -> Bool
class RealmSectionedDataSource<Item,Cell>: NSObject, UITableViewDataSource
where Item: Object & DtoConvertible,
Item.Dto: Identifiable & Dated,
Cell: UITableViewCell & ConfigurableCell,
Cell.Item == Item.Dto {
private var tv: UITableView
private var data: Results<Item>
private var notificationToken: NotificationToken?
private var sections: [DateSection<Item.Dto>] = []
private var cellIdentifier: String
private var filterPredicate: FilterPredicate<Item.Dto>?
private var searchPredicate: FilterPredicate<Item.Dto>?
private let groupQueue = DispatchQueue(label: "group")
private var objects: [Item.Dto] = []
private let onSizeChanged: ((Int) -> Void)?
init(table: UITableView, data: Results<Item>, cellIdentifier: String = String(describing: Cell.self), onSizeChanged: ((Int) -> Void)? = nil) {
self.tv = table
self.data = data
self.cellIdentifier = cellIdentifier
self.onSizeChanged = onSizeChanged
super.init()
self.tv.dataSource = self
self.tv.reloadData()
startObservingChanges()
}
func startObservingChanges() {
self.notificationToken = self.data.observe { changes in
switch changes {
case .initial:
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].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 {
// // Most common use case - adding new element to the top of the list
// // Example - checking new number
// self.insertFirst()
// } else if deletions.isEmpty && insertions.isEmpty && modifications.count == 1 && modifications.first == 0 {
// self.reloadFirst()
// } else if modifications.isEmpty && deletions.count == 1 && insertions.count == 1 && insertions.first == 0 {
// // Probably moving an item to the first position
// // For example - updating number info
// self.moveToFirst(deleteIndex: deletions.first!)
// } else {
// self.reload(animated: false)
// }
case .error(let err):
print("Realm observer error: \(err)")
}
}
}
// MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].elements.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier, for: indexPath) as? Cell else {
return UITableViewCell()
}
let item = self.sections[indexPath.section].elements[indexPath.row]
cell.configure(with: item)
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section].header
}
// MARK: - Public
func item(at indexPath: IndexPath) -> Item.Dto {
return self.sections[indexPath.section].elements[indexPath.row]
}
func reload(animated: Bool = true) {
var items = objects
if let filterPredicate = self.filterPredicate {
items = items.filter(filterPredicate)
}
if let searchPredicate = self.searchPredicate {
items = items.filter(searchPredicate)
}
self.onSizeChanged?(items.count)
self.sections = items.groupedByDate()
self.tv.reloadData()
}
func insertFirst() {
guard let item = data.first?.shallowDto else {
reload(animated: false)
return
}
if sections.isEmpty {
sections = [item].groupedByDate()
tv.insertSections(IndexSet(integer: 0), with: .fade)
} else {
sections[0].insert(item, at: 0)
tv.insertRows(at: [IndexPath(row: 0, section: 0)], with: .fade)
}
}
func reloadFirst() {
guard !sections.isEmpty, let item = data.first?.dto else {
reload(animated: false)
return
}
sections[0].replace(item, at: 0)
tv.reloadRows(at: [IndexPath(row: 0, section: 0)], with: .none)
}
func moveToFirst(deleteIndex: Int) {
var itemIndex = deleteIndex
for index in 0..<sections.count {
if sections[index].elements.count <= itemIndex {
itemIndex -= sections[index].elements.count
continue
}
sections[index].remove(at: itemIndex)
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)
return
}
reload(animated: false)
}
func getSectionIndex(by numberIndex: Int) -> Int? {
return nil
}
func setFilterPredicate(_ predicate: FilterPredicate<Item.Dto>?) {
guard self.filterPredicate != nil || predicate != nil else {
return
}
self.filterPredicate = predicate
self.reload()
}
func setSearchPredicate(_ predicate: FilterPredicate<Item.Dto>?) {
guard self.searchPredicate != nil || predicate != nil else {
return
}
self.searchPredicate = predicate
self.reload()
}
func makeCsv() throws -> String {
guard let type = Item.self as? Exportable.Type else {
throw CocoaError.error("Type \(Item.self) is not exportable")
}
var items = self.data.toArray().map(\.dto)
if let predicate = self.filterPredicate {
items = items.filter(predicate)
}
var result = ""
let newLine: Character = "\r\n" //"\u{000B}"
result.append(type.csvHeader)
result.append(newLine)
for item in items {
if let exportableItem = item as? Exportable {
result.append(exportableItem.csvLine)
result.append(newLine)
}
}
return result
}
}

View File

@ -1,87 +0,0 @@
import UIKit
import AutoCatCore
class SectionedDataSource<Item,Cell>: 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<Item>] = []
private var items: [Item] = []
private var sortParam: SortParameter = .updatedDate
private(set) var pageToken: String? = nil
private(set) var count: Int? = nil
init(table: UITableView, cellIdentifier: String = String(describing: Cell.self)) {
self.tv = table
self.cellIdentifier = cellIdentifier
super.init()
self.tv.dataSource = self
}
// MARK: - UITableViewDataSource
func numberOfSections(in tableView: UITableView) -> Int {
return self.sections.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return self.sections[section].elements.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: self.cellIdentifier, for: indexPath) as? Cell else {
return UITableViewCell()
}
let item = self.sections[indexPath.section].elements[indexPath.row]
cell.configure(with: item)
return cell
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
return self.sections[section].header
}
// MARK: - Public
func item(at indexPath: IndexPath) -> Item {
return self.sections[indexPath.section].elements[indexPath.row]
}
func set(item: Item, at indexPath: IndexPath) {
self.sections[indexPath.section].elements[indexPath.row] = item
}
func update(with data: PagedResponse<Item>) {
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 {
guard let count = self.count else { return true }
return self.items.count < count
}
func reset() {
self.pageToken = nil
self.count = nil
self.items = []
}
func setSortParameter(_ param: SortParameter) {
self.sortParam = param
}
}