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 <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-02-08 12:09:37 -05:00
parent 6b0f90178b
commit 4512318e8f
51 changed files with 1470 additions and 609 deletions

View file

@ -8,20 +8,26 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; }; 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 */; }; 0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; };
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; }; 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; };
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.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 */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.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 */; }; 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 */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.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 */; }; 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; }; 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.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 */; }; 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; }; 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 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 */; }; 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 */; }; 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; };
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; }; 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; };
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; }; 0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */; };
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */; }; 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */; };
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */; };
0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; }; 0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; };
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 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 */; }; 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */; };
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; }; 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; };
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceListView.swift */; }; 0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsPluginListView.swift */; };
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.swift */; }; 0CA05459288EE9E600850554 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* PluginManager.swift */; };
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */; }; 0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */; };
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BB288903F000DE2211 /* SettingsView.swift */; }; 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BB288903F000DE2211 /* SettingsView.swift */; };
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BC288903F000DE2211 /* LoginWebView.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 */; }; 0CA148DB288903F000DE2211 /* NavView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C1288903F000DE2211 /* NavView.swift */; };
0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; }; 0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; };
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */; }; 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 */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.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 */; }; 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; }; 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; }; 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; }; 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 */; }; 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; }; 0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginList+CoreDataClass.swift"; sourceTree = "<group>"; };
0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginList+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; }; 0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; };
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; }; 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; };
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; }; 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; }; 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListView.swift; sourceTree = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; }; 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; }; 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; }; 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; };
0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; }; 0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; };
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; }; 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; };
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; }; 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionModels.swift; sourceTree = "<group>"; };
0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginsView.swift; sourceTree = "<group>"; };
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginModels.swift; sourceTree = "<group>"; };
0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB_v2.xcdatamodel; sourceTree = "<group>"; };
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; }; 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; }; 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = "<group>"; }; 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = "<group>"; };
@ -146,8 +164,13 @@
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; }; 0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = "<group>"; };
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = "<group>"; };
0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataClass.swift"; sourceTree = "<group>"; };
0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnAppearHandler.swift; sourceTree = "<group>"; };
0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DidAppearModifier.swift; sourceTree = "<group>"; };
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; }; 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
@ -160,9 +183,8 @@
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; }; 0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = "<group>"; }; 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledPluginButtonView.swift; sourceTree = "<group>"; };
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceButtonView.swift; sourceTree = "<group>"; }; 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginCatalogButtonView.swift; sourceTree = "<group>"; };
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogButtonView.swift; sourceTree = "<group>"; };
0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; }; 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; };
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; };
@ -175,16 +197,14 @@
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = "<group>"; }; 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = "<group>"; }; 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataClass.swift"; sourceTree = "<group>"; };
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceList+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionsPickerViews.swift; sourceTree = "<group>"; }; 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionsPickerViews.swift; sourceTree = "<group>"; };
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; }; 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
0CA05456288EE58200850554 /* SettingsSourceListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSourceListView.swift; sourceTree = "<group>"; }; 0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = "<group>"; };
0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = "<group>"; }; 0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = "<group>"; }; 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListEditorView.swift; sourceTree = "<group>"; };
0CA148BB288903F000DE2211 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; }; 0CA148BB288903F000DE2211 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
0CA148BC288903F000DE2211 /* LoginWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginWebView.swift; sourceTree = "<group>"; }; 0CA148BC288903F000DE2211 /* LoginWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginWebView.swift; sourceTree = "<group>"; };
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagnetChoiceView.swift; sourceTree = "<group>"; }; 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ActionChoiceView.swift; sourceTree = "<group>"; };
0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = "<group>"; }; 0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = "<group>"; };
0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = "<group>"; }; 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = "<group>"; };
@ -216,11 +236,14 @@
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataClass.swift"; sourceTree = "<group>"; };
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Action+CoreDataProperties.swift"; sourceTree = "<group>"; };
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; }; 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; }; 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; }; 0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = "<group>"; };
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = "<group>"; };
0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; }; 0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -246,7 +269,7 @@
0C0755C22934241F00ECA142 /* SheetViews */ = { 0C0755C22934241F00ECA142 /* SheetViews */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */,
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
); );
path = SheetViews; path = SheetViews;
@ -255,11 +278,11 @@
0C0755C32934244500ECA142 /* ComponentViews */ = { 0C0755C32934244500ECA142 /* ComponentViews */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C3E00D4296F560800ECECB2 /* Plugin */,
0C0755C42934245800ECA142 /* Debrid */, 0C0755C42934245800ECA142 /* Debrid */,
0CA3B23528C265FD00616D3A /* Library */, 0CA3B23528C265FD00616D3A /* Library */,
0C44E2AB28D4E126007711AE /* SearchResult */, 0C44E2AB28D4E126007711AE /* SearchResult */,
0CA0545C288F7CB200850554 /* Settings */, 0CA0545C288F7CB200850554 /* Settings */,
0C794B65289DAC9F00DD1CC8 /* Source */,
); );
path = ComponentViews; path = ComponentViews;
sourceTree = "<group>"; sourceTree = "<group>";
@ -276,6 +299,12 @@
0C0D50DE288DF72D0035ECC8 /* Classes */ = { 0C0D50DE288DF72D0035ECC8 /* Classes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( 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 */, 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */,
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */, 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */,
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */, 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */,
@ -292,8 +321,6 @@
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */, 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */,
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */, 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */,
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */, 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */,
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */,
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */,
); );
path = Classes; path = Classes;
sourceTree = "<group>"; sourceTree = "<group>";
@ -301,6 +328,7 @@
0C0D50E3288DFE6E0035ECC8 /* Models */ = { 0C0D50E3288DFE6E0035ECC8 /* Models */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */,
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */, 0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
0C7ED14028D61BBA009E29AD /* BackupModels.swift */, 0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */, 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
@ -310,6 +338,7 @@
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
); );
path = Models; path = Models;
sourceTree = "<group>"; sourceTree = "<group>";
@ -324,6 +353,17 @@
path = Cloud; path = Cloud;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0C3E00D4296F560800ECECB2 /* Plugin */ = {
isa = PBXGroup;
children = (
0C44E2AA28D4E09B007711AE /* Buttons */,
0C794B65289DAC9F00DD1CC8 /* Source */,
0C0D50E6288DFF850035ECC8 /* PluginListView.swift */,
0C5005512992B6750064606A /* PluginTagsView.swift */,
);
path = Plugin;
sourceTree = "<group>";
};
0C44E2A628D4DDC6007711AE /* Classes */ = { 0C44E2A628D4DDC6007711AE /* Classes */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -340,6 +380,7 @@
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
0CB6516428C5A5D700DCA721 /* InlinedList.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */,
); );
path = Modifiers; path = Modifiers;
sourceTree = "<group>"; sourceTree = "<group>";
@ -347,9 +388,8 @@
0C44E2AA28D4E09B007711AE /* Buttons */ = { 0C44E2AA28D4E09B007711AE /* Buttons */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */, 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */,
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */, 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */,
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */,
); );
path = Buttons; path = Buttons;
sourceTree = "<group>"; sourceTree = "<group>";
@ -363,10 +403,17 @@
path = SearchResult; path = SearchResult;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
0C5005552992B9C20064606A /* Protocols */ = {
isa = PBXGroup;
children = (
0CE1C4172981E8D700418F20 /* Plugin.swift */,
);
path = Protocols;
sourceTree = "<group>";
};
0C794B65289DAC9F00DD1CC8 /* Source */ = { 0C794B65289DAC9F00DD1CC8 /* Source */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C44E2AA28D4E09B007711AE /* Buttons */,
0C733286289C4C820058D1FE /* SourceSettingsView.swift */, 0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
); );
path = Source; path = Source;
@ -376,8 +423,8 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C44E2AE28D52E8A007711AE /* BackupsView.swift */, 0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
0CA05456288EE58200850554 /* SettingsSourceListView.swift */, 0CA05456288EE58200850554 /* SettingsPluginListView.swift */,
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */, 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */, 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */, 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
); );
@ -394,6 +441,7 @@
0CA148EF2889061600DE2211 /* ViewModels */, 0CA148EF2889061600DE2211 /* ViewModels */,
0CA148EE2889061200DE2211 /* Views */, 0CA148EE2889061200DE2211 /* Views */,
0C44E2A628D4DDC6007711AE /* Classes */, 0C44E2A628D4DDC6007711AE /* Classes */,
0C5005552992B9C20064606A /* Protocols */,
0CA148C8288903F000DE2211 /* Extensions */, 0CA148C8288903F000DE2211 /* Extensions */,
0CA148C5288903F000DE2211 /* Preview Content */, 0CA148C5288903F000DE2211 /* Preview Content */,
0CA148C7288903F000DE2211 /* FerriteApp.swift */, 0CA148C7288903F000DE2211 /* FerriteApp.swift */,
@ -415,6 +463,7 @@
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */,
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
0C32FB562890D1F2002BD219 /* ListRowViews.swift */, 0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
0C2D9652299316CC00A504B6 /* Tag.swift */,
); );
path = CommonViews; path = CommonViews;
sourceTree = "<group>"; sourceTree = "<group>";
@ -457,7 +506,7 @@
0CA148D4288903F000DE2211 /* ContentView.swift */, 0CA148D4288903F000DE2211 /* ContentView.swift */,
0CA148D3288903F000DE2211 /* SearchResultsView.swift */, 0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
0CA3B23328C2658700616D3A /* LibraryView.swift */, 0CA3B23328C2658700616D3A /* LibraryView.swift */,
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */,
0CA148BB288903F000DE2211 /* SettingsView.swift */, 0CA148BB288903F000DE2211 /* SettingsView.swift */,
0C32FB522890D19D002BD219 /* AboutView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */,
0CA148BC288903F000DE2211 /* LoginWebView.swift */, 0CA148BC288903F000DE2211 /* LoginWebView.swift */,
@ -472,7 +521,7 @@
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */, 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */,
0CA148CF288903F000DE2211 /* ToastViewModel.swift */, 0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */, 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
0CA05458288EE9E600850554 /* SourceManager.swift */, 0CA05458288EE9E600850554 /* PluginManager.swift */,
0C44E2AC28D51C63007711AE /* BackupManager.swift */, 0C44E2AC28D51C63007711AE /* BackupManager.swift */,
); );
path = ViewModels; path = ViewModels;
@ -482,6 +531,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0CA148CE288903F000DE2211 /* WebView.swift */, 0CA148CE288903F000DE2211 /* WebView.swift */,
0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */,
); );
path = RepresentableViews; path = RepresentableViews;
sourceTree = "<group>"; sourceTree = "<group>";
@ -652,46 +702,55 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */, 0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */,
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */, 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */,
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */,
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */, 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */,
0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */, 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */,
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */, 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */,
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */, 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */,
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.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 */, 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */, 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */,
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */, 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */, 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */, 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */, 0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */,
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */, 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */, 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */,
0CA148EC288903F000DE2211 /* ContentView.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 */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.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 */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */, 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */, 0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */,
@ -699,8 +758,7 @@
0C445C62293F9A0B0060744D /* Bundle.swift in Sources */, 0C445C62293F9A0B0060744D /* Bundle.swift in Sources */,
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */, 0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */,
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */,
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */, 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */,
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
@ -712,7 +770,6 @@
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */, 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
0CA148E6288903F000DE2211 /* WebView.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */, 0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */,
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
@ -722,23 +779,27 @@
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */, 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */,
0C42B5982932F6DD008057A0 /* Set.swift in Sources */, 0C42B5982932F6DD008057A0 /* Set.swift in Sources */,
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0CA05459288EE9E600850554 /* PluginManager.swift in Sources */,
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */, 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */,
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */, 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */, 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */,
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */, 0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */, 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */, 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */, 0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */,
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */, 0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
@ -1076,9 +1137,10 @@
0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = { 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = {
isa = XCVersionGroup; isa = XCVersionGroup;
children = ( children = (
0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */,
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */, 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */,
); );
currentVersion = 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */; currentVersion = 0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */;
path = FerriteDB.xcdatamodeld; path = FerriteDB.xcdatamodeld;
sourceTree = "<group>"; sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel; versionGroupType = wrapper.xcdatamodel;

View file

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

View file

@ -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<Action> {
return NSFetchRequest<Action>(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 {
}

View file

@ -10,15 +10,4 @@ import CoreData
import Foundation import Foundation
@objc(Bookmark) @objc(Bookmark)
public class Bookmark: NSManagedObject { public class Bookmark: NSManagedObject {}
func toSearchResult() -> SearchResult {
SearchResult(
title: title,
source: source,
size: size,
magnet: Magnet(hash: magnetHash, link: magnetLink),
seeders: seeders,
leechers: leechers
)
}
}

View file

@ -22,6 +22,17 @@ public extension Bookmark {
@NSManaged var source: String @NSManaged var source: String
@NSManaged var title: String? @NSManaged var title: String?
@NSManaged var orderNum: Int16 @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 {} extension Bookmark: Identifiable {}

View file

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

View file

@ -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<PluginList> {
return NSFetchRequest<PluginList>(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 {
}

View file

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

View file

@ -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<PluginTag> {
return NSFetchRequest<PluginTag>(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 {
}

View file

@ -10,4 +10,4 @@ import CoreData
import Foundation import Foundation
@objc(Source) @objc(Source)
public class Source: NSManagedObject {} public class Source: NSManagedObject, Plugin {}

View file

@ -2,33 +2,77 @@
// Source+CoreDataProperties.swift // Source+CoreDataProperties.swift
// Ferrite // Ferrite
// //
// Created by Brian Dashore on 8/3/22. // Created by Brian Dashore on 2/6/23.
// //
// //
import CoreData
import Foundation import Foundation
import CoreData
public extension Source {
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> { extension Source {
NSFetchRequest<Source>(entityName: "Source")
@nonobjc public class func fetchRequest() -> NSFetchRequest<Source> {
return NSFetchRequest<Source>(entityName: "Source")
} }
@NSManaged var id: UUID @NSManaged public var id: UUID
@NSManaged var baseUrl: String? @NSManaged public var baseUrl: String?
@NSManaged var fallbackUrls: [String]? @NSManaged public var fallbackUrls: [String]?
@NSManaged var dynamicBaseUrl: Bool @NSManaged public var dynamicBaseUrl: Bool
@NSManaged var enabled: Bool @NSManaged public var enabled: Bool
@NSManaged var name: String @NSManaged public var name: String
@NSManaged var author: String @NSManaged public var author: String
@NSManaged var listId: UUID? @NSManaged public var listId: UUID?
@NSManaged var preferredParser: Int16 @NSManaged public var preferredParser: Int16
@NSManaged var version: Int16 @NSManaged public var version: Int16
@NSManaged var htmlParser: SourceHtmlParser? @NSManaged public var htmlParser: SourceHtmlParser?
@NSManaged var rssParser: SourceRssParser? @NSManaged public var rssParser: SourceRssParser?
@NSManaged var jsonParser: SourceJsonParser? @NSManaged public var jsonParser: SourceJsonParser?
@NSManaged var api: SourceApi? @NSManaged public var api: SourceApi?
@NSManaged var trackers: [String]? @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 {
}

View file

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

View file

@ -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<SourceList> {
NSFetchRequest<SourceList>(entityName: "SourceList")
}
@NSManaged var id: UUID
@NSManaged var author: String
@NSManaged var name: String
@NSManaged var urlString: String
}
extension SourceList: Identifiable {}

View file

@ -3,6 +3,6 @@
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>_XCCurrentVersionName</key> <key>_XCCurrentVersionName</key>
<string>FerriteDB.xcdatamodel</string> <string>FerriteDB_v2.xcdatamodel</string>
</dict> </dict>
</plist> </plist>

View file

@ -0,0 +1,159 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21513" systemVersion="22D49" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="Action" representedClassName="Action" syncable="YES">
<attribute name="author" attributeType="String" defaultValueString=""/>
<attribute name="deeplink" optional="YES" attributeType="String"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="listId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="requires" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="version" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PluginTag" inverseName="parentAction" inverseEntity="PluginTag"/>
</entity>
<entity name="Bookmark" representedClassName="Bookmark" syncable="YES">
<attribute name="leechers" optional="YES" attributeType="String"/>
<attribute name="magnetHash" optional="YES" attributeType="String"/>
<attribute name="magnetLink" optional="YES" attributeType="String"/>
<attribute name="orderNum" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="seeders" optional="YES" attributeType="String"/>
<attribute name="size" optional="YES" attributeType="String"/>
<attribute name="source" attributeType="String" defaultValueString=""/>
<attribute name="title" optional="YES" attributeType="String"/>
</entity>
<entity name="History" representedClassName="History" syncable="YES">
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="dateString" optional="YES" attributeType="String"/>
<relationship name="entries" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="HistoryEntry" inverseName="parentHistory" inverseEntity="HistoryEntry"/>
</entity>
<entity name="HistoryEntry" representedClassName="HistoryEntry" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/>
<attribute name="source" optional="YES" attributeType="String"/>
<attribute name="subName" optional="YES" attributeType="String"/>
<attribute name="timeStamp" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
<attribute name="url" optional="YES" attributeType="String"/>
<relationship name="parentHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="History" inverseName="entries" inverseEntity="History"/>
</entity>
<entity name="PluginList" representedClassName="PluginList" syncable="YES">
<attribute name="author" attributeType="String" defaultValueString=""/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="urlString" attributeType="String" defaultValueString=""/>
</entity>
<entity name="PluginTag" representedClassName="PluginTag" syncable="YES">
<attribute name="colorHex" optional="YES" attributeType="String"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<relationship name="parentAction" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Action" inverseName="tags" inverseEntity="Action"/>
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="tags" inverseEntity="Source"/>
</entity>
<entity name="Source" representedClassName="Source" syncable="YES">
<attribute name="author" attributeType="String" defaultValueString=""/>
<attribute name="baseUrl" optional="YES" attributeType="String"/>
<attribute name="dynamicBaseUrl" optional="YES" attributeType="Boolean" usesScalarValueType="YES"/>
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="fallbackUrls" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="listId" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="name" attributeType="String" defaultValueString=""/>
<attribute name="preferredParser" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="trackers" optional="YES" attributeType="Transformable" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="version" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
<relationship name="api" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApi" inverseName="parentSource" inverseEntity="SourceApi"/>
<relationship name="htmlParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceHtmlParser" inverseName="parentSource" inverseEntity="SourceHtmlParser"/>
<relationship name="jsonParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceJsonParser" inverseName="parentSource" inverseEntity="SourceJsonParser"/>
<relationship name="rssParser" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceRssParser" inverseName="parentSource" inverseEntity="SourceRssParser"/>
<relationship name="tags" optional="YES" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="PluginTag" inverseName="parentSource" inverseEntity="PluginTag"/>
</entity>
<entity name="SourceApi" representedClassName="SourceApi" syncable="YES" codeGenerationType="class">
<attribute name="apiUrl" optional="YES" attributeType="String"/>
<relationship name="clientId" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApiClientId" inverseName="parentApi" inverseEntity="SourceApiClientId"/>
<relationship name="clientSecret" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceApiClientSecret" inverseName="parentApi" inverseEntity="SourceApiClientSecret"/>
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="api" inverseEntity="Source"/>
</entity>
<entity name="SourceApiClientId" representedClassName="SourceApiClientId" parentEntity="SourceApiCredential" syncable="YES" codeGenerationType="class">
<relationship name="parentApi" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceApi" inverseName="clientId" inverseEntity="SourceApi"/>
</entity>
<entity name="SourceApiClientSecret" representedClassName="SourceApiClientSecret" parentEntity="SourceApiCredential" syncable="YES" codeGenerationType="class">
<relationship name="parentApi" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceApi" inverseName="clientSecret" inverseEntity="SourceApi"/>
</entity>
<entity name="SourceApiCredential" representedClassName="SourceApiCredential" isAbstract="YES" syncable="YES" codeGenerationType="class">
<attribute name="dynamic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
<attribute name="expiryLength" optional="YES" attributeType="Double" defaultValueString="0" usesScalarValueType="YES"/>
<attribute name="query" optional="YES" attributeType="String"/>
<attribute name="responseType" optional="YES" attributeType="String"/>
<attribute name="timeStamp" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
<attribute name="urlString" optional="YES" attributeType="String"/>
<attribute name="value" optional="YES" attributeType="String"/>
</entity>
<entity name="SourceComplexQuery" representedClassName="SourceComplexQuery" isAbstract="YES" syncable="YES">
<attribute name="attribute" attributeType="String" defaultValueString="text"/>
<attribute name="discriminator" optional="YES" attributeType="String"/>
<attribute name="query" attributeType="String" defaultValueString=""/>
<attribute name="regex" optional="YES" attributeType="String"/>
</entity>
<entity name="SourceHtmlParser" representedClassName="SourceHtmlParser" syncable="YES">
<attribute name="rows" attributeType="String" defaultValueString=""/>
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentHtmlParser" inverseEntity="SourceMagnetHash"/>
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentHtmlParser" inverseEntity="SourceMagnetLink"/>
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="htmlParser" inverseEntity="Source"/>
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentHtmlParser" inverseEntity="SourceSeedLeech"/>
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentHtmlParser" inverseEntity="SourceSize"/>
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentHtmlParser" inverseEntity="SourceTitle"/>
</entity>
<entity name="SourceJsonParser" representedClassName="SourceJsonParser" syncable="YES">
<attribute name="results" optional="YES" attributeType="String" valueTransformerName="NSSecureUnarchiveFromData" customClassName="[String]"/>
<attribute name="searchUrl" optional="YES" attributeType="String"/>
<attribute name="subResults" optional="YES" attributeType="String"/>
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentJsonParser" inverseEntity="SourceMagnetHash"/>
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentJsonParser" inverseEntity="SourceMagnetLink"/>
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="jsonParser" inverseEntity="Source"/>
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentJsonParser" inverseEntity="SourceSeedLeech"/>
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentJsonParser" inverseEntity="SourceSize"/>
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentJsonParser" inverseEntity="SourceTitle"/>
</entity>
<entity name="SourceMagnetHash" representedClassName="SourceMagnetHash" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnetHash" inverseEntity="SourceHtmlParser"/>
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="magnetHash" inverseEntity="SourceJsonParser"/>
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="magnetHash" inverseEntity="SourceRssParser"/>
</entity>
<entity name="SourceMagnetLink" representedClassName="SourceMagnetLink" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
<attribute name="externalLinkQuery" optional="YES" attributeType="String"/>
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="magnetLink" inverseEntity="SourceHtmlParser"/>
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="magnetLink" inverseEntity="SourceJsonParser"/>
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="magnetLink" inverseEntity="SourceRssParser"/>
</entity>
<entity name="SourceRssParser" representedClassName="SourceRssParser" syncable="YES">
<attribute name="items" attributeType="String" defaultValueString=""/>
<attribute name="rssUrl" optional="YES" attributeType="String"/>
<attribute name="searchUrl" attributeType="String" defaultValueString=""/>
<relationship name="magnetHash" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetHash" inverseName="parentRssParser" inverseEntity="SourceMagnetHash"/>
<relationship name="magnetLink" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceMagnetLink" inverseName="parentRssParser" inverseEntity="SourceMagnetLink"/>
<relationship name="parentSource" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="rssParser" inverseEntity="Source"/>
<relationship name="seedLeech" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSeedLeech" inverseName="parentRssParser" inverseEntity="SourceSeedLeech"/>
<relationship name="size" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceSize" inverseName="parentRssParser" inverseEntity="SourceSize"/>
<relationship name="title" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="SourceTitle" inverseName="parentRssParser" inverseEntity="SourceTitle"/>
</entity>
<entity name="SourceSeedLeech" representedClassName="SourceSeedLeech" syncable="YES">
<attribute name="attribute" attributeType="String" defaultValueString=""/>
<attribute name="combined" optional="YES" attributeType="String"/>
<attribute name="discriminator" optional="YES" attributeType="String"/>
<attribute name="leecherRegex" optional="YES" attributeType="String"/>
<attribute name="leechers" optional="YES" attributeType="String"/>
<attribute name="seederRegex" optional="YES" attributeType="String"/>
<attribute name="seeders" optional="YES" attributeType="String"/>
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="seedLeech" inverseEntity="SourceHtmlParser"/>
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="seedLeech" inverseEntity="SourceJsonParser"/>
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="seedLeech" inverseEntity="SourceRssParser"/>
</entity>
<entity name="SourceSize" representedClassName="SourceSize" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="size" inverseEntity="SourceHtmlParser"/>
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="size" inverseEntity="SourceJsonParser"/>
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="size" inverseEntity="SourceRssParser"/>
</entity>
<entity name="SourceTitle" representedClassName="SourceTitle" parentEntity="SourceComplexQuery" syncable="YES" codeGenerationType="class">
<relationship name="parentHtmlParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceHtmlParser" inverseName="title" inverseEntity="SourceHtmlParser"/>
<relationship name="parentJsonParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceJsonParser" inverseName="title" inverseEntity="SourceJsonParser"/>
<relationship name="parentRssParser" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="SourceRssParser" inverseName="title" inverseEntity="SourceRssParser"/>
</entity>
</model>

View file

@ -91,7 +91,7 @@ struct PersistenceController {
save() save()
} }
func createBookmark(_ bookmarkJson: BookmarkJson) { func createBookmark(_ bookmarkJson: BookmarkJson, performSave: Bool) {
let bookmarkRequest = Bookmark.fetchRequest() let bookmarkRequest = Bookmark.fetchRequest()
bookmarkRequest.predicate = NSPredicate( bookmarkRequest.predicate = NSPredicate(
format: "source == %@ AND title == %@ AND magnetLink == %@", format: "source == %@ AND title == %@ AND magnetLink == %@",
@ -113,32 +113,31 @@ struct PersistenceController {
newBookmark.seeders = bookmarkJson.seeders newBookmark.seeders = bookmarkJson.seeders
newBookmark.leechers = bookmarkJson.leechers 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 historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate) 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() let historyRequest = History.fetchRequest()
historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString) 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) { if var histories = try? backgroundContext.fetch(historyRequest) {
for (i, history) in histories.enumerated() { 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 { if !existingEntries.isEmpty {
for entry in existingEntries { if isBackup {
PersistenceController.shared.delete(entry, context: backgroundContext) 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) existingHistory = histories.first
} else {
newHistoryEntry.parentHistory = History(context: backgroundContext)
} }
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?.dateString = historyDateString
newHistoryEntry.parentHistory?.date = historyDate newHistoryEntry.parentHistory?.date = historyDate
save(backgroundContext) if performSave {
save(backgroundContext)
}
} }
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {
@ -200,8 +208,7 @@ struct PersistenceController {
return predicate return predicate
} }
// Always use the background context to batch delete // Wrapper to batch delete history objects
// Merge changes into both contexts to update views
func batchDeleteHistory(range: HistoryDeleteRange) throws { func batchDeleteHistory(range: HistoryDeleteRange) throws {
let predicate = getHistoryPredicate(range: range) 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?") 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<NSFetchRequestResult>(entityName: entity)
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
batchDeleteRequest.resultType = .resultTypeObjectIDs batchDeleteRequest.resultType = .resultTypeObjectIDs

View file

@ -15,7 +15,7 @@ struct FerriteApp: App {
@StateObject var toastModel: ToastViewModel = .init() @StateObject var toastModel: ToastViewModel = .init()
@StateObject var debridManager: DebridManager = .init() @StateObject var debridManager: DebridManager = .init()
@StateObject var navModel: NavigationViewModel = .init() @StateObject var navModel: NavigationViewModel = .init()
@StateObject var sourceManager: SourceManager = .init() @StateObject var pluginManager: PluginManager = .init()
@StateObject var backupManager: BackupManager = .init() @StateObject var backupManager: BackupManager = .init()
var body: some Scene { var body: some Scene {
@ -24,7 +24,7 @@ struct FerriteApp: App {
.onAppear { .onAppear {
scrapingModel.toastModel = toastModel scrapingModel.toastModel = toastModel
debridManager.toastModel = toastModel debridManager.toastModel = toastModel
sourceManager.toastModel = toastModel pluginManager.toastModel = toastModel
backupManager.toastModel = toastModel backupManager.toastModel = toastModel
navModel.toastModel = toastModel navModel.toastModel = toastModel
} }
@ -32,7 +32,7 @@ struct FerriteApp: App {
.environmentObject(scrapingModel) .environmentObject(scrapingModel)
.environmentObject(toastModel) .environmentObject(toastModel)
.environmentObject(navModel) .environmentObject(navModel)
.environmentObject(sourceManager) .environmentObject(pluginManager)
.environmentObject(backupManager) .environmentObject(backupManager)
.environment(\.managedObjectContext, persistenceController.container.viewContext) .environment(\.managedObjectContext, persistenceController.container.viewContext)
} }

View file

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

View file

@ -12,7 +12,11 @@ public struct Backup: Codable {
var bookmarks: [BookmarkJson]? var bookmarks: [BookmarkJson]?
var history: [HistoryJson]? var history: [HistoryJson]?
var sourceNames: [String]? 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 // MARK: - CoreData translation
@ -43,8 +47,8 @@ struct HistoryEntryJson: Codable {
let source: String? let source: String?
} }
// Differs from SourceListJson // Differs from PluginListJson
struct SourceListBackupJson: Codable { struct PluginListBackupJson: Codable {
let name: String let name: String
let author: String let author: String
let id: String let id: String

View file

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

View file

@ -12,26 +12,28 @@ public enum ApiCredentialResponseType: String, Codable, Hashable, Sendable {
case text case text
} }
public struct SourceListJson: Codable, Sendable { public struct SourceJson: Codable, Hashable, Sendable, PluginJson {
let name: String public let name: String
let author: String public let version: Int16
var sources: [SourceJson]
}
public struct SourceJson: Codable, Hashable, Sendable {
let name: String
let version: Int16
let minVersion: String? let minVersion: String?
let baseUrl: String? let baseUrl: String?
let fallbackUrls: [String]? let fallbackUrls: [String]?
var dynamicBaseUrl: Bool? var dynamicBaseUrl: Bool?
var author: String?
var listId: UUID?
let trackers: [String]? let trackers: [String]?
let api: SourceApiJson? let api: SourceApiJson?
let jsonParser: SourceJsonParserJson? let jsonParser: SourceJsonParserJson?
let rssParser: SourceRssParserJson? let rssParser: SourceRssParserJson?
let htmlParser: SourceHtmlParserJson? 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 { public enum SourcePreferredParser: Int16, CaseIterable, Sendable {

View file

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

View file

@ -9,18 +9,33 @@ import Foundation
public class BackupManager: ObservableObject { public class BackupManager: ObservableObject {
// Constant variable for backup versions // Constant variable for backup versions
let latestBackupVersion: Int = 1 let latestBackupVersion: Int = 2
var toastModel: ToastViewModel? var toastModel: ToastViewModel?
@Published var showRestoreAlert = false @Published var showRestoreAlert = false
@Published var showRestoreCompletedAlert = false @Published var showRestoreCompletedAlert = false
@Published var restoreCompletedMessage: [String] = []
@Published var backupUrls: [URL] = [] @Published var backupUrls: [URL] = []
@Published var backupSourceNames: [String] = []
@Published var selectedBackupUrl: URL? @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) var backup = Backup(version: latestBackupVersion)
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
@ -71,16 +86,14 @@ public class BackupManager: ObservableObject {
backup.sourceNames = sources.map(\.name) backup.sourceNames = sources.map(\.name)
} }
let sourceListRequest = SourceList.fetchRequest() let actionRequest = Action.fetchRequest()
if let sourceLists = try? backgroundContext.fetch(sourceListRequest) { if let actions = try? backgroundContext.fetch(actionRequest) {
backup.sourceLists = sourceLists.map { backup.actionNames = actions.map(\.name)
SourceListBackupJson( }
name: $0.name,
author: $0.author, let pluginListRequest = PluginList.fetchRequest()
id: $0.id.uuidString, if let pluginLists = try? backgroundContext.fetch(pluginListRequest) {
urlString: $0.urlString backup.pluginListUrls = pluginLists.map(\.urlString)
)
}
} }
do { do {
@ -94,18 +107,20 @@ public class BackupManager: ObservableObject {
let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb") let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb")
try encodedJson.write(to: writeUrl) try encodedJson.write(to: writeUrl)
backupUrls.append(writeUrl)
await updateBackupUrls(newUrl: writeUrl)
} catch { } 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 // 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 { 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 return
} }
@ -113,64 +128,72 @@ public class BackupManager: ObservableObject {
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
do { 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 file = try Data(contentsOf: backupUrl)
let backup = try JSONDecoder().decode(Backup.self, from: file) let backup = try JSONDecoder().decode(Backup.self, from: file)
if let bookmarks = backup.bookmarks { if let bookmarks = backup.bookmarks {
for bookmark in bookmarks { for bookmark in bookmarks {
PersistenceController.shared.createBookmark(bookmark) PersistenceController.shared.createBookmark(bookmark, performSave: false)
} }
} }
if let storedHistories = backup.history { if let storedHistories = backup.history {
for storedHistory in storedHistories { for storedHistory in storedHistories {
for storedEntry in storedHistory.entries { 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 { for list in storedLists {
let sourceListRequest = SourceList.fetchRequest() try await pluginManager.addPluginList(list.urlString, existingPluginList: nil)
let urlPredicate = NSPredicate(format: "urlString == %@", list.urlString) }
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", list.author, list.name) } else if let pluginListUrls = backup.pluginListUrls {
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) // v2 and up
sourceListRequest.fetchLimit = 1 for listUrl in pluginListUrls {
try await pluginManager.addPluginList(listUrl, existingPluginList: nil)
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
} }
} }
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) PersistenceController.shared.save(backgroundContext)
// if iOS 14 is available, sleep to prevent any issues with alerts // if iOS 14 is available, sleep to prevent any issues with alerts
if #available(iOS 15, *) { if #available(iOS 15, *) {
showRestoreCompletedAlert.toggle() await toggleRestoreCompletedAlert()
} else { } else {
Task { try? await Task.sleep(seconds: 0.1)
try? await Task.sleep(seconds: 0.1)
Task { @MainActor in await toggleRestoreCompletedAlert()
showRestoreCompletedAlert.toggle()
}
}
} }
} catch { } catch {
Task { await toastModel?.updateToastDescription("Backup restore error: \(error)")
await toastModel?.updateToastDescription("Backup restore: \(error)") print("Backup restore error: \(error)")
}
} }
} }
@ -187,7 +210,8 @@ public class BackupManager: ObservableObject {
} }
} catch { } catch {
Task { Task {
await toastModel?.updateToastDescription("Backup removal: \(error)") await toastModel?.updateToastDescription("Backup removal error: \(error)")
print("Backup removal error: \(error)")
} }
} }
} }

View file

@ -9,7 +9,7 @@ import SwiftUI
enum ViewTab { enum ViewTab {
case search case search
case sources case plugins
case settings case settings
case library case library
} }
@ -56,7 +56,6 @@ class NavigationViewModel: ObservableObject {
var selectedSource: Source? var selectedSource: Source?
@Published var showSourceListEditor: Bool = false @Published var showSourceListEditor: Bool = false
var selectedSourceList: SourceList?
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none

View file

@ -5,27 +5,28 @@
// Created by Brian Dashore on 7/25/22. // Created by Brian Dashore on 7/25/22.
// //
import CoreData
import Foundation import Foundation
import SwiftUI import SwiftUI
public class SourceManager: ObservableObject { public class PluginManager: ObservableObject {
var toastModel: ToastViewModel? var toastModel: ToastViewModel?
@Published var availableSources: [SourceJson] = [] @Published var availableSources: [SourceJson] = []
@Published var availableActions: [ActionJson] = []
var urlErrorAlertText = ""
@Published var showUrlErrorAlert = false
@MainActor @MainActor
public func fetchSourcesFromUrl() async { public func fetchPluginsFromUrl() async {
let sourceListRequest = SourceList.fetchRequest() let pluginListRequest = PluginList.fetchRequest()
do { do {
let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest) let pluginLists = try PersistenceController.shared.backgroundContext.fetch(pluginListRequest)
var tempAvailableSources: [SourceJson] = []
for sourceList in sourceLists { if pluginLists.isEmpty {
guard let url = URL(string: sourceList.urlString) else { availableSources = []
availableActions = []
}
for pluginList in pluginLists {
guard let url = URL(string: pluginList.urlString) else {
return return
} }
@ -33,39 +34,107 @@ public class SourceManager: ObservableObject {
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
let (data, _) = try await URLSession.shared.data(for: request) 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 let sources = pluginResponse.sources {
// If there is a minVersion, check and see if the source is valid // Faster and more performant to map instead of a for loop
if checkAppVersion(minVersion: source.minVersion) { availableSources = sources.compactMap { inputJson in
source.author = sourceList.author if checkAppVersion(minVersion: inputJson.minVersion) {
source.listId = sourceList.id 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 { } catch {
print(error) toastModel?.updateToastDescription("Plugin fetch error: \(error)")
print("Plugin fetch error: \(error)")
} }
} }
func fetchUpdatedSources(installedSources: FetchedResults<Source>) -> [SourceJson] { // Check if underlying type is Source or Action
var updatedSources: [SourceJson] = [] func fetchFilteredPlugins<P: Plugin, PJ: PluginJson>(installedPlugins: FetchedResults<P>, searchText: String) -> [PJ] {
let availablePlugins: [PJ] = fetchCastedPlugins(PJ.self)
for source in installedSources { return availablePlugins
if let availableSource = availableSources.first(where: { .filter { availablePlugin in
source.listId == $0.listId && source.name == $0.name && source.author == $0.author 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<P: Plugin, PJ: PluginJson>(installedPlugins: FetchedResults<P>, 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<PJ: PluginJson>(_ 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 // 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 let backgroundContext = PersistenceController.shared.backgroundContext
// If there's no base URL and it isn't dynamic, return before any transactions occur // 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 { if !dynamicBaseUrl, sourceJson.baseUrl == nil {
await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.") await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
print("Not adding source \(sourceJson.name) because base URL parameters are malformed")
print("Not adding this source because base URL parameters are malformed")
return return
} }
@ -112,6 +272,8 @@ public class SourceManager: ObservableObject {
PersistenceController.shared.delete(existingSource, context: backgroundContext) PersistenceController.shared.delete(existingSource, context: backgroundContext)
} else { } else {
await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.") await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.")
print("Source name \(sourceJson.name) already exists")
return return
} }
} }
@ -127,6 +289,16 @@ public class SourceManager: ObservableObject {
newSource.listId = sourceJson.listId newSource.listId = sourceJson.listId
newSource.trackers = sourceJson.trackers 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 { if let sourceApiJson = sourceJson.api {
addSourceApi(newSource: newSource, apiJson: sourceApiJson) addSourceApi(newSource: newSource, apiJson: sourceApiJson)
} }
@ -159,7 +331,8 @@ public class SourceManager: ObservableObject {
do { do {
try backgroundContext.save() try backgroundContext.save()
} catch { } 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 newSource.htmlParser = newSourceHtmlParser
} }
@MainActor // Adds a plugin list
public func addSourceList(sourceUrl: String, existingSourceList: SourceList?) async -> Bool { // 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 let backgroundContext = PersistenceController.shared.backgroundContext
if sourceUrl.isEmpty || URL(string: sourceUrl) == nil { if url.isEmpty || URL(string: url) == nil {
urlErrorAlertText = "The provided source list is invalid. Please check if the URL is formatted properly." throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.")
showUrlErrorAlert.toggle()
return false
} }
do { let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!))
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!)) let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
if let existingSourceList { if let existingPluginList {
existingSourceList.urlString = sourceUrl existingPluginList.urlString = url
existingSourceList.name = rawResponse.name existingPluginList.name = rawResponse.name
existingSourceList.author = rawResponse.author existingPluginList.author = rawResponse.author
try PersistenceController.shared.container.viewContext.save() try PersistenceController.shared.container.viewContext.save()
} else { } else {
let sourceListRequest = SourceList.fetchRequest() let pluginListRequest = PluginList.fetchRequest()
let urlPredicate = NSPredicate(format: "urlString == %@", sourceUrl) let urlPredicate = NSPredicate(format: "urlString == %@", url)
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name) let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
sourceListRequest.fetchLimit = 1 pluginListRequest.fetchLimit = 1
if (try? backgroundContext.fetch(sourceListRequest).first) != nil { if let existingPluginList = try? backgroundContext.fetch(pluginListRequest).first, !isSheet {
urlErrorAlertText = "An existing source with this information was found. Please try editing the source list instead." PersistenceController.shared.delete(existingPluginList, context: backgroundContext)
showUrlErrorAlert.toggle() } 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 false
}
let newSourceUrl = SourceList(context: backgroundContext)
newSourceUrl.id = UUID()
newSourceUrl.urlString = sourceUrl
newSourceUrl.name = rawResponse.name
newSourceUrl.author = rawResponse.author
try backgroundContext.save()
} }
return true let newPluginList = PluginList(context: backgroundContext)
} catch { newPluginList.id = UUID()
print(error) newPluginList.urlString = url
urlErrorAlertText = error.localizedDescription newPluginList.name = rawResponse.name
showUrlErrorAlert.toggle() newPluginList.author = rawResponse.author
return false try backgroundContext.save()
} }
} }
} }

View file

@ -24,7 +24,7 @@ struct DynamicFetchRequest<T: NSManagedObject, Content: View>: View {
sortDescriptors: [NSSortDescriptor] = [], sortDescriptors: [NSSortDescriptor] = [],
@ViewBuilder content: @escaping (FetchedResults<T>) -> Content) @ViewBuilder content: @escaping (FetchedResults<T>) -> Content)
{ {
_fetchRequest = FetchRequest<T>(sortDescriptors: sortDescriptors, predicate: predicate) _fetchRequest = FetchRequest<T>(entity: T.entity(), sortDescriptors: sortDescriptors, predicate: predicate)
self.content = content self.content = content
} }
} }

View file

@ -11,19 +11,16 @@
import SwiftUI import SwiftUI
struct NavView<Content: View>: View { struct NavView<Content: View>: View {
let content: () -> Content @ViewBuilder var content: Content
init(@ViewBuilder _ content: @escaping () -> Content) {
self.content = content
}
var body: some View { var body: some View {
if #available(iOS 16, *) { if #available(iOS 16, *) {
NavigationStack { NavigationStack {
content() content
} }
} else { } else {
NavigationView { NavigationView {
content() content
} }
.navigationViewStyle(.stack) .navigationViewStyle(.stack)
} }

View file

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

View file

@ -15,31 +15,31 @@ struct DebridLabelView: View {
var body: some View { var body: some View {
if let selectedDebridType = debridManager.selectedDebridType { if let selectedDebridType = debridManager.selectedDebridType {
Text(selectedDebridType.toString(abbreviated: true)) Tag(
.fontWeight(.bold) name: selectedDebridType.toString(abbreviated: true),
.padding(2) color: getTagColor(),
.background { horizontalPadding: 5,
Group { verticalPadding: 3
if let magnet, cloudLinks.isEmpty { )
switch debridManager.matchMagnetHash(magnet) { }
case .full: }
Color.green
case .partial: func getTagColor() -> Color {
Color.orange if let magnet, cloudLinks.isEmpty {
case .none: switch debridManager.matchMagnetHash(magnet) {
Color.red case .full:
} return Color.green
} else if cloudLinks.count == 1 { case .partial:
Color.green return Color.orange
} else if cloudLinks.count > 1 { case .none:
Color.orange return Color.red
} else { }
Color.red } else if cloudLinks.count == 1 {
} return Color.green
} } else if cloudLinks.count > 1 {
.cornerRadius(4) return Color.orange
.opacity(0.5) } else {
} return Color.red
} }
} }
} }

View file

@ -37,7 +37,7 @@ struct AllDebridCloudView: View {
if !debridManager.downloadUrl.isEmpty { if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo) PersistenceController.shared.createHistory(historyInfo, performSave: true)
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)
} }
} }

View file

@ -34,7 +34,8 @@ struct PremiumizeCloudView: View {
name: item.name, name: item.name,
url: debridManager.downloadUrl, url: debridManager.downloadUrl,
source: DebridType.premiumize.toString() source: DebridType.premiumize.toString()
) ),
performSave: true
) )
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)

View file

@ -31,7 +31,8 @@ struct RealDebridCloudView: View {
name: downloadResponse.filename, name: downloadResponse.filename,
url: downloadResponse.download, url: downloadResponse.download,
source: DebridType.realDebrid.toString() source: DebridType.realDebrid.toString()
) ),
performSave: true
) )
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)
@ -69,7 +70,7 @@ struct RealDebridCloudView: View {
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink) await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
if !debridManager.downloadUrl.isEmpty { if !debridManager.downloadUrl.isEmpty {
historyInfo.url = debridManager.downloadUrl historyInfo.url = debridManager.downloadUrl
PersistenceController.shared.createHistory(historyInfo) PersistenceController.shared.createHistory(historyInfo, performSave: true)
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)
} }

View file

@ -77,4 +77,12 @@ struct HistoryButtonView: View {
.backport.tint(.primary) .backport.tint(.primary)
.disableInteraction(navModel.currentChoiceSheet != nil) .disableInteraction(navModel.currentChoiceSheet != nil)
} }
func getTagColor() -> Color {
if let url = entry.url, url.starts(with: "https://") {
return Color.green
} else {
return Color.red
}
}
} }

