From 4d3a16f77eca1fe53de145abfc820f59cf3a63a8 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 6 Sep 2022 00:05:34 -0400 Subject: [PATCH] Library: Add history functionality Action history is logged and displayed to the user's library. These are triggered whenever the magnet choice sheet is displayed. Also redo alerts and action sheets to avoid deprecation notices for >iOS 14. These will be removed when iOS 14 support is dropped. There was also a problem with sheet presentation not working after a sheet was dismissed. Disable the appropriate view when a sheet is being presented. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 48 +++++++- .../Classes/History+CoreDataClass.swift | 15 +++ .../Classes/History+CoreDataProperties.swift | 51 ++++++++ .../FerriteDB.xcdatamodel/contents | 6 +- .../PersistenceController.swift | 75 ++++++++++++ Ferrite/Extensions/DateFormatter.swift | 17 +++ Ferrite/Extensions/View.swift | 26 ++++ Ferrite/ViewModels/NavigationViewModel.swift | 57 +++++++-- Ferrite/Views/BatchChoiceView.swift | 16 ++- Ferrite/Views/CommonViews/AlertButton.swift | 69 +++++++++++ .../CommonViews/DisableInteraction.swift | 25 ++++ .../CommonViews/DisabledAppearance.swift | 21 ++++ .../CommonViews/DynamicActionSheet.swift | 44 +++++++ Ferrite/Views/CommonViews/DynamicAlert.swift | 56 +++++++++ Ferrite/Views/ContentView.swift | 36 +++--- Ferrite/Views/LibraryView.swift | 19 ++- .../LibraryViews/HistoryActionsView.swift | 54 +++++++++ .../LibraryViews/HistoryButtonView.swift | 76 ++++++++++++ Ferrite/Views/LibraryViews/HistoryView.swift | 59 ++++++++-- Ferrite/Views/MagnetChoiceView.swift | 39 +++--- Ferrite/Views/MainView.swift | 18 +-- Ferrite/Views/SearchProgressView.swift | 20 ---- Ferrite/Views/SearchResultButtonView.swift | 111 ++++++++++-------- Ferrite/Views/SearchResultRDView.swift | 24 ++-- .../SettingsViews/SourceListEditorView.swift | 13 +- 25 files changed, 823 insertions(+), 172 deletions(-) create mode 100644 Ferrite/DataManagement/Classes/History+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/History+CoreDataProperties.swift create mode 100644 Ferrite/Extensions/DateFormatter.swift create mode 100644 Ferrite/Views/CommonViews/AlertButton.swift create mode 100644 Ferrite/Views/CommonViews/DisableInteraction.swift create mode 100644 Ferrite/Views/CommonViews/DisabledAppearance.swift create mode 100644 Ferrite/Views/CommonViews/DynamicActionSheet.swift create mode 100644 Ferrite/Views/CommonViews/DynamicAlert.swift create mode 100644 Ferrite/Views/LibraryViews/HistoryActionsView.swift create mode 100644 Ferrite/Views/LibraryViews/HistoryButtonView.swift delete mode 100644 Ferrite/Views/SearchProgressView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 497ae48..051a2e8 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -10,19 +10,24 @@ 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; + 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; + 0C391EC928CA63F0009F1CA1 /* DynamicActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */; }; + 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; }; 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; }; 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; }; 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; }; 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; }; 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; }; + 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; }; + 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; }; - 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */; }; + 0C626A9528CADB25003C7129 /* DynamicAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626A9428CADB25003C7129 /* DynamicAlert.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; }; @@ -80,14 +85,18 @@ 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */; }; 0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */; }; 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; + 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; }; 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; + 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; + 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; }; + 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; }; 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; }; /* End PBXBuildFile section */ @@ -96,18 +105,23 @@ 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; + 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = ""; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = ""; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = ""; }; 0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = ""; }; 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = ""; }; + 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicActionSheet.swift; sourceTree = ""; }; + 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = ""; }; 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = ""; }; 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = ""; }; 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; + 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; + 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; - 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.swift; sourceTree = ""; }; + 0C626A9428CADB25003C7129 /* DynamicAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAlert.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; 0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = ""; }; 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = ""; }; @@ -161,14 +175,18 @@ 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataClass.swift"; sourceTree = ""; }; 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataProperties.swift"; sourceTree = ""; }; 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; + 0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; 0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = ""; }; 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = ""; }; + 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = ""; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = ""; }; + 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = ""; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -194,6 +212,8 @@ 0C0D50DE288DF72D0035ECC8 /* Classes */ = { isa = PBXGroup; children = ( + 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */, + 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */, 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */, 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */, 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */, @@ -280,6 +300,11 @@ 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */, + 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */, + 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */, + 0C626A9428CADB25003C7129 /* DynamicAlert.swift */, + 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, + 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, ); path = CommonViews; sourceTree = ""; @@ -302,6 +327,7 @@ 0C7D11FD28AA03FE00ED92DB /* View.swift */, 0C78041C28BFB3EA001E8CA3 /* String.swift */, 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */, + 0CA429F728C5098D000D0610 /* DateFormatter.swift */, ); path = Extensions; sourceTree = ""; @@ -315,6 +341,7 @@ 0CA148C0288903F000DE2211 /* CommonViews */, 0CA0545C288F7CB200850554 /* SettingsViews */, 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, + 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */, 0CA148D4288903F000DE2211 /* ContentView.swift */, 0CA148D1288903F000DE2211 /* MainView.swift */, @@ -324,9 +351,7 @@ 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, - 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */, 0CA3B23328C2658700616D3A /* LibraryView.swift */, - 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, ); path = Views; sourceTree = ""; @@ -365,6 +390,8 @@ children = ( 0CA3B23828C2660D00616D3A /* BookmarksView.swift */, 0CA3B23628C2660700616D3A /* HistoryView.swift */, + 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */, + 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */, ); path = LibraryViews; sourceTree = ""; @@ -486,11 +513,11 @@ files = ( 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, - 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, + 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, @@ -499,8 +526,10 @@ 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, + 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */, + 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */, @@ -515,6 +544,7 @@ 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, + 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, @@ -523,9 +553,11 @@ 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, + 0C626A9528CADB25003C7129 /* DynamicAlert.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */, + 0C391EC928CA63F0009F1CA1 /* DynamicActionSheet.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, @@ -539,6 +571,7 @@ 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, + 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */, 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */, 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */, 0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */, @@ -550,9 +583,12 @@ 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, + 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, + 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, 0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */, + 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */, 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, @@ -705,6 +741,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -738,6 +775,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/Ferrite/DataManagement/Classes/History+CoreDataClass.swift b/Ferrite/DataManagement/Classes/History+CoreDataClass.swift new file mode 100644 index 0000000..b7108b3 --- /dev/null +++ b/Ferrite/DataManagement/Classes/History+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// History+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// + +import Foundation +import CoreData + +@objc(History) +public class History: NSManagedObject { + +} diff --git a/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift new file mode 100644 index 0000000..7e384ab --- /dev/null +++ b/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift @@ -0,0 +1,51 @@ +// +// History+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// + +import Foundation +import CoreData + + +extension History { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "History") + } + + @NSManaged public var date: Date? + @NSManaged public var dateString: String? + @NSManaged public var entries: NSSet? + + var entryArray: [HistoryEntry] { + let entrySet = entries as? Set ?? [] + + return entrySet.sorted { + $0.timeStamp > $1.timeStamp + } + } +} + +// MARK: Generated accessors for entries +extension History { + + @objc(addEntriesObject:) + @NSManaged public func addToEntries(_ value: HistoryEntry) + + @objc(removeEntriesObject:) + @NSManaged public func removeFromEntries(_ value: HistoryEntry) + + @objc(addEntries:) + @NSManaged public func addToEntries(_ values: NSSet) + + @objc(removeEntries:) + @NSManaged public func removeFromEntries(_ values: NSSet) + +} + +extension History : Identifiable { + +} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents index f2d6e3f..5407971 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -10,13 +10,15 @@ - + + + diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index de19701..1fecdc2 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -7,6 +7,18 @@ import CoreData +enum HistoryDeleteRange { + case day + case week + case month + case allTime +} + +enum HistoryDeleteError: Error { + case noDate(String) + case unknown(String) +} + // No iCloud until finalized sources struct PersistenceController { static var shared = PersistenceController() @@ -78,4 +90,67 @@ struct PersistenceController { container.viewContext.delete(object) save() } + + + func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { + if range == .allTime { + return nil + } + + var components = Calendar.current.dateComponents([.day, .month, .year], from: Date()) + components.hour = 0 + components.minute = 0 + components.second = 0 + + guard let today = Calendar.current.date(from: components) else { + return nil + } + + var offsetComponents = DateComponents(day: 1) + guard let tomorrow = Calendar.current.date(byAdding: offsetComponents, to: today) else { + return nil + } + + switch range { + case .week: + offsetComponents.day = -7 + case .month: + offsetComponents.day = -28 + default: + break + } + + guard var offsetDate = Calendar.current.date(byAdding: offsetComponents, to: today) else { + return nil + } + + if TimeZone.current.isDaylightSavingTime(for: offsetDate) { + offsetDate = offsetDate.addingTimeInterval(3600) + } + + let predicate = NSPredicate(format: "date >= %@ && date < %@", range == .day ? today as NSDate : offsetDate as NSDate, tomorrow as NSDate) + + return predicate + } + + // Always use the background context to batch delete + // Merge changes into both contexts to update views + func batchDeleteHistory(range: HistoryDeleteRange) throws { + let predicate = getHistoryPredicate(range: range) + + let fetchRequest = NSFetchRequest(entityName: "History") + + if let predicate = predicate { + fetchRequest.predicate = predicate + } else if range != .allTime { + throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?") + } + + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + batchDeleteRequest.resultType = .resultTypeObjectIDs + let result = try backgroundContext.execute(batchDeleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [container.viewContext, backgroundContext]) + } } diff --git a/Ferrite/Extensions/DateFormatter.swift b/Ferrite/Extensions/DateFormatter.swift new file mode 100644 index 0000000..4177912 --- /dev/null +++ b/Ferrite/Extensions/DateFormatter.swift @@ -0,0 +1,17 @@ +// +// DateFormatter.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// + +import Foundation + +extension DateFormatter { + static let historyDateFormatter: DateFormatter = { + let df = DateFormatter() + df.dateFormat = "ddMMyyyy" + + return df + }() +} diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index e7debe2..2226805 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -33,6 +33,10 @@ extension View { modifier(ConditionalId(id: id)) } + func disabledAppearance(_ disabled: Bool, dimmedOpacity: Double? = nil, animation: Animation? = nil) -> some View { + modifier(DisabledAppearance(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation)) + } + func inlinedList() -> some View { modifier(InlinedList()) } @@ -43,4 +47,26 @@ extension View { ) -> some View { modifier(ConditionalContextMenu(internalContent, id: id)) } + + func dynamicActionSheet( + isPresented: Binding, + title: String, + message: String? = nil, + buttons: [AlertButton]) -> some View + { + modifier(DynamicActionSheet(isPresented: isPresented, title: title, message: message, buttons: buttons)) + } + + func dynamicAlert( + isPresented: Binding, + title: String, + message: String? = nil, + buttons: [AlertButton]) -> some View + { + modifier(DynamicAlert(isPresented: isPresented, title: title, message: message, buttons: buttons)) + } + + func disableInteraction(_ disabled: Bool) -> some View { + modifier(DisableInteraction(disabled: disabled)) + } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 6dba198..4fa24bf 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -55,7 +55,7 @@ class NavigationViewModel: ObservableObject { @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none - public func runDebridAction(action: DefaultDebridActionType?, urlString: String) { + public func runDebridAction(urlString: String, _ action: DefaultDebridActionType? = nil) { let selectedAction = action ?? defaultDebridAction switch selectedAction { @@ -89,17 +89,10 @@ class NavigationViewModel: ObservableObject { } } - public func runMagnetAction(_ action: DefaultMagnetActionType? = nil) { - guard let searchResult = selectedSearchResult else { - toastModel?.updateToastDescription("Magnet action error: A search result was not selected.") - print("Magnet action error: A search result was not selected.") - - return - } - + public func runMagnetAction(magnetString: String?, _ action: DefaultMagnetActionType? = nil) { let selectedAction = action ?? defaultMagnetAction - guard let magnetLink = searchResult.magnetLink else { + guard let magnetLink = magnetString else { toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") print("Magnet action error: The magnet link is invalid.") @@ -126,4 +119,48 @@ class NavigationViewModel: ObservableObject { } } } + + public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) { + let backgroundContext = PersistenceController.shared.backgroundContext + + let newHistoryEntry = HistoryEntry(context: backgroundContext) + newHistoryEntry.name = name + newHistoryEntry.source = source + newHistoryEntry.url = url + newHistoryEntry.subName = subName + + let now = Date() + newHistoryEntry.timeStamp = now.timeIntervalSince1970 + + let dateString = DateFormatter.historyDateFormatter.string(from: now) + + let historyRequest = History.fetchRequest() + historyRequest.predicate = NSPredicate(format: "dateString = %@", dateString) + + if var histories = try? backgroundContext.fetch(historyRequest) { + for (i, history) in histories.enumerated() { + let existingEntries = history.entryArray.filter { $0.url == newHistoryEntry.url && $0.name == newHistoryEntry.name } + + if !existingEntries.isEmpty { + for entry in existingEntries { + PersistenceController.shared.delete(entry, context: backgroundContext) + } + } + + if history.entryArray.isEmpty { + PersistenceController.shared.delete(history, context: backgroundContext) + histories.remove(at: i) + } + } + + newHistoryEntry.parentHistory = histories.first ?? History(context: backgroundContext) + } else { + newHistoryEntry.parentHistory = History(context: backgroundContext) + } + + newHistoryEntry.parentHistory?.dateString = dateString + newHistoryEntry.parentHistory?.date = now + + PersistenceController.shared.save(backgroundContext) + } } diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index 60e1c64..eb7369c 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -8,12 +8,12 @@ import SwiftUI struct BatchChoiceView: View { - @Environment(\.presentationMode) var presentationMode - @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel + let backgroundContext = PersistenceController.shared.backgroundContext + var body: some View { NavView { List { @@ -28,7 +28,8 @@ struct BatchChoiceView: View { if !debridManager.realDebridDownloadUrl.isEmpty { // The download may complete before this sheet dismisses try? await Task.sleep(seconds: 1) - navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl) + navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) } debridManager.selectedRealDebridFile = nil @@ -36,7 +37,7 @@ struct BatchChoiceView: View { } } - presentationMode.wrappedValue.dismiss() + navModel.currentChoiceSheet = nil } .dynamicAccentColor(.primary) } @@ -47,9 +48,12 @@ struct BatchChoiceView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { - debridManager.selectedRealDebridItem = nil + navModel.currentChoiceSheet = nil - presentationMode.wrappedValue.dismiss() + Task { + try? await Task.sleep(seconds: 1) + debridManager.selectedRealDebridItem = nil + } } } } diff --git a/Ferrite/Views/CommonViews/AlertButton.swift b/Ferrite/Views/CommonViews/AlertButton.swift new file mode 100644 index 0000000..8d6922e --- /dev/null +++ b/Ferrite/Views/CommonViews/AlertButton.swift @@ -0,0 +1,69 @@ +// +// AlertButton.swift +// Ferrite +// +// Created by Brian Dashore on 9/8/22. +// + +import SwiftUI + +struct AlertButton: Identifiable { + enum Role { + case destructive + case cancel + } + + let id: UUID + let label: String + let action: () -> Void + let role: Role? + + // Used for all buttons + init(_ label: String, role: Role? = nil, action: @escaping () -> Void) { + self.id = UUID() + self.label = label + self.action = action + self.role = role + } + + // Used for buttons with no action + init(_ label: String = "Cancel", role: Role? = nil) { + self.id = UUID() + self.label = label + self.action = { } + self.role = role + } + + func toActionButton() -> Alert.Button { + if let role = role { + switch role { + case .cancel: + return .cancel(Text(label)) + case .destructive: + return .destructive(Text(label), action: action) + } + } else { + return .default(Text(label), action: action) + } + } + + @available(iOS 15.0, *) + @ViewBuilder + func toButtonView() -> some View { + Button(label, role: toButtonRole(role), action: action) + } + + @available(iOS 15.0, *) + func toButtonRole(_ role: Role?) -> ButtonRole? { + if let role = role { + switch role { + case .destructive: + return .destructive + case .cancel: + return .cancel + } + } else { + return nil + } + } +} diff --git a/Ferrite/Views/CommonViews/DisableInteraction.swift b/Ferrite/Views/CommonViews/DisableInteraction.swift new file mode 100644 index 0000000..a8073d8 --- /dev/null +++ b/Ferrite/Views/CommonViews/DisableInteraction.swift @@ -0,0 +1,25 @@ +// +// DisableInteraction.swift +// Ferrite +// +// Created by Brian Dashore on 9/13/22. +// +// Disables interaction without applying the appearance +// + +import SwiftUI + +struct DisableInteraction: ViewModifier { + let disabled: Bool + + func body(content: Content) -> some View { + content + .overlay { + if disabled { + Color.clear + .contentShape(Rectangle()) + .gesture(TapGesture()) + } + } + } +} diff --git a/Ferrite/Views/CommonViews/DisabledAppearance.swift b/Ferrite/Views/CommonViews/DisabledAppearance.swift new file mode 100644 index 0000000..132d23e --- /dev/null +++ b/Ferrite/Views/CommonViews/DisabledAppearance.swift @@ -0,0 +1,21 @@ +// +// DisabledAppearance.swift +// Ferrite +// +// Created by Brian Dashore on 9/10/22. +// + +import SwiftUI + +struct DisabledAppearance: ViewModifier { + let disabled: Bool + let dimmedOpacity: Double? + let animation: Animation? + + func body(content: Content) -> some View { + content + .disabled(disabled) + .opacity(disabled ? dimmedOpacity.map { $0 } ?? 0.5 : 1) + .animation(animation.map { $0 } ?? .none, value: disabled) + } +} diff --git a/Ferrite/Views/CommonViews/DynamicActionSheet.swift b/Ferrite/Views/CommonViews/DynamicActionSheet.swift new file mode 100644 index 0000000..9e2a3c5 --- /dev/null +++ b/Ferrite/Views/CommonViews/DynamicActionSheet.swift @@ -0,0 +1,44 @@ +// +// DynamicActionSheet.swift +// Ferrite +// +// Created by Brian Dashore on 9/8/22. +// + +import SwiftUI + +struct DynamicActionSheet: ViewModifier { + @Binding var isPresented: Bool + + let title: String + let message: String? + let buttons: [AlertButton] + + func body(content: Content) -> some View { + if #available(iOS 15, *) { + content + .confirmationDialog( + title, + isPresented: $isPresented, + titleVisibility: .visible + ) { + ForEach(buttons) { button in + button.toButtonView() + } + } message: { + if let message = message { + Text(message) + } + } + } else { + content + .actionSheet(isPresented: $isPresented) { + ActionSheet( + title: Text(title), + message: message.map { Text($0) } ?? nil, + buttons: [buttons.map { $0.toActionButton() }, [.cancel()]].flatMap{ $0 } + ) + } + } + } +} diff --git a/Ferrite/Views/CommonViews/DynamicAlert.swift b/Ferrite/Views/CommonViews/DynamicAlert.swift new file mode 100644 index 0000000..2a15546 --- /dev/null +++ b/Ferrite/Views/CommonViews/DynamicAlert.swift @@ -0,0 +1,56 @@ +// +// DynamicAlert.swift +// Ferrite +// +// Created by Brian Dashore on 9/8/22. +// + +import SwiftUI + +struct DynamicAlert: ViewModifier { + @Binding var isPresented: Bool + + let title: String + let message: String? + let buttons: [AlertButton] + + func body(content: Content) -> some View { + if #available(iOS 15, *) { + content + .alert( + title, + isPresented: $isPresented, + actions: { + ForEach(buttons) { button in + button.toButtonView() + } + }, + message: { + if let message = message { + Text(message) + } + } + ) + } else { + content + .alert(isPresented: $isPresented) { + if let primaryButton = buttons[safe: 0], + let secondaryButton = buttons[safe: 1] + { + return Alert( + title: Text(title), + message: message.map { Text($0) } ?? nil, + primaryButton: primaryButton.toActionButton(), + secondaryButton: secondaryButton.toActionButton() + ) + } else { + return Alert( + title: Text(title), + message: message.map { Text($0) } ?? nil, + dismissButton: buttons[0].toActionButton() + ) + } + } + } + } +} diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index fc3bb21..a821842 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -73,25 +73,23 @@ struct ContentView: View { SearchResultsView() } .sheet(item: $navModel.currentChoiceSheet) { item in - Group { - switch item { - case .magnet: - MagnetChoiceView() - .environmentObject(debridManager) - .environmentObject(scrapingModel) - .environmentObject(navModel) - case .batch: - BatchChoiceView() - .environmentObject(debridManager) - .environmentObject(scrapingModel) - .environmentObject(navModel) - case .activity: - if #available(iOS 16, *) { - AppActivityView(activityItems: navModel.activityItems) - .presentationDetents([.medium, .large]) - } else { - AppActivityView(activityItems: navModel.activityItems) - } + switch item { + case .magnet: + MagnetChoiceView() + .environmentObject(debridManager) + .environmentObject(scrapingModel) + .environmentObject(navModel) + case .batch: + BatchChoiceView() + .environmentObject(debridManager) + .environmentObject(scrapingModel) + .environmentObject(navModel) + case .activity: + if #available(iOS 16, *) { + AppActivityView(activityItems: navModel.activityItems) + .presentationDetents([.medium, .large]) + } else { + AppActivityView(activityItems: navModel.activityItems) } } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 61caa19..90d560e 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -22,6 +22,13 @@ struct LibraryView: View { ] ) var bookmarks: FetchedResults + @FetchRequest( + entity: History.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \History.date, ascending: false) + ] + ) var history: FetchedResults + @State private var historyEmpty = true @State private var selectedSegment: LibraryPickerSegment = .bookmarks @@ -42,7 +49,7 @@ struct LibraryView: View { case .bookmarks: BookmarksView(bookmarks: bookmarks) case .history: - HistoryView() + HistoryView(history: history) } Spacer() @@ -54,7 +61,7 @@ struct LibraryView: View { EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") } case .history: - if historyEmpty { + if history.isEmpty { EmptyInstructionView(title: "No History", message: "Start watching to build history") } } @@ -62,7 +69,13 @@ struct LibraryView: View { .navigationTitle("Library") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - EditButton() + HStack { + EditButton() + + if selectedSegment == .history { + HistoryActionsView() + } + } } } .environment(\.editMode, $editMode) diff --git a/Ferrite/Views/LibraryViews/HistoryActionsView.swift b/Ferrite/Views/LibraryViews/HistoryActionsView.swift new file mode 100644 index 0000000..5b3e6a1 --- /dev/null +++ b/Ferrite/Views/LibraryViews/HistoryActionsView.swift @@ -0,0 +1,54 @@ +// +// HistoryActionsView.swift +// Ferrite +// +// Created by Brian Dashore on 9/7/22. +// + +import SwiftUI + +struct HistoryActionsView: View { + @EnvironmentObject var toastModel: ToastViewModel + + @State private var showActionSheet = false + + var body: some View { + Button("Clear") { + showActionSheet.toggle() + } + .dynamicAccentColor(.red) + .dynamicActionSheet( + isPresented: $showActionSheet, + title: "Clear watch history", + message: "This is an irreversible action!", + buttons: [ + AlertButton("Past day", role: .destructive) { + deleteHistory(.day) + }, + AlertButton("Past week", role: .destructive) { + deleteHistory(.week) + }, + AlertButton("Past month", role: .destructive) { + deleteHistory(.month) + }, + AlertButton("All time", role: .destructive) { + deleteHistory(.allTime) + } + ] + ) + } + + func deleteHistory(_ deleteRange: HistoryDeleteRange) { + do { + try PersistenceController.shared.batchDeleteHistory(range: deleteRange) + } catch { + toastModel.updateToastDescription("History delete error: \(error)") + } + } +} + +struct HistoryActionsView_Previews: PreviewProvider { + static var previews: some View { + HistoryActionsView() + } +} diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/LibraryViews/HistoryButtonView.swift new file mode 100644 index 0000000..cf607c3 --- /dev/null +++ b/Ferrite/Views/LibraryViews/HistoryButtonView.swift @@ -0,0 +1,76 @@ +// +// HistoryButtonView.swift +// Ferrite +// +// Created by Brian Dashore on 9/9/22. +// + +import SwiftUI + +struct HistoryButtonView: View { + @EnvironmentObject var toastModel: ToastViewModel + @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager + + let entry: HistoryEntry + + var body: some View { + Button { + if let url = entry.url { + if url.starts(with: "https://") { + Task { + debridManager.realDebridDownloadUrl = url + navModel.runDebridAction(urlString: url) + + if navModel.currentChoiceSheet != .magnet { + debridManager.realDebridDownloadUrl = "" + } + } + } else { + navModel.runMagnetAction(magnetString: url) + } + } else { + toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.") + } + } label: { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 3) { + Text(entry.name ?? "Unknown title") + .font(entry.subName == nil ? .body : .subheadline) + + if let subName = entry.subName { + Text(subName) + .foregroundColor(.gray) + .font(.subheadline) + } + } + + HStack { + Text(entry.source ?? "Unknown source") + + Spacer() + + Text("DEBRID") + .fontWeight(.bold) + .padding(3) + .background { + Group { + if let url = entry.url, url.starts(with: "https://") { + Color.green + } else { + Color.red + } + } + .cornerRadius(4) + .opacity(0.5) + } + } + .font(.caption) + } + .lineLimit(1) + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + } + .dynamicAccentColor(.white) + .disableInteraction(navModel.currentChoiceSheet != nil) + } +} diff --git a/Ferrite/Views/LibraryViews/HistoryView.swift b/Ferrite/Views/LibraryViews/HistoryView.swift index 078ac30..e53ffb5 100644 --- a/Ferrite/Views/LibraryViews/HistoryView.swift +++ b/Ferrite/Views/LibraryViews/HistoryView.swift @@ -8,15 +8,58 @@ import SwiftUI struct HistoryView: View { + @EnvironmentObject var navModel: NavigationViewModel + + let backgroundContext = PersistenceController.shared.backgroundContext + + var history: FetchedResults + var formatter: DateFormatter = .init() + + @State private var historyIndex = 0 + + init(history: FetchedResults) { + self.history = history + + formatter.dateStyle = .medium + formatter.timeStyle = .none + } + + func groupedEntries(_ result: FetchedResults) -> [[History]] { + Dictionary(grouping: result) { (element: History) in + element.dateString ?? "" + }.values.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() } + } + var body: some View { - ZStack { - EmptyView() + if !history.isEmpty { + List { + ForEach(groupedEntries(history), id: \.self) { (section: [History]) in + Section(header: Text(formatter.string(from: section[0].date ?? Date()))) { + ForEach(section, id: \.self) { history in + ForEach(history.entryArray) { entry in + HistoryButtonView(entry: entry) + } + .onDelete { offsets in + removeEntry(at: offsets, from: history) + } + } + } + } + } + .listStyle(.insetGrouped) + } + } + + func removeEntry(at offsets: IndexSet, from history: History) { + for index in offsets { + if let entry = history.entryArray[safe: index] { + history.removeFromEntries(entry) + PersistenceController.shared.delete(entry, context: backgroundContext) + } + + if history.entryArray.isEmpty { + PersistenceController.shared.delete(history, context: backgroundContext) + } } } } - -struct HistoryView_Previews: PreviewProvider { - static var previews: some View { - HistoryView() - } -} diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index e3eafc5..4ab9ab2 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -23,31 +23,30 @@ struct MagnetChoiceView: View { var body: some View { NavView { Form { - if realDebridEnabled, debridManager.matchSearchResult(result: navModel.selectedSearchResult) != .none { + if !debridManager.realDebridDownloadUrl.isEmpty { Section(header: "Real Debrid options") { ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .outplayer) } ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(action: .vlc, urlString: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .vlc) } ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(action: .infuse, urlString: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .infuse) } ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { UIPasteboard.general.string = debridManager.realDebridDownloadUrl showLinkCopyAlert.toggle() } - .alert(isPresented: $showLinkCopyAlert) { - Alert( - title: Text("Copied"), - message: Text("Download link copied successfully"), - dismissButton: .cancel(Text("OK")) - ) - } + .dynamicAlert( + isPresented: $showLinkCopyAlert , + title: "Copied", + message: "Download link copied successfully", + buttons: [AlertButton("OK")] + ) ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") { if let url = URL(string: debridManager.realDebridDownloadUrl) { @@ -63,13 +62,12 @@ struct MagnetChoiceView: View { UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink showMagnetCopyAlert.toggle() } - .alert(isPresented: $showMagnetCopyAlert) { - Alert( - title: Text("Copied"), - message: Text("Magnet link copied successfully"), - dismissButton: .cancel(Text("OK")) - ) - } + .dynamicAlert( + isPresented: $showMagnetCopyAlert, + title: "Copied", + message: "Magnet link copied successfully", + buttons: [AlertButton("OK")] + ) ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") { if let result = navModel.selectedSearchResult, @@ -82,7 +80,7 @@ struct MagnetChoiceView: View { } ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { - navModel.runMagnetAction(.webtor) + navModel.runMagnetAction(magnetString: navModel.selectedSearchResult?.magnetLink, .webtor) } } } @@ -95,6 +93,9 @@ struct MagnetChoiceView: View { AppActivityView(activityItems: navModel.activityItems) } } + .onDisappear { + debridManager.realDebridDownloadUrl = "" + } .navigationTitle("Link actions") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 0580c28..3e20efc 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -46,20 +46,21 @@ struct MainView: View { } .tag(ViewTab.settings) } - .alert(isPresented: $showUpdateAlert) { - Alert( - title: Text("Update available"), - message: Text("Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings."), - primaryButton: .default(Text("Download")) { + .dynamicAlert( + isPresented: $showUpdateAlert, + title: "Update available", + message: "Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings.", + buttons: [ + AlertButton("Download") { guard let releaseUrl = URL(string: releaseUrlString) else { return } UIApplication.shared.open(releaseUrl) }, - secondaryButton: .cancel() - ) - } + AlertButton(role: .cancel) + ] + ) .onAppear { if autoUpdateNotifs { viewTask = Task { @@ -71,7 +72,6 @@ struct MainView: View { let releaseVersion = String(latestRelease.tagName.dropFirst()) if releaseVersion > UIApplication.shared.appVersion { - print("Greater") releaseVersionString = latestRelease.tagName releaseUrlString = latestRelease.htmlUrl showUpdateAlert.toggle() diff --git a/Ferrite/Views/SearchProgressView.swift b/Ferrite/Views/SearchProgressView.swift deleted file mode 100644 index 0923ccb..0000000 --- a/Ferrite/Views/SearchProgressView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SearchProgressView.swift -// Ferrite -// -// Created by Brian Dashore on 8/8/22. -// - -import SwiftUI - -struct SearchProgressView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -struct SearchProgressView_Previews: PreviewProvider { - static var previews: some View { - SearchProgressView() - } -} diff --git a/Ferrite/Views/SearchResultButtonView.swift b/Ferrite/Views/SearchResultButtonView.swift index 06a90b1..b952c2a 100644 --- a/Ferrite/Views/SearchResultButtonView.swift +++ b/Ferrite/Views/SearchResultButtonView.swift @@ -14,73 +14,82 @@ struct SearchResultButtonView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false + var result: SearchResult @State private var runOnce = false @State var existingBookmark: Bookmark? = nil var body: some View { - VStack(alignment: .leading) { - Button { - if debridManager.currentDebridTask == nil { - navModel.selectedSearchResult = result - - switch debridManager.matchSearchResult(result: result) { - case .full: - debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: result) - - if !debridManager.realDebridDownloadUrl.isEmpty { - navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl) + Button { + if debridManager.currentDebridTask == nil { + navModel.selectedSearchResult = result + + switch debridManager.matchSearchResult(result: result) { + case .full: + debridManager.currentDebridTask = Task { + await debridManager.fetchRdDownload(searchResult: result) + + if !debridManager.realDebridDownloadUrl.isEmpty { + navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) + + if navModel.currentChoiceSheet != .magnet { + debridManager.realDebridDownloadUrl = "" } } - case .partial: - if debridManager.setSelectedRdResult(result: result) { - navModel.currentChoiceSheet = .batch - } - case .none: - navModel.runMagnetAction() } + case .partial: + if debridManager.setSelectedRdResult(result: result) { + navModel.currentChoiceSheet = .batch + } + case .none: + navModel.addToHistory(name: result.title, source: result.source, url: result.magnetLink) + navModel.runMagnetAction(magnetString: result.magnetLink) } - } label: { + } + } label: { + VStack(alignment: .leading, spacing: 10) { Text(result.title ?? "No title") .font(.callout) .fixedSize(horizontal: false, vertical: true) - } - .dynamicAccentColor(.primary) - .padding(.bottom, 5) - .conditionalContextMenu(id: existingBookmark) { - if let bookmark = existingBookmark { - Button { - PersistenceController.shared.delete(bookmark, context: backgroundContext) - // When the entity is deleted, let other instances know to remove that reference - NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) - } label: { - Text("Remove bookmark") - Image(systemName: "bookmark.slash.fill") - } - } else { - Button { - let newBookmark = Bookmark(context: backgroundContext) - newBookmark.title = result.title - newBookmark.source = result.source - newBookmark.magnetHash = result.magnetHash - newBookmark.magnetLink = result.magnetLink - newBookmark.seeders = result.seeders - newBookmark.leechers = result.leechers - - existingBookmark = newBookmark - - PersistenceController.shared.save(backgroundContext) - } label: { - Text("Bookmark") - Image(systemName: "bookmark") - } + SearchResultRDView(result: result) + } + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + } + .disableInteraction(navModel.currentChoiceSheet != nil) + .dynamicAccentColor(.primary) + .conditionalContextMenu(id: existingBookmark) { + if let bookmark = existingBookmark { + Button { + PersistenceController.shared.delete(bookmark, context: backgroundContext) + + // When the entity is deleted, let other instances know to remove that reference + NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) + } label: { + Text("Remove bookmark") + Image(systemName: "bookmark.slash.fill") + } + } else { + Button { + let newBookmark = Bookmark(context: backgroundContext) + newBookmark.title = result.title + newBookmark.source = result.source + newBookmark.magnetHash = result.magnetHash + newBookmark.magnetLink = result.magnetLink + newBookmark.seeders = result.seeders + newBookmark.leechers = result.leechers + + existingBookmark = newBookmark + + PersistenceController.shared.save(backgroundContext) + } label: { + Text("Bookmark") + Image(systemName: "bookmark") } } - - SearchResultRDView(result: result) } .onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in existingBookmark = nil diff --git a/Ferrite/Views/SearchResultRDView.swift b/Ferrite/Views/SearchResultRDView.swift index dbcc913..8c51280 100644 --- a/Ferrite/Views/SearchResultRDView.swift +++ b/Ferrite/Views/SearchResultRDView.swift @@ -37,20 +37,18 @@ struct SearchResultRDView: View { .fontWeight(.bold) .padding(2) .background { - switch debridManager.matchSearchResult(result: result) { - case .full: - Color.green - .cornerRadius(4) - .opacity(0.5) - case .partial: - Color.orange - .cornerRadius(4) - .opacity(0.5) - case .none: - Color.red - .cornerRadius(4) - .opacity(0.5) + Group { + switch debridManager.matchSearchResult(result: result) { + case .full: + Color.green + case .partial: + Color.orange + case .none: + Color.red + } } + .cornerRadius(4) + .opacity(0.5) } } } diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index 5ddfae3..5c20198 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -32,13 +32,12 @@ struct SourceListEditorView: View { sourceUrl = navModel.selectedSourceList?.urlString ?? "" sourceUrlSet = true } - .alert(isPresented: $sourceManager.showUrlErrorAlert) { - Alert( - title: Text("Error"), - message: Text(sourceManager.urlErrorAlertText), - dismissButton: .default(Text("OK")) - ) - } + .dynamicAlert( + isPresented: $sourceManager.showUrlErrorAlert, + title: "Error", + message: sourceManager.urlErrorAlertText, + buttons: [AlertButton("OK")] + ) .navigationTitle("Editing source list") .navigationBarTitleDisplayMode(.inline) .toolbar {