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 <bdashore3@proton.me>
This commit is contained in:
kingbri 2022-09-06 00:05:34 -04:00
parent 2f870b9410
commit 4d3a16f77e
25 changed files with 823 additions and 172 deletions

View file

@ -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 = "<group>"; };
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; };
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; };
0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicActionSheet.swift; sourceTree = "<group>"; };
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.swift; sourceTree = "<group>"; };
0C626A9428CADB25003C7129 /* DynamicAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAlert.swift; sourceTree = "<group>"; };
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = "<group>"; };
@ -161,14 +175,18 @@
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataClass.swift"; sourceTree = "<group>"; };
0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataProperties.swift"; sourceTree = "<group>"; };
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; };
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
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 = "<group>"; };
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = "<group>"; };
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
/* 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 = "<group>";
@ -302,6 +327,7 @@
0C7D11FD28AA03FE00ED92DB /* View.swift */,
0C78041C28BFB3EA001E8CA3 /* String.swift */,
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
);
path = Extensions;
sourceTree = "<group>";
@ -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 = "<group>";
@ -365,6 +390,8 @@
children = (
0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
0CA3B23628C2660700616D3A /* HistoryView.swift */,
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */,
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */,
);
path = LibraryViews;
sourceTree = "<group>";
@ -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";
};

View file

@ -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 {
}

View file

@ -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<History> {
return NSFetchRequest<History>(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<HistoryEntry> ?? []
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 {
}

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21277" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21279" systemVersion="21G83" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Bookmark" representedClassName="Bookmark" syncable="YES">
<attribute name="leechers" optional="YES" attributeType="String"/>
<attribute name="magnetHash" optional="YES" attributeType="String"/>
@ -10,13 +10,15 @@
<attribute name="source" attributeType="String" defaultValueString=""/>
<attribute name="title" optional="YES" attributeType="String"/>
</entity>
<entity name="History" representedClassName="History" syncable="YES" codeGenerationType="class">
<entity name="History" representedClassName="History" syncable="YES">
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="dateString" optional="YES" attributeType="String"/>
<relationship name="entries" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="HistoryEntry" inverseName="parentHistory" inverseEntity="HistoryEntry"/>
</entity>
<entity name="HistoryEntry" representedClassName="HistoryEntry" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="source" optional="YES" attributeType="String"/>
<attribute name="subName" optional="YES" attributeType="String"/>
<attribute name="timeStamp" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="String"/>
<relationship name="parentHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="History" inverseName="entries" inverseEntity="History"/>

View file

@ -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<NSFetchRequestResult>(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])
}
}

View file

@ -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
}()
}

View file

@ -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<Bool>,
title: String,
message: String? = nil,
buttons: [AlertButton]) -> some View
{
modifier(DynamicActionSheet(isPresented: isPresented, title: title, message: message, buttons: buttons))
}
func dynamicAlert(
isPresented: Binding<Bool>,
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))
}
}

View file

@ -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)
}
}

View file

@ -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
}
}
}
}

View file

@ -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
}
}
}

View file

@ -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())
}
}
}
}

View file

@ -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)
}
}

View file

@ -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 }
)
}
}
}
}

View file

@ -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()
)
}
}
}
}
}

View file

@ -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)
}
}
}

View file

@ -22,6 +22,13 @@ struct LibraryView: View {
]
) var bookmarks: FetchedResults<Bookmark>
@FetchRequest(
entity: History.entity(),
sortDescriptors: [
NSSortDescriptor(keyPath: \History.date, ascending: false)
]
) var history: FetchedResults<History>
@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)

View file

@ -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()
}
}

View file

@ -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)
}
}

View file

@ -8,15 +8,58 @@
import SwiftUI
struct HistoryView: View {
@EnvironmentObject var navModel: NavigationViewModel
let backgroundContext = PersistenceController.shared.backgroundContext
var history: FetchedResults<History>
var formatter: DateFormatter = .init()
@State private var historyIndex = 0
init(history: FetchedResults<History>) {
self.history = history
formatter.dateStyle = .medium
formatter.timeStyle = .none
}
func groupedEntries(_ result: FetchedResults<History>) -> [[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()
}
}

View file

@ -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 {

View file

@ -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()

View file

@ -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()
}
}

View file

@ -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

View file

@ -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)
}
}
}

View file

@ -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 {