View file

@ -7,52 +7,60 @@
import SwiftUI import SwiftUI
struct InstalledSourceButtonView: View { struct InstalledPluginButtonView<P: Plugin>: View {
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@ObservedObject var installedSource: Source @ObservedObject var installedPlugin: P
var body: some View { var body: some View {
Toggle(isOn: Binding<Bool>( Toggle(isOn: Binding<Bool>(
get: { installedSource.enabled }, get: { installedPlugin.enabled },
set: { set: {
installedSource.enabled = $0 installedPlugin.enabled = $0
PersistenceController.shared.save() PersistenceController.shared.save()
} }
)) { )) {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading) {
HStack { VStack(alignment: .leading, spacing: 5) {
Text(installedSource.name) HStack {
Text("v\(installedSource.version)") Text(installedPlugin.name)
Text("v\(installedPlugin.version)")
.foregroundColor(.secondary)
}
Text("by \(installedPlugin.author)")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
Text("by \(installedSource.author)") if let tags = installedPlugin.getTags(), !tags.isEmpty {
.foregroundColor(.secondary) PluginTagsView(tags: tags)
}
} }
.padding(.vertical, 2) .padding(.vertical, 2)
} }
.contextMenu { .contextMenu {
Button { if let installedSource = installedPlugin as? Source {
navModel.selectedSource = installedSource Button {
navModel.showSourceSettings.toggle() navModel.selectedSource = installedSource
} label: { navModel.showSourceSettings.toggle()
Text("Settings") } label: {
Image(systemName: "gear") Text("Settings")
Image(systemName: "gear")
}
} }
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
Button(role: .destructive) { Button(role: .destructive) {
PersistenceController.shared.delete(installedSource, context: backgroundContext) PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
} label: { } label: {
Text("Remove") Text("Remove")
Image(systemName: "trash") Image(systemName: "trash")
} }
} else { } else {
Button { Button {
PersistenceController.shared.delete(installedSource, context: backgroundContext) PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
} label: { } label: {
Text("Remove") Text("Remove")
Image(systemName: "trash") Image(systemName: "trash")

View file

@ -0,0 +1,51 @@
//
// SourceCatalogButtonView.swift
// Ferrite
//
// Created by Brian Dashore on 8/5/22.
//
import SwiftUI
struct PluginCatalogButtonView<PJ: PluginJson>: 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)
}
}

View file

@ -8,7 +8,7 @@
import SwiftUI import SwiftUI
struct SourceCatalogButtonView: View { struct SourceCatalogButtonView: View {
@EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var pluginManager: PluginManager
let availableSource: SourceJson let availableSource: SourceJson
@ -29,7 +29,7 @@ struct SourceCatalogButtonView: View {
Button("Install") { Button("Install") {
Task { Task {
await sourceManager.installSource(sourceJson: availableSource) await pluginManager.installSource(sourceJson: availableSource)
} }
} }
} }

View file

@ -0,0 +1,80 @@
//
// SourceListView.swift
// Ferrite
//
// Created by Brian Dashore on 7/24/22.
//
import SwiftUI
struct PluginListView<P: Plugin, PJ: PluginJson>: 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<P>) 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)
}
}
}
}

