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 {