From 4512318e8f359f541f4823ea281003c8ee7eca47 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 8 Feb 2023 12:09:37 -0500 Subject: [PATCH] Ferrite: Add actions, plugins, and tags Plugins are now a unified format for both sources and actions. Actions dictate what to do with a link and can now be added through a plugin JSON file. Backups have also been versioned to improve performance and add action support. Tags are used to give small amounts of information before a user installs a plugin. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 148 ++++++--- .../Classes/Action+CoreDataClass.swift | 13 + .../Classes/Action+CoreDataProperties.swift | 71 ++++ .../Classes/Bookmark+CoreDataClass.swift | 13 +- .../Classes/Bookmark+CoreDataProperties.swift | 11 + .../Classes/PluginList+CoreDataClass.swift | 15 + .../PluginList+CoreDataProperties.swift | 28 ++ .../Classes/PluginTag+CoreDataClass.swift | 14 + .../PluginTag+CoreDataProperties.swift | 31 ++ .../Classes/Source+CoreDataClass.swift | 2 +- .../Classes/Source+CoreDataProperties.swift | 86 +++-- .../Classes/SourceList+CoreDataClass.swift | 13 - .../SourceList+CoreDataProperties.swift | 23 -- .../FerriteDB.xcdatamodeld/.xccurrentversion | 2 +- .../FerriteDB_v2.xcdatamodel/contents | 159 +++++++++ .../PersistenceController.swift | 56 ++-- Ferrite/FerriteApp.swift | 6 +- Ferrite/Models/ActionModels.swift | 32 ++ Ferrite/Models/BackupModels.swift | 10 +- Ferrite/Models/PluginModels.swift | 32 ++ Ferrite/Models/SourceModels.swift | 24 +- Ferrite/Protocols/Plugin.swift | 35 ++ Ferrite/ViewModels/BackupManager.swift | 122 ++++--- Ferrite/ViewModels/NavigationViewModel.swift | 3 +- ...ourceManager.swift => PluginManager.swift} | 308 +++++++++++++----- .../CommonViews/DynamicFetchRequest.swift | 2 +- Ferrite/Views/CommonViews/NavView.swift | 9 +- Ferrite/Views/CommonViews/Tag.swift | 28 ++ .../Debrid/DebridLabelView.swift | 50 +-- .../Library/Cloud/AllDebridCloudView.swift | 2 +- .../Library/Cloud/PremiumizeCloudView.swift | 3 +- .../Library/Cloud/RealDebridCloudView.swift | 5 +- .../Library/HistoryButtonView.swift | 8 + .../Buttons/InstalledPluginButtonView.swift} | 44 ++- .../Buttons/PluginCatalogButtonView.swift | 51 +++ .../Buttons/SourceCatalogButtonView.swift | 4 +- .../Plugin/PluginListView.swift | 80 +++++ .../Plugin/PluginTagsView.swift | 22 ++ .../Source/SourceSettingsView.swift | 44 +-- .../SearchResult/SearchResultButtonView.swift | 6 +- .../ComponentViews/Settings/BackupsView.swift | 4 +- ...rView.swift => PluginListEditorView.swift} | 38 ++- ...iew.swift => SettingsPluginListView.swift} | 36 +- .../Buttons/SourceUpdateButtonView.swift | 38 --- Ferrite/Views/ContentView.swift | 4 +- Ferrite/Views/MainView.swift | 43 ++- Ferrite/Views/PluginsView.swift | 111 +++++++ Ferrite/Views/SettingsView.swift | 6 +- ...hoiceView.swift => ActionChoiceView.swift} | 40 ++- .../Views/SheetViews/BatchChoiceView.swift | 2 +- Ferrite/Views/SourcesView.swift | 142 -------- 51 files changed, 1470 insertions(+), 609 deletions(-) create mode 100644 Ferrite/DataManagement/Classes/Action+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/Action+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/PluginList+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/PluginList+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/PluginTag+CoreDataProperties.swift delete mode 100644 Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift delete mode 100644 Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift create mode 100644 Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents create mode 100644 Ferrite/Models/ActionModels.swift create mode 100644 Ferrite/Models/PluginModels.swift create mode 100644 Ferrite/Protocols/Plugin.swift rename Ferrite/ViewModels/{SourceManager.swift => PluginManager.swift} (55%) create mode 100644 Ferrite/Views/CommonViews/Tag.swift rename Ferrite/Views/ComponentViews/{Source/Buttons/InstalledSourceButtonView.swift => Plugin/Buttons/InstalledPluginButtonView.swift} (50%) create mode 100644 Ferrite/Views/ComponentViews/Plugin/Buttons/PluginCatalogButtonView.swift rename Ferrite/Views/ComponentViews/{Source => Plugin}/Buttons/SourceCatalogButtonView.swift (86%) create mode 100644 Ferrite/Views/ComponentViews/Plugin/PluginListView.swift create mode 100644 Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift rename Ferrite/Views/ComponentViews/{ => Plugin}/Source/SourceSettingsView.swift (84%) rename Ferrite/Views/ComponentViews/Settings/{SourceListEditorView.swift => PluginListEditorView.swift} (59%) rename Ferrite/Views/ComponentViews/Settings/{SettingsSourceListView.swift => SettingsPluginListView.swift} (75%) delete mode 100644 Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift create mode 100644 Ferrite/Views/PluginsView.swift rename Ferrite/Views/SheetViews/{MagnetChoiceView.swift => ActionChoiceView.swift} (80%) delete mode 100644 Ferrite/Views/SourcesView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index b47a44e..a252eac 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -8,20 +8,26 @@ /* Begin PBXBuildFile section */ 0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; }; + 0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */; }; + 0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */; }; 0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; }; 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; }; 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; }; - 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; + 0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; }; + 0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.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 */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; }; + 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */; }; + 0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */; }; + 0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */; }; 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; }; 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; }; 0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */; }; @@ -35,8 +41,13 @@ 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 */; }; + 0C5005522992B6750064606A /* PluginTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005512992B6750064606A /* PluginTagsView.swift */; }; + 0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */; }; + 0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005592992BA6A0064606A /* PluginTag+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 */; }; + 0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */; }; + 0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */; }; 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; }; 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; @@ -53,9 +64,8 @@ 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; }; 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; }; 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; }; - 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; }; - 0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */; }; - 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */; }; + 0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */; }; + 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */; }; 0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; }; 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; }; 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; }; @@ -68,16 +78,14 @@ 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */; }; 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */; }; 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */; }; - 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */; }; - 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */; }; 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */; }; 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; }; - 0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceListView.swift */; }; - 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.swift */; }; - 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */; }; + 0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsPluginListView.swift */; }; + 0CA05459288EE9E600850554 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* PluginManager.swift */; }; + 0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */; }; 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BB288903F000DE2211 /* SettingsView.swift */; }; 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BC288903F000DE2211 /* LoginWebView.swift */; }; - 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */; }; + 0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */; }; 0CA148DB288903F000DE2211 /* NavView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C1288903F000DE2211 /* NavView.swift */; }; 0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; }; 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */; }; @@ -110,30 +118,40 @@ 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 */; }; + 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */; }; + 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; }; 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; }; 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; }; 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; }; 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; 0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; }; + 0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; }; 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ 0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = ""; }; + 0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginList+CoreDataClass.swift"; sourceTree = ""; }; + 0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginList+CoreDataProperties.swift"; sourceTree = ""; }; 0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = ""; }; 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = ""; }; 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; - 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; + 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListView.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 = ""; }; 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = ""; }; 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = ""; }; + 0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.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 = ""; }; 0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = ""; }; 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = ""; }; 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; + 0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionModels.swift; sourceTree = ""; }; + 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginsView.swift; sourceTree = ""; }; + 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginModels.swift; sourceTree = ""; }; + 0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB_v2.xcdatamodel; 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 = ""; }; 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = ""; }; @@ -146,8 +164,13 @@ 0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.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 = ""; }; + 0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = ""; }; + 0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataClass.swift"; sourceTree = ""; }; + 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+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 = ""; }; + 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnAppearHandler.swift; sourceTree = ""; }; + 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DidAppearModifier.swift; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; @@ -160,9 +183,8 @@ 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = ""; }; 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; - 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = ""; }; - 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceButtonView.swift; sourceTree = ""; }; - 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogButtonView.swift; sourceTree = ""; }; + 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledPluginButtonView.swift; sourceTree = ""; }; + 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginCatalogButtonView.swift; sourceTree = ""; }; 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = ""; }; 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = ""; }; @@ -175,16 +197,14 @@ 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = ""; }; 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = ""; }; 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = ""; }; - 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataClass.swift"; sourceTree = ""; }; - 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataProperties.swift"; sourceTree = ""; }; 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionsPickerViews.swift; sourceTree = ""; }; 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; - 0CA05456288EE58200850554 /* SettingsSourceListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSourceListView.swift; sourceTree = ""; }; - 0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = ""; }; - 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = ""; }; + 0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = ""; }; + 0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; + 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListEditorView.swift; sourceTree = ""; }; 0CA148BB288903F000DE2211 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; 0CA148BC288903F000DE2211 /* LoginWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginWebView.swift; sourceTree = ""; }; - 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagnetChoiceView.swift; sourceTree = ""; }; + 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionChoiceView.swift; sourceTree = ""; }; 0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = ""; }; 0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = ""; }; @@ -216,11 +236,14 @@ 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 = ""; }; + 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = ""; }; + 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.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 = ""; }; 0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; + 0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = ""; }; 0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -246,7 +269,7 @@ 0C0755C22934241F00ECA142 /* SheetViews */ = { isa = PBXGroup; children = ( - 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, + 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */, 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, ); path = SheetViews; @@ -255,11 +278,11 @@ 0C0755C32934244500ECA142 /* ComponentViews */ = { isa = PBXGroup; children = ( + 0C3E00D4296F560800ECECB2 /* Plugin */, 0C0755C42934245800ECA142 /* Debrid */, 0CA3B23528C265FD00616D3A /* Library */, 0C44E2AB28D4E126007711AE /* SearchResult */, 0CA0545C288F7CB200850554 /* Settings */, - 0C794B65289DAC9F00DD1CC8 /* Source */, ); path = ComponentViews; sourceTree = ""; @@ -276,6 +299,12 @@ 0C0D50DE288DF72D0035ECC8 /* Classes */ = { isa = PBXGroup; children = ( + 0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */, + 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */, + 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */, + 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */, + 0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */, + 0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */, 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */, 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */, 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */, @@ -292,8 +321,6 @@ 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */, 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */, 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */, - 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */, - 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */, ); path = Classes; sourceTree = ""; @@ -301,6 +328,7 @@ 0C0D50E3288DFE6E0035ECC8 /* Models */ = { isa = PBXGroup; children = ( + 0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */, 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */, 0C7ED14028D61BBA009E29AD /* BackupModels.swift */, 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */, @@ -310,6 +338,7 @@ 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, + 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, ); path = Models; sourceTree = ""; @@ -324,6 +353,17 @@ path = Cloud; sourceTree = ""; }; + 0C3E00D4296F560800ECECB2 /* Plugin */ = { + isa = PBXGroup; + children = ( + 0C44E2AA28D4E09B007711AE /* Buttons */, + 0C794B65289DAC9F00DD1CC8 /* Source */, + 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */, + 0C5005512992B6750064606A /* PluginTagsView.swift */, + ); + path = Plugin; + sourceTree = ""; + }; 0C44E2A628D4DDC6007711AE /* Classes */ = { isa = PBXGroup; children = ( @@ -340,6 +380,7 @@ 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, + 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */, ); path = Modifiers; sourceTree = ""; @@ -347,9 +388,8 @@ 0C44E2AA28D4E09B007711AE /* Buttons */ = { isa = PBXGroup; children = ( - 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */, - 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */, - 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */, + 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */, + 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */, ); path = Buttons; sourceTree = ""; @@ -363,10 +403,17 @@ path = SearchResult; sourceTree = ""; }; + 0C5005552992B9C20064606A /* Protocols */ = { + isa = PBXGroup; + children = ( + 0CE1C4172981E8D700418F20 /* Plugin.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 0C794B65289DAC9F00DD1CC8 /* Source */ = { isa = PBXGroup; children = ( - 0C44E2AA28D4E09B007711AE /* Buttons */, 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, ); path = Source; @@ -376,8 +423,8 @@ isa = PBXGroup; children = ( 0C44E2AE28D52E8A007711AE /* BackupsView.swift */, - 0CA05456288EE58200850554 /* SettingsSourceListView.swift */, - 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */, + 0CA05456288EE58200850554 /* SettingsPluginListView.swift */, + 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */, 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */, 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */, ); @@ -394,6 +441,7 @@ 0CA148EF2889061600DE2211 /* ViewModels */, 0CA148EE2889061200DE2211 /* Views */, 0C44E2A628D4DDC6007711AE /* Classes */, + 0C5005552992B9C20064606A /* Protocols */, 0CA148C8288903F000DE2211 /* Extensions */, 0CA148C5288903F000DE2211 /* Preview Content */, 0CA148C7288903F000DE2211 /* FerriteApp.swift */, @@ -415,6 +463,7 @@ 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, 0C32FB562890D1F2002BD219 /* ListRowViews.swift */, + 0C2D9652299316CC00A504B6 /* Tag.swift */, ); path = CommonViews; sourceTree = ""; @@ -457,7 +506,7 @@ 0CA148D4288903F000DE2211 /* ContentView.swift */, 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, 0CA3B23328C2658700616D3A /* LibraryView.swift */, - 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, + 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */, 0CA148BB288903F000DE2211 /* SettingsView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, 0CA148BC288903F000DE2211 /* LoginWebView.swift */, @@ -472,7 +521,7 @@ 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */, 0CA148CF288903F000DE2211 /* ToastViewModel.swift */, 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */, - 0CA05458288EE9E600850554 /* SourceManager.swift */, + 0CA05458288EE9E600850554 /* PluginManager.swift */, 0C44E2AC28D51C63007711AE /* BackupManager.swift */, ); path = ViewModels; @@ -482,6 +531,7 @@ isa = PBXGroup; children = ( 0CA148CE288903F000DE2211 /* WebView.swift */, + 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */, ); path = RepresentableViews; sourceTree = ""; @@ -652,46 +702,55 @@ buildActionMask = 2147483647; files = ( 0C7ED14328D65518009E29AD /* FileManager.swift in Sources */, + 0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */, 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, + 0C2D9653299316CC00A504B6 /* Tag.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 */, 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */, + 0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */, 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, - 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, + 0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */, + 0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */, 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, + 0C5005522992B6750064606A /* PluginTagsView.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */, 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */, - 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, - 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */, + 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, + 0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */, 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */, - 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */, + 0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */, 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */, 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, + 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, + 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */, + 0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */, + 0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, - 0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */, + 0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */, 0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */, @@ -699,8 +758,7 @@ 0C445C62293F9A0B0060744D /* Bundle.swift in Sources */, 0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */, 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, - 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, - 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, + 0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */, 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, @@ -712,7 +770,6 @@ 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, - 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */, 0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */, 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, @@ -722,23 +779,27 @@ 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */, 0C42B5982932F6DD008057A0 /* Set.swift in Sources */, 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, - 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, + 0CA05459288EE9E600850554 /* PluginManager.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 */, + 0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */, 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */, 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, + 0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */, + 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, 0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */, 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, + 0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, + 0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, 0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */, @@ -1076,9 +1137,10 @@ 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = { isa = XCVersionGroup; children = ( + 0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */, 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */, ); - currentVersion = 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */; + currentVersion = 0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */; path = FerriteDB.xcdatamodeld; sourceTree = ""; versionGroupType = wrapper.xcdatamodel; diff --git a/Ferrite/DataManagement/Classes/Action+CoreDataClass.swift b/Ferrite/DataManagement/Classes/Action+CoreDataClass.swift new file mode 100644 index 0000000..c060b75 --- /dev/null +++ b/Ferrite/DataManagement/Classes/Action+CoreDataClass.swift @@ -0,0 +1,13 @@ +// +// Action+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 1/12/23. +// +// + +import Foundation +import CoreData + +@objc(Action) +public class Action: NSManagedObject, Plugin {} diff --git a/Ferrite/DataManagement/Classes/Action+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Action+CoreDataProperties.swift new file mode 100644 index 0000000..77a867b --- /dev/null +++ b/Ferrite/DataManagement/Classes/Action+CoreDataProperties.swift @@ -0,0 +1,71 @@ +// +// Action+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 2/6/23. +// +// + +import Foundation +import CoreData + + +extension Action { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Action") + } + + @NSManaged public var id: UUID + @NSManaged public var listId: UUID? + @NSManaged public var name: String + @NSManaged public var deeplink: String? + @NSManaged public var version: Int16 + @NSManaged public var requires: [String] + @NSManaged public var author: String + @NSManaged public var enabled: Bool + @NSManaged public var tags: NSOrderedSet? + + public func getTags() -> [PluginTagJson] { + return requires.map { PluginTagJson(name: $0, colorHex: nil) } + tagArray.map { $0.toJson() } + } +} + +// MARK: Generated accessors for tags +extension Action { + + @objc(insertObject:inTagsAtIndex:) + @NSManaged public func insertIntoTags(_ value: PluginTag, at idx: Int) + + @objc(removeObjectFromTagsAtIndex:) + @NSManaged public func removeFromTags(at idx: Int) + + @objc(insertTags:atIndexes:) + @NSManaged public func insertIntoTags(_ values: [PluginTag], at indexes: NSIndexSet) + + @objc(removeTagsAtIndexes:) + @NSManaged public func removeFromTags(at indexes: NSIndexSet) + + @objc(replaceObjectInTagsAtIndex:withObject:) + @NSManaged public func replaceTags(at idx: Int, with value: PluginTag) + + @objc(replaceTagsAtIndexes:withTags:) + @NSManaged public func replaceTags(at indexes: NSIndexSet, with values: [PluginTag]) + + @objc(addTagsObject:) + @NSManaged public func addToTags(_ value: PluginTag) + + @objc(removeTagsObject:) + @NSManaged public func removeFromTags(_ value: PluginTag) + + @objc(addTags:) + @NSManaged public func addToTags(_ values: NSOrderedSet) + + @objc(removeTags:) + @NSManaged public func removeFromTags(_ values: NSOrderedSet) + +} + +extension Action : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift b/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift index c12c191..730fbdb 100644 --- a/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift +++ b/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift @@ -10,15 +10,4 @@ import CoreData import Foundation @objc(Bookmark) -public class Bookmark: NSManagedObject { - func toSearchResult() -> SearchResult { - SearchResult( - title: title, - source: source, - size: size, - magnet: Magnet(hash: magnetHash, link: magnetLink), - seeders: seeders, - leechers: leechers - ) - } -} +public class Bookmark: NSManagedObject {} diff --git a/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift index 36f2e9e..39a6268 100644 --- a/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift @@ -22,6 +22,17 @@ public extension Bookmark { @NSManaged var source: String @NSManaged var title: String? @NSManaged var orderNum: Int16 + + func toSearchResult() -> SearchResult { + SearchResult( + title: title, + source: source, + size: size, + magnet: Magnet(hash: magnetHash, link: magnetLink), + seeders: seeders, + leechers: leechers + ) + } } extension Bookmark: Identifiable {} diff --git a/Ferrite/DataManagement/Classes/PluginList+CoreDataClass.swift b/Ferrite/DataManagement/Classes/PluginList+CoreDataClass.swift new file mode 100644 index 0000000..3788461 --- /dev/null +++ b/Ferrite/DataManagement/Classes/PluginList+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// PluginList+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 1/11/23. +// +// + +import Foundation +import CoreData + +@objc(PluginList) +public class PluginList: NSManagedObject { + +} diff --git a/Ferrite/DataManagement/Classes/PluginList+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/PluginList+CoreDataProperties.swift new file mode 100644 index 0000000..1acc669 --- /dev/null +++ b/Ferrite/DataManagement/Classes/PluginList+CoreDataProperties.swift @@ -0,0 +1,28 @@ +// +// PluginList+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 1/11/23. +// +// + +import Foundation +import CoreData + + +extension PluginList { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "PluginList") + } + + @NSManaged public var author: String + @NSManaged public var id: UUID + @NSManaged public var name: String + @NSManaged public var urlString: String + +} + +extension PluginList : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift b/Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift new file mode 100644 index 0000000..cb9a095 --- /dev/null +++ b/Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift @@ -0,0 +1,14 @@ +// +// PluginTag+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 2/7/23. +// +// + +import Foundation +import CoreData + +@objc(PluginTag) +public class PluginTag: NSManagedObject { +} diff --git a/Ferrite/DataManagement/Classes/PluginTag+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/PluginTag+CoreDataProperties.swift new file mode 100644 index 0000000..b10b144 --- /dev/null +++ b/Ferrite/DataManagement/Classes/PluginTag+CoreDataProperties.swift @@ -0,0 +1,31 @@ +// +// PluginTag+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 2/7/23. +// +// + +import Foundation +import CoreData + + +extension PluginTag { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "PluginTag") + } + + @NSManaged public var colorHex: String? + @NSManaged public var name: String + @NSManaged public var parentAction: Action? + @NSManaged public var parentSource: Source? + + func toJson() -> PluginTagJson { + return PluginTagJson(name: name, colorHex: colorHex) + } +} + +extension PluginTag : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift b/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift index 1196354..1158192 100644 --- a/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift +++ b/Ferrite/DataManagement/Classes/Source+CoreDataClass.swift @@ -10,4 +10,4 @@ import CoreData import Foundation @objc(Source) -public class Source: NSManagedObject {} +public class Source: NSManagedObject, Plugin {} diff --git a/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift index 97b85ee..daeffc8 100644 --- a/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/Source+CoreDataProperties.swift @@ -2,33 +2,77 @@ // Source+CoreDataProperties.swift // Ferrite // -// Created by Brian Dashore on 8/3/22. +// Created by Brian Dashore on 2/6/23. // // -import CoreData import Foundation +import CoreData -public extension Source { - @nonobjc class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "Source") + +extension Source { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Source") } - @NSManaged var id: UUID - @NSManaged var baseUrl: String? - @NSManaged var fallbackUrls: [String]? - @NSManaged var dynamicBaseUrl: Bool - @NSManaged var enabled: Bool - @NSManaged var name: String - @NSManaged var author: String - @NSManaged var listId: UUID? - @NSManaged var preferredParser: Int16 - @NSManaged var version: Int16 - @NSManaged var htmlParser: SourceHtmlParser? - @NSManaged var rssParser: SourceRssParser? - @NSManaged var jsonParser: SourceJsonParser? - @NSManaged var api: SourceApi? - @NSManaged var trackers: [String]? + @NSManaged public var id: UUID + @NSManaged public var baseUrl: String? + @NSManaged public var fallbackUrls: [String]? + @NSManaged public var dynamicBaseUrl: Bool + @NSManaged public var enabled: Bool + @NSManaged public var name: String + @NSManaged public var author: String + @NSManaged public var listId: UUID? + @NSManaged public var preferredParser: Int16 + @NSManaged public var version: Int16 + @NSManaged public var htmlParser: SourceHtmlParser? + @NSManaged public var rssParser: SourceRssParser? + @NSManaged public var jsonParser: SourceJsonParser? + @NSManaged public var api: SourceApi? + @NSManaged public var trackers: [String]? + @NSManaged public var tags: NSOrderedSet? + + public func getTags() -> [PluginTagJson] { + return tagArray.map { $0.toJson() } + } } -extension Source: Identifiable {} +// MARK: Generated accessors for tags +extension Source { + + @objc(insertObject:inTagsAtIndex:) + @NSManaged public func insertIntoTags(_ value: PluginTag, at idx: Int) + + @objc(removeObjectFromTagsAtIndex:) + @NSManaged public func removeFromTags(at idx: Int) + + @objc(insertTags:atIndexes:) + @NSManaged public func insertIntoTags(_ values: [PluginTag], at indexes: NSIndexSet) + + @objc(removeTagsAtIndexes:) + @NSManaged public func removeFromTags(at indexes: NSIndexSet) + + @objc(replaceObjectInTagsAtIndex:withObject:) + @NSManaged public func replaceTags(at idx: Int, with value: PluginTag) + + @objc(replaceTagsAtIndexes:withTags:) + @NSManaged public func replaceTags(at indexes: NSIndexSet, with values: [PluginTag]) + + @objc(addTagsObject:) + @NSManaged public func addToTags(_ value: PluginTag) + + @objc(removeTagsObject:) + @NSManaged public func removeFromTags(_ value: PluginTag) + + @objc(addTags:) + @NSManaged public func addToTags(_ values: NSOrderedSet) + + @objc(removeTags:) + @NSManaged public func removeFromTags(_ values: NSOrderedSet) + +} + +extension Source : Identifiable { + +} diff --git a/Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift b/Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift deleted file mode 100644 index c6f26e5..0000000 --- a/Ferrite/DataManagement/Classes/SourceList+CoreDataClass.swift +++ /dev/null @@ -1,13 +0,0 @@ -// -// SourceList+CoreDataClass.swift -// Ferrite -// -// Created by Brian Dashore on 7/30/22. -// -// - -import CoreData -import Foundation - -@objc(SourceList) -public class SourceList: NSManagedObject {} diff --git a/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift deleted file mode 100644 index db7bb72..0000000 --- a/Ferrite/DataManagement/Classes/SourceList+CoreDataProperties.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// SourceList+CoreDataProperties.swift -// Ferrite -// -// Created by Brian Dashore on 7/30/22. -// -// - -import CoreData -import Foundation - -public extension SourceList { - @nonobjc class func fetchRequest() -> NSFetchRequest { - NSFetchRequest(entityName: "SourceList") - } - - @NSManaged var id: UUID - @NSManaged var author: String - @NSManaged var name: String - @NSManaged var urlString: String -} - -extension SourceList: Identifiable {} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion index c089bb1..f0842c7 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/.xccurrentversion @@ -3,6 +3,6 @@ _XCCurrentVersionName - FerriteDB.xcdatamodel + FerriteDB_v2.xcdatamodel diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents new file mode 100644 index 0000000..6a3cdd0 --- /dev/null +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index fc389a2..dd0895d 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -91,7 +91,7 @@ struct PersistenceController { save() } - func createBookmark(_ bookmarkJson: BookmarkJson) { + func createBookmark(_ bookmarkJson: BookmarkJson, performSave: Bool) { let bookmarkRequest = Bookmark.fetchRequest() bookmarkRequest.predicate = NSPredicate( format: "source == %@ AND title == %@ AND magnetLink == %@", @@ -113,32 +113,31 @@ struct PersistenceController { newBookmark.seeders = bookmarkJson.seeders newBookmark.leechers = bookmarkJson.leechers - save(backgroundContext) + if performSave { + save(backgroundContext) + } } - func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) { + func createHistory(_ entryJson: HistoryEntryJson, performSave: Bool, isBackup: Bool = false, date: Double? = nil) { let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date() let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate) - let newHistoryEntry = HistoryEntry(context: backgroundContext) - - newHistoryEntry.source = entryJson.source - newHistoryEntry.name = entryJson.name - newHistoryEntry.url = entryJson.url - newHistoryEntry.subName = entryJson.subName - newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970 - let historyRequest = History.fetchRequest() historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString) + var existingHistory: History? - // Safely add entries to a parent history if it exists 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 } + let existingEntries = history.entryArray.filter { $0.url == entryJson.url && $0.name == entryJson.name } + // Maybe add !isBackup here if !existingEntries.isEmpty { - for entry in existingEntries { - PersistenceController.shared.delete(entry, context: backgroundContext) + if isBackup { + continue + } else { + for entry in existingEntries { + PersistenceController.shared.delete(entry, context: backgroundContext) + } } } @@ -148,15 +147,24 @@ struct PersistenceController { } } - newHistoryEntry.parentHistory = histories.first ?? History(context: backgroundContext) - } else { - newHistoryEntry.parentHistory = History(context: backgroundContext) + existingHistory = histories.first } + let newHistoryEntry = HistoryEntry(context: backgroundContext) + + newHistoryEntry.source = entryJson.source + newHistoryEntry.name = entryJson.name + newHistoryEntry.url = entryJson.url + newHistoryEntry.subName = entryJson.subName + newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970 + + newHistoryEntry.parentHistory = existingHistory ?? History(context: backgroundContext) newHistoryEntry.parentHistory?.dateString = historyDateString newHistoryEntry.parentHistory?.date = historyDate - save(backgroundContext) + if performSave { + save(backgroundContext) + } } func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { @@ -200,8 +208,7 @@ struct PersistenceController { return predicate } - // Always use the background context to batch delete - // Merge changes into both contexts to update views + // Wrapper to batch delete history objects func batchDeleteHistory(range: HistoryDeleteRange) throws { let predicate = getHistoryPredicate(range: range) @@ -213,6 +220,13 @@ struct PersistenceController { throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?") } + try batchDelete("History", predicate: predicate) + } + + // Always use the background context to batch delete + // Merge changes into both contexts to update views + func batchDelete(_ entity: String, predicate: NSPredicate? = nil) throws { + let fetchRequest = NSFetchRequest(entityName: entity) let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) batchDeleteRequest.resultType = .resultTypeObjectIDs diff --git a/Ferrite/FerriteApp.swift b/Ferrite/FerriteApp.swift index 3f26413..8f4657d 100644 --- a/Ferrite/FerriteApp.swift +++ b/Ferrite/FerriteApp.swift @@ -15,7 +15,7 @@ struct FerriteApp: App { @StateObject var toastModel: ToastViewModel = .init() @StateObject var debridManager: DebridManager = .init() @StateObject var navModel: NavigationViewModel = .init() - @StateObject var sourceManager: SourceManager = .init() + @StateObject var pluginManager: PluginManager = .init() @StateObject var backupManager: BackupManager = .init() var body: some Scene { @@ -24,7 +24,7 @@ struct FerriteApp: App { .onAppear { scrapingModel.toastModel = toastModel debridManager.toastModel = toastModel - sourceManager.toastModel = toastModel + pluginManager.toastModel = toastModel backupManager.toastModel = toastModel navModel.toastModel = toastModel } @@ -32,7 +32,7 @@ struct FerriteApp: App { .environmentObject(scrapingModel) .environmentObject(toastModel) .environmentObject(navModel) - .environmentObject(sourceManager) + .environmentObject(pluginManager) .environmentObject(backupManager) .environment(\.managedObjectContext, persistenceController.container.viewContext) } diff --git a/Ferrite/Models/ActionModels.swift b/Ferrite/Models/ActionModels.swift new file mode 100644 index 0000000..410605b --- /dev/null +++ b/Ferrite/Models/ActionModels.swift @@ -0,0 +1,32 @@ +// +// ActionModels.swift +// Ferrite +// +// Created by Brian Dashore on 1/11/23. +// + +import Foundation + +public struct ActionJson: Codable, Hashable, PluginJson { + public let name: String + public let version: Int16 + let minVersion: String? + let requires: [ActionRequirement] + let deeplink: String? + public var author: String? + public var listId: UUID? + public var tags: [PluginTagJson]? +} + +extension ActionJson { + // Fetches all tags without optional requirement + // Avoids the need for extra tag additions in DB + public func getTags() -> [PluginTagJson] { + return requires.map { PluginTagJson(name: $0.rawValue, colorHex: nil) } + (tags.map { $0 } ?? []) + } +} + +public enum ActionRequirement: String, Codable { + case magnet + case debrid +} diff --git a/Ferrite/Models/BackupModels.swift b/Ferrite/Models/BackupModels.swift index 210eea8..c1a7b24 100644 --- a/Ferrite/Models/BackupModels.swift +++ b/Ferrite/Models/BackupModels.swift @@ -12,7 +12,11 @@ public struct Backup: Codable { var bookmarks: [BookmarkJson]? var history: [HistoryJson]? var sourceNames: [String]? - var sourceLists: [SourceListBackupJson]? + var actionNames: [String]? + var pluginListUrls: [String]? + + // MARK: Remove once v1 backups are unsupported + var sourceLists: [PluginListBackupJson]? } // MARK: - CoreData translation @@ -43,8 +47,8 @@ struct HistoryEntryJson: Codable { let source: String? } -// Differs from SourceListJson -struct SourceListBackupJson: Codable { +// Differs from PluginListJson +struct PluginListBackupJson: Codable { let name: String let author: String let id: String diff --git a/Ferrite/Models/PluginModels.swift b/Ferrite/Models/PluginModels.swift new file mode 100644 index 0000000..ee97b3f --- /dev/null +++ b/Ferrite/Models/PluginModels.swift @@ -0,0 +1,32 @@ +// +// PluginModels.swift +// Ferrite +// +// Created by Brian Dashore on 1/11/23. +// + +import Foundation + +public struct PluginListJson: Codable { + let name: String + let author: String + var sources: [SourceJson]? + var actions: [ActionJson]? +} + +// Color: Hex value +public struct PluginTagJson: Codable, Hashable, Sendable { + public let name: String + public let colorHex: String? + + enum CodingKeys: String, CodingKey { + case name + case colorHex = "color" + } +} + +extension PluginManager { + enum PluginManagerError: Error { + case ListAddition(description: String) + } +} diff --git a/Ferrite/Models/SourceModels.swift b/Ferrite/Models/SourceModels.swift index 8cf3221..fe676b3 100644 --- a/Ferrite/Models/SourceModels.swift +++ b/Ferrite/Models/SourceModels.swift @@ -12,26 +12,28 @@ public enum ApiCredentialResponseType: String, Codable, Hashable, Sendable { case text } -public struct SourceListJson: Codable, Sendable { - let name: String - let author: String - var sources: [SourceJson] -} - -public struct SourceJson: Codable, Hashable, Sendable { - let name: String - let version: Int16 +public struct SourceJson: Codable, Hashable, Sendable, PluginJson { + public let name: String + public let version: Int16 let minVersion: String? let baseUrl: String? let fallbackUrls: [String]? var dynamicBaseUrl: Bool? - var author: String? - var listId: UUID? let trackers: [String]? let api: SourceApiJson? let jsonParser: SourceJsonParserJson? let rssParser: SourceRssParserJson? let htmlParser: SourceHtmlParserJson? + public var author: String? + public var listId: UUID? + public var tags: [PluginTagJson]? +} + +extension SourceJson { + // Fetches all tags without optional requirement + public func getTags() -> [PluginTagJson] { + return tags ?? [] + } } public enum SourcePreferredParser: Int16, CaseIterable, Sendable { diff --git a/Ferrite/Protocols/Plugin.swift b/Ferrite/Protocols/Plugin.swift new file mode 100644 index 0000000..5e53d27 --- /dev/null +++ b/Ferrite/Protocols/Plugin.swift @@ -0,0 +1,35 @@ +// +// Plugin.swift +// Ferrite +// +// Created by Brian Dashore on 1/25/23. +// + +import CoreData +import Foundation + +public protocol Plugin: ObservableObject, NSManagedObject { + var id: UUID { get set } + var listId: UUID? { get set } + var name: String { get set } + var version: Int16 { get set } + var author: String { get set } + var enabled: Bool { get set } + var tags: NSOrderedSet? { get set } + func getTags() -> [PluginTagJson] +} + +extension Plugin { + var tagArray: [PluginTag] { + return self.tags?.array as? [PluginTag] ?? [] + } +} + +public protocol PluginJson: Hashable { + var name: String { get } + var version: Int16 { get } + var author: String? { get set } + var listId: UUID? { get set } + var tags: [PluginTagJson]? { get set } + func getTags() -> [PluginTagJson] +} diff --git a/Ferrite/ViewModels/BackupManager.swift b/Ferrite/ViewModels/BackupManager.swift index bf7e534..67b40a5 100644 --- a/Ferrite/ViewModels/BackupManager.swift +++ b/Ferrite/ViewModels/BackupManager.swift @@ -9,18 +9,33 @@ import Foundation public class BackupManager: ObservableObject { // Constant variable for backup versions - let latestBackupVersion: Int = 1 + let latestBackupVersion: Int = 2 var toastModel: ToastViewModel? @Published var showRestoreAlert = false @Published var showRestoreCompletedAlert = false + @Published var restoreCompletedMessage: [String] = [] @Published var backupUrls: [URL] = [] - @Published var backupSourceNames: [String] = [] @Published var selectedBackupUrl: URL? - func createBackup() { + @MainActor + func updateRestoreCompletedMessage(newString: String) { + restoreCompletedMessage.append(newString) + } + + @MainActor + func toggleRestoreCompletedAlert() { + showRestoreCompletedAlert.toggle() + } + + @MainActor + func updateBackupUrls(newUrl: URL) { + backupUrls.append(newUrl) + } + + func createBackup() async { var backup = Backup(version: latestBackupVersion) let backgroundContext = PersistenceController.shared.backgroundContext @@ -71,16 +86,14 @@ public class BackupManager: ObservableObject { backup.sourceNames = sources.map(\.name) } - let sourceListRequest = SourceList.fetchRequest() - if let sourceLists = try? backgroundContext.fetch(sourceListRequest) { - backup.sourceLists = sourceLists.map { - SourceListBackupJson( - name: $0.name, - author: $0.author, - id: $0.id.uuidString, - urlString: $0.urlString - ) - } + let actionRequest = Action.fetchRequest() + if let actions = try? backgroundContext.fetch(actionRequest) { + backup.actionNames = actions.map(\.name) + } + + let pluginListRequest = PluginList.fetchRequest() + if let pluginLists = try? backgroundContext.fetch(pluginListRequest) { + backup.pluginListUrls = pluginLists.map(\.urlString) } do { @@ -94,18 +107,20 @@ public class BackupManager: ObservableObject { let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb") try encodedJson.write(to: writeUrl) - backupUrls.append(writeUrl) + + await updateBackupUrls(newUrl: writeUrl) } catch { - print(error) + await toastModel?.updateToastDescription("Backup error: \(error)") + print("Backup error: \(error)") } } // Backup is in local documents directory, so no need to restore it from the shared URL - func restoreBackup() { + // Pass the pluginManager reference since it's not used throughout the class like toastModel + func restoreBackup(pluginManager: PluginManager, doOverwrite: Bool) async { guard let backupUrl = selectedBackupUrl else { - Task { - await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.") - } + await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.") + print("Backup restore error: Could not find backup in app directory.") return } @@ -113,64 +128,72 @@ public class BackupManager: ObservableObject { let backgroundContext = PersistenceController.shared.backgroundContext do { + // Delete all relevant entities to prevent issues with restoration if overwrite is selected + if doOverwrite { + try PersistenceController.shared.batchDelete("Bookmark") + try PersistenceController.shared.batchDelete("History") + try PersistenceController.shared.batchDelete("HistoryEntry") + try PersistenceController.shared.batchDelete("PluginList") + try PersistenceController.shared.batchDelete("Source") + try PersistenceController.shared.batchDelete("Action") + } + let file = try Data(contentsOf: backupUrl) let backup = try JSONDecoder().decode(Backup.self, from: file) if let bookmarks = backup.bookmarks { for bookmark in bookmarks { - PersistenceController.shared.createBookmark(bookmark) + PersistenceController.shared.createBookmark(bookmark, performSave: false) } } if let storedHistories = backup.history { for storedHistory in storedHistories { for storedEntry in storedHistory.entries { - PersistenceController.shared.createHistory(storedEntry, date: storedHistory.date) + PersistenceController.shared.createHistory( + storedEntry, + performSave: false, + isBackup: true, + date: storedHistory.date + ) } } } - if let storedLists = backup.sourceLists { + if let storedLists = backup.sourceLists, (backup.version == 1) { + // Only present in v1 backups for list in storedLists { - let sourceListRequest = SourceList.fetchRequest() - let urlPredicate = NSPredicate(format: "urlString == %@", list.urlString) - let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", list.author, list.name) - sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) - sourceListRequest.fetchLimit = 1 - - if (try? backgroundContext.fetch(sourceListRequest).first) != nil { - continue - } - - let newSourceList = SourceList(context: backgroundContext) - newSourceList.name = list.name - newSourceList.urlString = list.urlString - newSourceList.id = UUID(uuidString: list.id) ?? UUID() - newSourceList.author = list.author + try await pluginManager.addPluginList(list.urlString, existingPluginList: nil) + } + } else if let pluginListUrls = backup.pluginListUrls { + // v2 and up + for listUrl in pluginListUrls { + try await pluginManager.addPluginList(listUrl, existingPluginList: nil) } } - backupSourceNames = backup.sourceNames ?? [] + if let sourceNames = backup.sourceNames { + await updateRestoreCompletedMessage(newString: sourceNames.isEmpty ? "No sources need to be reinstalled" : "Reinstall sources: \(sourceNames.joined(separator: ", "))") + } + + if let actionNames = backup.actionNames { + await updateRestoreCompletedMessage(newString: actionNames.isEmpty ? "No actions need to be reinstalled" : "Reinstall actions: \(actionNames.joined(separator: ", "))") + } PersistenceController.shared.save(backgroundContext) // if iOS 14 is available, sleep to prevent any issues with alerts if #available(iOS 15, *) { - showRestoreCompletedAlert.toggle() + await toggleRestoreCompletedAlert() } else { - Task { - try? await Task.sleep(seconds: 0.1) + try? await Task.sleep(seconds: 0.1) - Task { @MainActor in - showRestoreCompletedAlert.toggle() - } - } + await toggleRestoreCompletedAlert() } } catch { - Task { - await toastModel?.updateToastDescription("Backup restore: \(error)") - } + await toastModel?.updateToastDescription("Backup restore error: \(error)") + print("Backup restore error: \(error)") } } @@ -187,7 +210,8 @@ public class BackupManager: ObservableObject { } } catch { Task { - await toastModel?.updateToastDescription("Backup removal: \(error)") + await toastModel?.updateToastDescription("Backup removal error: \(error)") + print("Backup removal error: \(error)") } } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 66aae92..41e6e4f 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -9,7 +9,7 @@ import SwiftUI enum ViewTab { case search - case sources + case plugins case settings case library } @@ -56,7 +56,6 @@ class NavigationViewModel: ObservableObject { var selectedSource: Source? @Published var showSourceListEditor: Bool = false - var selectedSourceList: SourceList? @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/PluginManager.swift similarity index 55% rename from Ferrite/ViewModels/SourceManager.swift rename to Ferrite/ViewModels/PluginManager.swift index de8989f..4399850 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -5,27 +5,28 @@ // Created by Brian Dashore on 7/25/22. // -import CoreData import Foundation import SwiftUI -public class SourceManager: ObservableObject { +public class PluginManager: ObservableObject { var toastModel: ToastViewModel? @Published var availableSources: [SourceJson] = [] - - var urlErrorAlertText = "" - @Published var showUrlErrorAlert = false + @Published var availableActions: [ActionJson] = [] @MainActor - public func fetchSourcesFromUrl() async { - let sourceListRequest = SourceList.fetchRequest() + public func fetchPluginsFromUrl() async { + let pluginListRequest = PluginList.fetchRequest() do { - let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest) - var tempAvailableSources: [SourceJson] = [] + let pluginLists = try PersistenceController.shared.backgroundContext.fetch(pluginListRequest) - for sourceList in sourceLists { - guard let url = URL(string: sourceList.urlString) else { + if pluginLists.isEmpty { + availableSources = [] + availableActions = [] + } + + for pluginList in pluginLists { + guard let url = URL(string: pluginList.urlString) else { return } @@ -33,39 +34,107 @@ public class SourceManager: ObservableObject { let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) let (data, _) = try await URLSession.shared.data(for: request) - let sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data) + let pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data) - for var source in sourceResponse.sources { - // If there is a minVersion, check and see if the source is valid - if checkAppVersion(minVersion: source.minVersion) { - source.author = sourceList.author - source.listId = sourceList.id + if let sources = pluginResponse.sources { + // Faster and more performant to map instead of a for loop + availableSources = sources.compactMap { inputJson in + if checkAppVersion(minVersion: inputJson.minVersion) { + return SourceJson( + name: inputJson.name, + version: inputJson.version, + minVersion: inputJson.minVersion, + baseUrl: inputJson.baseUrl, + fallbackUrls: inputJson.fallbackUrls, + trackers: inputJson.trackers, + api: inputJson.api, + jsonParser: inputJson.jsonParser, + rssParser: inputJson.rssParser, + htmlParser: inputJson.htmlParser, + author: pluginList.author, + listId: pluginList.id, + tags: inputJson.tags + ) + } else { + return nil + } + } + } - tempAvailableSources.append(source) + if let actions = pluginResponse.actions { + availableActions = actions.compactMap { inputJson in + if checkAppVersion(minVersion: inputJson.minVersion) { + return ActionJson( + name: inputJson.name, + version: inputJson.version, + minVersion: inputJson.minVersion, + requires: inputJson.requires, + deeplink: inputJson.deeplink, + author: pluginList.author, + listId: pluginList.id, + tags: inputJson.tags + ) + } else { + return nil + } } } } - - availableSources = tempAvailableSources } catch { - print(error) + toastModel?.updateToastDescription("Plugin fetch error: \(error)") + print("Plugin fetch error: \(error)") } } - func fetchUpdatedSources(installedSources: FetchedResults) -> [SourceJson] { - var updatedSources: [SourceJson] = [] + // Check if underlying type is Source or Action + func fetchFilteredPlugins(installedPlugins: FetchedResults