View file

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

View file

@ -17,28 +17,34 @@ struct SourceSettingsView: View {
List { List {
if let selectedSource = navModel.selectedSource { if let selectedSource = navModel.selectedSource {
Section(header: InlineHeader("Info")) { Section(header: InlineHeader("Info")) {
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading) {
HStack { VStack(alignment: .leading, spacing: 5) {
Text(selectedSource.name) HStack {
Text(selectedSource.name)
Text("v\(selectedSource.version)") Text("v\(selectedSource.version)")
.foregroundColor(.secondary) .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("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) .padding(.vertical, 2)
} }

View file

@ -38,7 +38,8 @@ struct SearchResultButtonView: View {
name: result.title, name: result.title,
url: debridManager.downloadUrl, url: debridManager.downloadUrl,
source: result.source source: result.source
) ),
performSave: true
) )
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)
@ -63,7 +64,8 @@ struct SearchResultButtonView: View {
name: result.title, name: result.title,
url: result.magnet.link, url: result.magnet.link,
source: result.source source: result.source
) ),
performSave: true
) )
navModel.runMagnetAction(magnet: result.magnet) navModel.runMagnetAction(magnet: result.magnet)

View file

@ -57,7 +57,9 @@ struct BackupsView: View {
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button { Button {
backupManager.createBackup() Task {
await backupManager.createBackup()
}
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
} }

View file

@ -7,37 +7,41 @@
import SwiftUI import SwiftUI
struct SourceListEditorView: View { struct PluginListEditorView: View {
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var pluginManager: PluginManager
let backgroundContext = PersistenceController.shared.backgroundContext 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 { var body: some View {
NavView { NavView {
Form { Form {
TextField("Enter URL", text: $sourceUrl) TextField("Enter URL", text: $pluginListUrl)
.disableAutocorrection(true) .disableAutocorrection(true)
.keyboardType(.URL) .keyboardType(.URL)
.autocapitalization(.none) .autocapitalization(.none)
.conditionalId(sourceUrlSet) .conditionalId(sourceUrlSet)
} }
.onAppear { .onAppear {
sourceUrl = navModel.selectedSourceList?.urlString ?? "" pluginListUrl = selectedPluginList?.urlString ?? ""
sourceUrlSet = true sourceUrlSet = true
} }
.backport.alert( .backport.alert(
isPresented: $sourceManager.showUrlErrorAlert, isPresented: $showUrlErrorAlert,
title: "Error", title: "Error",
message: sourceManager.urlErrorAlertText message: urlErrorAlertText
) )
.navigationTitle("Editing source list") .navigationTitle("Editing Plugin List")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarLeading) { ToolbarItem(placement: .navigationBarLeading) {
@ -49,25 +53,23 @@ struct SourceListEditorView: View {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button("Save") { Button("Save") {
Task { Task {
if await sourceManager.addSourceList( do {
sourceUrl: sourceUrl, try await pluginManager.addPluginList(pluginListUrl, existingPluginList: selectedPluginList)
existingSourceList: navModel.selectedSourceList
) {
presentationMode.wrappedValue.dismiss() 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 { static var previews: some View {
SourceListEditorView() PluginListEditorView()
} }
} }

View file

@ -7,40 +7,40 @@
import SwiftUI import SwiftUI
struct SettingsSourceListView: View { struct SettingsPluginListView: View {
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@FetchRequest( @FetchRequest(
entity: SourceList.entity(), entity: PluginList.entity(),
sortDescriptors: [] sortDescriptors: []
) var sourceLists: FetchedResults<SourceList> ) var pluginLists: FetchedResults<PluginList>
@State private var presentSourceSheet = false @State private var presentSourceSheet = false
@State private var selectedSourceList: SourceList? @State private var selectedPluginList: PluginList?
var body: some View { var body: some View {
ZStack { ZStack {
if sourceLists.isEmpty { if pluginLists.isEmpty {
EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right") EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right")
} else { } else {
List { List {
ForEach(sourceLists, id: \.self) { sourceList in ForEach(pluginLists, id: \.self) { pluginList in
VStack(alignment: .leading, spacing: 5) { VStack(alignment: .leading, spacing: 5) {
Text(sourceList.name) Text(pluginList.name)
Text(sourceList.author) Text(pluginList.author)
.foregroundColor(.gray) .foregroundColor(.gray)
Text("ID: \(sourceList.id)") Text("ID: \(pluginList.id)")
.font(.caption) .font(.caption)
.foregroundColor(.gray) .foregroundColor(.gray)
} }
.padding(.vertical, 2) .padding(.vertical, 2)
.contextMenu { .contextMenu {
Button { Button {
navModel.selectedSourceList = sourceList selectedPluginList = pluginList
presentSourceSheet.toggle() presentSourceSheet.toggle()
} label: { } label: {
Text("Edit") Text("Edit")
@ -49,14 +49,14 @@ struct SettingsSourceListView: View {
if #available(iOS 15.0, *) { if #available(iOS 15.0, *) {
Button(role: .destructive) { Button(role: .destructive) {
PersistenceController.shared.delete(sourceList, context: backgroundContext) PersistenceController.shared.delete(pluginList, context: backgroundContext)
} label: { } label: {
Text("Remove") Text("Remove")
Image(systemName: "trash") Image(systemName: "trash")
} }
} else { } else {
Button { Button {
PersistenceController.shared.delete(sourceList, context: backgroundContext) PersistenceController.shared.delete(pluginList, context: backgroundContext)
} label: { } label: {
Text("Remove") Text("Remove")
Image(systemName: "trash") Image(systemName: "trash")
@ -66,7 +66,7 @@ struct SettingsSourceListView: View {
} }
.onDelete { offsets in .onDelete { offsets in
for index in offsets { for index in offsets {
if let list = sourceLists[safe: index] { if let list = pluginLists[safe: index] {
PersistenceController.shared.delete(list, context: backgroundContext) PersistenceController.shared.delete(list, context: backgroundContext)
} }
} }
@ -78,13 +78,13 @@ struct SettingsSourceListView: View {
} }
.sheet(isPresented: $presentSourceSheet) { .sheet(isPresented: $presentSourceSheet) {
if #available(iOS 16, *) { if #available(iOS 16, *) {
SourceListEditorView() PluginListEditorView(selectedPluginList: selectedPluginList)
.presentationDetents([.medium]) .presentationDetents([.medium])
} else { } else {
SourceListEditorView() PluginListEditorView(selectedPluginList: selectedPluginList)
} }
} }
.navigationTitle("Source Lists") .navigationTitle("Plugin Lists")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@ -98,8 +98,8 @@ struct SettingsSourceListView: View {
} }
} }
struct SettingsSourceListView_Previews: PreviewProvider { struct SettingsPluginListView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
SettingsSourceListView() SettingsPluginListView()
} }
} }

View file

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

View file

@ -12,7 +12,7 @@ struct ContentView: View {
@EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var pluginManager: PluginManager
@FetchRequest( @FetchRequest(
entity: Source.entity(), entity: Source.entity(),
@ -87,7 +87,7 @@ struct ContentView: View {
navModel.isSearching = true navModel.isSearching = true
navModel.showSearchProgress = true navModel.showSearchProgress = true
let sources = sourceManager.fetchInstalledSources() let sources = pluginManager.fetchInstalledSources()
await scrapingModel.scanSources(sources: sources) await scrapingModel.scanSources(sources: sources)
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {

View file

@ -14,6 +14,7 @@ struct MainView: View {
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var backupManager: BackupManager @EnvironmentObject var backupManager: BackupManager
@EnvironmentObject var pluginManager: PluginManager
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
@ -36,11 +37,11 @@ struct MainView: View {
} }
.tag(ViewTab.library) .tag(ViewTab.library)
SourcesView() PluginsView()
.tabItem { .tabItem {
Label("Sources", systemImage: "doc.text") Label("Plugins", systemImage: "doc.text")
} }
.tag(ViewTab.sources) .tag(ViewTab.plugins)
SettingsView() SettingsView()
.tabItem { .tabItem {
@ -51,10 +52,12 @@ struct MainView: View {
.sheet(item: $navModel.currentChoiceSheet) { item in .sheet(item: $navModel.currentChoiceSheet) { item in
switch item { switch item {
case .magnet: case .magnet:
MagnetChoiceView() ActionChoiceView()
.environmentObject(debridManager) .environmentObject(debridManager)
.environmentObject(scrapingModel) .environmentObject(scrapingModel)
.environmentObject(navModel) .environmentObject(navModel)
.environmentObject(pluginManager)
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
case .batch: case .batch:
BatchChoiceView() BatchChoiceView()
.environmentObject(debridManager) .environmentObject(debridManager)
@ -101,30 +104,42 @@ struct MainView: View {
backupManager.showRestoreAlert.toggle() backupManager.showRestoreAlert.toggle()
} }
} }
// Global alerts for backups // Global alerts and dialogs for backups
.backport.alert( .backport.confirmationDialog(
isPresented: $backupManager.showRestoreAlert, isPresented: $backupManager.showRestoreAlert,
title: "Restore backup?", 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: [ buttons: [
.init("Restore", role: .destructive) { .init("Merge", role: .destructive) {
backupManager.restoreBackup() 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( .backport.alert(
isPresented: $backupManager.showRestoreCompletedAlert, isPresented: $backupManager.showRestoreCompletedAlert,
title: "Backup restored", title: "Backup restored",
message: backupManager.backupSourceNames.isEmpty ? message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"),
"No sources need to be reinstalled" : buttons: [
"Reinstall sources: \(backupManager.backupSourceNames.joined(separator: ", "))" .init("OK") {
backupManager.restoreCompletedMessage = []
}
]
) )
// Updater alert // Updater alert
.backport.alert( .backport.alert(
isPresented: $showUpdateAlert, isPresented: $showUpdateAlert,
title: "Update available", 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: [ buttons: [
.init("Download") { .init("Download") {
guard let releaseUrl = URL(string: releaseUrlString) else { guard let releaseUrl = URL(string: releaseUrlString) else {

View file

@ -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<Source>
@FetchRequest(
entity: Action.entity(),
sortDescriptors: []
) var actions: FetchedResults<Action>
@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<Void, Never>?
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<Source, SourceJson>(searchText: $searchText)
case .actions:
PluginListView<Action, ActionJson>(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()
}
}

View file

@ -11,7 +11,7 @@ import SwiftUI
struct SettingsView: View { struct SettingsView: View {
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var pluginManager: PluginManager
let backgroundContext = PersistenceController.shared.backgroundContext let backgroundContext = PersistenceController.shared.backgroundContext
@ -84,8 +84,8 @@ struct SettingsView: View {
} }
} }
Section(header: Text("Source management")) { Section(header: Text("Plugin management")) {
NavigationLink("Source lists", destination: SettingsSourceListView()) NavigationLink("Plugin lists", destination: SettingsPluginListView())
} }
Section(header: Text("Default actions")) { Section(header: Text("Default actions")) {

View file

@ -8,14 +8,18 @@
import SwiftUI import SwiftUI
import SwiftUIX import SwiftUIX
struct MagnetChoiceView: View { struct ActionChoiceView: View {
@Environment(\.presentationMode) var presentationMode @Environment(\.presentationMode) var presentationMode
@EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var scrapingModel: ScrapingViewModel
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
@EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var navModel: NavigationViewModel
@EnvironmentObject var pluginManager: PluginManager
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false @FetchRequest(
entity: Action.entity(),
sortDescriptors: []
) var actions: FetchedResults<Action>
@State private var showLinkCopyAlert = false @State private var showLinkCopyAlert = false
@State private var showMagnetCopyAlert = false @State private var showMagnetCopyAlert = false
@ -39,16 +43,12 @@ struct MagnetChoiceView: View {
if !debridManager.downloadUrl.isEmpty { if !debridManager.downloadUrl.isEmpty {
Section(header: "Debrid options") { Section(header: "Debrid options") {
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { ForEach(actions, id: \.id) { action in
navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer) if action.requires.contains(ActionRequirement.debrid.rawValue) {
} ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
pluginManager.runDeeplinkAction(action, urlString: debridManager.downloadUrl)
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)
} }
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
@ -73,6 +73,14 @@ struct MagnetChoiceView: View {
if !navModel.resultFromCloud { if !navModel.resultFromCloud {
Section(header: "Magnet options") { 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") { ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
UIPasteboard.general.string = navModel.selectedMagnet?.link UIPasteboard.general.string = navModel.selectedMagnet?.link
showMagnetCopyAlert.toggle() showMagnetCopyAlert.toggle()
@ -92,10 +100,6 @@ struct MagnetChoiceView: View {
navModel.showLocalActivitySheet.toggle() 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 { static var previews: some View {
MagnetChoiceView() ActionChoiceView()
} }
} }

View file

@ -79,7 +79,7 @@ struct BatchChoiceView: View {
if var selectedHistoryInfo = navModel.selectedHistoryInfo { if var selectedHistoryInfo = navModel.selectedHistoryInfo {
selectedHistoryInfo.url = debridManager.downloadUrl selectedHistoryInfo.url = debridManager.downloadUrl
selectedHistoryInfo.subName = fileName selectedHistoryInfo.subName = fileName
PersistenceController.shared.createHistory(selectedHistoryInfo) PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true)
} }
navModel.runDebridAction(urlString: debridManager.downloadUrl) navModel.runDebridAction(urlString: debridManager.downloadUrl)

View file

@ -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<Void, Never>? = 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<Source>) 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()
}
}