Remove some old code
This commit is contained in:
parent
6ce8c31635
commit
a9bbbfa35b
@ -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 */,
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,7 +0,0 @@
|
||||
import UIKit
|
||||
|
||||
@MainActor
|
||||
protocol ConfigurableCell {
|
||||
associatedtype Item
|
||||
func configure(with item: Item)
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -71,5 +71,5 @@ struct VehicleCellView: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
VehicleCell()
|
||||
VehicleCellView(vehicle: .preview)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
@ -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()
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user