, searchText: String) -> [PJ] { + let availablePlugins: [PJ] = fetchCastedPlugins(PJ.self) - for source in installedSources { - if let availableSource = availableSources.first(where: { - source.listId == $0.listId && source.name == $0.name && source.author == $0.author + return availablePlugins + .filter { availablePlugin in + let pluginExists = installedPlugins.contains(where: { + availablePlugin.name == $0.name && + availablePlugin.listId == $0.listId && + availablePlugin.author == $0.author + }) + + if searchText.isEmpty { + return !pluginExists + } else { + return !pluginExists && availablePlugin.name.lowercased().contains(searchText.lowercased()) + } + } + } + + func fetchUpdatedPlugins(installedPlugins: FetchedResults

, searchText: String) -> [PJ] { + var updatedPlugins: [PJ] = [] + let availablePlugins: [PJ] = fetchCastedPlugins(PJ.self) + + for plugin in installedPlugins { + if let availablePlugin = availablePlugins.first(where: { + plugin.listId == $0.listId && plugin.name == $0.name && plugin.author == $0.author }), - availableSource.version > source.version + availablePlugin.version > plugin.version { - updatedSources.append(availableSource) + updatedPlugins.append(availablePlugin) } } - return updatedSources + return updatedPlugins + .filter { + searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) + } + } + + func fetchCastedPlugins(_ forType: PJ.Type) -> [PJ] { + switch String(describing: PJ.self) { + case "SourceJson": + return availableSources as? [PJ] ?? [] + case "ActionJson": + return availableActions as? [PJ] ?? [] + default: + return [] + } } // Checks if the current app version is supported by the source @@ -89,7 +158,98 @@ public class SourceManager: ObservableObject { } } - public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) async { + // The iOS version of Ferrite only runs deeplink actions + @MainActor + public func runDeeplinkAction(_ action: Action, urlString: String?) { + guard let deeplink = action.deeplink, let urlString else { + toastModel?.updateToastDescription("Could not run action: \(action.name) since there is no deeplink to execute. Contact the action dev!") + print("Could not run action: \(action.name) since there is no deeplink to execute.") + + return + } + + let playbackUrl = URL(string: deeplink.replacingOccurrences(of: "{link}", with: urlString)) + + if let playbackUrl { + UIApplication.shared.open(playbackUrl) + } else { + toastModel?.updateToastDescription("Could not run action: \(action.name) because the created deeplink was invalid. Contact the action dev!") + print("Could not run action: \(action.name) because the created deeplink (\(String(describing: playbackUrl))) was invalid") + } + } + + public func installAction(actionJson: ActionJson?, doUpsert: Bool = false) async { + guard let actionJson else { + await toastModel?.updateToastDescription("Action addition error: No action present. Contact the app dev!") + return + } + + let backgroundContext = PersistenceController.shared.backgroundContext + + if actionJson.requires.count < 1 { + await toastModel?.updateToastDescription("Action addition error: actions must require an input. Please contact the action dev!") + print("Action name \(actionJson.name) does not have a requires parameter") + + return + } + + guard let deeplink = actionJson.deeplink else { + await toastModel?.updateToastDescription("Action addition error: only deeplink actions can be added to Ferrite iOS. Please contact the action dev!") + print("Action name \(actionJson.name) did not have a deeplink") + + return + } + + let existingActionRequest = Action.fetchRequest() + existingActionRequest.predicate = NSPredicate(format: "name == %@", actionJson.name) + existingActionRequest.fetchLimit = 1 + + if let existingAction = try? backgroundContext.fetch(existingActionRequest).first { + if doUpsert { + PersistenceController.shared.delete(existingAction, context: backgroundContext) + } else { + await toastModel?.updateToastDescription("Could not install action with name \(actionJson.name) because it is already installed") + print("Action name \(actionJson.name) already exists in user's DB") + + return + } + } + + let newAction = Action(context: backgroundContext) + newAction.id = UUID() + newAction.name = actionJson.name + newAction.version = actionJson.version + newAction.author = actionJson.author ?? "Unknown" + newAction.listId = actionJson.listId + newAction.requires = actionJson.requires.map { $0.rawValue } + newAction.enabled = true + + if let jsonTags = actionJson.tags { + for tag in jsonTags { + let newTag = PluginTag(context: backgroundContext) + newTag.name = tag.name + newTag.colorHex = tag.colorHex + + newTag.parentAction = newAction + } + } + + newAction.deeplink = deeplink + + do { + try backgroundContext.save() + } catch { + await toastModel?.updateToastDescription("Action addition error: \(error)") + print("Action addition error: \(error)") + } + } + + public func installSource(sourceJson: SourceJson?, doUpsert: Bool = false) async { + guard let sourceJson else { + await toastModel?.updateToastDescription("Source addition error: No source present. Contact the app dev!") + return + } + let backgroundContext = PersistenceController.shared.backgroundContext // If there's no base URL and it isn't dynamic, return before any transactions occur @@ -97,8 +257,8 @@ public class SourceManager: ObservableObject { if !dynamicBaseUrl, sourceJson.baseUrl == nil { await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.") - - print("Not adding this source because base URL parameters are malformed") + print("Not adding source \(sourceJson.name) because base URL parameters are malformed") + return } @@ -112,6 +272,8 @@ public class SourceManager: ObservableObject { PersistenceController.shared.delete(existingSource, context: backgroundContext) } else { await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.") + print("Source name \(sourceJson.name) already exists") + return } } @@ -127,6 +289,16 @@ public class SourceManager: ObservableObject { newSource.listId = sourceJson.listId newSource.trackers = sourceJson.trackers + if let jsonTags = sourceJson.tags { + for tag in jsonTags { + let newTag = PluginTag(context: backgroundContext) + newTag.name = tag.name + newTag.colorHex = tag.colorHex + + newTag.parentSource = newSource + } + } + if let sourceApiJson = sourceJson.api { addSourceApi(newSource: newSource, apiJson: sourceApiJson) } @@ -159,7 +331,8 @@ public class SourceManager: ObservableObject { do { try backgroundContext.save() } catch { - await toastModel?.updateToastDescription(error.localizedDescription) + await toastModel?.updateToastDescription("Source addition error: \(error)") + print("Source addition error: \(error)") } } @@ -370,57 +543,44 @@ public class SourceManager: ObservableObject { newSource.htmlParser = newSourceHtmlParser } - @MainActor - public func addSourceList(sourceUrl: String, existingSourceList: SourceList?) async -> Bool { + // Adds a plugin list + // Can move this to PersistenceController if needed + public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws { let backgroundContext = PersistenceController.shared.backgroundContext - if sourceUrl.isEmpty || URL(string: sourceUrl) == nil { - urlErrorAlertText = "The provided source list is invalid. Please check if the URL is formatted properly." - showUrlErrorAlert.toggle() - - return false + if url.isEmpty || URL(string: url) == nil { + throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.") } - do { - let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!)) - let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data) + let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!)) + let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data) - if let existingSourceList { - existingSourceList.urlString = sourceUrl - existingSourceList.name = rawResponse.name - existingSourceList.author = rawResponse.author + if let existingPluginList { + existingPluginList.urlString = url + existingPluginList.name = rawResponse.name + existingPluginList.author = rawResponse.author - try PersistenceController.shared.container.viewContext.save() - } else { - let sourceListRequest = SourceList.fetchRequest() - let urlPredicate = NSPredicate(format: "urlString == %@", sourceUrl) - let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name) - sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) - sourceListRequest.fetchLimit = 1 + try PersistenceController.shared.container.viewContext.save() + } else { + let pluginListRequest = PluginList.fetchRequest() + let urlPredicate = NSPredicate(format: "urlString == %@", url) + let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name) + pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) + pluginListRequest.fetchLimit = 1 - if (try? backgroundContext.fetch(sourceListRequest).first) != nil { - urlErrorAlertText = "An existing source with this information was found. Please try editing the source list instead." - showUrlErrorAlert.toggle() - - return false - } - - let newSourceUrl = SourceList(context: backgroundContext) - newSourceUrl.id = UUID() - newSourceUrl.urlString = sourceUrl - newSourceUrl.name = rawResponse.name - newSourceUrl.author = rawResponse.author - - try backgroundContext.save() + if let existingPluginList = try? backgroundContext.fetch(pluginListRequest).first, !isSheet { + PersistenceController.shared.delete(existingPluginList, context: backgroundContext) + } else if isSheet { + throw PluginManagerError.ListAddition(description: "An existing plugin list with this information was found. Please try editing an existing plugin list instead.") } - return true - } catch { - print(error) - urlErrorAlertText = error.localizedDescription - showUrlErrorAlert.toggle() + let newPluginList = PluginList(context: backgroundContext) + newPluginList.id = UUID() + newPluginList.urlString = url + newPluginList.name = rawResponse.name + newPluginList.author = rawResponse.author - return false + try backgroundContext.save() } } } diff --git a/Ferrite/Views/CommonViews/DynamicFetchRequest.swift b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift index 2672bed..dafbbfc 100644 --- a/Ferrite/Views/CommonViews/DynamicFetchRequest.swift +++ b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift @@ -24,7 +24,7 @@ struct DynamicFetchRequest: View { sortDescriptors: [NSSortDescriptor] = [], @ViewBuilder content: @escaping (FetchedResults) -> Content) { - _fetchRequest = FetchRequest(sortDescriptors: sortDescriptors, predicate: predicate) + _fetchRequest = FetchRequest(entity: T.entity(), sortDescriptors: sortDescriptors, predicate: predicate) self.content = content } } diff --git a/Ferrite/Views/CommonViews/NavView.swift b/Ferrite/Views/CommonViews/NavView.swift index b89b95f..32077f7 100644 --- a/Ferrite/Views/CommonViews/NavView.swift +++ b/Ferrite/Views/CommonViews/NavView.swift @@ -11,19 +11,16 @@ import SwiftUI struct NavView: View { - let content: () -> Content - init(@ViewBuilder _ content: @escaping () -> Content) { - self.content = content - } + @ViewBuilder var content: Content var body: some View { if #available(iOS 16, *) { NavigationStack { - content() + content } } else { NavigationView { - content() + content } .navigationViewStyle(.stack) } diff --git a/Ferrite/Views/CommonViews/Tag.swift b/Ferrite/Views/CommonViews/Tag.swift new file mode 100644 index 0000000..7d3b218 --- /dev/null +++ b/Ferrite/Views/CommonViews/Tag.swift @@ -0,0 +1,28 @@ +// +// Tag.swift +// Ferrite +// +// Created by Brian Dashore on 2/7/23. +// + +import SwiftUI + +struct Tag: View { + let name: String + let color: Color? + var horizontalPadding: CGFloat = 7 + var verticalPadding: CGFloat = 4 + + var body: some View { + Text(name.capitalizingFirstLetter()) + .font(.caption) + .opacity(0.8) + .padding(.horizontal, horizontalPadding) + .padding(.vertical, verticalPadding) + .background( + RoundedRectangle(cornerRadius: 5) + .foregroundColor(color.map { $0 } ?? .tertiaryLabel) + .opacity(0.3) + ) + } +} diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift index e6134d7..e3fd2b0 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -15,31 +15,31 @@ struct DebridLabelView: View { var body: some View { if let selectedDebridType = debridManager.selectedDebridType { - Text(selectedDebridType.toString(abbreviated: true)) - .fontWeight(.bold) - .padding(2) - .background { - Group { - if let magnet, cloudLinks.isEmpty { - switch debridManager.matchMagnetHash(magnet) { - case .full: - Color.green - case .partial: - Color.orange - case .none: - Color.red - } - } else if cloudLinks.count == 1 { - Color.green - } else if cloudLinks.count > 1 { - Color.orange - } else { - Color.red - } - } - .cornerRadius(4) - .opacity(0.5) - } + Tag( + name: selectedDebridType.toString(abbreviated: true), + color: getTagColor(), + horizontalPadding: 5, + verticalPadding: 3 + ) + } + } + + func getTagColor() -> Color { + if let magnet, cloudLinks.isEmpty { + switch debridManager.matchMagnetHash(magnet) { + case .full: + return Color.green + case .partial: + return Color.orange + case .none: + return Color.red + } + } else if cloudLinks.count == 1 { + return Color.green + } else if cloudLinks.count > 1 { + return Color.orange + } else { + return Color.red } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index e76418b..12ac5fe 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -37,7 +37,7 @@ struct AllDebridCloudView: View { if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl - PersistenceController.shared.createHistory(historyInfo) + PersistenceController.shared.createHistory(historyInfo, performSave: true) navModel.runDebridAction(urlString: debridManager.downloadUrl) } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index 96e902c..432d988 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -34,7 +34,8 @@ struct PremiumizeCloudView: View { name: item.name, url: debridManager.downloadUrl, source: DebridType.premiumize.toString() - ) + ), + performSave: true ) navModel.runDebridAction(urlString: debridManager.downloadUrl) diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 9e07bc6..06cc77d 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -31,7 +31,8 @@ struct RealDebridCloudView: View { name: downloadResponse.filename, url: downloadResponse.download, source: DebridType.realDebrid.toString() - ) + ), + performSave: true ) navModel.runDebridAction(urlString: debridManager.downloadUrl) @@ -69,7 +70,7 @@ struct RealDebridCloudView: View { await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl - PersistenceController.shared.createHistory(historyInfo) + PersistenceController.shared.createHistory(historyInfo, performSave: true) navModel.runDebridAction(urlString: debridManager.downloadUrl) } diff --git a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift index 8e85859..799cebe 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift @@ -77,4 +77,12 @@ struct HistoryButtonView: View { .backport.tint(.primary) .disableInteraction(navModel.currentChoiceSheet != nil) } + + func getTagColor() -> Color { + if let url = entry.url, url.starts(with: "https://") { + return Color.green + } else { + return Color.red + } + } } diff --git a/Ferrite/Views/ComponentViews/Source/Buttons/InstalledSourceButtonView.swift b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift similarity index 50% rename from Ferrite/Views/ComponentViews/Source/Buttons/InstalledSourceButtonView.swift rename to Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift index 3564629..b99061d 100644 --- a/Ferrite/Views/ComponentViews/Source/Buttons/InstalledSourceButtonView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift @@ -7,52 +7,60 @@ import SwiftUI -struct InstalledSourceButtonView: View { +struct InstalledPluginButtonView: View { let backgroundContext = PersistenceController.shared.backgroundContext @EnvironmentObject var navModel: NavigationViewModel - @ObservedObject var installedSource: Source + @ObservedObject var installedPlugin: P var body: some View { Toggle(isOn: Binding( - get: { installedSource.enabled }, + get: { installedPlugin.enabled }, set: { - installedSource.enabled = $0 + installedPlugin.enabled = $0 PersistenceController.shared.save() } )) { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(installedSource.name) - Text("v\(installedSource.version)") + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(installedPlugin.name) + Text("v\(installedPlugin.version)") + .foregroundColor(.secondary) + } + + Text("by \(installedPlugin.author)") .foregroundColor(.secondary) } - Text("by \(installedSource.author)") - .foregroundColor(.secondary) + if let tags = installedPlugin.getTags(), !tags.isEmpty { + PluginTagsView(tags: tags) + } } .padding(.vertical, 2) } .contextMenu { - Button { - navModel.selectedSource = installedSource - navModel.showSourceSettings.toggle() - } label: { - Text("Settings") - Image(systemName: "gear") + if let installedSource = installedPlugin as? Source { + Button { + navModel.selectedSource = installedSource + navModel.showSourceSettings.toggle() + } label: { + Text("Settings") + Image(systemName: "gear") + } } if #available(iOS 15.0, *) { Button(role: .destructive) { - PersistenceController.shared.delete(installedSource, context: backgroundContext) + PersistenceController.shared.delete(installedPlugin, context: backgroundContext) } label: { Text("Remove") Image(systemName: "trash") } } else { Button { - PersistenceController.shared.delete(installedSource, context: backgroundContext) + PersistenceController.shared.delete(installedPlugin, context: backgroundContext) } label: { Text("Remove") Image(systemName: "trash") diff --git a/Ferrite/Views/ComponentViews/Plugin/Buttons/PluginCatalogButtonView.swift b/Ferrite/Views/ComponentViews/Plugin/Buttons/PluginCatalogButtonView.swift new file mode 100644 index 0000000..a2c525b --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/Buttons/PluginCatalogButtonView.swift @@ -0,0 +1,51 @@ +// +// SourceCatalogButtonView.swift +// Ferrite +// +// Created by Brian Dashore on 8/5/22. +// + +import SwiftUI + +struct PluginCatalogButtonView: View { + @EnvironmentObject var pluginManager: PluginManager + + let availablePlugin: PJ + let doUpsert: Bool + + var body: some View { + HStack { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(availablePlugin.name) + Text("v\(availablePlugin.version)") + .foregroundColor(.secondary) + } + + Text("by \(availablePlugin.author ?? "No author")") + .foregroundColor(.secondary) + } + + if let tags = availablePlugin.getTags(), !tags.isEmpty { + PluginTagsView(tags: tags) + } + } + + Spacer() + + Button("Install") { + Task { + if let availableSource = availablePlugin as? SourceJson { + await pluginManager.installSource(sourceJson: availableSource, doUpsert: doUpsert) + } else if let availableAction = availablePlugin as? ActionJson { + await pluginManager.installAction(actionJson: availableAction, doUpsert: doUpsert) + } else { + return + } + } + } + } + .padding(.vertical, 2) + } +} diff --git a/Ferrite/Views/ComponentViews/Source/Buttons/SourceCatalogButtonView.swift b/Ferrite/Views/ComponentViews/Plugin/Buttons/SourceCatalogButtonView.swift similarity index 86% rename from Ferrite/Views/ComponentViews/Source/Buttons/SourceCatalogButtonView.swift rename to Ferrite/Views/ComponentViews/Plugin/Buttons/SourceCatalogButtonView.swift index cdfa129..c8a7479 100644 --- a/Ferrite/Views/ComponentViews/Source/Buttons/SourceCatalogButtonView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Buttons/SourceCatalogButtonView.swift @@ -8,7 +8,7 @@ import SwiftUI struct SourceCatalogButtonView: View { - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager let availableSource: SourceJson @@ -29,7 +29,7 @@ struct SourceCatalogButtonView: View { Button("Install") { Task { - await sourceManager.installSource(sourceJson: availableSource) + await pluginManager.installSource(sourceJson: availableSource) } } } diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift new file mode 100644 index 0000000..bd55e0e --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift @@ -0,0 +1,80 @@ +// +// SourceListView.swift +// Ferrite +// +// Created by Brian Dashore on 7/24/22. +// + +import SwiftUI + +struct PluginListView: View { + @EnvironmentObject var pluginManager: PluginManager + @EnvironmentObject var navModel: NavigationViewModel + + let backgroundContext = PersistenceController.shared.backgroundContext + + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true + + @Binding var searchText: String + + @State private var isEditingSearch = false + @State private var isSearching = false + + @State private var filteredUpdatedPlugins: [PJ] = [] + @State private var filteredAvailablePlugins: [PJ] = [] + @State private var sourcePredicate: NSPredicate? + + var body: some View { + DynamicFetchRequest(predicate: sourcePredicate) { (installedPlugins: FetchedResults

) in + List { + if !filteredUpdatedPlugins.isEmpty { + Section(header: InlineHeader("Updates")) { + ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in + PluginCatalogButtonView(availablePlugin: updatedPlugin, doUpsert: true) + } + } + } + + if !installedPlugins.isEmpty { + Section(header: InlineHeader("Installed")) { + ForEach(installedPlugins, id: \.self) { source in + InstalledPluginButtonView(installedPlugin: source) + } + } + } + + if !filteredAvailablePlugins.isEmpty { + Section(header: InlineHeader("Catalog")) { + ForEach(filteredAvailablePlugins, id: \.self) { availablePlugin in + if !installedPlugins.contains(where: { + availablePlugin.name == $0.name && + availablePlugin.listId == $0.listId && + availablePlugin.author == $0.author + }) { + PluginCatalogButtonView(availablePlugin: availablePlugin, doUpsert: false) + } + } + } + } + } + .listStyle(.insetGrouped) + .sheet(isPresented: $navModel.showSourceSettings) { + if String(describing: P.self) == "Source" { + SourceSettingsView() + .environmentObject(navModel) + } + } + .onAppear { + filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) + filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) + } + .onChange(of: searchText) { _ in + sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + .onReceive(installedPlugins.publisher.count()) { _ in + filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) + filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift new file mode 100644 index 0000000..568d873 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift @@ -0,0 +1,22 @@ +// +// PluginTagView.swift +// Ferrite +// +// Created by Brian Dashore on 2/7/23. +// + +import SwiftUI + +struct PluginTagsView: View { + let tags: [PluginTagJson] + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(tags, id: \.self) { tag in + Tag(name: tag.name, color: tag.colorHex.map { Color(hexadecimal: $0) }) + } + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift similarity index 84% rename from Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift rename to Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift index 2d70f74..7087a49 100644 --- a/Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift @@ -17,28 +17,34 @@ struct SourceSettingsView: View { List { if let selectedSource = navModel.selectedSource { Section(header: InlineHeader("Info")) { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(selectedSource.name) + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(selectedSource.name) - Text("v\(selectedSource.version)") - .foregroundColor(.secondary) - } - - Text("by \(selectedSource.author)") - .foregroundColor(.secondary) - - Group { - Text("ID: \(selectedSource.id)") - - if let listId = selectedSource.listId { - Text("List ID: \(listId)") - } else { - Text("No list ID found. This source should be removed.") + Text("v\(selectedSource.version)") + .foregroundColor(.secondary) } + + Text("by \(selectedSource.author)") + .foregroundColor(.secondary) + + Group { + Text("ID: \(selectedSource.id)") + + if let listId = selectedSource.listId { + Text("List ID: \(listId)") + } else { + Text("No list ID found. This source should be removed.") + } + } + .foregroundColor(.secondary) + .font(.caption) + } + + if let tags = selectedSource.getTags(), !tags.isEmpty { + PluginTagsView(tags: tags) } - .foregroundColor(.secondary) - .font(.caption) } .padding(.vertical, 2) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index 40a1ffa..5933bfd 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -38,7 +38,8 @@ struct SearchResultButtonView: View { name: result.title, url: debridManager.downloadUrl, source: result.source - ) + ), + performSave: true ) navModel.runDebridAction(urlString: debridManager.downloadUrl) @@ -63,7 +64,8 @@ struct SearchResultButtonView: View { name: result.title, url: result.magnet.link, source: result.source - ) + ), + performSave: true ) navModel.runMagnetAction(magnet: result.magnet) diff --git a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift index 6a91153..2c5b416 100644 --- a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift +++ b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift @@ -57,7 +57,9 @@ struct BackupsView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { - backupManager.createBackup() + Task { + await backupManager.createBackup() + } } label: { Image(systemName: "plus") } diff --git a/Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift b/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift similarity index 59% rename from Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift rename to Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift index 81a4c3b..67b8988 100644 --- a/Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift +++ b/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift @@ -7,37 +7,41 @@ import SwiftUI -struct SourceListEditorView: View { +struct PluginListEditorView: View { @Environment(\.presentationMode) var presentationMode @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager let backgroundContext = PersistenceController.shared.backgroundContext - @State private var sourceUrlSet = false + @State var selectedPluginList: PluginList? - @State private var sourceUrl: String = "" + @State private var sourceUrlSet = false + @State private var showUrlErrorAlert = false + + @State private var pluginListUrl: String = "" + @State private var urlErrorAlertText: String = "" var body: some View { NavView { Form { - TextField("Enter URL", text: $sourceUrl) + TextField("Enter URL", text: $pluginListUrl) .disableAutocorrection(true) .keyboardType(.URL) .autocapitalization(.none) .conditionalId(sourceUrlSet) } .onAppear { - sourceUrl = navModel.selectedSourceList?.urlString ?? "" + pluginListUrl = selectedPluginList?.urlString ?? "" sourceUrlSet = true } .backport.alert( - isPresented: $sourceManager.showUrlErrorAlert, + isPresented: $showUrlErrorAlert, title: "Error", - message: sourceManager.urlErrorAlertText + message: urlErrorAlertText ) - .navigationTitle("Editing source list") + .navigationTitle("Editing Plugin List") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { @@ -49,25 +53,23 @@ struct SourceListEditorView: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { Task { - if await sourceManager.addSourceList( - sourceUrl: sourceUrl, - existingSourceList: navModel.selectedSourceList - ) { + do { + try await pluginManager.addPluginList(pluginListUrl, existingPluginList: selectedPluginList) presentationMode.wrappedValue.dismiss() + } catch { + urlErrorAlertText = error.localizedDescription + showUrlErrorAlert.toggle() } } } } } - .onDisappear { - navModel.selectedSourceList = nil - } } } } -struct SourceListEditorView_Previews: PreviewProvider { +struct PluginListEditorView_Previews: PreviewProvider { static var previews: some View { - SourceListEditorView() + PluginListEditorView() } } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift similarity index 75% rename from Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift rename to Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift index 7ab13d4..4abd6ed 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift @@ -7,40 +7,40 @@ import SwiftUI -struct SettingsSourceListView: View { +struct SettingsPluginListView: View { let backgroundContext = PersistenceController.shared.backgroundContext @EnvironmentObject var navModel: NavigationViewModel @FetchRequest( - entity: SourceList.entity(), + entity: PluginList.entity(), sortDescriptors: [] - ) var sourceLists: FetchedResults + ) var pluginLists: FetchedResults @State private var presentSourceSheet = false - @State private var selectedSourceList: SourceList? + @State private var selectedPluginList: PluginList? var body: some View { ZStack { - if sourceLists.isEmpty { + if pluginLists.isEmpty { EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right") } else { List { - ForEach(sourceLists, id: \.self) { sourceList in + ForEach(pluginLists, id: \.self) { pluginList in VStack(alignment: .leading, spacing: 5) { - Text(sourceList.name) + Text(pluginList.name) - Text(sourceList.author) + Text(pluginList.author) .foregroundColor(.gray) - Text("ID: \(sourceList.id)") + Text("ID: \(pluginList.id)") .font(.caption) .foregroundColor(.gray) } .padding(.vertical, 2) .contextMenu { Button { - navModel.selectedSourceList = sourceList + selectedPluginList = pluginList presentSourceSheet.toggle() } label: { Text("Edit") @@ -49,14 +49,14 @@ struct SettingsSourceListView: View { if #available(iOS 15.0, *) { Button(role: .destructive) { - PersistenceController.shared.delete(sourceList, context: backgroundContext) + PersistenceController.shared.delete(pluginList, context: backgroundContext) } label: { Text("Remove") Image(systemName: "trash") } } else { Button { - PersistenceController.shared.delete(sourceList, context: backgroundContext) + PersistenceController.shared.delete(pluginList, context: backgroundContext) } label: { Text("Remove") Image(systemName: "trash") @@ -66,7 +66,7 @@ struct SettingsSourceListView: View { } .onDelete { offsets in for index in offsets { - if let list = sourceLists[safe: index] { + if let list = pluginLists[safe: index] { PersistenceController.shared.delete(list, context: backgroundContext) } } @@ -78,13 +78,13 @@ struct SettingsSourceListView: View { } .sheet(isPresented: $presentSourceSheet) { if #available(iOS 16, *) { - SourceListEditorView() + PluginListEditorView(selectedPluginList: selectedPluginList) .presentationDetents([.medium]) } else { - SourceListEditorView() + PluginListEditorView(selectedPluginList: selectedPluginList) } } - .navigationTitle("Source Lists") + .navigationTitle("Plugin Lists") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -98,8 +98,8 @@ struct SettingsSourceListView: View { } } -struct SettingsSourceListView_Previews: PreviewProvider { +struct SettingsPluginListView_Previews: PreviewProvider { static var previews: some View { - SettingsSourceListView() + SettingsPluginListView() } } diff --git a/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift b/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift deleted file mode 100644 index 2b0f1f2..0000000 --- a/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// SourceUpdateButtonView.swift -// Ferrite -// -// Created by Brian Dashore on 8/5/22. -// - -import SwiftUI - -struct SourceUpdateButtonView: View { - @EnvironmentObject var sourceManager: SourceManager - - let updatedSource: SourceJson - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(updatedSource.name) - Text("v\(updatedSource.version)") - .foregroundColor(.secondary) - } - - Text("by \(updatedSource.author ?? "Unknown")") - .foregroundColor(.secondary) - } - .padding(.vertical, 2) - - Spacer() - - Button("Update") { - Task { - await sourceManager.installSource(sourceJson: updatedSource, doUpsert: true) - } - } - } - } -} diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 28471f6..88e1ba8 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -12,7 +12,7 @@ struct ContentView: View { @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager @FetchRequest( entity: Source.entity(), @@ -87,7 +87,7 @@ struct ContentView: View { navModel.isSearching = true navModel.showSearchProgress = true - let sources = sourceManager.fetchInstalledSources() + let sources = pluginManager.fetchInstalledSources() await scrapingModel.scanSources(sources: sources) if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index d36ff6b..a3068c9 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -14,6 +14,7 @@ struct MainView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var backupManager: BackupManager + @EnvironmentObject var pluginManager: PluginManager @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true @@ -36,11 +37,11 @@ struct MainView: View { } .tag(ViewTab.library) - SourcesView() + PluginsView() .tabItem { - Label("Sources", systemImage: "doc.text") + Label("Plugins", systemImage: "doc.text") } - .tag(ViewTab.sources) + .tag(ViewTab.plugins) SettingsView() .tabItem { @@ -51,10 +52,12 @@ struct MainView: View { .sheet(item: $navModel.currentChoiceSheet) { item in switch item { case .magnet: - MagnetChoiceView() + ActionChoiceView() .environmentObject(debridManager) .environmentObject(scrapingModel) .environmentObject(navModel) + .environmentObject(pluginManager) + .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext) case .batch: BatchChoiceView() .environmentObject(debridManager) @@ -101,30 +104,42 @@ struct MainView: View { backupManager.showRestoreAlert.toggle() } } - // Global alerts for backups - .backport.alert( + // Global alerts and dialogs for backups + .backport.confirmationDialog( isPresented: $backupManager.showRestoreAlert, title: "Restore backup?", - message: "Restoring this backup will merge all your data!", + message: + "Merge (preferred): Will merge your current data with the backup \n\n" + + "Overwrite: Will delete and replace all your data \n\n" + + "If Merge causes app instability, uninstall Ferrite and use the Overwrite option.", buttons: [ - .init("Restore", role: .destructive) { - backupManager.restoreBackup() + .init("Merge", role: .destructive) { + Task { + await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false) + } }, - .init(role: .cancel) + .init("Overwrite", role: .destructive) { + Task { + await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true) + } + } ] ) .backport.alert( isPresented: $backupManager.showRestoreCompletedAlert, title: "Backup restored", - message: backupManager.backupSourceNames.isEmpty ? - "No sources need to be reinstalled" : - "Reinstall sources: \(backupManager.backupSourceNames.joined(separator: ", "))" + message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"), + buttons: [ + .init("OK") { + backupManager.restoreCompletedMessage = [] + } + ] ) // Updater alert .backport.alert( isPresented: $showUpdateAlert, title: "Update available", - message: "Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings.", + message: "Ferrite \(releaseVersionString) can be downloaded. \n\nThis alert can be disabled in Settings.", buttons: [ .init("Download") { guard let releaseUrl = URL(string: releaseUrlString) else { diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift new file mode 100644 index 0000000..3b7bbba --- /dev/null +++ b/Ferrite/Views/PluginsView.swift @@ -0,0 +1,111 @@ +// +// PluginsView.swift +// Ferrite +// +// Created by Brian Dashore on 1/11/23. +// + +import SwiftUI +import SwiftUIX + +struct PluginsView: View { + enum PluginPickerSegment { + case sources + case actions + } + + @EnvironmentObject var pluginManager: PluginManager + + @FetchRequest( + entity: Source.entity(), + sortDescriptors: [] + ) var sources: FetchedResults + + @FetchRequest( + entity: Action.entity(), + sortDescriptors: [] + ) var actions: FetchedResults + + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true + + @State private var selectedSegment: PluginPickerSegment = .sources + @State private var checkedForPlugins = false + + @State private var isEditingSearch = false + @State private var isSearching = false + @State private var searchText: String = "" + + @State private var viewTask: Task? + + var body: some View { + NavView { + VStack { + Picker("Segments", selection: $selectedSegment) { + Text("Sources").tag(PluginPickerSegment.sources) + Text("Actions").tag(PluginPickerSegment.actions) + } + .pickerStyle(.segmented) + .padding(.horizontal) + .padding(.vertical, 5) + + if checkedForPlugins { + switch selectedSegment { + case .sources: + PluginListView(searchText: $searchText) + case .actions: + PluginListView(searchText: $searchText) + } + } + + Spacer() + } + .overlay { + if checkedForPlugins { + switch selectedSegment { + case .sources: + if sources.isEmpty && pluginManager.availableSources.isEmpty { + EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings") + } + case .actions: + if actions.isEmpty && pluginManager.availableActions.isEmpty { + EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings") + } + } + } else { + ProgressView() + } + } + .onAppear { + viewTask = Task { + await pluginManager.fetchPluginsFromUrl() + checkedForPlugins = true + } + } + .onDisappear { + viewTask?.cancel() + checkedForPlugins = false + } + .navigationTitle("Plugins") + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { + isSearching = true + }) + .showsCancelButton(isEditingSearch || isSearching) + .onCancel { + searchText = "" + isSearching = false + } + } + .introspectSearchController { searchController in + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + } + } + } +} + +struct PluginsView_Previews: PreviewProvider { + static var previews: some View { + PluginsView() + } +} diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index f4656c0..ee4d521 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -11,7 +11,7 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var debridManager: DebridManager - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager let backgroundContext = PersistenceController.shared.backgroundContext @@ -84,8 +84,8 @@ struct SettingsView: View { } } - Section(header: Text("Source management")) { - NavigationLink("Source lists", destination: SettingsSourceListView()) + Section(header: Text("Plugin management")) { + NavigationLink("Plugin lists", destination: SettingsPluginListView()) } Section(header: Text("Default actions")) { diff --git a/Ferrite/Views/SheetViews/MagnetChoiceView.swift b/Ferrite/Views/SheetViews/ActionChoiceView.swift similarity index 80% rename from Ferrite/Views/SheetViews/MagnetChoiceView.swift rename to Ferrite/Views/SheetViews/ActionChoiceView.swift index 1690ddb..e24a3e3 100644 --- a/Ferrite/Views/SheetViews/MagnetChoiceView.swift +++ b/Ferrite/Views/SheetViews/ActionChoiceView.swift @@ -8,14 +8,18 @@ import SwiftUI import SwiftUIX -struct MagnetChoiceView: View { +struct ActionChoiceView: View { @Environment(\.presentationMode) var presentationMode @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var pluginManager: PluginManager - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false + @FetchRequest( + entity: Action.entity(), + sortDescriptors: [] + ) var actions: FetchedResults @State private var showLinkCopyAlert = false @State private var showMagnetCopyAlert = false @@ -39,16 +43,12 @@ struct MagnetChoiceView: View { if !debridManager.downloadUrl.isEmpty { Section(header: "Debrid options") { - ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer) - } - - ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc) - } - - ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse) + ForEach(actions, id: \.id) { action in + if action.requires.contains(ActionRequirement.debrid.rawValue) { + ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") { + pluginManager.runDeeplinkAction(action, urlString: debridManager.downloadUrl) + } + } } ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { @@ -73,6 +73,14 @@ struct MagnetChoiceView: View { if !navModel.resultFromCloud { Section(header: "Magnet options") { + ForEach(actions, id: \.id) { action in + if action.requires.contains(ActionRequirement.magnet.rawValue) { + ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") { + pluginManager.runDeeplinkAction(action, urlString: navModel.selectedMagnet?.link) + } + } + } + ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") { UIPasteboard.general.string = navModel.selectedMagnet?.link showMagnetCopyAlert.toggle() @@ -92,10 +100,6 @@ struct MagnetChoiceView: View { navModel.showLocalActivitySheet.toggle() } } - - ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { - navModel.runMagnetAction(magnet: navModel.selectedMagnet, .webtor) - } } } } @@ -131,8 +135,8 @@ struct MagnetChoiceView: View { } } -struct MagnetChoiceView_Previews: PreviewProvider { +struct ActionChoiceView_Previews: PreviewProvider { static var previews: some View { - MagnetChoiceView() + ActionChoiceView() } } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 00edb19..783a35e 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -79,7 +79,7 @@ struct BatchChoiceView: View { if var selectedHistoryInfo = navModel.selectedHistoryInfo { selectedHistoryInfo.url = debridManager.downloadUrl selectedHistoryInfo.subName = fileName - PersistenceController.shared.createHistory(selectedHistoryInfo) + PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true) } navModel.runDebridAction(urlString: debridManager.downloadUrl) diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift deleted file mode 100644 index eb9c708..0000000 --- a/Ferrite/Views/SourcesView.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// SourceListView.swift -// Ferrite -// -// Created by Brian Dashore on 7/24/22. -// - -import Introspect -import SwiftUI -import SwiftUIX - -struct SourcesView: View { - @EnvironmentObject var sourceManager: SourceManager - @EnvironmentObject var navModel: NavigationViewModel - - let backgroundContext = PersistenceController.shared.backgroundContext - - @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true - - @State private var checkedForSources = false - @State private var isEditingSearch = false - @State private var isSearching = false - - @State private var viewTask: Task? = nil - @State private var searchText: String = "" - @State private var filteredUpdatedSources: [SourceJson] = [] - @State private var filteredAvailableSources: [SourceJson] = [] - @State private var sourcePredicate: NSPredicate? - - var body: some View { - NavView { - DynamicFetchRequest(predicate: sourcePredicate) { (installedSources: FetchedResults) in - ZStack { - if !checkedForSources { - ProgressView() - } else if installedSources.isEmpty, sourceManager.availableSources.isEmpty { - EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") - } else { - List { - if !filteredUpdatedSources.isEmpty { - Section(header: InlineHeader("Updates")) { - ForEach(filteredUpdatedSources, id: \.self) { source in - SourceUpdateButtonView(updatedSource: source) - } - } - } - - if !installedSources.isEmpty { - Section(header: InlineHeader("Installed")) { - ForEach(installedSources, id: \.self) { source in - InstalledSourceButtonView(installedSource: source) - } - } - } - - if !filteredAvailableSources.isEmpty { - Section(header: InlineHeader("Catalog")) { - ForEach(filteredAvailableSources, id: \.self) { availableSource in - if !installedSources.contains(where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - }) { - SourceCatalogButtonView(availableSource: availableSource) - } - } - } - } - } - .conditionalId(UUID()) - .listStyle(.insetGrouped) - } - } - .sheet(isPresented: $navModel.showSourceSettings) { - SourceSettingsView() - .environmentObject(navModel) - } - .onAppear { - viewTask = Task { - await sourceManager.fetchSourcesFromUrl() - filteredAvailableSources = sourceManager.availableSources.filter { availableSource in - !installedSources.contains(where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - }) - } - - filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources) - checkedForSources = true - } - } - .onDisappear { - viewTask?.cancel() - } - .onChange(of: searchText) { _ in - sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) - } - .onReceive(installedSources.publisher.count()) { _ in - filteredAvailableSources = sourceManager.availableSources.filter { availableSource in - let sourceExists = installedSources.contains(where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - }) - - if searchText.isEmpty { - return !sourceExists - } else { - return !sourceExists && availableSource.name.lowercased().contains(searchText.lowercased()) - } - } - - filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources).filter { - searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) - } - } - .navigationTitle("Sources") - .navigationSearchBar { - SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { - isSearching = true - }) - .showsCancelButton(isEditingSearch || isSearching) - .onCancel { - searchText = "" - isSearching = false - } - } - .introspectSearchController { searchController in - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none - } - } - } - } -} - -struct SourcesView_Previews: PreviewProvider { - static var previews: some View { - SourcesView() - } -}