mirror of
https://github.com/Ferrite-iOS/Ferrite.git
synced 2026-01-11 20:10:27 +00:00
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:
parent
6b0f90178b
commit
4512318e8f
51 changed files with 1470 additions and 609 deletions
|
|
@ -8,20 +8,26 @@
|
|||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; };
|
||||
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */; };
|
||||
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */; };
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; };
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */; };
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; };
|
||||
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; };
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */; };
|
||||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */; };
|
||||
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */; };
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */; };
|
||||
|
|
@ -35,8 +41,13 @@
|
|||
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
|
||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
|
||||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005512992B6750064606A /* PluginTagsView.swift */; };
|
||||
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */; };
|
||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */; };
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
|
||||
0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */; };
|
||||
0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */; };
|
||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||
|
|
@ -53,9 +64,8 @@
|
|||
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; };
|
||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; };
|
||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; };
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; };
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */; };
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */; };
|
||||
0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */; };
|
||||
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */; };
|
||||
0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; };
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; };
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; };
|
||||
|
|
@ -68,16 +78,14 @@
|
|||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */; };
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */; };
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */; };
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */; };
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */; };
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */; };
|
||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; };
|
||||
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceListView.swift */; };
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.swift */; };
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */; };
|
||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsPluginListView.swift */; };
|
||||
0CA05459288EE9E600850554 /* PluginManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* PluginManager.swift */; };
|
||||
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */; };
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BB288903F000DE2211 /* SettingsView.swift */; };
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BC288903F000DE2211 /* LoginWebView.swift */; };
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */; };
|
||||
0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* ActionChoiceView.swift */; };
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C1288903F000DE2211 /* NavView.swift */; };
|
||||
0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; };
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */; };
|
||||
|
|
@ -110,30 +118,40 @@
|
|||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
|
||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389512970AD900066D06F /* Action+CoreDataClass.swift */; };
|
||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */; };
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; };
|
||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
|
||||
0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */ = {isa = PBXBuildFile; productRef = 0CDDDE042935235E006810B1 /* BetterSafariView */; };
|
||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE1C4172981E8D700418F20 /* Plugin.swift */; };
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -146,8 +164,13 @@
|
|||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -160,9 +183,8 @@
|
|||
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>"; };
|
||||
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 /* InstalledSourceButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledPluginButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginCatalogButtonView.swift; 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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = "<group>"; };
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = "<group>"; };
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = "<group>"; };
|
||||
0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -216,11 +236,14 @@
|
|||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -246,7 +269,7 @@
|
|||
0C0755C22934241F00ECA142 /* SheetViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
0CA148BD288903F000DE2211 /* ActionChoiceView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
);
|
||||
path = SheetViews;
|
||||
|
|
@ -255,11 +278,11 @@
|
|||
0C0755C32934244500ECA142 /* ComponentViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C3E00D4296F560800ECECB2 /* Plugin */,
|
||||
0C0755C42934245800ECA142 /* Debrid */,
|
||||
0CA3B23528C265FD00616D3A /* Library */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResult */,
|
||||
0CA0545C288F7CB200850554 /* Settings */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
);
|
||||
path = ComponentViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -276,6 +299,12 @@
|
|||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */,
|
||||
0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */,
|
||||
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */,
|
||||
0CC389522970AD900066D06F /* Action+CoreDataProperties.swift */,
|
||||
0C03EB6F296F619900162E9A /* PluginList+CoreDataClass.swift */,
|
||||
0C03EB70296F619900162E9A /* PluginList+CoreDataProperties.swift */,
|
||||
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */,
|
||||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */,
|
||||
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */,
|
||||
|
|
@ -292,8 +321,6 @@
|
|||
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */,
|
||||
0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */,
|
||||
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */,
|
||||
0C84F47E2895BFED0074B7C9 /* SourceList+CoreDataClass.swift */,
|
||||
0C84F47F2895BFED0074B7C9 /* SourceList+CoreDataProperties.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -301,6 +328,7 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */,
|
||||
0C6C7C9C29315292002DF910 /* AllDebridModels.swift */,
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */,
|
||||
|
|
@ -310,6 +338,7 @@
|
|||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -324,6 +353,17 @@
|
|||
path = Cloud;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C3E00D4296F560800ECECB2 /* Plugin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
0C0D50E6288DFF850035ECC8 /* PluginListView.swift */,
|
||||
0C5005512992B6750064606A /* PluginTagsView.swift */,
|
||||
);
|
||||
path = Plugin;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2A628D4DDC6007711AE /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -340,6 +380,7 @@
|
|||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
|
||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
||||
0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */,
|
||||
);
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -347,9 +388,8 @@
|
|||
0C44E2AA28D4E09B007711AE /* Buttons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */,
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */,
|
||||
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */,
|
||||
0C794B68289DACC800DD1CC8 /* InstalledPluginButtonView.swift */,
|
||||
0C794B6A289DACF100DD1CC8 /* PluginCatalogButtonView.swift */,
|
||||
);
|
||||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -363,10 +403,17 @@
|
|||
path = SearchResult;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C5005552992B9C20064606A /* Protocols */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CE1C4172981E8D700418F20 /* Plugin.swift */,
|
||||
);
|
||||
path = Protocols;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
|
||||
);
|
||||
path = Source;
|
||||
|
|
@ -376,8 +423,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsSourceListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
);
|
||||
|
|
@ -394,6 +441,7 @@
|
|||
0CA148EF2889061600DE2211 /* ViewModels */,
|
||||
0CA148EE2889061200DE2211 /* Views */,
|
||||
0C44E2A628D4DDC6007711AE /* Classes */,
|
||||
0C5005552992B9C20064606A /* Protocols */,
|
||||
0CA148C8288903F000DE2211 /* Extensions */,
|
||||
0CA148C5288903F000DE2211 /* Preview Content */,
|
||||
0CA148C7288903F000DE2211 /* FerriteApp.swift */,
|
||||
|
|
@ -415,6 +463,7 @@
|
|||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */,
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||
0C2D9652299316CC00A504B6 /* Tag.swift */,
|
||||
);
|
||||
path = CommonViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -457,7 +506,7 @@
|
|||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
0CA3B23328C2658700616D3A /* LibraryView.swift */,
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
||||
0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */,
|
||||
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||
|
|
@ -472,7 +521,7 @@
|
|||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */,
|
||||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */,
|
||||
0CA05458288EE9E600850554 /* PluginManager.swift */,
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
|
|
@ -482,6 +531,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148CE288903F000DE2211 /* WebView.swift */,
|
||||
0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */,
|
||||
);
|
||||
path = RepresentableViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -652,46 +702,55 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
|
||||
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */,
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */,
|
||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||
0C2D9653299316CC00A504B6 /* Tag.swift in Sources */,
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */,
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */,
|
||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||
0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */,
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* PluginListEditorView.swift in Sources */,
|
||||
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */,
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
||||
0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */,
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
||||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */,
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||
0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */,
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */,
|
||||
0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */,
|
||||
0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */,
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */,
|
||||
0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */,
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
|
||||
0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */,
|
||||
|
|
@ -699,8 +758,7 @@
|
|||
0C445C62293F9A0B0060744D /* Bundle.swift in Sources */,
|
||||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */,
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */,
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */,
|
||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||
|
|
@ -712,7 +770,6 @@
|
|||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||
0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */,
|
||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -722,23 +779,27 @@
|
|||
0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */,
|
||||
0C42B5982932F6DD008057A0 /* Set.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* PluginManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */,
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */,
|
||||
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */,
|
||||
0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */,
|
||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||
0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */,
|
||||
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
|
||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||
|
|
@ -1076,9 +1137,10 @@
|
|||
0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */,
|
||||
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */,
|
||||
);
|
||||
currentVersion = 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */;
|
||||
currentVersion = 0C3E00D9296F5E4F00ECECB2 /* FerriteDB_v2.xcdatamodel */;
|
||||
path = FerriteDB.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
|
|
|
|||
13
Ferrite/DataManagement/Classes/Action+CoreDataClass.swift
Normal file
13
Ferrite/DataManagement/Classes/Action+CoreDataClass.swift
Normal 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 {}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -10,15 +10,4 @@ import CoreData
|
|||
import Foundation
|
||||
|
||||
@objc(Bookmark)
|
||||
public class Bookmark: NSManagedObject {
|
||||
func toSearchResult() -> SearchResult {
|
||||
SearchResult(
|
||||
title: title,
|
||||
source: source,
|
||||
size: size,
|
||||
magnet: Magnet(hash: magnetHash, link: magnetLink),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
}
|
||||
}
|
||||
public class Bookmark: NSManagedObject {}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,17 @@ public extension Bookmark {
|
|||
@NSManaged var source: String
|
||||
@NSManaged var title: String?
|
||||
@NSManaged var orderNum: Int16
|
||||
|
||||
func toSearchResult() -> SearchResult {
|
||||
SearchResult(
|
||||
title: title,
|
||||
source: source,
|
||||
size: size,
|
||||
magnet: Magnet(hash: magnetHash, link: magnetLink),
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
extension Bookmark: Identifiable {}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
14
Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift
Normal file
14
Ferrite/DataManagement/Classes/PluginTag+CoreDataClass.swift
Normal 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 {
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
@ -10,4 +10,4 @@ import CoreData
|
|||
import Foundation
|
||||
|
||||
@objc(Source)
|
||||
public class Source: NSManagedObject {}
|
||||
public class Source: NSManagedObject, Plugin {}
|
||||
|
|
|
|||
|
|
@ -2,33 +2,77 @@
|
|||
// Source+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/3/22.
|
||||
// Created by Brian Dashore on 2/6/23.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
public extension Source {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source> {
|
||||
NSFetchRequest<Source>(entityName: "Source")
|
||||
|
||||
extension Source {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Source> {
|
||||
return NSFetchRequest<Source>(entityName: "Source")
|
||||
}
|
||||
|
||||
@NSManaged var id: UUID
|
||||
@NSManaged var baseUrl: String?
|
||||
@NSManaged var fallbackUrls: [String]?
|
||||
@NSManaged var dynamicBaseUrl: Bool
|
||||
@NSManaged var enabled: Bool
|
||||
@NSManaged var name: String
|
||||
@NSManaged var author: String
|
||||
@NSManaged var listId: UUID?
|
||||
@NSManaged var preferredParser: Int16
|
||||
@NSManaged var version: Int16
|
||||
@NSManaged var htmlParser: SourceHtmlParser?
|
||||
@NSManaged var rssParser: SourceRssParser?
|
||||
@NSManaged var jsonParser: SourceJsonParser?
|
||||
@NSManaged var api: SourceApi?
|
||||
@NSManaged var trackers: [String]?
|
||||
@NSManaged public var id: UUID
|
||||
@NSManaged public var baseUrl: String?
|
||||
@NSManaged public var fallbackUrls: [String]?
|
||||
@NSManaged public var dynamicBaseUrl: Bool
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var author: String
|
||||
@NSManaged public var listId: UUID?
|
||||
@NSManaged public var preferredParser: Int16
|
||||
@NSManaged public var version: Int16
|
||||
@NSManaged public var htmlParser: SourceHtmlParser?
|
||||
@NSManaged public var rssParser: SourceRssParser?
|
||||
@NSManaged public var jsonParser: SourceJsonParser?
|
||||
@NSManaged public var api: SourceApi?
|
||||
@NSManaged public var trackers: [String]?
|
||||
@NSManaged public var tags: NSOrderedSet?
|
||||
|
||||
public func getTags() -> [PluginTagJson] {
|
||||
return tagArray.map { $0.toJson() }
|
||||
}
|
||||
}
|
||||
|
||||
extension Source: Identifiable {}
|
||||
// MARK: Generated accessors for tags
|
||||
extension Source {
|
||||
|
||||
@objc(insertObject:inTagsAtIndex:)
|
||||
@NSManaged public func insertIntoTags(_ value: PluginTag, at idx: Int)
|
||||
|
||||
@objc(removeObjectFromTagsAtIndex:)
|
||||
@NSManaged public func removeFromTags(at idx: Int)
|
||||
|
||||
@objc(insertTags:atIndexes:)
|
||||
@NSManaged public func insertIntoTags(_ values: [PluginTag], at indexes: NSIndexSet)
|
||||
|
||||
@objc(removeTagsAtIndexes:)
|
||||
@NSManaged public func removeFromTags(at indexes: NSIndexSet)
|
||||
|
||||
@objc(replaceObjectInTagsAtIndex:withObject:)
|
||||
@NSManaged public func replaceTags(at idx: Int, with value: PluginTag)
|
||||
|
||||
@objc(replaceTagsAtIndexes:withTags:)
|
||||
@NSManaged public func replaceTags(at indexes: NSIndexSet, with values: [PluginTag])
|
||||
|
||||
@objc(addTagsObject:)
|
||||
@NSManaged public func addToTags(_ value: PluginTag)
|
||||
|
||||
@objc(removeTagsObject:)
|
||||
@NSManaged public func removeFromTags(_ value: PluginTag)
|
||||
|
||||
@objc(addTags:)
|
||||
@NSManaged public func addToTags(_ values: NSOrderedSet)
|
||||
|
||||
@objc(removeTags:)
|
||||
@NSManaged public func removeFromTags(_ values: NSOrderedSet)
|
||||
|
||||
}
|
||||
|
||||
extension Source : Identifiable {
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -3,6 +3,6 @@
|
|||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>FerriteDB.xcdatamodel</string>
|
||||
<string>FerriteDB_v2.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -91,7 +91,7 @@ struct PersistenceController {
|
|||
save()
|
||||
}
|
||||
|
||||
func createBookmark(_ bookmarkJson: BookmarkJson) {
|
||||
func createBookmark(_ bookmarkJson: BookmarkJson, performSave: Bool) {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
bookmarkRequest.predicate = NSPredicate(
|
||||
format: "source == %@ AND title == %@ AND magnetLink == %@",
|
||||
|
|
@ -113,32 +113,31 @@ struct PersistenceController {
|
|||
newBookmark.seeders = bookmarkJson.seeders
|
||||
newBookmark.leechers = bookmarkJson.leechers
|
||||
|
||||
save(backgroundContext)
|
||||
if performSave {
|
||||
save(backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
func createHistory(_ entryJson: HistoryEntryJson, date: Double? = nil) {
|
||||
func createHistory(_ entryJson: HistoryEntryJson, performSave: Bool, isBackup: Bool = false, date: Double? = nil) {
|
||||
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
|
||||
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
|
||||
|
||||
let newHistoryEntry = HistoryEntry(context: backgroundContext)
|
||||
|
||||
newHistoryEntry.source = entryJson.source
|
||||
newHistoryEntry.name = entryJson.name
|
||||
newHistoryEntry.url = entryJson.url
|
||||
newHistoryEntry.subName = entryJson.subName
|
||||
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
|
||||
|
||||
let historyRequest = History.fetchRequest()
|
||||
historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString)
|
||||
var existingHistory: History?
|
||||
|
||||
// Safely add entries to a parent history if it exists
|
||||
if var histories = try? backgroundContext.fetch(historyRequest) {
|
||||
for (i, history) in histories.enumerated() {
|
||||
let existingEntries = history.entryArray.filter { $0.url == newHistoryEntry.url && $0.name == newHistoryEntry.name }
|
||||
let existingEntries = history.entryArray.filter { $0.url == entryJson.url && $0.name == entryJson.name }
|
||||
|
||||
// Maybe add !isBackup here
|
||||
if !existingEntries.isEmpty {
|
||||
for entry in existingEntries {
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
if isBackup {
|
||||
continue
|
||||
} else {
|
||||
for entry in existingEntries {
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,15 +147,24 @@ struct PersistenceController {
|
|||
}
|
||||
}
|
||||
|
||||
newHistoryEntry.parentHistory = histories.first ?? History(context: backgroundContext)
|
||||
} else {
|
||||
newHistoryEntry.parentHistory = History(context: backgroundContext)
|
||||
existingHistory = histories.first
|
||||
}
|
||||
|
||||
let newHistoryEntry = HistoryEntry(context: backgroundContext)
|
||||
|
||||
newHistoryEntry.source = entryJson.source
|
||||
newHistoryEntry.name = entryJson.name
|
||||
newHistoryEntry.url = entryJson.url
|
||||
newHistoryEntry.subName = entryJson.subName
|
||||
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
|
||||
|
||||
newHistoryEntry.parentHistory = existingHistory ?? History(context: backgroundContext)
|
||||
newHistoryEntry.parentHistory?.dateString = historyDateString
|
||||
newHistoryEntry.parentHistory?.date = historyDate
|
||||
|
||||
save(backgroundContext)
|
||||
if performSave {
|
||||
save(backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {
|
||||
|
|
@ -200,8 +208,7 @@ struct PersistenceController {
|
|||
return predicate
|
||||
}
|
||||
|
||||
// Always use the background context to batch delete
|
||||
// Merge changes into both contexts to update views
|
||||
// Wrapper to batch delete history objects
|
||||
func batchDeleteHistory(range: HistoryDeleteRange) throws {
|
||||
let predicate = getHistoryPredicate(range: range)
|
||||
|
||||
|
|
@ -213,6 +220,13 @@ struct PersistenceController {
|
|||
throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?")
|
||||
}
|
||||
|
||||
try batchDelete("History", predicate: predicate)
|
||||
}
|
||||
|
||||
// Always use the background context to batch delete
|
||||
// Merge changes into both contexts to update views
|
||||
func batchDelete(_ entity: String, predicate: NSPredicate? = nil) throws {
|
||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entity)
|
||||
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
batchDeleteRequest.resultType = .resultTypeObjectIDs
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ struct FerriteApp: App {
|
|||
@StateObject var toastModel: ToastViewModel = .init()
|
||||
@StateObject var debridManager: DebridManager = .init()
|
||||
@StateObject var navModel: NavigationViewModel = .init()
|
||||
@StateObject var sourceManager: SourceManager = .init()
|
||||
@StateObject var pluginManager: PluginManager = .init()
|
||||
@StateObject var backupManager: BackupManager = .init()
|
||||
|
||||
var body: some Scene {
|
||||
|
|
@ -24,7 +24,7 @@ struct FerriteApp: App {
|
|||
.onAppear {
|
||||
scrapingModel.toastModel = toastModel
|
||||
debridManager.toastModel = toastModel
|
||||
sourceManager.toastModel = toastModel
|
||||
pluginManager.toastModel = toastModel
|
||||
backupManager.toastModel = toastModel
|
||||
navModel.toastModel = toastModel
|
||||
}
|
||||
|
|
@ -32,7 +32,7 @@ struct FerriteApp: App {
|
|||
.environmentObject(scrapingModel)
|
||||
.environmentObject(toastModel)
|
||||
.environmentObject(navModel)
|
||||
.environmentObject(sourceManager)
|
||||
.environmentObject(pluginManager)
|
||||
.environmentObject(backupManager)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
}
|
||||
|
|
|
|||
32
Ferrite/Models/ActionModels.swift
Normal file
32
Ferrite/Models/ActionModels.swift
Normal 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
|
||||
}
|
||||
|
|
@ -12,7 +12,11 @@ public struct Backup: Codable {
|
|||
var bookmarks: [BookmarkJson]?
|
||||
var history: [HistoryJson]?
|
||||
var sourceNames: [String]?
|
||||
var sourceLists: [SourceListBackupJson]?
|
||||
var actionNames: [String]?
|
||||
var pluginListUrls: [String]?
|
||||
|
||||
// MARK: Remove once v1 backups are unsupported
|
||||
var sourceLists: [PluginListBackupJson]?
|
||||
}
|
||||
|
||||
// MARK: - CoreData translation
|
||||
|
|
@ -43,8 +47,8 @@ struct HistoryEntryJson: Codable {
|
|||
let source: String?
|
||||
}
|
||||
|
||||
// Differs from SourceListJson
|
||||
struct SourceListBackupJson: Codable {
|
||||
// Differs from PluginListJson
|
||||
struct PluginListBackupJson: Codable {
|
||||
let name: String
|
||||
let author: String
|
||||
let id: String
|
||||
|
|
|
|||
32
Ferrite/Models/PluginModels.swift
Normal file
32
Ferrite/Models/PluginModels.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,26 +12,28 @@ public enum ApiCredentialResponseType: String, Codable, Hashable, Sendable {
|
|||
case text
|
||||
}
|
||||
|
||||
public struct SourceListJson: Codable, Sendable {
|
||||
let name: String
|
||||
let author: String
|
||||
var sources: [SourceJson]
|
||||
}
|
||||
|
||||
public struct SourceJson: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let version: Int16
|
||||
public struct SourceJson: Codable, Hashable, Sendable, PluginJson {
|
||||
public let name: String
|
||||
public let version: Int16
|
||||
let minVersion: String?
|
||||
let baseUrl: String?
|
||||
let fallbackUrls: [String]?
|
||||
var dynamicBaseUrl: Bool?
|
||||
var author: String?
|
||||
var listId: UUID?
|
||||
let trackers: [String]?
|
||||
let api: SourceApiJson?
|
||||
let jsonParser: SourceJsonParserJson?
|
||||
let rssParser: SourceRssParserJson?
|
||||
let htmlParser: SourceHtmlParserJson?
|
||||
public var author: String?
|
||||
public var listId: UUID?
|
||||
public var tags: [PluginTagJson]?
|
||||
}
|
||||
|
||||
extension SourceJson {
|
||||
// Fetches all tags without optional requirement
|
||||
public func getTags() -> [PluginTagJson] {
|
||||
return tags ?? []
|
||||
}
|
||||
}
|
||||
|
||||
public enum SourcePreferredParser: Int16, CaseIterable, Sendable {
|
||||
|
|
|
|||
35
Ferrite/Protocols/Plugin.swift
Normal file
35
Ferrite/Protocols/Plugin.swift
Normal 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]
|
||||
}
|
||||
|
|
@ -9,18 +9,33 @@ import Foundation
|
|||
|
||||
public class BackupManager: ObservableObject {
|
||||
// Constant variable for backup versions
|
||||
let latestBackupVersion: Int = 1
|
||||
let latestBackupVersion: Int = 2
|
||||
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var showRestoreAlert = false
|
||||
@Published var showRestoreCompletedAlert = false
|
||||
@Published var restoreCompletedMessage: [String] = []
|
||||
|
||||
@Published var backupUrls: [URL] = []
|
||||
@Published var backupSourceNames: [String] = []
|
||||
@Published var selectedBackupUrl: URL?
|
||||
|
||||
func createBackup() {
|
||||
@MainActor
|
||||
func updateRestoreCompletedMessage(newString: String) {
|
||||
restoreCompletedMessage.append(newString)
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func toggleRestoreCompletedAlert() {
|
||||
showRestoreCompletedAlert.toggle()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func updateBackupUrls(newUrl: URL) {
|
||||
backupUrls.append(newUrl)
|
||||
}
|
||||
|
||||
func createBackup() async {
|
||||
var backup = Backup(version: latestBackupVersion)
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
|
|
@ -71,16 +86,14 @@ public class BackupManager: ObservableObject {
|
|||
backup.sourceNames = sources.map(\.name)
|
||||
}
|
||||
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
if let sourceLists = try? backgroundContext.fetch(sourceListRequest) {
|
||||
backup.sourceLists = sourceLists.map {
|
||||
SourceListBackupJson(
|
||||
name: $0.name,
|
||||
author: $0.author,
|
||||
id: $0.id.uuidString,
|
||||
urlString: $0.urlString
|
||||
)
|
||||
}
|
||||
let actionRequest = Action.fetchRequest()
|
||||
if let actions = try? backgroundContext.fetch(actionRequest) {
|
||||
backup.actionNames = actions.map(\.name)
|
||||
}
|
||||
|
||||
let pluginListRequest = PluginList.fetchRequest()
|
||||
if let pluginLists = try? backgroundContext.fetch(pluginListRequest) {
|
||||
backup.pluginListUrls = pluginLists.map(\.urlString)
|
||||
}
|
||||
|
||||
do {
|
||||
|
|
@ -94,18 +107,20 @@ public class BackupManager: ObservableObject {
|
|||
let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb")
|
||||
|
||||
try encodedJson.write(to: writeUrl)
|
||||
backupUrls.append(writeUrl)
|
||||
|
||||
await updateBackupUrls(newUrl: writeUrl)
|
||||
} catch {
|
||||
print(error)
|
||||
await toastModel?.updateToastDescription("Backup error: \(error)")
|
||||
print("Backup error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
// Backup is in local documents directory, so no need to restore it from the shared URL
|
||||
func restoreBackup() {
|
||||
// Pass the pluginManager reference since it's not used throughout the class like toastModel
|
||||
func restoreBackup(pluginManager: PluginManager, doOverwrite: Bool) async {
|
||||
guard let backupUrl = selectedBackupUrl else {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.")
|
||||
}
|
||||
await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.")
|
||||
print("Backup restore error: Could not find backup in app directory.")
|
||||
|
||||
return
|
||||
}
|
||||
|
|
@ -113,64 +128,72 @@ public class BackupManager: ObservableObject {
|
|||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
do {
|
||||
// Delete all relevant entities to prevent issues with restoration if overwrite is selected
|
||||
if doOverwrite {
|
||||
try PersistenceController.shared.batchDelete("Bookmark")
|
||||
try PersistenceController.shared.batchDelete("History")
|
||||
try PersistenceController.shared.batchDelete("HistoryEntry")
|
||||
try PersistenceController.shared.batchDelete("PluginList")
|
||||
try PersistenceController.shared.batchDelete("Source")
|
||||
try PersistenceController.shared.batchDelete("Action")
|
||||
}
|
||||
|
||||
let file = try Data(contentsOf: backupUrl)
|
||||
|
||||
let backup = try JSONDecoder().decode(Backup.self, from: file)
|
||||
|
||||
if let bookmarks = backup.bookmarks {
|
||||
for bookmark in bookmarks {
|
||||
PersistenceController.shared.createBookmark(bookmark)
|
||||
PersistenceController.shared.createBookmark(bookmark, performSave: false)
|
||||
}
|
||||
}
|
||||
|
||||
if let storedHistories = backup.history {
|
||||
for storedHistory in storedHistories {
|
||||
for storedEntry in storedHistory.entries {
|
||||
PersistenceController.shared.createHistory(storedEntry, date: storedHistory.date)
|
||||
PersistenceController.shared.createHistory(
|
||||
storedEntry,
|
||||
performSave: false,
|
||||
isBackup: true,
|
||||
date: storedHistory.date
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let storedLists = backup.sourceLists {
|
||||
if let storedLists = backup.sourceLists, (backup.version == 1) {
|
||||
// Only present in v1 backups
|
||||
for list in storedLists {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", list.urlString)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", list.author, list.name)
|
||||
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
sourceListRequest.fetchLimit = 1
|
||||
|
||||
if (try? backgroundContext.fetch(sourceListRequest).first) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
let newSourceList = SourceList(context: backgroundContext)
|
||||
newSourceList.name = list.name
|
||||
newSourceList.urlString = list.urlString
|
||||
newSourceList.id = UUID(uuidString: list.id) ?? UUID()
|
||||
newSourceList.author = list.author
|
||||
try await pluginManager.addPluginList(list.urlString, existingPluginList: nil)
|
||||
}
|
||||
} else if let pluginListUrls = backup.pluginListUrls {
|
||||
// v2 and up
|
||||
for listUrl in pluginListUrls {
|
||||
try await pluginManager.addPluginList(listUrl, existingPluginList: nil)
|
||||
}
|
||||
}
|
||||
|
||||
backupSourceNames = backup.sourceNames ?? []
|
||||
if let sourceNames = backup.sourceNames {
|
||||
await updateRestoreCompletedMessage(newString: sourceNames.isEmpty ? "No sources need to be reinstalled" : "Reinstall sources: \(sourceNames.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
if let actionNames = backup.actionNames {
|
||||
await updateRestoreCompletedMessage(newString: actionNames.isEmpty ? "No actions need to be reinstalled" : "Reinstall actions: \(actionNames.joined(separator: ", "))")
|
||||
}
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
|
||||
// if iOS 14 is available, sleep to prevent any issues with alerts
|
||||
if #available(iOS 15, *) {
|
||||
showRestoreCompletedAlert.toggle()
|
||||
await toggleRestoreCompletedAlert()
|
||||
} else {
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 0.1)
|
||||
try? await Task.sleep(seconds: 0.1)
|
||||
|
||||
Task { @MainActor in
|
||||
showRestoreCompletedAlert.toggle()
|
||||
}
|
||||
}
|
||||
await toggleRestoreCompletedAlert()
|
||||
}
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup restore: \(error)")
|
||||
}
|
||||
await toastModel?.updateToastDescription("Backup restore error: \(error)")
|
||||
print("Backup restore error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -187,7 +210,8 @@ public class BackupManager: ObservableObject {
|
|||
}
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup removal: \(error)")
|
||||
await toastModel?.updateToastDescription("Backup removal error: \(error)")
|
||||
print("Backup removal error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import SwiftUI
|
|||
|
||||
enum ViewTab {
|
||||
case search
|
||||
case sources
|
||||
case plugins
|
||||
case settings
|
||||
case library
|
||||
}
|
||||
|
|
@ -56,7 +56,6 @@ class NavigationViewModel: ObservableObject {
|
|||
var selectedSource: Source?
|
||||
|
||||
@Published var showSourceListEditor: Bool = false
|
||||
var selectedSourceList: SourceList?
|
||||
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none
|
||||
|
|
|
|||
|
|
@ -5,27 +5,28 @@
|
|||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
public class SourceManager: ObservableObject {
|
||||
public class PluginManager: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var availableSources: [SourceJson] = []
|
||||
|
||||
var urlErrorAlertText = ""
|
||||
@Published var showUrlErrorAlert = false
|
||||
@Published var availableActions: [ActionJson] = []
|
||||
|
||||
@MainActor
|
||||
public func fetchSourcesFromUrl() async {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
public func fetchPluginsFromUrl() async {
|
||||
let pluginListRequest = PluginList.fetchRequest()
|
||||
do {
|
||||
let sourceLists = try PersistenceController.shared.backgroundContext.fetch(sourceListRequest)
|
||||
var tempAvailableSources: [SourceJson] = []
|
||||
let pluginLists = try PersistenceController.shared.backgroundContext.fetch(pluginListRequest)
|
||||
|
||||
for sourceList in sourceLists {
|
||||
guard let url = URL(string: sourceList.urlString) else {
|
||||
if pluginLists.isEmpty {
|
||||
availableSources = []
|
||||
availableActions = []
|
||||
}
|
||||
|
||||
for pluginList in pluginLists {
|
||||
guard let url = URL(string: pluginList.urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -33,39 +34,107 @@ public class SourceManager: ObservableObject {
|
|||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
let sourceResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
||||
let pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
|
||||
|
||||
for var source in sourceResponse.sources {
|
||||
// If there is a minVersion, check and see if the source is valid
|
||||
if checkAppVersion(minVersion: source.minVersion) {
|
||||
source.author = sourceList.author
|
||||
source.listId = sourceList.id
|
||||
if let sources = pluginResponse.sources {
|
||||
// Faster and more performant to map instead of a for loop
|
||||
availableSources = sources.compactMap { inputJson in
|
||||
if checkAppVersion(minVersion: inputJson.minVersion) {
|
||||
return SourceJson(
|
||||
name: inputJson.name,
|
||||
version: inputJson.version,
|
||||
minVersion: inputJson.minVersion,
|
||||
baseUrl: inputJson.baseUrl,
|
||||
fallbackUrls: inputJson.fallbackUrls,
|
||||
trackers: inputJson.trackers,
|
||||
api: inputJson.api,
|
||||
jsonParser: inputJson.jsonParser,
|
||||
rssParser: inputJson.rssParser,
|
||||
htmlParser: inputJson.htmlParser,
|
||||
author: pluginList.author,
|
||||
listId: pluginList.id,
|
||||
tags: inputJson.tags
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tempAvailableSources.append(source)
|
||||
if let actions = pluginResponse.actions {
|
||||
availableActions = actions.compactMap { inputJson in
|
||||
if checkAppVersion(minVersion: inputJson.minVersion) {
|
||||
return ActionJson(
|
||||
name: inputJson.name,
|
||||
version: inputJson.version,
|
||||
minVersion: inputJson.minVersion,
|
||||
requires: inputJson.requires,
|
||||
deeplink: inputJson.deeplink,
|
||||
author: pluginList.author,
|
||||
listId: pluginList.id,
|
||||
tags: inputJson.tags
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
availableSources = tempAvailableSources
|
||||
} catch {
|
||||
print(error)
|
||||
toastModel?.updateToastDescription("Plugin fetch error: \(error)")
|
||||
print("Plugin fetch error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUpdatedSources(installedSources: FetchedResults<Source>) -> [SourceJson] {
|
||||
var updatedSources: [SourceJson] = []
|
||||
// Check if underlying type is Source or Action
|
||||
func fetchFilteredPlugins<P: Plugin, PJ: PluginJson>(installedPlugins: FetchedResults<P>, searchText: String) -> [PJ] {
|
||||
let availablePlugins: [PJ] = fetchCastedPlugins(PJ.self)
|
||||
|
||||
for source in installedSources {
|
||||
if let availableSource = availableSources.first(where: {
|
||||
source.listId == $0.listId && source.name == $0.name && source.author == $0.author
|
||||
return availablePlugins
|
||||
.filter { availablePlugin in
|
||||
let pluginExists = installedPlugins.contains(where: {
|
||||
availablePlugin.name == $0.name &&
|
||||
availablePlugin.listId == $0.listId &&
|
||||
availablePlugin.author == $0.author
|
||||
})
|
||||
|
||||
if searchText.isEmpty {
|
||||
return !pluginExists
|
||||
} else {
|
||||
return !pluginExists && availablePlugin.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUpdatedPlugins<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
|
||||
|
|
@ -89,7 +158,98 @@ public class SourceManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) async {
|
||||
// The iOS version of Ferrite only runs deeplink actions
|
||||
@MainActor
|
||||
public func runDeeplinkAction(_ action: Action, urlString: String?) {
|
||||
guard let deeplink = action.deeplink, let urlString else {
|
||||
toastModel?.updateToastDescription("Could not run action: \(action.name) since there is no deeplink to execute. Contact the action dev!")
|
||||
print("Could not run action: \(action.name) since there is no deeplink to execute.")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let playbackUrl = URL(string: deeplink.replacingOccurrences(of: "{link}", with: urlString))
|
||||
|
||||
if let playbackUrl {
|
||||
UIApplication.shared.open(playbackUrl)
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not run action: \(action.name) because the created deeplink was invalid. Contact the action dev!")
|
||||
print("Could not run action: \(action.name) because the created deeplink (\(String(describing: playbackUrl))) was invalid")
|
||||
}
|
||||
}
|
||||
|
||||
public func installAction(actionJson: ActionJson?, doUpsert: Bool = false) async {
|
||||
guard let actionJson else {
|
||||
await toastModel?.updateToastDescription("Action addition error: No action present. Contact the app dev!")
|
||||
return
|
||||
}
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if actionJson.requires.count < 1 {
|
||||
await toastModel?.updateToastDescription("Action addition error: actions must require an input. Please contact the action dev!")
|
||||
print("Action name \(actionJson.name) does not have a requires parameter")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
guard let deeplink = actionJson.deeplink else {
|
||||
await toastModel?.updateToastDescription("Action addition error: only deeplink actions can be added to Ferrite iOS. Please contact the action dev!")
|
||||
print("Action name \(actionJson.name) did not have a deeplink")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let existingActionRequest = Action.fetchRequest()
|
||||
existingActionRequest.predicate = NSPredicate(format: "name == %@", actionJson.name)
|
||||
existingActionRequest.fetchLimit = 1
|
||||
|
||||
if let existingAction = try? backgroundContext.fetch(existingActionRequest).first {
|
||||
if doUpsert {
|
||||
PersistenceController.shared.delete(existingAction, context: backgroundContext)
|
||||
} else {
|
||||
await toastModel?.updateToastDescription("Could not install action with name \(actionJson.name) because it is already installed")
|
||||
print("Action name \(actionJson.name) already exists in user's DB")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let newAction = Action(context: backgroundContext)
|
||||
newAction.id = UUID()
|
||||
newAction.name = actionJson.name
|
||||
newAction.version = actionJson.version
|
||||
newAction.author = actionJson.author ?? "Unknown"
|
||||
newAction.listId = actionJson.listId
|
||||
newAction.requires = actionJson.requires.map { $0.rawValue }
|
||||
newAction.enabled = true
|
||||
|
||||
if let jsonTags = actionJson.tags {
|
||||
for tag in jsonTags {
|
||||
let newTag = PluginTag(context: backgroundContext)
|
||||
newTag.name = tag.name
|
||||
newTag.colorHex = tag.colorHex
|
||||
|
||||
newTag.parentAction = newAction
|
||||
}
|
||||
}
|
||||
|
||||
newAction.deeplink = deeplink
|
||||
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription("Action addition error: \(error)")
|
||||
print("Action addition error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func installSource(sourceJson: SourceJson?, doUpsert: Bool = false) async {
|
||||
guard let sourceJson else {
|
||||
await toastModel?.updateToastDescription("Source addition error: No source present. Contact the app dev!")
|
||||
return
|
||||
}
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// If there's no base URL and it isn't dynamic, return before any transactions occur
|
||||
|
|
@ -97,8 +257,8 @@ public class SourceManager: ObservableObject {
|
|||
|
||||
if !dynamicBaseUrl, sourceJson.baseUrl == nil {
|
||||
await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
|
||||
|
||||
print("Not adding this source because base URL parameters are malformed")
|
||||
print("Not adding source \(sourceJson.name) because base URL parameters are malformed")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -112,6 +272,8 @@ public class SourceManager: ObservableObject {
|
|||
PersistenceController.shared.delete(existingSource, context: backgroundContext)
|
||||
} else {
|
||||
await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.")
|
||||
print("Source name \(sourceJson.name) already exists")
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -127,6 +289,16 @@ public class SourceManager: ObservableObject {
|
|||
newSource.listId = sourceJson.listId
|
||||
newSource.trackers = sourceJson.trackers
|
||||
|
||||
if let jsonTags = sourceJson.tags {
|
||||
for tag in jsonTags {
|
||||
let newTag = PluginTag(context: backgroundContext)
|
||||
newTag.name = tag.name
|
||||
newTag.colorHex = tag.colorHex
|
||||
|
||||
newTag.parentSource = newSource
|
||||
}
|
||||
}
|
||||
|
||||
if let sourceApiJson = sourceJson.api {
|
||||
addSourceApi(newSource: newSource, apiJson: sourceApiJson)
|
||||
}
|
||||
|
|
@ -159,7 +331,8 @@ public class SourceManager: ObservableObject {
|
|||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription(error.localizedDescription)
|
||||
await toastModel?.updateToastDescription("Source addition error: \(error)")
|
||||
print("Source addition error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -370,57 +543,44 @@ public class SourceManager: ObservableObject {
|
|||
newSource.htmlParser = newSourceHtmlParser
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func addSourceList(sourceUrl: String, existingSourceList: SourceList?) async -> Bool {
|
||||
// Adds a plugin list
|
||||
// Can move this to PersistenceController if needed
|
||||
public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if sourceUrl.isEmpty || URL(string: sourceUrl) == nil {
|
||||
urlErrorAlertText = "The provided source list is invalid. Please check if the URL is formatted properly."
|
||||
showUrlErrorAlert.toggle()
|
||||
|
||||
return false
|
||||
if url.isEmpty || URL(string: url) == nil {
|
||||
throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.")
|
||||
}
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
||||
let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!))
|
||||
let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data)
|
||||
|
||||
if let existingSourceList {
|
||||
existingSourceList.urlString = sourceUrl
|
||||
existingSourceList.name = rawResponse.name
|
||||
existingSourceList.author = rawResponse.author
|
||||
if let existingPluginList {
|
||||
existingPluginList.urlString = url
|
||||
existingPluginList.name = rawResponse.name
|
||||
existingPluginList.author = rawResponse.author
|
||||
|
||||
try PersistenceController.shared.container.viewContext.save()
|
||||
} else {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", sourceUrl)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
|
||||
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
sourceListRequest.fetchLimit = 1
|
||||
try PersistenceController.shared.container.viewContext.save()
|
||||
} else {
|
||||
let pluginListRequest = PluginList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", url)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name)
|
||||
pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
pluginListRequest.fetchLimit = 1
|
||||
|
||||
if (try? backgroundContext.fetch(sourceListRequest).first) != nil {
|
||||
urlErrorAlertText = "An existing source with this information was found. Please try editing the source list instead."
|
||||
showUrlErrorAlert.toggle()
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
let newSourceUrl = SourceList(context: backgroundContext)
|
||||
newSourceUrl.id = UUID()
|
||||
newSourceUrl.urlString = sourceUrl
|
||||
newSourceUrl.name = rawResponse.name
|
||||
newSourceUrl.author = rawResponse.author
|
||||
|
||||
try backgroundContext.save()
|
||||
if let existingPluginList = try? backgroundContext.fetch(pluginListRequest).first, !isSheet {
|
||||
PersistenceController.shared.delete(existingPluginList, context: backgroundContext)
|
||||
} else if isSheet {
|
||||
throw PluginManagerError.ListAddition(description: "An existing plugin list with this information was found. Please try editing an existing plugin list instead.")
|
||||
}
|
||||
|
||||
return true
|
||||
} catch {
|
||||
print(error)
|
||||
urlErrorAlertText = error.localizedDescription
|
||||
showUrlErrorAlert.toggle()
|
||||
let newPluginList = PluginList(context: backgroundContext)
|
||||
newPluginList.id = UUID()
|
||||
newPluginList.urlString = url
|
||||
newPluginList.name = rawResponse.name
|
||||
newPluginList.author = rawResponse.author
|
||||
|
||||
return false
|
||||
try backgroundContext.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -24,7 +24,7 @@ struct DynamicFetchRequest<T: NSManagedObject, Content: View>: View {
|
|||
sortDescriptors: [NSSortDescriptor] = [],
|
||||
@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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,19 +11,16 @@
|
|||
import SwiftUI
|
||||
|
||||
struct NavView<Content: View>: View {
|
||||
let content: () -> Content
|
||||
init(@ViewBuilder _ content: @escaping () -> Content) {
|
||||
self.content = content
|
||||
}
|
||||
@ViewBuilder var content: Content
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16, *) {
|
||||
NavigationStack {
|
||||
content()
|
||||
content
|
||||
}
|
||||
} else {
|
||||
NavigationView {
|
||||
content()
|
||||
content
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
|
|
|
|||
28
Ferrite/Views/CommonViews/Tag.swift
Normal file
28
Ferrite/Views/CommonViews/Tag.swift
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -15,31 +15,31 @@ struct DebridLabelView: View {
|
|||
|
||||
var body: some View {
|
||||
if let selectedDebridType = debridManager.selectedDebridType {
|
||||
Text(selectedDebridType.toString(abbreviated: true))
|
||||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
Group {
|
||||
if let magnet, cloudLinks.isEmpty {
|
||||
switch debridManager.matchMagnetHash(magnet) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
} else if cloudLinks.count == 1 {
|
||||
Color.green
|
||||
} else if cloudLinks.count > 1 {
|
||||
Color.orange
|
||||
} else {
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
Tag(
|
||||
name: selectedDebridType.toString(abbreviated: true),
|
||||
color: getTagColor(),
|
||||
horizontalPadding: 5,
|
||||
verticalPadding: 3
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func getTagColor() -> Color {
|
||||
if let magnet, cloudLinks.isEmpty {
|
||||
switch debridManager.matchMagnetHash(magnet) {
|
||||
case .full:
|
||||
return Color.green
|
||||
case .partial:
|
||||
return Color.orange
|
||||
case .none:
|
||||
return Color.red
|
||||
}
|
||||
} else if cloudLinks.count == 1 {
|
||||
return Color.green
|
||||
} else if cloudLinks.count > 1 {
|
||||
return Color.orange
|
||||
} else {
|
||||
return Color.red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ struct AllDebridCloudView: View {
|
|||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo)
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,8 @@ struct PremiumizeCloudView: View {
|
|||
name: item.name,
|
||||
url: debridManager.downloadUrl,
|
||||
source: DebridType.premiumize.toString()
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
|
|
|
|||
|
|
@ -31,7 +31,8 @@ struct RealDebridCloudView: View {
|
|||
name: downloadResponse.filename,
|
||||
url: downloadResponse.download,
|
||||
source: DebridType.realDebrid.toString()
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
|
|
@ -69,7 +70,7 @@ struct RealDebridCloudView: View {
|
|||
await debridManager.fetchDebridDownload(magnet: nil, cloudInfo: torrentLink)
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
historyInfo.url = debridManager.downloadUrl
|
||||
PersistenceController.shared.createHistory(historyInfo)
|
||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -77,4 +77,12 @@ struct HistoryButtonView: View {
|
|||
.backport.tint(.primary)
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
}
|
||||
|
||||
func getTagColor() -> Color {
|
||||
if let url = entry.url, url.starts(with: "https://") {
|
||||
return Color.green
|
||||
} else {
|
||||
return Color.red
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,52 +7,60 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct InstalledSourceButtonView: View {
|
||||
struct InstalledPluginButtonView<P: Plugin>: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@ObservedObject var installedSource: Source
|
||||
@ObservedObject var installedPlugin: P
|
||||
|
||||
var body: some View {
|
||||
Toggle(isOn: Binding<Bool>(
|
||||
get: { installedSource.enabled },
|
||||
get: { installedPlugin.enabled },
|
||||
set: {
|
||||
installedSource.enabled = $0
|
||||
installedPlugin.enabled = $0
|
||||
PersistenceController.shared.save()
|
||||
}
|
||||
)) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(installedSource.name)
|
||||
Text("v\(installedSource.version)")
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(installedPlugin.name)
|
||||
Text("v\(installedPlugin.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(installedPlugin.author)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(installedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
if let tags = installedPlugin.getTags(), !tags.isEmpty {
|
||||
PluginTagsView(tags: tags)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedSource = installedSource
|
||||
navModel.showSourceSettings.toggle()
|
||||
} label: {
|
||||
Text("Settings")
|
||||
Image(systemName: "gear")
|
||||
if let installedSource = installedPlugin as? Source {
|
||||
Button {
|
||||
navModel.selectedSource = installedSource
|
||||
navModel.showSourceSettings.toggle()
|
||||
} label: {
|
||||
Text("Settings")
|
||||
Image(systemName: "gear")
|
||||
}
|
||||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
PersistenceController.shared.delete(installedPlugin, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@
|
|||
import SwiftUI
|
||||
|
||||
struct SourceCatalogButtonView: View {
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let availableSource: SourceJson
|
||||
|
||||
|
|
@ -29,7 +29,7 @@ struct SourceCatalogButtonView: View {
|
|||
|
||||
Button("Install") {
|
||||
Task {
|
||||
await sourceManager.installSource(sourceJson: availableSource)
|
||||
await pluginManager.installSource(sourceJson: availableSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
80
Ferrite/Views/ComponentViews/Plugin/PluginListView.swift
Normal file
80
Ferrite/Views/ComponentViews/Plugin/PluginListView.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift
Normal file
22
Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift
Normal 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) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -17,28 +17,34 @@ struct SourceSettingsView: View {
|
|||
List {
|
||||
if let selectedSource = navModel.selectedSource {
|
||||
Section(header: InlineHeader("Info")) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(selectedSource.name)
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(selectedSource.name)
|
||||
|
||||
Text("v\(selectedSource.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(selectedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Group {
|
||||
Text("ID: \(selectedSource.id)")
|
||||
|
||||
if let listId = selectedSource.listId {
|
||||
Text("List ID: \(listId)")
|
||||
} else {
|
||||
Text("No list ID found. This source should be removed.")
|
||||
Text("v\(selectedSource.version)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
|
||||
Text("by \(selectedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
Group {
|
||||
Text("ID: \(selectedSource.id)")
|
||||
|
||||
if let listId = selectedSource.listId {
|
||||
Text("List ID: \(listId)")
|
||||
} else {
|
||||
Text("No list ID found. This source should be removed.")
|
||||
}
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
|
||||
if let tags = selectedSource.getTags(), !tags.isEmpty {
|
||||
PluginTagsView(tags: tags)
|
||||
}
|
||||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
|
@ -38,7 +38,8 @@ struct SearchResultButtonView: View {
|
|||
name: result.title,
|
||||
url: debridManager.downloadUrl,
|
||||
source: result.source
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
|
|
@ -63,7 +64,8 @@ struct SearchResultButtonView: View {
|
|||
name: result.title,
|
||||
url: result.magnet.link,
|
||||
source: result.source
|
||||
)
|
||||
),
|
||||
performSave: true
|
||||
)
|
||||
|
||||
navModel.runMagnetAction(magnet: result.magnet)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,9 @@ struct BackupsView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
backupManager.createBackup()
|
||||
Task {
|
||||
await backupManager.createBackup()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,37 +7,41 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct SourceListEditorView: View {
|
||||
struct PluginListEditorView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@State private var sourceUrlSet = false
|
||||
@State var selectedPluginList: PluginList?
|
||||
|
||||
@State private var sourceUrl: String = ""
|
||||
@State private var sourceUrlSet = false
|
||||
@State private var showUrlErrorAlert = false
|
||||
|
||||
@State private var pluginListUrl: String = ""
|
||||
@State private var urlErrorAlertText: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
TextField("Enter URL", text: $sourceUrl)
|
||||
TextField("Enter URL", text: $pluginListUrl)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.conditionalId(sourceUrlSet)
|
||||
}
|
||||
.onAppear {
|
||||
sourceUrl = navModel.selectedSourceList?.urlString ?? ""
|
||||
pluginListUrl = selectedPluginList?.urlString ?? ""
|
||||
sourceUrlSet = true
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $sourceManager.showUrlErrorAlert,
|
||||
isPresented: $showUrlErrorAlert,
|
||||
title: "Error",
|
||||
message: sourceManager.urlErrorAlertText
|
||||
message: urlErrorAlertText
|
||||
)
|
||||
.navigationTitle("Editing source list")
|
||||
.navigationTitle("Editing Plugin List")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
|
|
@ -49,25 +53,23 @@ struct SourceListEditorView: View {
|
|||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
Task {
|
||||
if await sourceManager.addSourceList(
|
||||
sourceUrl: sourceUrl,
|
||||
existingSourceList: navModel.selectedSourceList
|
||||
) {
|
||||
do {
|
||||
try await pluginManager.addPluginList(pluginListUrl, existingPluginList: selectedPluginList)
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} catch {
|
||||
urlErrorAlertText = error.localizedDescription
|
||||
showUrlErrorAlert.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
navModel.selectedSourceList = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceListEditorView_Previews: PreviewProvider {
|
||||
struct PluginListEditorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SourceListEditorView()
|
||||
PluginListEditorView()
|
||||
}
|
||||
}
|
||||
|
|
@ -7,40 +7,40 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsSourceListView: View {
|
||||
struct SettingsPluginListView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: SourceList.entity(),
|
||||
entity: PluginList.entity(),
|
||||
sortDescriptors: []
|
||||
) var sourceLists: FetchedResults<SourceList>
|
||||
) var pluginLists: FetchedResults<PluginList>
|
||||
|
||||
@State private var presentSourceSheet = false
|
||||
@State private var selectedSourceList: SourceList?
|
||||
@State private var selectedPluginList: PluginList?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if sourceLists.isEmpty {
|
||||
if pluginLists.isEmpty {
|
||||
EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right")
|
||||
} else {
|
||||
List {
|
||||
ForEach(sourceLists, id: \.self) { sourceList in
|
||||
ForEach(pluginLists, id: \.self) { pluginList in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(sourceList.name)
|
||||
Text(pluginList.name)
|
||||
|
||||
Text(sourceList.author)
|
||||
Text(pluginList.author)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Text("ID: \(sourceList.id)")
|
||||
Text("ID: \(pluginList.id)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedSourceList = sourceList
|
||||
selectedPluginList = pluginList
|
||||
presentSourceSheet.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
|
|
@ -49,14 +49,14 @@ struct SettingsSourceListView: View {
|
|||
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
|
|
@ -66,7 +66,7 @@ struct SettingsSourceListView: View {
|
|||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let list = sourceLists[safe: index] {
|
||||
if let list = pluginLists[safe: index] {
|
||||
PersistenceController.shared.delete(list, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
|
|
@ -78,13 +78,13 @@ struct SettingsSourceListView: View {
|
|||
}
|
||||
.sheet(isPresented: $presentSourceSheet) {
|
||||
if #available(iOS 16, *) {
|
||||
SourceListEditorView()
|
||||
PluginListEditorView(selectedPluginList: selectedPluginList)
|
||||
.presentationDetents([.medium])
|
||||
} else {
|
||||
SourceListEditorView()
|
||||
PluginListEditorView(selectedPluginList: selectedPluginList)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Source Lists")
|
||||
.navigationTitle("Plugin Lists")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
|
@ -98,8 +98,8 @@ struct SettingsSourceListView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct SettingsSourceListView_Previews: PreviewProvider {
|
||||
struct SettingsPluginListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsSourceListView()
|
||||
SettingsPluginListView()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ struct ContentView: View {
|
|||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
|
|
@ -87,7 +87,7 @@ struct ContentView: View {
|
|||
navModel.isSearching = true
|
||||
navModel.showSearchProgress = true
|
||||
|
||||
let sources = sourceManager.fetchInstalledSources()
|
||||
let sources = pluginManager.fetchInstalledSources()
|
||||
await scrapingModel.scanSources(sources: sources)
|
||||
|
||||
if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ struct MainView: View {
|
|||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var backupManager: BackupManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
|
||||
|
||||
|
|
@ -36,11 +37,11 @@ struct MainView: View {
|
|||
}
|
||||
.tag(ViewTab.library)
|
||||
|
||||
SourcesView()
|
||||
PluginsView()
|
||||
.tabItem {
|
||||
Label("Sources", systemImage: "doc.text")
|
||||
Label("Plugins", systemImage: "doc.text")
|
||||
}
|
||||
.tag(ViewTab.sources)
|
||||
.tag(ViewTab.plugins)
|
||||
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
|
|
@ -51,10 +52,12 @@ struct MainView: View {
|
|||
.sheet(item: $navModel.currentChoiceSheet) { item in
|
||||
switch item {
|
||||
case .magnet:
|
||||
MagnetChoiceView()
|
||||
ActionChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
.environmentObject(pluginManager)
|
||||
.environment(\.managedObjectContext, PersistenceController.shared.container.viewContext)
|
||||
case .batch:
|
||||
BatchChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
|
|
@ -101,30 +104,42 @@ struct MainView: View {
|
|||
backupManager.showRestoreAlert.toggle()
|
||||
}
|
||||
}
|
||||
// Global alerts for backups
|
||||
.backport.alert(
|
||||
// Global alerts and dialogs for backups
|
||||
.backport.confirmationDialog(
|
||||
isPresented: $backupManager.showRestoreAlert,
|
||||
title: "Restore backup?",
|
||||
message: "Restoring this backup will merge all your data!",
|
||||
message:
|
||||
"Merge (preferred): Will merge your current data with the backup \n\n" +
|
||||
"Overwrite: Will delete and replace all your data \n\n" +
|
||||
"If Merge causes app instability, uninstall Ferrite and use the Overwrite option.",
|
||||
buttons: [
|
||||
.init("Restore", role: .destructive) {
|
||||
backupManager.restoreBackup()
|
||||
.init("Merge", role: .destructive) {
|
||||
Task {
|
||||
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false)
|
||||
}
|
||||
},
|
||||
.init(role: .cancel)
|
||||
.init("Overwrite", role: .destructive) {
|
||||
Task {
|
||||
await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true)
|
||||
}
|
||||
}
|
||||
]
|
||||
)
|
||||
.backport.alert(
|
||||
isPresented: $backupManager.showRestoreCompletedAlert,
|
||||
title: "Backup restored",
|
||||
message: backupManager.backupSourceNames.isEmpty ?
|
||||
"No sources need to be reinstalled" :
|
||||
"Reinstall sources: \(backupManager.backupSourceNames.joined(separator: ", "))"
|
||||
message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"),
|
||||
buttons: [
|
||||
.init("OK") {
|
||||
backupManager.restoreCompletedMessage = []
|
||||
}
|
||||
]
|
||||
)
|
||||
// Updater alert
|
||||
.backport.alert(
|
||||
isPresented: $showUpdateAlert,
|
||||
title: "Update available",
|
||||
message: "Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings.",
|
||||
message: "Ferrite \(releaseVersionString) can be downloaded. \n\nThis alert can be disabled in Settings.",
|
||||
buttons: [
|
||||
.init("Download") {
|
||||
guard let releaseUrl = URL(string: releaseUrlString) else {
|
||||
|
|
|
|||
111
Ferrite/Views/PluginsView.swift
Normal file
111
Ferrite/Views/PluginsView.swift
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,7 +11,7 @@ import SwiftUI
|
|||
|
||||
struct SettingsView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
|
|
@ -84,8 +84,8 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Source management")) {
|
||||
NavigationLink("Source lists", destination: SettingsSourceListView())
|
||||
Section(header: Text("Plugin management")) {
|
||||
NavigationLink("Plugin lists", destination: SettingsPluginListView())
|
||||
}
|
||||
|
||||
Section(header: Text("Default actions")) {
|
||||
|
|
|
|||
|
|
@ -8,14 +8,18 @@
|
|||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct MagnetChoiceView: View {
|
||||
struct ActionChoiceView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
@FetchRequest(
|
||||
entity: Action.entity(),
|
||||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
|
||||
@State private var showLinkCopyAlert = false
|
||||
@State private var showMagnetCopyAlert = false
|
||||
|
|
@ -39,16 +43,12 @@ struct MagnetChoiceView: View {
|
|||
|
||||
if !debridManager.downloadUrl.isEmpty {
|
||||
Section(header: "Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse)
|
||||
ForEach(actions, id: \.id) { action in
|
||||
if action.requires.contains(ActionRequirement.debrid.rawValue) {
|
||||
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
||||
pluginManager.runDeeplinkAction(action, urlString: debridManager.downloadUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||
|
|
@ -73,6 +73,14 @@ struct MagnetChoiceView: View {
|
|||
|
||||
if !navModel.resultFromCloud {
|
||||
Section(header: "Magnet options") {
|
||||
ForEach(actions, id: \.id) { action in
|
||||
if action.requires.contains(ActionRequirement.magnet.rawValue) {
|
||||
ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") {
|
||||
pluginManager.runDeeplinkAction(action, urlString: navModel.selectedMagnet?.link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = navModel.selectedMagnet?.link
|
||||
showMagnetCopyAlert.toggle()
|
||||
|
|
@ -92,10 +100,6 @@ struct MagnetChoiceView: View {
|
|||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runMagnetAction(magnet: navModel.selectedMagnet, .webtor)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -131,8 +135,8 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
|
||||
struct MagnetChoiceView_Previews: PreviewProvider {
|
||||
struct ActionChoiceView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MagnetChoiceView()
|
||||
ActionChoiceView()
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +79,7 @@ struct BatchChoiceView: View {
|
|||
if var selectedHistoryInfo = navModel.selectedHistoryInfo {
|
||||
selectedHistoryInfo.url = debridManager.downloadUrl
|
||||
selectedHistoryInfo.subName = fileName
|
||||
PersistenceController.shared.createHistory(selectedHistoryInfo)
|
||||
PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true)
|
||||
}
|
||||
|
||||
navModel.runDebridAction(urlString: debridManager.downloadUrl)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue