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

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

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

) in + List { + if !filteredUpdatedPlugins.isEmpty { + Section(header: InlineHeader("Updates")) { + ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in + PluginCatalogButtonView(availablePlugin: updatedPlugin, doUpsert: true) + } + } + } + + if !installedPlugins.isEmpty { + Section(header: InlineHeader("Installed")) { + ForEach(installedPlugins, id: \.self) { source in + InstalledPluginButtonView(installedPlugin: source) + } + } + } + + if !filteredAvailablePlugins.isEmpty { + Section(header: InlineHeader("Catalog")) { + ForEach(filteredAvailablePlugins, id: \.self) { availablePlugin in + if !installedPlugins.contains(where: { + availablePlugin.name == $0.name && + availablePlugin.listId == $0.listId && + availablePlugin.author == $0.author + }) { + PluginCatalogButtonView(availablePlugin: availablePlugin, doUpsert: false) + } + } + } + } + } + .listStyle(.insetGrouped) + .sheet(isPresented: $navModel.showSourceSettings) { + if String(describing: P.self) == "Source" { + SourceSettingsView() + .environmentObject(navModel) + } + } + .onAppear { + filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) + filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) + } + .onChange(of: searchText) { _ in + sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + .onReceive(installedPlugins.publisher.count()) { _ in + filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) + filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift new file mode 100644 index 0000000..568d873 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/PluginTagsView.swift @@ -0,0 +1,22 @@ +// +// PluginTagView.swift +// Ferrite +// +// Created by Brian Dashore on 2/7/23. +// + +import SwiftUI + +struct PluginTagsView: View { + let tags: [PluginTagJson] + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + ForEach(tags, id: \.self) { tag in + Tag(name: tag.name, color: tag.colorHex.map { Color(hexadecimal: $0) }) + } + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift similarity index 84% rename from Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift rename to Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift index 2d70f74..7087a49 100644 --- a/Ferrite/Views/ComponentViews/Source/SourceSettingsView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift @@ -17,28 +17,34 @@ struct SourceSettingsView: View { List { if let selectedSource = navModel.selectedSource { Section(header: InlineHeader("Info")) { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(selectedSource.name) + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(selectedSource.name) - Text("v\(selectedSource.version)") - .foregroundColor(.secondary) - } - - Text("by \(selectedSource.author)") - .foregroundColor(.secondary) - - Group { - Text("ID: \(selectedSource.id)") - - if let listId = selectedSource.listId { - Text("List ID: \(listId)") - } else { - Text("No list ID found. This source should be removed.") + Text("v\(selectedSource.version)") + .foregroundColor(.secondary) } + + Text("by \(selectedSource.author)") + .foregroundColor(.secondary) + + Group { + Text("ID: \(selectedSource.id)") + + if let listId = selectedSource.listId { + Text("List ID: \(listId)") + } else { + Text("No list ID found. This source should be removed.") + } + } + .foregroundColor(.secondary) + .font(.caption) + } + + if let tags = selectedSource.getTags(), !tags.isEmpty { + PluginTagsView(tags: tags) } - .foregroundColor(.secondary) - .font(.caption) } .padding(.vertical, 2) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index 40a1ffa..5933bfd 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -38,7 +38,8 @@ struct SearchResultButtonView: View { name: result.title, url: debridManager.downloadUrl, source: result.source - ) + ), + performSave: true ) navModel.runDebridAction(urlString: debridManager.downloadUrl) @@ -63,7 +64,8 @@ struct SearchResultButtonView: View { name: result.title, url: result.magnet.link, source: result.source - ) + ), + performSave: true ) navModel.runMagnetAction(magnet: result.magnet) diff --git a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift index 6a91153..2c5b416 100644 --- a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift +++ b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift @@ -57,7 +57,9 @@ struct BackupsView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button { - backupManager.createBackup() + Task { + await backupManager.createBackup() + } } label: { Image(systemName: "plus") } diff --git a/Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift b/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift similarity index 59% rename from Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift rename to Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift index 81a4c3b..67b8988 100644 --- a/Ferrite/Views/ComponentViews/Settings/SourceListEditorView.swift +++ b/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift @@ -7,37 +7,41 @@ import SwiftUI -struct SourceListEditorView: View { +struct PluginListEditorView: View { @Environment(\.presentationMode) var presentationMode @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager let backgroundContext = PersistenceController.shared.backgroundContext - @State private var sourceUrlSet = false + @State var selectedPluginList: PluginList? - @State private var sourceUrl: String = "" + @State private var sourceUrlSet = false + @State private var showUrlErrorAlert = false + + @State private var pluginListUrl: String = "" + @State private var urlErrorAlertText: String = "" var body: some View { NavView { Form { - TextField("Enter URL", text: $sourceUrl) + TextField("Enter URL", text: $pluginListUrl) .disableAutocorrection(true) .keyboardType(.URL) .autocapitalization(.none) .conditionalId(sourceUrlSet) } .onAppear { - sourceUrl = navModel.selectedSourceList?.urlString ?? "" + pluginListUrl = selectedPluginList?.urlString ?? "" sourceUrlSet = true } .backport.alert( - isPresented: $sourceManager.showUrlErrorAlert, + isPresented: $showUrlErrorAlert, title: "Error", - message: sourceManager.urlErrorAlertText + message: urlErrorAlertText ) - .navigationTitle("Editing source list") + .navigationTitle("Editing Plugin List") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarLeading) { @@ -49,25 +53,23 @@ struct SourceListEditorView: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { Task { - if await sourceManager.addSourceList( - sourceUrl: sourceUrl, - existingSourceList: navModel.selectedSourceList - ) { + do { + try await pluginManager.addPluginList(pluginListUrl, existingPluginList: selectedPluginList) presentationMode.wrappedValue.dismiss() + } catch { + urlErrorAlertText = error.localizedDescription + showUrlErrorAlert.toggle() } } } } } - .onDisappear { - navModel.selectedSourceList = nil - } } } } -struct SourceListEditorView_Previews: PreviewProvider { +struct PluginListEditorView_Previews: PreviewProvider { static var previews: some View { - SourceListEditorView() + PluginListEditorView() } } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift similarity index 75% rename from Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift rename to Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift index 7ab13d4..4abd6ed 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsSourceListView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift @@ -7,40 +7,40 @@ import SwiftUI -struct SettingsSourceListView: View { +struct SettingsPluginListView: View { let backgroundContext = PersistenceController.shared.backgroundContext @EnvironmentObject var navModel: NavigationViewModel @FetchRequest( - entity: SourceList.entity(), + entity: PluginList.entity(), sortDescriptors: [] - ) var sourceLists: FetchedResults + ) var pluginLists: FetchedResults @State private var presentSourceSheet = false - @State private var selectedSourceList: SourceList? + @State private var selectedPluginList: PluginList? var body: some View { ZStack { - if sourceLists.isEmpty { + if pluginLists.isEmpty { EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right") } else { List { - ForEach(sourceLists, id: \.self) { sourceList in + ForEach(pluginLists, id: \.self) { pluginList in VStack(alignment: .leading, spacing: 5) { - Text(sourceList.name) + Text(pluginList.name) - Text(sourceList.author) + Text(pluginList.author) .foregroundColor(.gray) - Text("ID: \(sourceList.id)") + Text("ID: \(pluginList.id)") .font(.caption) .foregroundColor(.gray) } .padding(.vertical, 2) .contextMenu { Button { - navModel.selectedSourceList = sourceList + selectedPluginList = pluginList presentSourceSheet.toggle() } label: { Text("Edit") @@ -49,14 +49,14 @@ struct SettingsSourceListView: View { if #available(iOS 15.0, *) { Button(role: .destructive) { - PersistenceController.shared.delete(sourceList, context: backgroundContext) + PersistenceController.shared.delete(pluginList, context: backgroundContext) } label: { Text("Remove") Image(systemName: "trash") } } else { Button { - PersistenceController.shared.delete(sourceList, context: backgroundContext) + PersistenceController.shared.delete(pluginList, context: backgroundContext) } label: { Text("Remove") Image(systemName: "trash") @@ -66,7 +66,7 @@ struct SettingsSourceListView: View { } .onDelete { offsets in for index in offsets { - if let list = sourceLists[safe: index] { + if let list = pluginLists[safe: index] { PersistenceController.shared.delete(list, context: backgroundContext) } } @@ -78,13 +78,13 @@ struct SettingsSourceListView: View { } .sheet(isPresented: $presentSourceSheet) { if #available(iOS 16, *) { - SourceListEditorView() + PluginListEditorView(selectedPluginList: selectedPluginList) .presentationDetents([.medium]) } else { - SourceListEditorView() + PluginListEditorView(selectedPluginList: selectedPluginList) } } - .navigationTitle("Source Lists") + .navigationTitle("Plugin Lists") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -98,8 +98,8 @@ struct SettingsSourceListView: View { } } -struct SettingsSourceListView_Previews: PreviewProvider { +struct SettingsPluginListView_Previews: PreviewProvider { static var previews: some View { - SettingsSourceListView() + SettingsPluginListView() } } diff --git a/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift b/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift deleted file mode 100644 index 2b0f1f2..0000000 --- a/Ferrite/Views/ComponentViews/Source/Buttons/SourceUpdateButtonView.swift +++ /dev/null @@ -1,38 +0,0 @@ -// -// SourceUpdateButtonView.swift -// Ferrite -// -// Created by Brian Dashore on 8/5/22. -// - -import SwiftUI - -struct SourceUpdateButtonView: View { - @EnvironmentObject var sourceManager: SourceManager - - let updatedSource: SourceJson - - var body: some View { - HStack { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(updatedSource.name) - Text("v\(updatedSource.version)") - .foregroundColor(.secondary) - } - - Text("by \(updatedSource.author ?? "Unknown")") - .foregroundColor(.secondary) - } - .padding(.vertical, 2) - - Spacer() - - Button("Update") { - Task { - await sourceManager.installSource(sourceJson: updatedSource, doUpsert: true) - } - } - } - } -} diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 28471f6..88e1ba8 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -12,7 +12,7 @@ struct ContentView: View { @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager @FetchRequest( entity: Source.entity(), @@ -87,7 +87,7 @@ struct ContentView: View { navModel.isSearching = true navModel.showSearchProgress = true - let sources = sourceManager.fetchInstalledSources() + let sources = pluginManager.fetchInstalledSources() await scrapingModel.scanSources(sources: sources) if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index d36ff6b..a3068c9 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -14,6 +14,7 @@ struct MainView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var backupManager: BackupManager + @EnvironmentObject var pluginManager: PluginManager @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true @@ -36,11 +37,11 @@ struct MainView: View { } .tag(ViewTab.library) - SourcesView() + PluginsView() .tabItem { - Label("Sources", systemImage: "doc.text") + Label("Plugins", systemImage: "doc.text") } - .tag(ViewTab.sources) + .tag(ViewTab.plugins) SettingsView() .tabItem { @@ -51,10 +52,12 @@ struct MainView: View { .sheet(item: $navModel.currentChoiceSheet) { item in switch item { case .magnet: - MagnetChoiceView() + ActionChoiceView() .environmentObject(debridManager) .environmentObject(scrapingModel) .environmentObject(navModel) + .environmentObject(pluginManager) + .environment(\.managedObjectContext, PersistenceController.shared.container.viewContext) case .batch: BatchChoiceView() .environmentObject(debridManager) @@ -101,30 +104,42 @@ struct MainView: View { backupManager.showRestoreAlert.toggle() } } - // Global alerts for backups - .backport.alert( + // Global alerts and dialogs for backups + .backport.confirmationDialog( isPresented: $backupManager.showRestoreAlert, title: "Restore backup?", - message: "Restoring this backup will merge all your data!", + message: + "Merge (preferred): Will merge your current data with the backup \n\n" + + "Overwrite: Will delete and replace all your data \n\n" + + "If Merge causes app instability, uninstall Ferrite and use the Overwrite option.", buttons: [ - .init("Restore", role: .destructive) { - backupManager.restoreBackup() + .init("Merge", role: .destructive) { + Task { + await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: false) + } }, - .init(role: .cancel) + .init("Overwrite", role: .destructive) { + Task { + await backupManager.restoreBackup(pluginManager: pluginManager, doOverwrite: true) + } + } ] ) .backport.alert( isPresented: $backupManager.showRestoreCompletedAlert, title: "Backup restored", - message: backupManager.backupSourceNames.isEmpty ? - "No sources need to be reinstalled" : - "Reinstall sources: \(backupManager.backupSourceNames.joined(separator: ", "))" + message: backupManager.restoreCompletedMessage.joined(separator: " \n\n"), + buttons: [ + .init("OK") { + backupManager.restoreCompletedMessage = [] + } + ] ) // Updater alert .backport.alert( isPresented: $showUpdateAlert, title: "Update available", - message: "Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings.", + message: "Ferrite \(releaseVersionString) can be downloaded. \n\nThis alert can be disabled in Settings.", buttons: [ .init("Download") { guard let releaseUrl = URL(string: releaseUrlString) else { diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift new file mode 100644 index 0000000..3b7bbba --- /dev/null +++ b/Ferrite/Views/PluginsView.swift @@ -0,0 +1,111 @@ +// +// PluginsView.swift +// Ferrite +// +// Created by Brian Dashore on 1/11/23. +// + +import SwiftUI +import SwiftUIX + +struct PluginsView: View { + enum PluginPickerSegment { + case sources + case actions + } + + @EnvironmentObject var pluginManager: PluginManager + + @FetchRequest( + entity: Source.entity(), + sortDescriptors: [] + ) var sources: FetchedResults + + @FetchRequest( + entity: Action.entity(), + sortDescriptors: [] + ) var actions: FetchedResults + + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true + + @State private var selectedSegment: PluginPickerSegment = .sources + @State private var checkedForPlugins = false + + @State private var isEditingSearch = false + @State private var isSearching = false + @State private var searchText: String = "" + + @State private var viewTask: Task? + + var body: some View { + NavView { + VStack { + Picker("Segments", selection: $selectedSegment) { + Text("Sources").tag(PluginPickerSegment.sources) + Text("Actions").tag(PluginPickerSegment.actions) + } + .pickerStyle(.segmented) + .padding(.horizontal) + .padding(.vertical, 5) + + if checkedForPlugins { + switch selectedSegment { + case .sources: + PluginListView(searchText: $searchText) + case .actions: + PluginListView(searchText: $searchText) + } + } + + Spacer() + } + .overlay { + if checkedForPlugins { + switch selectedSegment { + case .sources: + if sources.isEmpty && pluginManager.availableSources.isEmpty { + EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings") + } + case .actions: + if actions.isEmpty && pluginManager.availableActions.isEmpty { + EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings") + } + } + } else { + ProgressView() + } + } + .onAppear { + viewTask = Task { + await pluginManager.fetchPluginsFromUrl() + checkedForPlugins = true + } + } + .onDisappear { + viewTask?.cancel() + checkedForPlugins = false + } + .navigationTitle("Plugins") + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { + isSearching = true + }) + .showsCancelButton(isEditingSearch || isSearching) + .onCancel { + searchText = "" + isSearching = false + } + } + .introspectSearchController { searchController in + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + } + } + } +} + +struct PluginsView_Previews: PreviewProvider { + static var previews: some View { + PluginsView() + } +} diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index f4656c0..ee4d521 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -11,7 +11,7 @@ import SwiftUI struct SettingsView: View { @EnvironmentObject var debridManager: DebridManager - @EnvironmentObject var sourceManager: SourceManager + @EnvironmentObject var pluginManager: PluginManager let backgroundContext = PersistenceController.shared.backgroundContext @@ -84,8 +84,8 @@ struct SettingsView: View { } } - Section(header: Text("Source management")) { - NavigationLink("Source lists", destination: SettingsSourceListView()) + Section(header: Text("Plugin management")) { + NavigationLink("Plugin lists", destination: SettingsPluginListView()) } Section(header: Text("Default actions")) { diff --git a/Ferrite/Views/SheetViews/MagnetChoiceView.swift b/Ferrite/Views/SheetViews/ActionChoiceView.swift similarity index 80% rename from Ferrite/Views/SheetViews/MagnetChoiceView.swift rename to Ferrite/Views/SheetViews/ActionChoiceView.swift index 1690ddb..e24a3e3 100644 --- a/Ferrite/Views/SheetViews/MagnetChoiceView.swift +++ b/Ferrite/Views/SheetViews/ActionChoiceView.swift @@ -8,14 +8,18 @@ import SwiftUI import SwiftUIX -struct MagnetChoiceView: View { +struct ActionChoiceView: View { @Environment(\.presentationMode) var presentationMode @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var pluginManager: PluginManager - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false + @FetchRequest( + entity: Action.entity(), + sortDescriptors: [] + ) var actions: FetchedResults @State private var showLinkCopyAlert = false @State private var showMagnetCopyAlert = false @@ -39,16 +43,12 @@ struct MagnetChoiceView: View { if !debridManager.downloadUrl.isEmpty { Section(header: "Debrid options") { - ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.downloadUrl, .outplayer) - } - - ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.downloadUrl, .vlc) - } - - ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(urlString: debridManager.downloadUrl, .infuse) + ForEach(actions, id: \.id) { action in + if action.requires.contains(ActionRequirement.debrid.rawValue) { + ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") { + pluginManager.runDeeplinkAction(action, urlString: debridManager.downloadUrl) + } + } } ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { @@ -73,6 +73,14 @@ struct MagnetChoiceView: View { if !navModel.resultFromCloud { Section(header: "Magnet options") { + ForEach(actions, id: \.id) { action in + if action.requires.contains(ActionRequirement.magnet.rawValue) { + ListRowButtonView(action.name, systemImage: "arrow.up.forward.app.fill") { + pluginManager.runDeeplinkAction(action, urlString: navModel.selectedMagnet?.link) + } + } + } + ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") { UIPasteboard.general.string = navModel.selectedMagnet?.link showMagnetCopyAlert.toggle() @@ -92,10 +100,6 @@ struct MagnetChoiceView: View { navModel.showLocalActivitySheet.toggle() } } - - ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { - navModel.runMagnetAction(magnet: navModel.selectedMagnet, .webtor) - } } } } @@ -131,8 +135,8 @@ struct MagnetChoiceView: View { } } -struct MagnetChoiceView_Previews: PreviewProvider { +struct ActionChoiceView_Previews: PreviewProvider { static var previews: some View { - MagnetChoiceView() + ActionChoiceView() } } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 00edb19..783a35e 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -79,7 +79,7 @@ struct BatchChoiceView: View { if var selectedHistoryInfo = navModel.selectedHistoryInfo { selectedHistoryInfo.url = debridManager.downloadUrl selectedHistoryInfo.subName = fileName - PersistenceController.shared.createHistory(selectedHistoryInfo) + PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true) } navModel.runDebridAction(urlString: debridManager.downloadUrl) diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift deleted file mode 100644 index eb9c708..0000000 --- a/Ferrite/Views/SourcesView.swift +++ /dev/null @@ -1,142 +0,0 @@ -// -// SourceListView.swift -// Ferrite -// -// Created by Brian Dashore on 7/24/22. -// - -import Introspect -import SwiftUI -import SwiftUIX - -struct SourcesView: View { - @EnvironmentObject var sourceManager: SourceManager - @EnvironmentObject var navModel: NavigationViewModel - - let backgroundContext = PersistenceController.shared.backgroundContext - - @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true - - @State private var checkedForSources = false - @State private var isEditingSearch = false - @State private var isSearching = false - - @State private var viewTask: Task? = nil - @State private var searchText: String = "" - @State private var filteredUpdatedSources: [SourceJson] = [] - @State private var filteredAvailableSources: [SourceJson] = [] - @State private var sourcePredicate: NSPredicate? - - var body: some View { - NavView { - DynamicFetchRequest(predicate: sourcePredicate) { (installedSources: FetchedResults) in - ZStack { - if !checkedForSources { - ProgressView() - } else if installedSources.isEmpty, sourceManager.availableSources.isEmpty { - EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") - } else { - List { - if !filteredUpdatedSources.isEmpty { - Section(header: InlineHeader("Updates")) { - ForEach(filteredUpdatedSources, id: \.self) { source in - SourceUpdateButtonView(updatedSource: source) - } - } - } - - if !installedSources.isEmpty { - Section(header: InlineHeader("Installed")) { - ForEach(installedSources, id: \.self) { source in - InstalledSourceButtonView(installedSource: source) - } - } - } - - if !filteredAvailableSources.isEmpty { - Section(header: InlineHeader("Catalog")) { - ForEach(filteredAvailableSources, id: \.self) { availableSource in - if !installedSources.contains(where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - }) { - SourceCatalogButtonView(availableSource: availableSource) - } - } - } - } - } - .conditionalId(UUID()) - .listStyle(.insetGrouped) - } - } - .sheet(isPresented: $navModel.showSourceSettings) { - SourceSettingsView() - .environmentObject(navModel) - } - .onAppear { - viewTask = Task { - await sourceManager.fetchSourcesFromUrl() - filteredAvailableSources = sourceManager.availableSources.filter { availableSource in - !installedSources.contains(where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - }) - } - - filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources) - checkedForSources = true - } - } - .onDisappear { - viewTask?.cancel() - } - .onChange(of: searchText) { _ in - sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) - } - .onReceive(installedSources.publisher.count()) { _ in - filteredAvailableSources = sourceManager.availableSources.filter { availableSource in - let sourceExists = installedSources.contains(where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - }) - - if searchText.isEmpty { - return !sourceExists - } else { - return !sourceExists && availableSource.name.lowercased().contains(searchText.lowercased()) - } - } - - filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources).filter { - searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) - } - } - .navigationTitle("Sources") - .navigationSearchBar { - SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { - isSearching = true - }) - .showsCancelButton(isEditingSearch || isSearching) - .onCancel { - searchText = "" - isSearching = false - } - } - .introspectSearchController { searchController in - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none - } - } - } - } -} - -struct SourcesView_Previews: PreviewProvider { - static var previews: some View { - SourcesView() - } -} -- 2.45.2 From 9ff7f5a7d51f1f990f4d1a9e6ea16232a7f64090 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 8 Feb 2023 14:40:04 -0500 Subject: [PATCH 04/18] Ferrite: Fix iOS 14 onAppear bug onAppear does not fire properly on iOS 14 due to a longstanding bug in SwiftUI. Add a UIKit onAppear hook for listening to these events and implement inside the backport namespace. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 16 +++---- Ferrite/Extensions/View.swift | 4 ++ Ferrite/FerriteApp.swift | 2 +- Ferrite/Models/BackupModels.swift | 3 +- Ferrite/ViewModels/BackupManager.swift | 6 ++- Ferrite/ViewModels/PluginManager.swift | 6 ++- Ferrite/Views/CommonViews/Backport.swift | 27 +++++++++++- .../IndeterminateProgressView.swift | 2 +- .../Modifiers/ViewDidAppearModifier.swift | 17 ++++++++ .../Library/BookmarksView.swift | 4 +- .../Library/Cloud/AllDebridCloudView.swift | 2 +- .../Library/Cloud/PremiumizeCloudView.swift | 2 +- .../Library/Cloud/RealDebridCloudView.swift | 2 +- .../ComponentViews/Library/HistoryView.swift | 2 +- .../Plugin/PluginListView.swift | 2 +- .../Plugin/Source/SourceSettingsView.swift | 6 +-- .../SearchResult/SearchResultButtonView.swift | 2 +- .../ComponentViews/Settings/BackupsView.swift | 2 +- .../Settings/PluginListEditorView.swift | 2 +- .../Settings/SettingsAppVersionView.swift | 2 +- Ferrite/Views/MainView.swift | 2 +- Ferrite/Views/PluginsView.swift | 2 +- .../ViewDidAppearHandler.swift | 43 +++++++++++++++++++ 23 files changed, 126 insertions(+), 32 deletions(-) create mode 100644 Ferrite/Views/CommonViews/Modifiers/ViewDidAppearModifier.swift create mode 100644 Ferrite/Views/RepresentableViews/ViewDidAppearHandler.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index a252eac..ca19997 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -46,8 +46,8 @@ 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 */; }; + 0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; }; + 0C572D4E299403B7003EEC05 /* ViewDidAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.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 */; }; @@ -169,8 +169,8 @@ 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataProperties.swift"; sourceTree = ""; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; - 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnAppearHandler.swift; sourceTree = ""; }; - 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DidAppearModifier.swift; sourceTree = ""; }; + 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = ""; }; + 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearModifier.swift; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; @@ -380,7 +380,7 @@ 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, - 0C572D4D299403B7003EEC05 /* DidAppearModifier.swift */, + 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */, ); path = Modifiers; sourceTree = ""; @@ -531,7 +531,7 @@ isa = PBXGroup; children = ( 0CA148CE288903F000DE2211 /* WebView.swift */, - 0C572D4B2993FC2A003EEC05 /* OnAppearHandler.swift */, + 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */, ); path = RepresentableViews; sourceTree = ""; @@ -745,7 +745,7 @@ 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */, 0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */, - 0C572D4C2993FC2A003EEC05 /* OnAppearHandler.swift in Sources */, + 0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, @@ -795,7 +795,7 @@ 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, - 0C572D4E299403B7003EEC05 /* DidAppearModifier.swift in Sources */, + 0C572D4E299403B7003EEC05 /* ViewDidAppearModifier.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 032567f..b7910aa 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -56,4 +56,8 @@ extension View { func inlinedList() -> some View { modifier(InlinedList()) } + + func viewDidAppear(_ callback: @escaping () -> Void) -> some View { + modifier(ViewDidAppearModifier(callback: callback)) + } } diff --git a/Ferrite/FerriteApp.swift b/Ferrite/FerriteApp.swift index 8f4657d..7e5fcc8 100644 --- a/Ferrite/FerriteApp.swift +++ b/Ferrite/FerriteApp.swift @@ -21,7 +21,7 @@ struct FerriteApp: App { var body: some Scene { WindowGroup { MainView() - .onAppear { + .backport.onAppear { scrapingModel.toastModel = toastModel debridManager.toastModel = toastModel pluginManager.toastModel = toastModel diff --git a/Ferrite/Models/BackupModels.swift b/Ferrite/Models/BackupModels.swift index c1a7b24..4a5e2ce 100644 --- a/Ferrite/Models/BackupModels.swift +++ b/Ferrite/Models/BackupModels.swift @@ -7,8 +7,9 @@ import Foundation +// Version is optional until v1 is phased out public struct Backup: Codable { - let version: Int + let version: Int? var bookmarks: [BookmarkJson]? var history: [HistoryJson]? var sourceNames: [String]? diff --git a/Ferrite/ViewModels/BackupManager.swift b/Ferrite/ViewModels/BackupManager.swift index 67b40a5..70fd27a 100644 --- a/Ferrite/ViewModels/BackupManager.swift +++ b/Ferrite/ViewModels/BackupManager.swift @@ -161,8 +161,10 @@ public class BackupManager: ObservableObject { } } - if let storedLists = backup.sourceLists, (backup.version == 1) { - // Only present in v1 backups + let version = backup.version ?? -1 + + if let storedLists = backup.sourceLists, version < 2 { + // Only present in v1 or no version backups for list in storedLists { try await pluginManager.addPluginList(list.urlString, existingPluginList: nil) } diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 4399850..4ab0c5f 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -81,7 +81,11 @@ public class PluginManager: ObservableObject { } } } catch { - toastModel?.updateToastDescription("Plugin fetch error: \(error)") + let error = error as NSError + if error.code != -999 { + toastModel?.updateToastDescription("Plugin fetch error: \(error)") + } + print("Plugin fetch error: \(error)") } } diff --git a/Ferrite/Views/CommonViews/Backport.swift b/Ferrite/Views/CommonViews/Backport.swift index ade49c0..139897b 100644 --- a/Ferrite/Views/CommonViews/Backport.swift +++ b/Ferrite/Views/CommonViews/Backport.swift @@ -20,7 +20,12 @@ extension View { } extension Backport where Content: View { - @ViewBuilder func alert(isPresented: Binding, title: String, message: String?, buttons: [AlertButton] = []) -> some View { + @ViewBuilder func alert( + isPresented: Binding, + title: String, + message: String?, + buttons: [AlertButton] = [] + ) -> some View { if #available(iOS 15, *) { content .alert( @@ -63,7 +68,11 @@ extension Backport where Content: View { } } - @ViewBuilder func confirmationDialog(isPresented: Binding, title: String, message: String?, buttons: [AlertButton]) -> some View { + @ViewBuilder func confirmationDialog( + isPresented: Binding, + title: String, message: String?, + buttons: [AlertButton] + ) -> some View { if #available(iOS 15, *) { content .confirmationDialog( @@ -100,4 +109,18 @@ extension Backport where Content: View { .accentColor(color) } } + + @ViewBuilder func onAppear(callback: @escaping () -> Void) -> some View { + if #available(iOS 15, *) { + content + .onAppear { + callback() + } + } else { + content + .viewDidAppear { + callback() + } + } + } } diff --git a/Ferrite/Views/CommonViews/IndeterminateProgressView.swift b/Ferrite/Views/CommonViews/IndeterminateProgressView.swift index c9f6e33..2ce666f 100644 --- a/Ferrite/Views/CommonViews/IndeterminateProgressView.swift +++ b/Ferrite/Views/CommonViews/IndeterminateProgressView.swift @@ -25,7 +25,7 @@ struct IndeterminateProgressView: View { .offset(x: -reader.size.width * 0.6, y: 0) .offset(x: reader.size.width * 1.2 * self.offset, y: 0) .animation(.default.repeatForever().speed(0.5), value: self.offset) - .onAppear { + .backport.onAppear { withAnimation { self.offset = 1 } diff --git a/Ferrite/Views/CommonViews/Modifiers/ViewDidAppearModifier.swift b/Ferrite/Views/CommonViews/Modifiers/ViewDidAppearModifier.swift new file mode 100644 index 0000000..80524c8 --- /dev/null +++ b/Ferrite/Views/CommonViews/Modifiers/ViewDidAppearModifier.swift @@ -0,0 +1,17 @@ +// +// ViewDidAppearModifier.swift +// Ferrite +// +// Created by Brian Dashore on 2/8/23. +// + +import SwiftUI + +struct ViewDidAppearModifier: ViewModifier { + let callback: () -> Void + + func body(content: Content) -> some View { + content + .background(ViewDidAppearHandler(callback: callback)) + } +} diff --git a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift index 2172101..fdf8690 100644 --- a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift +++ b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift @@ -54,7 +54,7 @@ struct BookmarksView: View { } .inlinedList() .listStyle(.insetGrouped) - .onAppear { + .backport.onAppear { if debridManager.enabledDebrids.count > 0 { viewTask = Task { let magnets = bookmarks.compactMap { @@ -72,7 +72,7 @@ struct BookmarksView: View { viewTask?.cancel() } } - .onAppear { + .backport.onAppear { applyPredicate() } .onChange(of: searchText) { _ in diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index 12ac5fe..d6d6b77 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -80,7 +80,7 @@ struct AllDebridCloudView: View { } } } - .onAppear { + .backport.onAppear { viewTask = Task { await debridManager.fetchAdCloud() } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index 432d988..8217ea7 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -55,7 +55,7 @@ struct PremiumizeCloudView: View { } } } - .onAppear { + .backport.onAppear { viewTask = Task { await debridManager.fetchPmCloud() } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 06cc77d..d826375 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -117,7 +117,7 @@ struct RealDebridCloudView: View { } } } - .onAppear { + .backport.onAppear { viewTask = Task { await debridManager.fetchRdCloud() } diff --git a/Ferrite/Views/ComponentViews/Library/HistoryView.swift b/Ferrite/Views/ComponentViews/Library/HistoryView.swift index 1e7b4cc..9567dcf 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryView.swift @@ -27,7 +27,7 @@ struct HistoryView: View { } .listStyle(.insetGrouped) } - .onAppear { + .backport.onAppear { applyPredicate() } .onChange(of: searchText) { _ in diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift index bd55e0e..ad515b8 100644 --- a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift @@ -64,7 +64,7 @@ struct PluginListView: View { .environmentObject(navModel) } } - .onAppear { + .backport.onAppear { filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) } diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift index 7087a49..b882874 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift @@ -97,7 +97,7 @@ struct SourceSettingsBaseUrlView: View { } }) .keyboardType(.URL) - .onAppear { + .backport.onAppear { tempBaseUrl = selectedSource.baseUrl ?? "" } } @@ -127,7 +127,7 @@ struct SourceSettingsApiView: View { } }) .autocapitalization(.none) - .onAppear { + .backport.onAppear { tempClientId = clientId.value ?? "" } } @@ -140,7 +140,7 @@ struct SourceSettingsApiView: View { } }) .autocapitalization(.none) - .onAppear { + .backport.onAppear { tempClientSecret = clientSecret.value ?? "" } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index 5933bfd..5a49bb7 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -138,7 +138,7 @@ struct SearchResultButtonView: View { existingBookmark = nil } } - .onAppear { + .backport.onAppear { // Only run a exists request if a bookmark isn't passed to the view if existingBookmark == nil, !runOnce { let bookmarkRequest = Bookmark.fetchRequest() diff --git a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift index 2c5b416..8142845 100644 --- a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift +++ b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift @@ -48,7 +48,7 @@ struct BackupsView: View { .listStyle(.insetGrouped) } } - .onAppear { + .backport.onAppear { backupManager.backupUrls = FileManager.default.appDirectory .appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded } diff --git a/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift b/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift index 67b8988..99c5bcf 100644 --- a/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift +++ b/Ferrite/Views/ComponentViews/Settings/PluginListEditorView.swift @@ -32,7 +32,7 @@ struct PluginListEditorView: View { .autocapitalization(.none) .conditionalId(sourceUrlSet) } - .onAppear { + .backport.onAppear { pluginListUrl = selectedPluginList?.urlString ?? "" sourceUrlSet = true } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsAppVersionView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsAppVersionView.swift index d340ccb..69b05f8 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsAppVersionView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsAppVersionView.swift @@ -30,7 +30,7 @@ struct SettingsAppVersionView: View { .listStyle(.insetGrouped) } } - .onAppear { + .backport.onAppear { viewTask = Task { do { if let fetchedReleases = try await Github().fetchReleases() { diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index a3068c9..003f75b 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -72,7 +72,7 @@ struct MainView: View { } } } - .onAppear { + .backport.onAppear { if autoUpdateNotifs { viewTask = Task { do { diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift index 3b7bbba..20f76d0 100644 --- a/Ferrite/Views/PluginsView.swift +++ b/Ferrite/Views/PluginsView.swift @@ -75,7 +75,7 @@ struct PluginsView: View { ProgressView() } } - .onAppear { + .backport.onAppear { viewTask = Task { await pluginManager.fetchPluginsFromUrl() checkedForPlugins = true diff --git a/Ferrite/Views/RepresentableViews/ViewDidAppearHandler.swift b/Ferrite/Views/RepresentableViews/ViewDidAppearHandler.swift new file mode 100644 index 0000000..c643486 --- /dev/null +++ b/Ferrite/Views/RepresentableViews/ViewDidAppearHandler.swift @@ -0,0 +1,43 @@ +// +// ViewDidAppearHandler.swift +// Ferrite +// +// Created by Brian Dashore on 2/8/23. +// +// UIKit onAppear hook to fix onAppear behavior in iOS 14 +// + +import SwiftUI + +struct ViewDidAppearHandler: UIViewControllerRepresentable { + let callback: () -> Void + + class Coordinator: UIViewController { + let callback: () -> Void + + init(callback: @escaping () -> Void) { + self.callback = callback + super.init(nibName: nil, bundle: nil) + } + + @available(*, unavailable) + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + callback() + } + } + + func makeCoordinator() -> Coordinator { + Coordinator(callback: callback) + } + + func makeUIViewController(context: Context) -> UIViewController { + context.coordinator + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} -- 2.45.2 From 41572362c79a4adcadc78330d51ae68a1cc29761 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 8 Feb 2023 17:08:55 -0500 Subject: [PATCH 05/18] PluginManager: Fix multiple plugin list bug Only entries from the first plugin list would be shown in plugins. This was because the availableSources array was set every time a list was iterated upon. Signed-off-by: kingbri --- Ferrite/ViewModels/PluginManager.swift | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 4ab0c5f..f850e3d 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -20,10 +20,9 @@ public class PluginManager: ObservableObject { do { let pluginLists = try PersistenceController.shared.backgroundContext.fetch(pluginListRequest) - if pluginLists.isEmpty { - availableSources = [] - availableActions = [] - } + // Clean availablePlugin arrays for repopulation + availableSources = [] + availableActions = [] for pluginList in pluginLists { guard let url = URL(string: pluginList.urlString) else { @@ -38,7 +37,7 @@ public class PluginManager: ObservableObject { if let sources = pluginResponse.sources { // Faster and more performant to map instead of a for loop - availableSources = sources.compactMap { inputJson in + availableSources += sources.compactMap { inputJson in if checkAppVersion(minVersion: inputJson.minVersion) { return SourceJson( name: inputJson.name, @@ -62,7 +61,7 @@ public class PluginManager: ObservableObject { } if let actions = pluginResponse.actions { - availableActions = actions.compactMap { inputJson in + availableActions += actions.compactMap { inputJson in if checkAppVersion(minVersion: inputJson.minVersion) { return ActionJson( name: inputJson.name, -- 2.45.2 From 88a2dc97424c624cb84a51c0a2c017012a698eca Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 8 Feb 2023 22:10:02 -0500 Subject: [PATCH 06/18] Sources: Add subName and fixup The subName parameter is for aggregate sources that pull from a child website. Make it so it's possible to include that child site in parsers. Also remove the magnet link/hash requirement since it's filtered out anyways after results are fetched. Signed-off-by: kingbri --- .../SourceHtmlParser+CoreDataProperties.swift | 1 + .../SourceJsonParser+CoreDataProperties.swift | 1 + .../SourceRssParser+CoreDataProperties.swift | 1 + .../FerriteDB_v2.xcdatamodel/contents | 8 +++ Ferrite/Models/SourceModels.swift | 27 +++++----- Ferrite/ViewModels/PluginManager.swift | 34 ++++++++++++- Ferrite/ViewModels/ScrapingViewModel.swift | 49 +++++++++++++------ 7 files changed, 93 insertions(+), 28 deletions(-) diff --git a/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift index 1cc03aa..e5f19bd 100644 --- a/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceHtmlParser+CoreDataProperties.swift @@ -22,6 +22,7 @@ public extension SourceHtmlParser { @NSManaged var seedLeech: SourceSeedLeech? @NSManaged var size: SourceSize? @NSManaged var title: SourceTitle? + @NSManaged var subName: SourceSubName? } extension SourceHtmlParser: Identifiable {} diff --git a/Ferrite/DataManagement/Classes/SourceJsonParser+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceJsonParser+CoreDataProperties.swift index 84de4d3..81d7287 100644 --- a/Ferrite/DataManagement/Classes/SourceJsonParser+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceJsonParser+CoreDataProperties.swift @@ -23,6 +23,7 @@ public extension SourceJsonParser { @NSManaged var seedLeech: SourceSeedLeech? @NSManaged var size: SourceSize? @NSManaged var title: SourceTitle? + @NSManaged var subName: SourceSubName? } extension SourceJsonParser: Identifiable {} diff --git a/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift index eef0e62..6e8cc8e 100644 --- a/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/SourceRssParser+CoreDataProperties.swift @@ -23,6 +23,7 @@ public extension SourceRssParser { @NSManaged var seedLeech: SourceSeedLeech? @NSManaged var size: SourceSize? @NSManaged var title: SourceTitle? + @NSManaged var subName: SourceSubName? } extension SourceRssParser: Identifiable {} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents index 6a3cdd0..305d94c 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB_v2.xcdatamodel/contents @@ -99,6 +99,7 @@ + @@ -110,6 +111,7 @@ + @@ -132,6 +134,7 @@ + @@ -151,6 +154,11 @@ + + + + + diff --git a/Ferrite/Models/SourceModels.swift b/Ferrite/Models/SourceModels.swift index fe676b3..3bb45b8 100644 --- a/Ferrite/Models/SourceModels.swift +++ b/Ferrite/Models/SourceModels.swift @@ -18,7 +18,7 @@ public struct SourceJson: Codable, Hashable, Sendable, PluginJson { let minVersion: String? let baseUrl: String? let fallbackUrls: [String]? - var dynamicBaseUrl: Bool? + let dynamicBaseUrl: Bool? let trackers: [String]? let api: SourceApiJson? let jsonParser: SourceJsonParserJson? @@ -62,10 +62,11 @@ public struct SourceJsonParserJson: Codable, Hashable, Sendable { let searchUrl: String let results: String? let subResults: String? - let magnetHash: SouceComplexQueryJson? - let magnetLink: SouceComplexQueryJson? - let title: SouceComplexQueryJson? - let size: SouceComplexQueryJson? + let magnetHash: SourceComplexQueryJson? + let magnetLink: SourceComplexQueryJson? + let subName: SourceComplexQueryJson? + let title: SourceComplexQueryJson? + let size: SourceComplexQueryJson? let sl: SourceSLJson? } @@ -73,10 +74,11 @@ public struct SourceRssParserJson: Codable, Hashable, Sendable { let rssUrl: String? let searchUrl: String let items: String - let magnetHash: SouceComplexQueryJson? - let magnetLink: SouceComplexQueryJson? - let title: SouceComplexQueryJson? - let size: SouceComplexQueryJson? + let magnetHash: SourceComplexQueryJson? + let magnetLink: SourceComplexQueryJson? + let subName: SourceComplexQueryJson? + let title: SourceComplexQueryJson? + let size: SourceComplexQueryJson? let sl: SourceSLJson? } @@ -84,12 +86,13 @@ public struct SourceHtmlParserJson: Codable, Hashable, Sendable { let searchUrl: String let rows: String let magnet: SourceMagnetJson - let title: SouceComplexQueryJson? - let size: SouceComplexQueryJson? + let subName: SourceComplexQueryJson? + let title: SourceComplexQueryJson? + let size: SourceComplexQueryJson? let sl: SourceSLJson? } -public struct SouceComplexQueryJson: Codable, Hashable, Sendable { +public struct SourceComplexQueryJson: Codable, Hashable, Sendable { let query: String let discriminator: String? let attribute: String? diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index f850e3d..02629c1 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -45,6 +45,7 @@ public class PluginManager: ObservableObject { minVersion: inputJson.minVersion, baseUrl: inputJson.baseUrl, fallbackUrls: inputJson.fallbackUrls, + dynamicBaseUrl: inputJson.dynamicBaseUrl, trackers: inputJson.trackers, api: inputJson.api, jsonParser: inputJson.jsonParser, @@ -257,7 +258,6 @@ public class PluginManager: ObservableObject { // If there's no base URL and it isn't dynamic, return before any transactions occur let dynamicBaseUrl = sourceJson.dynamicBaseUrl ?? false - 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 source \(sourceJson.name) because base URL parameters are malformed") @@ -401,6 +401,15 @@ public class PluginManager: ObservableObject { newSourceJsonParser.magnetHash = newSourceMagnetHash } + if let subNameJson = jsonParserJson.subName { + let newSourceSubName = SourceSubName(context: backgroundContext) + newSourceSubName.query = subNameJson.query + newSourceSubName.attribute = subNameJson.query + newSourceSubName.discriminator = subNameJson.discriminator + + newSourceJsonParser.subName = newSourceSubName + } + if let titleJson = jsonParserJson.title { let newSourceTitle = SourceTitle(context: backgroundContext) newSourceTitle.query = titleJson.query @@ -448,6 +457,7 @@ public class PluginManager: ObservableObject { newSourceMagnetLink.query = magnetLinkJson.query newSourceMagnetLink.attribute = magnetLinkJson.attribute ?? "text" newSourceMagnetLink.discriminator = magnetLinkJson.discriminator + newSourceMagnetLink.regex = magnetLinkJson.regex newSourceRssParser.magnetLink = newSourceMagnetLink } @@ -457,15 +467,27 @@ public class PluginManager: ObservableObject { newSourceMagnetHash.query = magnetHashJson.query newSourceMagnetHash.attribute = magnetHashJson.attribute ?? "text" newSourceMagnetHash.discriminator = magnetHashJson.discriminator + newSourceMagnetHash.regex = magnetHashJson.regex newSourceRssParser.magnetHash = newSourceMagnetHash } + if let subNameJson = rssParserJson.subName { + let newSourceSubName = SourceSubName(context: backgroundContext) + newSourceSubName.query = subNameJson.query + newSourceSubName.attribute = subNameJson.attribute ?? "text" + newSourceSubName.discriminator = subNameJson.discriminator + newSourceSubName.regex = subNameJson.regex + + newSourceRssParser.subName = newSourceSubName + } + if let titleJson = rssParserJson.title { let newSourceTitle = SourceTitle(context: backgroundContext) newSourceTitle.query = titleJson.query newSourceTitle.attribute = titleJson.attribute ?? "text" newSourceTitle.discriminator = titleJson.discriminator + newSourceTitle.regex = titleJson.regex newSourceRssParser.title = newSourceTitle } @@ -475,6 +497,7 @@ public class PluginManager: ObservableObject { newSourceSize.query = sizeJson.query newSourceSize.attribute = sizeJson.attribute ?? "text" newSourceSize.discriminator = sizeJson.discriminator + newSourceSize.regex = sizeJson.regex newSourceRssParser.size = newSourceSize } @@ -502,6 +525,15 @@ public class PluginManager: ObservableObject { newSourceHtmlParser.searchUrl = htmlParserJson.searchUrl newSourceHtmlParser.rows = htmlParserJson.rows + if let subNameJson = htmlParserJson.subName { + let newSourceSubName = SourceSubName(context: backgroundContext) + newSourceSubName.query = subNameJson.query + newSourceSubName.attribute = subNameJson.attribute ?? "text" + newSourceSubName.regex = subNameJson.regex + + newSourceHtmlParser.subName = newSourceSubName + } + // Adds a title complex query if present if let titleJson = htmlParserJson.title { let newSourceTitle = SourceTitle(context: backgroundContext) diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index e1fdab2..1abdbbe 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -399,6 +399,12 @@ class ScrapingViewModel: ObservableObject { } } + var subName: String? + if let subNameParser = jsonParser.subName { + let rawSubName = result[subNameParser.query.components(separatedBy: ".")].rawValue + subName = rawSubName is NSNull ? nil : String(describing: rawSubName) + } + var link: String? = existingSearchResult?.magnet.link if let magnetLinkParser = jsonParser.magnetLink, link == nil { let rawLink = result[magnetLinkParser.query.components(separatedBy: ".")].rawValue @@ -432,7 +438,7 @@ class ScrapingViewModel: ObservableObject { let result = SearchResult( title: title, - source: source.name, + source: subName.map { "\(source.name) - \($0)" } ?? source.name, size: size, magnet: Magnet(hash: magnetHash, link: link, title: title, trackers: source.trackers), seeders: seeders, @@ -474,6 +480,18 @@ class ScrapingViewModel: ObservableObject { ) } + // Fetches the subName for the source if there is one + var subName: String? + if let subNameParser = rssParser.subName { + subName = try? runRssComplexQuery( + item: item, + query: subNameParser.query, + attribute: subNameParser.attribute, + discriminator: subNameParser.discriminator, + regexString: subNameParser.regex + ) + } + var title: String? if let titleParser = rssParser.title { title = try? runRssComplexQuery( @@ -485,9 +503,9 @@ class ScrapingViewModel: ObservableObject { ) } - var link: String? + var href: String? if let magnetLinkParser = rssParser.magnetLink { - link = try? runRssComplexQuery( + href = try? runRssComplexQuery( item: item, query: magnetLinkParser.query, attribute: magnetLinkParser.attribute, @@ -498,10 +516,6 @@ class ScrapingViewModel: ObservableObject { continue } - guard let href = link, href.starts(with: "magnet:") else { - continue - } - var size: String? if let sizeParser = rssParser.size { size = try? runRssComplexQuery( @@ -543,7 +557,7 @@ class ScrapingViewModel: ObservableObject { let result = SearchResult( title: title ?? "No title", - source: source.name, + source: subName.map { "\(source.name) - \($0)" } ?? source.name, size: size ?? "", magnet: Magnet(hash: magnetHash, link: href, title: title, trackers: source.trackers), seeders: seeders, @@ -649,10 +663,6 @@ class ScrapingViewModel: ObservableObject { href = link } - if !href.starts(with: "magnet:") { - continue - } - // Fetches the episode/movie title var title: String? if let titleParser = htmlParser.title { @@ -664,8 +674,17 @@ class ScrapingViewModel: ObservableObject { ) } - // Fetches the torrent's size - // TODO: Add int translation + var subName: String? + if let subNameParser = htmlParser.subName { + subName = try? runHtmlComplexQuery( + row: row, + query: subNameParser.query, + attribute: subNameParser.attribute, + regexString: subNameParser.regex + ) + } + + // Fetches the size var size: String? if let sizeParser = htmlParser.size { size = try? runHtmlComplexQuery( @@ -718,7 +737,7 @@ class ScrapingViewModel: ObservableObject { let result = SearchResult( title: title ?? "No title", - source: source.name, + source: subName.map { "\(source.name) - \($0)" } ?? source.name, size: size ?? "", magnet: Magnet(hash: nil, link: href), seeders: seeders, -- 2.45.2 From 0f081d071633c0372611bbb17b354f4f4056e69a Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 10 Feb 2023 00:40:36 -0500 Subject: [PATCH 07/18] Ferrite: Modify UI The overall UI of Ferrite has been changed to make animations smoother and streamline the experiences. A new search filter interface has been added for all iOS versions, but iOS 15 and up have smooth UI applied due to bugs with searchbars in iOS 14 (which shouldn't even have a searchbar in the first place). Also fix the plugin fetching logic to not listen to a combine publisher and instead use a notification that is easier to control. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 66 +++++--- Ferrite/API/RealDebridWrapper.swift | 18 ++- Ferrite/Extensions/NotificationCenter.swift | 4 + Ferrite/Extensions/UIDevice.swift | 18 +++ Ferrite/Extensions/View.swift | 36 ++--- Ferrite/ViewModels/NavigationViewModel.swift | 36 ++++- Ferrite/ViewModels/PluginManager.swift | 2 +- Ferrite/Views/CommonViews/Backport.swift | 14 ++ .../Views/CommonViews/FilterLabelView.swift | 27 ++++ .../Views/CommonViews/LibraryHeaderView.swift | 17 +++ .../CommonViews/Modifiers/InlinedList.swift | 6 +- .../Modifiers/SearchAppearance.swift | 63 ++++++++ .../Views/CommonViews/SearchableContent.swift | 37 +++++ .../Views/CommonViews/SectionHeaderView.swift | 20 +++ .../Views/CommonViews/TestHostingView.swift | 75 +++++++++ ...hoiceView.swift => DebridPickerView.swift} | 15 +- .../Library/BookmarksView.swift | 39 +++-- .../Library/LibraryPickerView.swift | 31 ++++ .../Buttons/InstalledPluginButtonView.swift | 2 + .../Plugin/PluginListView.swift | 5 +- .../Plugin/PluginPickerView.swift | 27 ++++ .../SearchResult/SearchFilterHeaderView.swift | 49 ++++++ .../ComponentViews/Settings/BackupsView.swift | 2 +- .../Settings/DefaultActionsPickerViews.swift | 4 +- .../Settings/SettingsPluginListView.swift | 2 +- Ferrite/Views/ContentView.swift | 142 +++++------------- Ferrite/Views/LibraryView.swift | 105 ++++++------- Ferrite/Views/MainView.swift | 8 +- Ferrite/Views/PluginsView.swift | 60 +++----- Ferrite/Views/SearchResultsView.swift | 2 +- .../Views/SheetViews/BatchChoiceView.swift | 4 +- 31 files changed, 634 insertions(+), 302 deletions(-) create mode 100644 Ferrite/Extensions/UIDevice.swift create mode 100644 Ferrite/Views/CommonViews/FilterLabelView.swift create mode 100644 Ferrite/Views/CommonViews/LibraryHeaderView.swift create mode 100644 Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift create mode 100644 Ferrite/Views/CommonViews/SearchableContent.swift create mode 100644 Ferrite/Views/CommonViews/SectionHeaderView.swift create mode 100644 Ferrite/Views/CommonViews/TestHostingView.swift rename Ferrite/Views/ComponentViews/Debrid/{DebridChoiceView.swift => DebridPickerView.swift} (72%) create mode 100644 Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift create mode 100644 Ferrite/Views/ComponentViews/Plugin/PluginPickerView.swift create mode 100644 Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index ca19997..94d1b5e 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -32,7 +32,7 @@ 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; }; 0C422E7E293542EA00486D65 /* PremiumizeWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */; }; 0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C422E7F293542F300486D65 /* PremiumizeModels.swift */; }; - 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */; }; + 0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridPickerView.swift */; }; 0C42B5982932F6DD008057A0 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Set.swift */; }; 0C445C62293F9A0B0060744D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C445C61293F9A0B0060744D /* Bundle.swift */; }; 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; }; @@ -44,6 +44,7 @@ 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 */; }; + 0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C50B7CF299DF63C00A9FA3C /* UIDevice.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 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; }; @@ -78,6 +79,7 @@ 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 */; }; + 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; }; 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */; }; 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; }; 0CA05457288EE58200850554 /* SettingsPluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsPluginListView.swift */; }; @@ -112,7 +114,6 @@ 0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; }; 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; - 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; }; 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; @@ -122,11 +123,16 @@ 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 */; }; + 0CD5F1FB299BEFBE00476DDB /* SearchAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FA299BEFBE00476DDB /* SearchAppearance.swift */; }; + 0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FC299C083B00476DDB /* PluginPickerView.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 */; }; + 0CEC8AAC299B03E5007BFE8F /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CEC8AAB299B03E5007BFE8F /* Introspect */; }; + 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; }; + 0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -156,7 +162,7 @@ 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = ""; }; 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeWrapper.swift; sourceTree = ""; }; 0C422E7F293542F300486D65 /* PremiumizeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeModels.swift; sourceTree = ""; }; - 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridChoiceView.swift; sourceTree = ""; }; + 0C42B5952932F2D5008057A0 /* DebridPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridPickerView.swift; sourceTree = ""; }; 0C42B5972932F6DD008057A0 /* Set.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Set.swift; sourceTree = ""; }; 0C445C61293F9A0B0060744D /* Bundle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bundle.swift; sourceTree = ""; }; 0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; @@ -167,6 +173,7 @@ 0C5005512992B6750064606A /* PluginTagsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginTagsView.swift; sourceTree = ""; }; 0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataClass.swift"; sourceTree = ""; }; 0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PluginTag+CoreDataProperties.swift"; sourceTree = ""; }; + 0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = ""; }; @@ -197,6 +204,7 @@ 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = ""; }; 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = ""; }; 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = ""; }; + 0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = ""; }; 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionsPickerViews.swift; sourceTree = ""; }; 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; 0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = ""; }; @@ -241,10 +249,14 @@ 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = ""; }; 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = ""; }; + 0CD5F1FA299BEFBE00476DDB /* SearchAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAppearance.swift; sourceTree = ""; }; + 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginPickerView.swift; sourceTree = ""; }; 0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; 0CE1C4172981E8D700418F20 /* Plugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Plugin.swift; sourceTree = ""; }; 0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; + 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFilterHeaderView.swift; sourceTree = ""; }; + 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryPickerView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -256,8 +268,8 @@ 0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, - 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, + 0CEC8AAC299B03E5007BFE8F /* Introspect in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, 0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */, ); @@ -290,7 +302,7 @@ 0C0755C42934245800ECA142 /* Debrid */ = { isa = PBXGroup; children = ( - 0C42B5952932F2D5008057A0 /* DebridChoiceView.swift */, + 0C42B5952932F2D5008057A0 /* DebridPickerView.swift */, 0C0755C5293424A200ECA142 /* DebridLabelView.swift */, ); path = Debrid; @@ -360,6 +372,7 @@ 0C794B65289DAC9F00DD1CC8 /* Source */, 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */, 0C5005512992B6750064606A /* PluginTagsView.swift */, + 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */, ); path = Plugin; sourceTree = ""; @@ -380,6 +393,7 @@ 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, + 0CD5F1FA299BEFBE00476DDB /* SearchAppearance.swift */, 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */, ); path = Modifiers; @@ -399,6 +413,7 @@ children = ( 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */, + 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */, ); path = SearchResult; sourceTree = ""; @@ -459,6 +474,7 @@ 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */, 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, + 0C871BDE29994D9D005279AC /* FilterLabelView.swift */, 0CA148C1288903F000DE2211 /* NavView.swift */, 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, @@ -491,6 +507,7 @@ 0C7ED14228D65518009E29AD /* FileManager.swift */, 0C42B5972932F6DD008057A0 /* Set.swift */, 0C7C128528DAA3CD00381CD1 /* URL.swift */, + 0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */, ); path = Extensions; sourceTree = ""; @@ -556,6 +573,7 @@ 0CA3B23628C2660700616D3A /* HistoryView.swift */, 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */, 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */, + 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */, ); path = Library; sourceTree = ""; @@ -610,8 +628,8 @@ 0C4CFC452897030D00AD9FAD /* Regex */, 0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, - 0CB6516728C5A5EC00DCA721 /* Introspect */, 0CDDDE042935235E006810B1 /* BetterSafariView */, + 0CEC8AAB299B03E5007BFE8F /* Introspect */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -648,8 +666,8 @@ 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */, 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, - 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */, + 0CEC8AAA299B03E5007BFE8F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -725,7 +743,7 @@ 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0C5005522992B6750064606A /* PluginTagsView.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, - 0C42B5962932F2D5008057A0 /* DebridChoiceView.swift in Sources */, + 0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */, 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, @@ -735,9 +753,11 @@ 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */, 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */, + 0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */, 0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */, 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */, + 0CD5F1FB299BEFBE00476DDB /* SearchAppearance.swift in Sources */, 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, @@ -789,6 +809,7 @@ 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */, + 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */, 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, 0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */, @@ -800,12 +821,15 @@ 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, 0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */, + 0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, + 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */, 0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */, 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */, 0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */, 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, + 0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */, 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */, 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, @@ -1072,14 +1096,6 @@ minimumVersion = 2.0.0; }; }; - 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; - requirement = { - branch = master; - kind = branch; - }; - }; 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/stleamist/BetterSafariView"; @@ -1088,6 +1104,14 @@ kind = branch; }; }; + 0CEC8AAA299B03E5007BFE8F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/"; + requirement = { + branch = master; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1121,16 +1145,16 @@ package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - 0CB6516728C5A5EC00DCA721 /* Introspect */ = { - isa = XCSwiftPackageProductDependency; - package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; - }; 0CDDDE042935235E006810B1 /* BetterSafariView */ = { isa = XCSwiftPackageProductDependency; package = 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */; productName = BetterSafariView; }; + 0CEC8AAB299B03E5007BFE8F /* Introspect */ = { + isa = XCSwiftPackageProductDependency; + package = 0CEC8AAA299B03E5007BFE8F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = Introspect; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index 9b92a58..a81f69c 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -18,6 +18,16 @@ public class RealDebrid { var authTask: Task? + @MainActor + func setUserDefaultsValue(_ value: Any, forKey: String) { + UserDefaults.standard.set(value, forKey: forKey) + } + + @MainActor + func removeUserDefaultsValue(forKey: String) { + UserDefaults.standard.removeObject(forKey: forKey) + } + // Fetches the device code from RD public func getVerificationInfo() async throws -> DeviceCodeResponse { var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")! @@ -72,7 +82,7 @@ public class RealDebrid { // If there's a client ID from the response, end the task successfully if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret { - UserDefaults.standard.set(clientId, forKey: "RealDebrid.ClientId") + await setUserDefaultsValue(clientId, forKey: "RealDebrid.ClientId") keychain.set(clientSecret, forKey: "RealDebrid.ClientSecret") try await getTokens(deviceCode: deviceCode) @@ -124,7 +134,7 @@ public class RealDebrid { keychain.set(rawResponse.refreshToken, forKey: "RealDebrid.RefreshToken") let accessTimestamp = Date().timeIntervalSince1970 + Double(rawResponse.expiresIn) - UserDefaults.standard.set(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp") + await setUserDefaultsValue(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp") } public func fetchToken() async -> String? { @@ -147,8 +157,8 @@ public class RealDebrid { public func deleteTokens() async throws { keychain.delete("RealDebrid.RefreshToken") keychain.delete("RealDebrid.ClientSecret") - UserDefaults.standard.removeObject(forKey: "RealDebrid.ClientId") - UserDefaults.standard.removeObject(forKey: "RealDebrid.AccessTokenStamp") + await removeUserDefaultsValue(forKey: "RealDebrid.ClientId") + await removeUserDefaultsValue(forKey: "RealDebrid.AccessTokenStamp") // Run the request, doesn't matter if it fails if let token = keychain.get("RealDebrid.AccessToken") { diff --git a/Ferrite/Extensions/NotificationCenter.swift b/Ferrite/Extensions/NotificationCenter.swift index 7bc0db5..63599a9 100644 --- a/Ferrite/Extensions/NotificationCenter.swift +++ b/Ferrite/Extensions/NotificationCenter.swift @@ -11,4 +11,8 @@ extension Notification.Name { static var didDeleteBookmark: Notification.Name { Notification.Name("Deleted bookmark") } + + static var didDeletePlugin: Notification.Name { + Notification.Name("Deleted plugin") + } } diff --git a/Ferrite/Extensions/UIDevice.swift b/Ferrite/Extensions/UIDevice.swift new file mode 100644 index 0000000..f2ae7e6 --- /dev/null +++ b/Ferrite/Extensions/UIDevice.swift @@ -0,0 +1,18 @@ +// +// UIDevice.swift +// Ferrite +// +// Created by Brian Dashore on 2/16/23. +// + +import SwiftUI + +extension UIDevice { + var hasNotch: Bool { + if #available(iOS 11.0, *) { + let keyWindow = UIApplication.shared.windows.filter(\.isKeyWindow).first + return keyWindow?.safeAreaInsets.bottom ?? 0 > 0 + } + return false + } +} diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index b7910aa..f92064c 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -9,30 +9,6 @@ import Introspect import SwiftUI extension View { - // MARK: Custom introspect functions - - func introspectCollectionView(customize: @escaping (UICollectionView) -> Void) -> some View { - inject(UIKitIntrospectionView( - selector: { introspectionView in - guard let viewHost = Introspect.findViewHost(from: introspectionView) else { - return nil - } - return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost) - }, - customize: customize - )) - } - - // From https://github.com/siteline/SwiftUI-Introspect/pull/129 - public func introspectSearchController(customize: @escaping (UISearchController) -> Void) -> some View { - introspectNavigationController { navigationController in - let navigationBar = navigationController.navigationBar - if let searchController = navigationBar.topItem?.searchController { - customize(searchController) - } - } - } - // MARK: Modifiers func conditionalContextMenu(id: some Hashable, @@ -53,11 +29,19 @@ extension View { modifier(DisableInteraction(disabled: disabled)) } - func inlinedList() -> some View { - modifier(InlinedList()) + func inlinedList(inset: CGFloat) -> some View { + modifier(InlinedList(inset: inset)) } func viewDidAppear(_ callback: @escaping () -> Void) -> some View { modifier(ViewDidAppearModifier(callback: callback)) } + + func searchAppearance(_ content: Content) -> some View { + modifier(SearchAppearance(hostingContent: content)) + } + + func searchAppearance(_ content: @escaping () -> Content) -> some View { + modifier(SearchAppearance(hostingContent: content())) + } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 41e6e4f..d3460e2 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -7,13 +7,6 @@ import SwiftUI -enum ViewTab { - case search - case plugins - case settings - case library -} - @MainActor class NavigationViewModel: ObservableObject { var toastModel: ToastViewModel? @@ -29,6 +22,24 @@ class NavigationViewModel: ObservableObject { case activity } + enum ViewTab { + case search + case plugins + case settings + case library + } + + enum LibraryPickerSegment { + case bookmarks + case history + case debridCloud + } + + enum PluginPickerSegment { + case sources + case actions + } + @Published var isEditingSearch: Bool = false @Published var isSearching: Bool = false @@ -57,10 +68,16 @@ class NavigationViewModel: ObservableObject { @Published var showSourceListEditor: Bool = false + @Published var libraryPickerSelection: LibraryPickerSegment = .bookmarks + @Published var pluginPickerSelection: PluginPickerSegment = .sources + @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none + // TODO: Fix for new Actions API public func runDebridAction(urlString: String, _ action: DefaultDebridActionType? = nil) { + currentChoiceSheet = .magnet + /* let selectedAction = action ?? defaultDebridAction switch selectedAction { @@ -92,10 +109,14 @@ class NavigationViewModel: ObservableObject { toastModel?.updateToastDescription("Could not create object for sharing") } } + */ } + // TODO: Fix for new Actions API public func runMagnetAction(magnet: Magnet?, _ action: DefaultMagnetActionType? = nil) { + currentChoiceSheet = .magnet // Fall back to selected magnet if the provided magnet is nil + /* let magnet = magnet ?? selectedMagnet guard let magnetLink = magnet?.link else { toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") @@ -125,5 +146,6 @@ class NavigationViewModel: ObservableObject { toastModel?.updateToastDescription("Could not create object for sharing") } } + */ } } diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 02629c1..2bac4d6 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -87,7 +87,7 @@ public class PluginManager: ObservableObject { } print("Plugin fetch error: \(error)") - } + } } // Check if underlying type is Source or Action diff --git a/Ferrite/Views/CommonViews/Backport.swift b/Ferrite/Views/CommonViews/Backport.swift index 139897b..2c7221d 100644 --- a/Ferrite/Views/CommonViews/Backport.swift +++ b/Ferrite/Views/CommonViews/Backport.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Introspect public struct Backport { public let content: Content @@ -123,4 +124,17 @@ extension Backport where Content: View { } } } + + @ViewBuilder func introspectSearchController(customize: @escaping (UISearchController) -> ()) -> some View { + if #available(iOS 15, *) { + content.introspectSearchController(customize: customize) + } else { + content.introspectNavigationController { navigationController in + let navigationBar = navigationController.navigationBar + if let searchController = navigationBar.topItem?.searchController { + customize(searchController) + } + } + } + } } diff --git a/Ferrite/Views/CommonViews/FilterLabelView.swift b/Ferrite/Views/CommonViews/FilterLabelView.swift new file mode 100644 index 0000000..deb36a2 --- /dev/null +++ b/Ferrite/Views/CommonViews/FilterLabelView.swift @@ -0,0 +1,27 @@ +// +// FilterLabelView.swift +// Ferrite +// +// Created by Brian Dashore on 2/12/23. +// + +import SwiftUI + +struct FilterLabelView: View { + var name: String + + var body: some View { + HStack(spacing: 4) { + Text(name) + .opacity(0.6) + .foregroundColor(.primary) + + Image(systemName: "chevron.down") + .foregroundColor(.tertiaryLabel) + } + .padding(.horizontal, 9) + .padding(.vertical, 7) + .font(.caption, weight: .medium) + .background(Capsule().foregroundColor(.secondarySystemFill)) + } +} diff --git a/Ferrite/Views/CommonViews/LibraryHeaderView.swift b/Ferrite/Views/CommonViews/LibraryHeaderView.swift new file mode 100644 index 0000000..e6009e1 --- /dev/null +++ b/Ferrite/Views/CommonViews/LibraryHeaderView.swift @@ -0,0 +1,17 @@ +// +// LibraryHeaderView.swift +// Ferrite +// +// Created by Brian Dashore on 2/12/23. +// + +import SwiftUI + +struct LibraryHeaderView: View { + @EnvironmentObject var debridManager: DebridManager + + @Binding var selectedSegment: LibraryPickerSegment + var body: some View { + + } +} diff --git a/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift b/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift index 3e2cac2..4c0b2dd 100644 --- a/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift +++ b/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift @@ -12,16 +12,18 @@ import Introspect import SwiftUI struct InlinedList: ViewModifier { + let inset: CGFloat + func body(content: Content) -> some View { if #available(iOS 16, *) { content .introspectCollectionView { collectionView in - collectionView.contentInset.top = -20 + collectionView.contentInset.top = inset } } else { content .introspectTableView { tableView in - tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20)) + tableView.contentInset.top = inset } } } diff --git a/Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift b/Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift new file mode 100644 index 0000000..22d232e --- /dev/null +++ b/Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift @@ -0,0 +1,63 @@ +// +// SearchAppearance.swift +// Ferrite +// +// Created by Brian Dashore on 2/14/23. +// + +import SwiftUI +import Introspect + +struct SearchAppearance: ViewModifier { + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true + + let hostingContent: V + let hostingController: UIHostingController + + init(hostingContent: V) { + self.hostingContent = hostingContent + hostingController = UIHostingController(rootView: hostingContent) + } + + func body(content: Content) -> some View { + if #available(iOS 15, *) { + content + .backport.introspectSearchController { searchController in + searchController.hidesNavigationBarDuringPresentation = true + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + searchController.searchBar.showsScopeBar = true + searchController.searchBar.scopeButtonTitles = [""] + (searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true + + hostingController.view.translatesAutoresizingMaskIntoConstraints = false + hostingController.view.backgroundColor = .clear + + guard let containerView = searchController.searchBar.value(forKey: "_scopeBarContainerView") as? UIView else { + return + } + containerView.addSubview(hostingController.view) + + NSLayoutConstraint.activate([ + hostingController.view.widthAnchor.constraint(equalTo: containerView.widthAnchor), + hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor), + hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor) + ]) + } + .introspectNavigationController { navigationController in + navigationController.navigationBar.prefersLargeTitles = true + navigationController.navigationBar.sizeToFit() + } + } else { + VStack { + hostingContent + content + Spacer() + } + .backport.introspectSearchController { searchController in + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + } + } + } +} diff --git a/Ferrite/Views/CommonViews/SearchableContent.swift b/Ferrite/Views/CommonViews/SearchableContent.swift new file mode 100644 index 0000000..754fa3a --- /dev/null +++ b/Ferrite/Views/CommonViews/SearchableContent.swift @@ -0,0 +1,37 @@ +// +// SearchableContent.swift +// Ferrite +// +// Created by Brian Dashore on 2/11/23. +// +// View to link animations together with searchbar +// Passes through geometry proxy and last height vars for any comparison +// + +import SwiftUI + +struct SearchableContent: View { + @Binding var searching: Bool + + @State private var lastHeight: CGFloat = 0.0 + + @ViewBuilder var content: (Bool) -> Content + + var body: some View { + GeometryReader { geom in + // Return if the height has changed as a closure variable for child transactions + content(geom.size.height != lastHeight) + .backport.onAppear { + lastHeight = geom.size.height + } + .onChange(of: geom.size.height) { newHeight in + lastHeight = newHeight + } + .transaction { + if geom.size.height != lastHeight && searching { + $0.animation = .default.speed(2) + } + } + } + } +} diff --git a/Ferrite/Views/CommonViews/SectionHeaderView.swift b/Ferrite/Views/CommonViews/SectionHeaderView.swift new file mode 100644 index 0000000..c46f60e --- /dev/null +++ b/Ferrite/Views/CommonViews/SectionHeaderView.swift @@ -0,0 +1,20 @@ +// +// SectionHeaderView.swift +// Ferrite +// +// Created by Brian Dashore on 2/15/23. +// + +import SwiftUI + +struct SectionHeaderView: View { + var body: some View { + Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) + } +} + +struct SectionHeaderView_Previews: PreviewProvider { + static var previews: some View { + SectionHeaderView() + } +} diff --git a/Ferrite/Views/CommonViews/TestHostingView.swift b/Ferrite/Views/CommonViews/TestHostingView.swift new file mode 100644 index 0000000..ebf4ae8 --- /dev/null +++ b/Ferrite/Views/CommonViews/TestHostingView.swift @@ -0,0 +1,75 @@ +// +// TestHostingView.swift +// Ferrite +// +// Created by Brian Dashore on 2/13/23. +// + +import SwiftUI + +struct TestHostingView: View { + @State private var textName = "First" + @State private var secondTextName = "First" + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack { + Menu { + Picker("", selection: $textName) { + Text("First").tag("First") + Text("Second").tag("Second") + Text("Third").tag("Third") + } + } label: { + HStack(spacing: 2) { + Text(textName) + .opacity(0.6) + .foregroundColor(.primary) + + Image(systemName: "chevron.down") + .foregroundColor(.tertiaryLabel) + } + .padding(.horizontal, 9) + .padding(.vertical, 7) + .font(.caption, weight: .bold) + .background(Capsule().foregroundColor(.secondarySystemFill)) + } + .id(textName) + .transaction { + $0.animation = .none + } + + Menu { + Picker("", selection: $secondTextName) { + Text("First").tag("First") + Text("Second").tag("Second") + Text("Third").tag("Third") + } + } label: { + HStack(spacing: 2) { + Text(secondTextName) + .opacity(0.6) + .foregroundColor(.primary) + + Image(systemName: "chevron.down") + .foregroundColor(.tertiaryLabel) + } + .padding(.horizontal, 9) + .padding(.vertical, 7) + .font(.caption, weight: .bold) + .background(Capsule().foregroundColor(.secondarySystemFill)) + } + .id(secondTextName) + .transaction { + $0.animation = .none + } + } + .padding(.horizontal, 18) + } + } +} + +struct TestHostingView_Previews: PreviewProvider { + static var previews: some View { + TestHostingView() + } +} diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridPickerView.swift similarity index 72% rename from Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift rename to Ferrite/Views/ComponentViews/Debrid/DebridPickerView.swift index f38ddf5..6aaf7f8 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridChoiceView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridPickerView.swift @@ -7,15 +7,17 @@ import SwiftUI -struct DebridChoiceView: View { +struct DebridPickerView: View { @EnvironmentObject var debridManager: DebridManager + @ViewBuilder var label: Content + var body: some View { Menu { Picker("", selection: $debridManager.selectedDebridType) { Text("None") .tag(nil as DebridType?) - + ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in if debridManager.enabledDebrids.contains(debridType) { Text(debridType.toString()) @@ -24,14 +26,7 @@ struct DebridChoiceView: View { } } } label: { - Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid") + label } - .animation(.none) - } -} - -struct DebridChoiceView_Previews: PreviewProvider { - static var previews: some View { - DebridChoiceView() } } diff --git a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift index fdf8690..7668409 100644 --- a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift +++ b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift @@ -26,34 +26,33 @@ struct BookmarksView: View { sortDescriptors: [NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)] ) { (bookmarks: FetchedResults) in List { - if !bookmarks.isEmpty { - ForEach(bookmarks, id: \.self) { bookmark in - SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark) - } - .onDelete { offsets in - for index in offsets { - if let bookmark = bookmarks[safe: index] { - PersistenceController.shared.delete(bookmark, context: backgroundContext) - - NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark) + if !bookmarks.isEmpty { + ForEach(bookmarks, id: \.self) { bookmark in + SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark) + } + .onDelete { offsets in + for index in offsets { + if let bookmark = bookmarks[safe: index] { + PersistenceController.shared.delete(bookmark, context: backgroundContext) + NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark) + } } } - } - .onMove { source, destination in - var changedBookmarks = bookmarks.map { $0 } + .onMove { source, destination in + var changedBookmarks = bookmarks.map { $0 } - changedBookmarks.move(fromOffsets: source, toOffset: destination) + changedBookmarks.move(fromOffsets: source, toOffset: destination) - for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) { - changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex) + for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) { + changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex) + } + + PersistenceController.shared.save() } - - PersistenceController.shared.save() } - } } - .inlinedList() .listStyle(.insetGrouped) + .inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 15 : -25) .backport.onAppear { if debridManager.enabledDebrids.count > 0 { viewTask = Task { diff --git a/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift b/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift new file mode 100644 index 0000000..8c97e58 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Library/LibraryPickerView.swift @@ -0,0 +1,31 @@ +// +// LibraryPickerView.swift +// Ferrite +// +// Created by Brian Dashore on 2/13/23. +// + +import SwiftUI + +struct LibraryPickerView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass + + @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var navModel: NavigationViewModel + + var body: some View { + HStack { + Picker("Segments", selection: $navModel.libraryPickerSelection) { + Text("Bookmarks").tag(NavigationViewModel.LibraryPickerSegment.bookmarks) + Text("History").tag(NavigationViewModel.LibraryPickerSegment.history) + + if !debridManager.enabledDebrids.isEmpty { + Text("Cloud").tag(NavigationViewModel.LibraryPickerSegment.debridCloud) + } + } + } + .pickerStyle(.segmented) + .padding(.horizontal, verticalSizeClass == .compact && UIDevice.current.hasNotch ? 65 : 18) + .padding(.vertical, 5) + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift index b99061d..0efdce4 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift @@ -54,6 +54,7 @@ struct InstalledPluginButtonView: View { if #available(iOS 15.0, *) { Button(role: .destructive) { PersistenceController.shared.delete(installedPlugin, context: backgroundContext) + NotificationCenter.default.post(name: .didDeletePlugin, object: nil) } label: { Text("Remove") Image(systemName: "trash") @@ -61,6 +62,7 @@ struct InstalledPluginButtonView: View { } else { Button { PersistenceController.shared.delete(installedPlugin, context: backgroundContext) + NotificationCenter.default.post(name: .didDeletePlugin, object: nil) } label: { Text("Remove") Image(systemName: "trash") diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift index ad515b8..5b18abb 100644 --- a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift @@ -57,6 +57,7 @@ struct PluginListView: View { } } } + .inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 0 : -25) .listStyle(.insetGrouped) .sheet(isPresented: $navModel.showSourceSettings) { if String(describing: P.self) == "Source" { @@ -70,8 +71,10 @@ struct PluginListView: View { } .onChange(of: searchText) { _ in sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) + filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) + filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) } - .onReceive(installedPlugins.publisher.count()) { _ in + .onReceive(NotificationCenter.default.publisher(for: .didDeletePlugin)) { _ in filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) } diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginPickerView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginPickerView.swift new file mode 100644 index 0000000..bb02771 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/PluginPickerView.swift @@ -0,0 +1,27 @@ +// +// PluginPickerView.swift +// Ferrite +// +// Created by Brian Dashore on 2/14/23. +// + +import SwiftUI + +struct PluginPickerView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass + + @EnvironmentObject var navModel: NavigationViewModel + + @State private var textName = "" + @State private var secondTextName = "" + + var body: some View { + Picker("Segments", selection: $navModel.pluginPickerSelection) { + Text("Sources").tag(NavigationViewModel.PluginPickerSegment.sources) + Text("Actions").tag(NavigationViewModel.PluginPickerSegment.actions) + } + .pickerStyle(.segmented) + .padding(.horizontal, verticalSizeClass == .compact && UIDevice.current.hasNotch ? 65 : 18) + .padding(.vertical, 5) + } +} diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift new file mode 100644 index 0000000..8834060 --- /dev/null +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -0,0 +1,49 @@ +// +// SearchFilterHeaderView.swift +// Ferrite +// +// Created by Brian Dashore on 2/13/23. +// + +import SwiftUI + +struct SearchFilterHeaderView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass + + @EnvironmentObject var scrapingModel: ScrapingViewModel + @EnvironmentObject var debridManager: DebridManager + + @FetchRequest( + entity: Source.entity(), + sortDescriptors: [] + ) var sources: FetchedResults + + var body: some View { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 6) { + Menu { + Picker("", selection: $scrapingModel.filteredSource) { + Text("None").tag(nil as Source?) + + ForEach(sources, id: \.self) { source in + if let name = source.name, source.enabled { + Text(name) + .tag(Source?.some(source)) + } + } + } + } label: { + FilterLabelView(name: scrapingModel.filteredSource?.name ?? "Source") + } + .id(scrapingModel.filteredSource) + + DebridPickerView() { + FilterLabelView(name: debridManager.selectedDebridType?.toString() ?? "Debrid") + } + .id(debridManager.selectedDebridType) + } + .padding(.horizontal, verticalSizeClass == .compact ? (Application.shared.osVersion.majorVersion > 14 ? 65 : 18) : 18) + .padding(.top, Application.shared.osVersion.majorVersion > 14 ? 0 : 10) + } + } +} diff --git a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift index 8142845..b6488b7 100644 --- a/Ferrite/Views/ComponentViews/Settings/BackupsView.swift +++ b/Ferrite/Views/ComponentViews/Settings/BackupsView.swift @@ -44,7 +44,7 @@ struct BackupsView: View { } } } - .inlinedList() + .inlinedList(inset: -20) .listStyle(.insetGrouped) } } diff --git a/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift b/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift index c9c069c..ab7b52c 100644 --- a/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift +++ b/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift @@ -29,7 +29,7 @@ struct MagnetActionPickerView: View { } } .listStyle(.insetGrouped) - .inlinedList() + .inlinedList(inset: -20) .navigationTitle("Default magnet action") .navigationBarTitleDisplayMode(.inline) } @@ -68,7 +68,7 @@ struct DebridActionPickerView: View { } } .listStyle(.insetGrouped) - .inlinedList() + .inlinedList(inset: -20) .navigationTitle("Default debrid action") .navigationBarTitleDisplayMode(.inline) } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift index 4abd6ed..1f23b57 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift @@ -73,7 +73,7 @@ struct SettingsPluginListView: View { } } .listStyle(.insetGrouped) - .inlinedList() + .inlinedList(inset: -20) } } .sheet(isPresented: $presentSourceSheet) { diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 88e1ba8..56ea9e6 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -14,118 +14,56 @@ struct ContentView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var pluginManager: PluginManager - @FetchRequest( - entity: Source.entity(), - sortDescriptors: [] - ) var sources: FetchedResults - - @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true - - @State private var selectedSource: Source? { - didSet { - scrapingModel.filteredSource = selectedSource - } - } - var body: some View { NavView { - VStack(spacing: 10) { - HStack(spacing: 6) { - Text("Filter") - .foregroundColor(.secondary) + SearchResultsView() + .listStyle(.insetGrouped) + .navigationTitle("Search") + .navigationSearchBar { + SearchBar("Search", + text: $scrapingModel.searchText, + isEditing: $navModel.isEditingSearch, + onCommit: { + scrapingModel.searchResults = [] + scrapingModel.runningSearchTask = Task { + navModel.isSearching = true + navModel.showSearchProgress = true - Menu { - Button { - selectedSource = nil - } label: { - Text("None") + let sources = pluginManager.fetchInstalledSources() + await scrapingModel.scanSources(sources: sources) - if selectedSource == nil { - Image(systemName: "checkmark") - } - } + if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { + debridManager.clearIAValues() - ForEach(sources, id: \.self) { source in - if let name = source.name, source.enabled { - Button { - selectedSource = source - } label: { - if selectedSource == source { - Label(name, systemImage: "checkmark") - } else { - Text(name) - } - } - } - } - } label: { - Text(selectedSource?.name ?? "Source") - .padding(.trailing, -3) - Image(systemName: "chevron.down") - } - .foregroundColor(.primary) - .animation(.none) - - Spacer() - } - .padding(.vertical, 5) - .padding(.horizontal, 20) - - SearchResultsView() - } - .navigationTitle("Search") - .navigationBarTitleDisplayMode( - navModel.isSearching && Application.shared.osVersion.majorVersion > 14 ? .inline : .large - ) - .navigationSearchBar { - SearchBar("Search", - text: $scrapingModel.searchText, - isEditing: $navModel.isEditingSearch, - onCommit: { - scrapingModel.searchResults = [] - scrapingModel.runningSearchTask = Task { - navModel.isSearching = true - navModel.showSearchProgress = true - - let sources = pluginManager.fetchInstalledSources() - await scrapingModel.scanSources(sources: sources) - - if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { - debridManager.clearIAValues() - - // Remove magnets that don't have a hash - let magnets = scrapingModel.searchResults.compactMap { - if let magnetHash = $0.magnet.hash { - return Magnet(hash: magnetHash, link: $0.magnet.link) - } else { - return nil + // Remove magnets that don't have a hash + let magnets = scrapingModel.searchResults.compactMap { + if let magnetHash = $0.magnet.hash { + return Magnet(hash: magnetHash, link: $0.magnet.link) + } else { + return nil + } } + await debridManager.populateDebridIA(magnets) } - await debridManager.populateDebridIA(magnets) - } - navModel.showSearchProgress = false + navModel.showSearchProgress = false + } + }) + .showsCancelButton(navModel.isEditingSearch || navModel.isSearching) + .onCancel { + scrapingModel.searchResults = [] + scrapingModel.runningSearchTask?.cancel() + scrapingModel.runningSearchTask = nil + navModel.isSearching = false + scrapingModel.searchText = "" } - }) - .showsCancelButton(navModel.isEditingSearch || navModel.isSearching) - .onCancel { - scrapingModel.searchResults = [] - scrapingModel.runningSearchTask?.cancel() - scrapingModel.runningSearchTask = nil - navModel.isSearching = false - scrapingModel.searchText = "" - } - } - .introspectSearchController { searchController in - searchController.hidesNavigationBarDuringPresentation = false - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none - } - .toolbar { - ToolbarItemGroup(placement: .navigationBarTrailing) { - DebridChoiceView() } - } + .navigationSearchBarHiddenWhenScrolling(false) + .searchAppearance { + SearchFilterHeaderView() + .environmentObject(scrapingModel) + .environmentObject(debridManager) + } } } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 3c494c1..eaa027f 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -9,14 +9,8 @@ import SwiftUI import SwiftUIX struct LibraryView: View { - enum LibraryPickerSegment { - case bookmarks - case history - case debridCloud - } - - @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var navModel: NavigationViewModel @FetchRequest( entity: Bookmark.entity(), @@ -32,7 +26,6 @@ struct LibraryView: View { @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true - @State private var selectedSegment: LibraryPickerSegment = .bookmarks @State private var editMode: EditMode = .inactive @State private var searchText: String = "" @@ -41,20 +34,8 @@ struct LibraryView: View { var body: some View { NavView { - VStack { - Picker("Segments", selection: $selectedSegment) { - Text("Bookmarks").tag(LibraryPickerSegment.bookmarks) - Text("History").tag(LibraryPickerSegment.history) - - if !debridManager.enabledDebrids.isEmpty { - Text("Cloud").tag(LibraryPickerSegment.debridCloud) - } - } - .pickerStyle(.segmented) - .padding(.horizontal) - .padding(.vertical, 5) - - switch selectedSegment { + ZStack { + switch navModel.libraryPickerSelection { case .bookmarks: BookmarksView(searchText: $searchText) case .history: @@ -62,8 +43,27 @@ struct LibraryView: View { case .debridCloud: DebridCloudView(searchText: $searchText) } + } + .navigationTitle("Library") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) { + Spacer() + EditButton() - Spacer() + switch navModel.libraryPickerSelection { + case .bookmarks, .debridCloud: + DebridPickerView() { + Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid") + } + .transaction { + $0.animation = .none + } + case .history: + HistoryActionsView() + } + } + } } .navigationSearchBar { SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { @@ -75,46 +75,31 @@ struct LibraryView: View { isSearching = false } } - .introspectSearchController { searchController in - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none - } - .overlay { - switch selectedSegment { - case .bookmarks: - if bookmarks.isEmpty { - EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") - } - case .history: - if history.isEmpty { - EmptyInstructionView(title: "No History", message: "Start watching to build history") - } - case .debridCloud: - if debridManager.selectedDebridType == nil { - EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") - } - } - } - .navigationTitle("Library") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) { - Spacer() - EditButton() - - switch selectedSegment { - case .bookmarks, .debridCloud: - DebridChoiceView() - case .history: - HistoryActionsView() - } - } - .animation(.none) - } + .navigationSearchBarHiddenWhenScrolling(false) + .searchAppearance { + LibraryPickerView() + .environmentObject(debridManager) + .environmentObject(navModel) } .environment(\.editMode, $editMode) } - .onChange(of: selectedSegment) { _ in + .overlay { + switch navModel.libraryPickerSelection { + case .bookmarks: + if bookmarks.isEmpty { + EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") + } + case .history: + if history.isEmpty { + EmptyInstructionView(title: "No History", message: "Start watching to build history") + } + case .debridCloud: + if debridManager.selectedDebridType == nil { + EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") + } + } + } + .onChange(of: navModel.libraryPickerSelection) { _ in editMode = .inactive } .onDisappear { diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 003f75b..69b2bd7 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -29,25 +29,25 @@ struct MainView: View { .tabItem { Label("Search", systemImage: "magnifyingglass") } - .tag(ViewTab.search) + .tag(NavigationViewModel.ViewTab.search) LibraryView() .tabItem { Label("Library", systemImage: "book.closed") } - .tag(ViewTab.library) + .tag(NavigationViewModel.ViewTab.library) PluginsView() .tabItem { Label("Plugins", systemImage: "doc.text") } - .tag(ViewTab.plugins) + .tag(NavigationViewModel.ViewTab.plugins) SettingsView() .tabItem { Label("Settings", systemImage: "gear") } - .tag(ViewTab.settings) + .tag(NavigationViewModel.ViewTab.settings) } .sheet(item: $navModel.currentChoiceSheet) { item in switch item { diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift index 20f76d0..7f3eb7f 100644 --- a/Ferrite/Views/PluginsView.swift +++ b/Ferrite/Views/PluginsView.swift @@ -9,12 +9,8 @@ import SwiftUI import SwiftUIX struct PluginsView: View { - enum PluginPickerSegment { - case sources - case actions - } - @EnvironmentObject var pluginManager: PluginManager + @EnvironmentObject var navModel: NavigationViewModel @FetchRequest( entity: Source.entity(), @@ -28,7 +24,6 @@ struct PluginsView: View { @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true - @State private var selectedSegment: PluginPickerSegment = .sources @State private var checkedForPlugins = false @State private var isEditingSearch = false @@ -39,41 +34,15 @@ struct PluginsView: View { 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) - + ZStack { if checkedForPlugins { - switch selectedSegment { + switch navModel.pluginPickerSelection { case .sources: PluginListView(searchText: $searchText) case .actions: PluginListView(searchText: $searchText) } } - - Spacer() - } - .overlay { - if checkedForPlugins { - switch selectedSegment { - case .sources: - if sources.isEmpty && pluginManager.availableSources.isEmpty { - EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings") - } - case .actions: - if actions.isEmpty && pluginManager.availableActions.isEmpty { - EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings") - } - } - } else { - ProgressView() - } } .backport.onAppear { viewTask = Task { @@ -96,9 +65,26 @@ struct PluginsView: View { isSearching = false } } - .introspectSearchController { searchController in - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + .navigationSearchBarHiddenWhenScrolling(false) + .searchAppearance { + PluginPickerView() + .environmentObject(navModel) + } + } + .overlay { + if checkedForPlugins { + switch navModel.pluginPickerSelection { + 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() } } } diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift index df775a7..dc2b699 100644 --- a/Ferrite/Views/SearchResultsView.swift +++ b/Ferrite/Views/SearchResultsView.swift @@ -21,7 +21,7 @@ struct SearchResultsView: View { } } .listStyle(.insetGrouped) - .inlinedList() + .inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 20 : -20) .overlay { if scrapingModel.searchResults.isEmpty { if navModel.showSearchProgress { diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 783a35e..b413b43 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -48,7 +48,7 @@ struct BatchChoiceView: View { } .backport.tint(.primary) .listStyle(.insetGrouped) - .inlinedList() + .inlinedList(inset: -20) .navigationTitle("Select a file") .navigationBarTitleDisplayMode(.inline) .toolbar { @@ -82,7 +82,7 @@ struct BatchChoiceView: View { PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true) } - navModel.runDebridAction(urlString: debridManager.downloadUrl) + navModel.runDebridAction(urlString: debridManager.downloadUrl, nil) } debridManager.clearSelectedDebridItems() -- 2.45.2 From cb4d935008faa83c057190d2b18af0e1a346d267 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 16 Feb 2023 16:45:50 -0500 Subject: [PATCH 08/18] Sources: Fix magnet link parsing with RSS Using the new magnet type auto-populates hashes and links Signed-off-by: kingbri --- Ferrite/ViewModels/ScrapingViewModel.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 1abdbbe..ab7ae86 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -468,6 +468,7 @@ class ScrapingViewModel: ObservableObject { } for item in items { + //print(item) // Parse magnet link or translate hash var magnetHash: String? if let magnetHashParser = rssParser.magnetHash { @@ -512,8 +513,6 @@ class ScrapingViewModel: ObservableObject { discriminator: magnetLinkParser.discriminator, regexString: magnetLinkParser.regex ) - } else { - continue } var size: String? -- 2.45.2 From 291aa0fe4606930b120fdf862414bb4ce44ce9c0 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 16 Feb 2023 18:52:29 -0500 Subject: [PATCH 09/18] Ferrite: Use InlineHeader in all sections Makes section presentation uniform across all iOS versions since iOS 15 defaults are wonky. Signed-off-by: kingbri --- .../Views/ComponentViews/Library/HistoryView.swift | 2 +- .../Views/ComponentViews/Plugin/PluginListView.swift | 2 +- Ferrite/Views/SettingsView.swift | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Ferrite/Views/ComponentViews/Library/HistoryView.swift b/Ferrite/Views/ComponentViews/Library/HistoryView.swift index 9567dcf..6c7d25c 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryView.swift @@ -71,7 +71,7 @@ struct HistorySectionView: View { var body: some View { if compareGroup(historyGroup) > 0 { - Section(header: Text(formatter.string(from: historyGroup[0].date ?? Date()))) { + Section(header: InlineHeader(formatter.string(from: historyGroup[0].date ?? Date()))) { ForEach(historyGroup, id: \.self) { history in ForEach(history.entryArray.filter { allEntries.contains($0) }, id: \.self) { entry in HistoryButtonView(entry: entry) diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift index 5b18abb..cc8232d 100644 --- a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift @@ -57,7 +57,7 @@ struct PluginListView: View { } } } - .inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 0 : -25) + .inlinedList(inset: 0) .listStyle(.insetGrouped) .sheet(isPresented: $navModel.showSourceSettings) { if String(describing: P.self) == "Source" { diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index ee4d521..edbdce0 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -78,17 +78,17 @@ struct SettingsView: View { } } - Section(header: Text("Behavior")) { + Section(header: InlineHeader("Behavior")) { Toggle(isOn: $autocorrectSearch) { Text("Autocorrect search") } } - Section(header: Text("Plugin management")) { + Section(header: InlineHeader("Plugin management")) { NavigationLink("Plugin lists", destination: SettingsPluginListView()) } - Section(header: Text("Default actions")) { + Section(header: InlineHeader("Default actions")) { if debridManager.enabledDebrids.count > 0 { NavigationLink( destination: DebridActionPickerView(), @@ -138,20 +138,20 @@ struct SettingsView: View { ) } - Section(header: Text("Backups")) { + Section(header: InlineHeader("Backups")) { NavigationLink(destination: BackupsView()) { Text("Backups") } } - Section(header: Text("Updates")) { + Section(header: InlineHeader("Updates")) { Toggle(isOn: $autoUpdateNotifs) { Text("Show update alerts") } NavigationLink("Version history", destination: SettingsAppVersionView()) } - Section(header: Text("Information")) { + Section(header: InlineHeader("Information")) { ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri") ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues") NavigationLink("About", destination: AboutView()) -- 2.45.2 From 988e607027618f6690446aaf32ef8e0fe20d2c7c Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 16 Feb 2023 22:39:47 -0500 Subject: [PATCH 10/18] Dependencies: Use Introspect v0.2.1 v0.2.2 causes issues with UI, wait until a fix is released. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 34 +++++++++++++++---------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 94d1b5e..fa6ae4f 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; }; + 0CBC07C8299F2E0300F78723 /* Introspect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 0CBC07C7299F2E0300F78723 /* Introspect-Static */; }; 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 */; }; @@ -130,7 +131,6 @@ 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 */; }; - 0CEC8AAC299B03E5007BFE8F /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CEC8AAB299B03E5007BFE8F /* Introspect */; }; 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AAD299B31B6007BFE8F /* SearchFilterHeaderView.swift */; }; 0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CEC8AB1299B3B57007BFE8F /* LibraryPickerView.swift */; }; /* End PBXBuildFile section */ @@ -265,11 +265,11 @@ buildActionMask = 2147483647; files = ( 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */, + 0CBC07C8299F2E0300F78723 /* Introspect-Static in Frameworks */, 0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, - 0CEC8AAC299B03E5007BFE8F /* Introspect in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, 0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */, ); @@ -629,7 +629,7 @@ 0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, 0CDDDE042935235E006810B1 /* BetterSafariView */, - 0CEC8AAB299B03E5007BFE8F /* Introspect */, + 0CBC07C7299F2E0300F78723 /* Introspect-Static */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -667,7 +667,7 @@ 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */, - 0CEC8AAA299B03E5007BFE8F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, + 0CBC07C6299F2E0200F78723 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -1096,6 +1096,14 @@ minimumVersion = 2.0.0; }; }; + 0CBC07C6299F2E0200F78723 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; + requirement = { + kind = exactVersion; + version = 0.2.1; + }; + }; 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/stleamist/BetterSafariView"; @@ -1104,14 +1112,6 @@ kind = branch; }; }; - 0CEC8AAA299B03E5007BFE8F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/"; - requirement = { - branch = master; - kind = branch; - }; - }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1145,16 +1145,16 @@ package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; + 0CBC07C7299F2E0300F78723 /* Introspect-Static */ = { + isa = XCSwiftPackageProductDependency; + package = 0CBC07C6299F2E0200F78723 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = "Introspect-Static"; + }; 0CDDDE042935235E006810B1 /* BetterSafariView */ = { isa = XCSwiftPackageProductDependency; package = 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */; productName = BetterSafariView; }; - 0CEC8AAB299B03E5007BFE8F /* Introspect */ = { - isa = XCSwiftPackageProductDependency; - package = 0CEC8AAA299B03E5007BFE8F /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = Introspect; - }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ -- 2.45.2 From 4a87d86e76cdd92917c0ba16dfa74cc383bbeb92 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 18 Feb 2023 11:39:03 -0500 Subject: [PATCH 11/18] Ferrite: Add fixes for Introspect v0.2.2 Introspect now properly updates on every view lifecycle change, so add a check to a reference on the UIHostingController and see if it has been instantiated already. Also clean up view struct names to reflect what is a modifier. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 50 +++++++++---------- Ferrite/Extensions/View.swift | 18 +++---- .../Modifiers/ConditionalContextMenu.swift | 2 +- .../CommonViews/Modifiers/ConditionalId.swift | 2 +- ...hAppearance.swift => CustomScopeBar.swift} | 10 ++-- .../Modifiers/DisableInteraction.swift | 2 +- .../Modifiers/DisabledAppearance.swift | 2 +- .../CommonViews/Modifiers/InlinedList.swift | 2 +- ...pearModifier.swift => ViewDidAppear.swift} | 0 Ferrite/Views/ContentView.swift | 2 +- Ferrite/Views/LibraryView.swift | 2 +- Ferrite/Views/PluginsView.swift | 8 +-- 12 files changed, 52 insertions(+), 48 deletions(-) rename Ferrite/Views/CommonViews/Modifiers/{SearchAppearance.swift => CustomScopeBar.swift} (88%) rename Ferrite/Views/CommonViews/Modifiers/{ViewDidAppearModifier.swift => ViewDidAppear.swift} (100%) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index fa6ae4f..d2f29cb 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -35,6 +35,7 @@ 0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5952932F2D5008057A0 /* DebridPickerView.swift */; }; 0C42B5982932F6DD008057A0 /* Set.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C42B5972932F6DD008057A0 /* Set.swift */; }; 0C445C62293F9A0B0060744D /* Bundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C445C61293F9A0B0060744D /* Bundle.swift */; }; + 0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 0C448BE829A135F100F4E266 /* Introspect-Static */; }; 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; }; 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; }; 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.swift */; }; @@ -48,7 +49,7 @@ 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 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; }; - 0C572D4E299403B7003EEC05 /* ViewDidAppearModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */; }; + 0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppear.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 */; }; @@ -116,7 +117,6 @@ 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; }; - 0CBC07C8299F2E0300F78723 /* Introspect-Static in Frameworks */ = {isa = PBXBuildFile; productRef = 0CBC07C7299F2E0300F78723 /* Introspect-Static */; }; 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 */; }; @@ -124,7 +124,7 @@ 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 */; }; - 0CD5F1FB299BEFBE00476DDB /* SearchAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FA299BEFBE00476DDB /* SearchAppearance.swift */; }; + 0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */; }; 0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */; }; 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD72E16293D9928001A7EA4 /* Array.swift */; }; 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; @@ -177,7 +177,7 @@ 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = ""; }; - 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearModifier.swift; sourceTree = ""; }; + 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppear.swift; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; @@ -249,7 +249,7 @@ 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = ""; }; 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = ""; }; - 0CD5F1FA299BEFBE00476DDB /* SearchAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchAppearance.swift; sourceTree = ""; }; + 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomScopeBar.swift; sourceTree = ""; }; 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginPickerView.swift; sourceTree = ""; }; 0CD72E16293D9928001A7EA4 /* Array.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Array.swift; sourceTree = ""; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; @@ -265,7 +265,7 @@ buildActionMask = 2147483647; files = ( 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */, - 0CBC07C8299F2E0300F78723 /* Introspect-Static in Frameworks */, + 0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */, 0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, @@ -393,8 +393,8 @@ 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, - 0CD5F1FA299BEFBE00476DDB /* SearchAppearance.swift */, - 0C572D4D299403B7003EEC05 /* ViewDidAppearModifier.swift */, + 0CD5F1FA299BEFBE00476DDB /* CustomScopeBar.swift */, + 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */, ); path = Modifiers; sourceTree = ""; @@ -629,7 +629,7 @@ 0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, 0CDDDE042935235E006810B1 /* BetterSafariView */, - 0CBC07C7299F2E0300F78723 /* Introspect-Static */, + 0C448BE829A135F100F4E266 /* Introspect-Static */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -667,7 +667,7 @@ 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */, - 0CBC07C6299F2E0200F78723 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, + 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -757,7 +757,7 @@ 0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */, 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, 0CD72E17293D9928001A7EA4 /* Array.swift in Sources */, - 0CD5F1FB299BEFBE00476DDB /* SearchAppearance.swift in Sources */, + 0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */, 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0CC389542970AD900066D06F /* Action+CoreDataProperties.swift in Sources */, 0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */, @@ -816,7 +816,7 @@ 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, - 0C572D4E299403B7003EEC05 /* ViewDidAppearModifier.swift in Sources */, + 0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, @@ -1048,6 +1048,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.2.2; + }; + }; 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/sindresorhus/Regex"; @@ -1096,14 +1104,6 @@ minimumVersion = 2.0.0; }; }; - 0CBC07C6299F2E0200F78723 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; - requirement = { - kind = exactVersion; - version = 0.2.1; - }; - }; 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/stleamist/BetterSafariView"; @@ -1115,6 +1115,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 0C448BE829A135F100F4E266 /* Introspect-Static */ = { + isa = XCSwiftPackageProductDependency; + package = 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = "Introspect-Static"; + }; 0C4CFC452897030D00AD9FAD /* Regex */ = { isa = XCSwiftPackageProductDependency; package = 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */; @@ -1145,11 +1150,6 @@ package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; - 0CBC07C7299F2E0300F78723 /* Introspect-Static */ = { - isa = XCSwiftPackageProductDependency; - package = 0CBC07C6299F2E0200F78723 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; - productName = "Introspect-Static"; - }; 0CDDDE042935235E006810B1 /* BetterSafariView */ = { isa = XCSwiftPackageProductDependency; package = 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */; diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index f92064c..1a7f245 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -14,34 +14,34 @@ extension View { func conditionalContextMenu(id: some Hashable, @ViewBuilder _ internalContent: @escaping () -> some View) -> some View { - modifier(ConditionalContextMenu(internalContent, id: id)) + modifier(ConditionalContextMenuModifier(internalContent, id: id)) } func conditionalId(_ id: some Hashable) -> some View { - modifier(ConditionalId(id: id)) + modifier(ConditionalIdModifier(id: id)) } func disabledAppearance(_ disabled: Bool, dimmedOpacity: Double? = nil, animation: Animation? = nil) -> some View { - modifier(DisabledAppearance(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation)) + modifier(DisabledAppearanceModifier(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation)) } func disableInteraction(_ disabled: Bool) -> some View { - modifier(DisableInteraction(disabled: disabled)) + modifier(DisableInteractionModifier(disabled: disabled)) } func inlinedList(inset: CGFloat) -> some View { - modifier(InlinedList(inset: inset)) + modifier(InlinedListModifier(inset: inset)) } func viewDidAppear(_ callback: @escaping () -> Void) -> some View { modifier(ViewDidAppearModifier(callback: callback)) } - func searchAppearance(_ content: Content) -> some View { - modifier(SearchAppearance(hostingContent: content)) + func customScopeBar(_ content: Content) -> some View { + modifier(CustomScopeBarModifier(hostingContent: content)) } - func searchAppearance(_ content: @escaping () -> Content) -> some View { - modifier(SearchAppearance(hostingContent: content())) + func customScopeBar(_ content: @escaping () -> Content) -> some View { + modifier(CustomScopeBarModifier(hostingContent: content())) } } diff --git a/Ferrite/Views/CommonViews/Modifiers/ConditionalContextMenu.swift b/Ferrite/Views/CommonViews/Modifiers/ConditionalContextMenu.swift index d981673..c18f3de 100644 --- a/Ferrite/Views/CommonViews/Modifiers/ConditionalContextMenu.swift +++ b/Ferrite/Views/CommonViews/Modifiers/ConditionalContextMenu.swift @@ -10,7 +10,7 @@ import SwiftUI -struct ConditionalContextMenu: ViewModifier { +struct ConditionalContextMenuModifier: ViewModifier { let internalContent: () -> InternalContent let id: ID diff --git a/Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift b/Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift index 146f87f..d9d89d6 100644 --- a/Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift +++ b/Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift @@ -10,7 +10,7 @@ import SwiftUI -struct ConditionalId: ViewModifier { +struct ConditionalIdModifier: ViewModifier { let id: ID func body(content: Content) -> some View { diff --git a/Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift similarity index 88% rename from Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift rename to Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift index 22d232e..98626bf 100644 --- a/Ferrite/Views/CommonViews/Modifiers/SearchAppearance.swift +++ b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift @@ -8,21 +8,22 @@ import SwiftUI import Introspect -struct SearchAppearance: ViewModifier { +struct CustomScopeBarModifier: ViewModifier { @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true let hostingContent: V - let hostingController: UIHostingController + @State private var hostingController: UIHostingController? init(hostingContent: V) { self.hostingContent = hostingContent - hostingController = UIHostingController(rootView: hostingContent) } func body(content: Content) -> some View { if #available(iOS 15, *) { content .backport.introspectSearchController { searchController in + guard hostingController == nil else { return } + searchController.hidesNavigationBarDuringPresentation = true searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none @@ -30,6 +31,7 @@ struct SearchAppearance: ViewModifier { searchController.searchBar.scopeButtonTitles = [""] (searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true + let hostingController = UIHostingController(rootView: hostingContent) hostingController.view.translatesAutoresizingMaskIntoConstraints = false hostingController.view.backgroundColor = .clear @@ -43,6 +45,8 @@ struct SearchAppearance: ViewModifier { hostingController.view.topAnchor.constraint(equalTo: containerView.topAnchor), hostingController.view.heightAnchor.constraint(equalTo: containerView.heightAnchor) ]) + + self.hostingController = hostingController } .introspectNavigationController { navigationController in navigationController.navigationBar.prefersLargeTitles = true diff --git a/Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift b/Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift index 97d428c..1dc3220 100644 --- a/Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift @@ -9,7 +9,7 @@ import SwiftUI -struct DisableInteraction: ViewModifier { +struct DisableInteractionModifier: ViewModifier { let disabled: Bool func body(content: Content) -> some View { diff --git a/Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift b/Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift index 1caeaf6..a0f8eb8 100644 --- a/Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift @@ -9,7 +9,7 @@ import SwiftUI -struct DisabledAppearance: ViewModifier { +struct DisabledAppearanceModifier: ViewModifier { let disabled: Bool let dimmedOpacity: Double? let animation: Animation? diff --git a/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift b/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift index 4c0b2dd..db8fb46 100644 --- a/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift +++ b/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift @@ -11,7 +11,7 @@ import Introspect import SwiftUI -struct InlinedList: ViewModifier { +struct InlinedListModifier: ViewModifier { let inset: CGFloat func body(content: Content) -> some View { diff --git a/Ferrite/Views/CommonViews/Modifiers/ViewDidAppearModifier.swift b/Ferrite/Views/CommonViews/Modifiers/ViewDidAppear.swift similarity index 100% rename from Ferrite/Views/CommonViews/Modifiers/ViewDidAppearModifier.swift rename to Ferrite/Views/CommonViews/Modifiers/ViewDidAppear.swift diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 56ea9e6..aaa4787 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -59,7 +59,7 @@ struct ContentView: View { } } .navigationSearchBarHiddenWhenScrolling(false) - .searchAppearance { + .customScopeBar { SearchFilterHeaderView() .environmentObject(scrapingModel) .environmentObject(debridManager) diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index eaa027f..6629c4b 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -76,7 +76,7 @@ struct LibraryView: View { } } .navigationSearchBarHiddenWhenScrolling(false) - .searchAppearance { + .customScopeBar { LibraryPickerView() .environmentObject(debridManager) .environmentObject(navModel) diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift index 7f3eb7f..07a71aa 100644 --- a/Ferrite/Views/PluginsView.swift +++ b/Ferrite/Views/PluginsView.swift @@ -66,10 +66,6 @@ struct PluginsView: View { } } .navigationSearchBarHiddenWhenScrolling(false) - .searchAppearance { - PluginPickerView() - .environmentObject(navModel) - } } .overlay { if checkedForPlugins { @@ -87,6 +83,10 @@ struct PluginsView: View { ProgressView() } } + .customScopeBar { + PluginPickerView() + .environmentObject(navModel) + } } } -- 2.45.2 From cbe3d17be17d43c2c6b0ed8aa9fed7126eb12e33 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 27 Feb 2023 12:22:36 -0500 Subject: [PATCH 12/18] Ferrite: Fix search and add progressive loading The searchbar had a lot of lag when scrolling down the search results view. This was due to a shared searchText variable which updated every time the searchbar text changed and caused UI blocking. Migrate searchText to a local variable and destroy the child SearchResultsView as it's not needed at this time (may come back with v0.7 due to searchable). Also sources now display results progressively without a ProgressView blocking when each source loads which allows the user to view media faster. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 38 +++--- Ferrite/ViewModels/DebridManager.swift | 11 +- Ferrite/ViewModels/NavigationViewModel.swift | 4 - Ferrite/ViewModels/ScrapingViewModel.swift | 41 ++++-- Ferrite/ViewModels/ToastViewModel.swift | 22 ++++ Ferrite/Views/ContentView.swift | 124 +++++++++++------- Ferrite/Views/LibraryView.swift | 10 +- Ferrite/Views/MainView.swift | 15 ++- Ferrite/Views/SearchResultsView.swift | 60 --------- .../Views/SheetViews/BatchChoiceView.swift | 1 + 10 files changed, 166 insertions(+), 160 deletions(-) delete mode 100644 Ferrite/Views/SearchResultsView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index d2f29cb..f6f5a18 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 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 */; }; + 0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C2DD4CE29A6D47400293FC3 /* SwiftUIX */; }; 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 */; }; @@ -61,7 +62,6 @@ 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; }; 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; }; 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; - 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; }; 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; }; 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 */; }; @@ -102,7 +102,6 @@ 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148CF288903F000DE2211 /* ToastViewModel.swift */; }; 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */; }; 0CA148E9288903F000DE2211 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D1288903F000DE2211 /* MainView.swift */; }; - 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; }; 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; }; 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23328C2658700616D3A /* LibraryView.swift */; }; 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23628C2660700616D3A /* HistoryView.swift */; }; @@ -226,7 +225,6 @@ 0CA148CF288903F000DE2211 /* ToastViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToastViewModel.swift; sourceTree = ""; }; 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RealDebridWrapper.swift; sourceTree = ""; }; 0CA148D1288903F000DE2211 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; - 0CA148D3288903F000DE2211 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; 0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 0CA3B23328C2658700616D3A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; 0CA3B23628C2660700616D3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; @@ -267,8 +265,8 @@ 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */, 0C448BE929A135F100F4E266 /* Introspect-Static in Frameworks */, 0C64A4B4288903680079976D /* Base32 in Frameworks */, + 0C2DD4CF29A6D47400293FC3 /* SwiftUIX in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, - 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, 0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */, @@ -521,7 +519,6 @@ 0C0755C22934241F00ECA142 /* SheetViews */, 0CA148D1288903F000DE2211 /* MainView.swift */, 0CA148D4288903F000DE2211 /* ContentView.swift */, - 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, 0CA3B23328C2658700616D3A /* LibraryView.swift */, 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */, 0CA148BB288903F000DE2211 /* SettingsView.swift */, @@ -626,10 +623,10 @@ 0C64A4B3288903680079976D /* Base32 */, 0C64A4B6288903880079976D /* KeychainSwift */, 0C4CFC452897030D00AD9FAD /* Regex */, - 0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, 0CDDDE042935235E006810B1 /* BetterSafariView */, 0C448BE829A135F100F4E266 /* Introspect-Static */, + 0C2DD4CE29A6D47400293FC3 /* SwiftUIX */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -664,10 +661,10 @@ 0C64A4B2288903680079976D /* XCRemoteSwiftPackageReference "Base32" */, 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */, 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */, - 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */, 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, + 0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -831,7 +828,6 @@ 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, 0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */, 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */, - 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, 0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */, ); @@ -1048,6 +1044,14 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ + 0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/SwiftUIX/SwiftUIX"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.1.3; + }; + }; 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/siteline/SwiftUI-Introspect/"; @@ -1080,14 +1084,6 @@ kind = branch; }; }; - 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/SwiftUIX/SwiftUIX"; - requirement = { - branch = master; - kind = branch; - }; - }; 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; @@ -1115,6 +1111,11 @@ /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ + 0C2DD4CE29A6D47400293FC3 /* SwiftUIX */ = { + isa = XCSwiftPackageProductDependency; + package = 0C2DD4CD29A6D47400293FC3 /* XCRemoteSwiftPackageReference "SwiftUIX" */; + productName = SwiftUIX; + }; 0C448BE829A135F100F4E266 /* Introspect-Static */ = { isa = XCSwiftPackageProductDependency; package = 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; @@ -1135,11 +1136,6 @@ package = 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; - 0C7376EF28A97D1400D60918 /* SwiftUIX */ = { - isa = XCSwiftPackageProductDependency; - package = 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */; - productName = SwiftUIX; - }; 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 8498868..b91e4a5 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -19,7 +19,6 @@ public class DebridManager: ObservableObject { // UI Variables @Published var showWebView: Bool = false @Published var showAuthSession: Bool = false - @Published var showLoadingProgress: Bool = false // Service agnostic variables @Published var enabledDebrids: Set = [] { @@ -50,6 +49,7 @@ public class DebridManager: ObservableObject { var selectedRealDebridFile: RealDebrid.IAFile? var selectedRealDebridID: String? + // TODO: Maybe make these generic? // RealDebrid cloud variables @Published var realDebridCloudTorrents: [RealDebrid.UserTorrentsResponse] = [] @Published var realDebridCloudDownloads: [RealDebrid.UserDownloadsResponse] = [] @@ -479,10 +479,13 @@ public class DebridManager: ObservableObject { public func fetchDebridDownload(magnet: Magnet?, cloudInfo: String? = nil) async { defer { currentDebridTask = nil - showLoadingProgress = false + toastModel?.hideIndeterminateToast() } - showLoadingProgress = true + toastModel?.updateIndeterminateToast("Loading content", cancelAction: { + self.currentDebridTask?.cancel() + self.currentDebridTask = nil + }) switch selectedDebridType { case .realDebrid: @@ -557,7 +560,7 @@ public class DebridManager: ObservableObject { await deleteRdTorrent(torrentID: selectedRealDebridID, presentError: false) } - showLoadingProgress = false + toastModel?.hideIndeterminateToast() } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index d3460e2..aed154a 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -40,9 +40,6 @@ class NavigationViewModel: ObservableObject { case actions } - @Published var isEditingSearch: Bool = false - @Published var isSearching: Bool = false - @Published var selectedMagnet: Magnet? @Published var selectedHistoryInfo: HistoryEntryJson? @Published var resultFromCloud: Bool = false @@ -60,7 +57,6 @@ class NavigationViewModel: ObservableObject { @Published var showLocalActivitySheet = false @Published var selectedTab: ViewTab = .search - @Published var showSearchProgress: Bool = false // Used between SourceListView and SourceSettingsView @Published var showSourceSettings: Bool = false diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index ab7ae86..9aff244 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -18,13 +18,23 @@ class ScrapingViewModel: ObservableObject { var runningSearchTask: Task? @Published var searchResults: [SearchResult] = [] - @Published var searchText: String = "" @Published var filteredSource: Source? @Published var currentSourceName: String? + // Only add results with valid magnet hashes to the search results array @MainActor func updateSearchResults(newResults: [SearchResult]) { - searchResults = newResults + searchResults += newResults.filter { $0.magnet.hash != nil } + } + + @MainActor + func clearSearchResults() { + searchResults = [] + } + + func cancelCurrentTask() { + runningSearchTask?.cancel() + runningSearchTask = nil } // Utility function to print source specific errors @@ -38,7 +48,7 @@ class ScrapingViewModel: ObservableObject { print(newDescription) } - public func scanSources(sources: [Source]) async { + public func scanSources(sources: [Source], searchText: String) async { if sources.isEmpty { await toastModel?.updateToastDescription("There are no sources to search!", newToastType: .info) @@ -46,13 +56,20 @@ class ScrapingViewModel: ObservableObject { return } - var tempResults: [SearchResult] = [] + await clearSearchResults() + + await toastModel?.updateIndeterminateToast("Loading", cancelAction: { + self.cancelCurrentTask() + }) for source in sources { + // If the search is cancelled, return + if let runningSearchTask, runningSearchTask.isCancelled { + return + } + if source.enabled { - Task { @MainActor in - currentSourceName = source.name - } + await toastModel?.updateIndeterminateToast("Loading \(source.name)", cancelAction: nil) guard let baseUrl = source.baseUrl else { await toastModel?.updateToastDescription("The base URL could not be found for source \(source.name)") @@ -86,7 +103,7 @@ class ScrapingViewModel: ObservableObject { let html = String(data: data, encoding: .utf8) { let sourceResults = await scrapeHtml(source: source, baseUrl: baseUrl, html: html) - tempResults += sourceResults + await updateSearchResults(newResults: sourceResults) } } case .rss: @@ -111,7 +128,7 @@ class ScrapingViewModel: ObservableObject { let rss = String(data: data, encoding: .utf8) { let sourceResults = await scrapeRss(source: source, rss: rss) - tempResults += sourceResults + await updateSearchResults(newResults: sourceResults) } } case .siteApi: @@ -155,7 +172,7 @@ class ScrapingViewModel: ObservableObject { if let data { let sourceResults = await scrapeJson(source: source, jsonData: data) - tempResults += sourceResults + await updateSearchResults(newResults: sourceResults) } } case .none: @@ -164,12 +181,10 @@ class ScrapingViewModel: ObservableObject { } } - // If the task is cancelled, return + // If the search is cancelled, return if let searchTask = runningSearchTask, searchTask.isCancelled { return } - - await updateSearchResults(newResults: tempResults) } // Checks the base URL for any website data then iterates through the fallback URLs diff --git a/Ferrite/ViewModels/ToastViewModel.swift b/Ferrite/ViewModels/ToastViewModel.swift index d3f1704..28045a4 100644 --- a/Ferrite/ViewModels/ToastViewModel.swift +++ b/Ferrite/ViewModels/ToastViewModel.swift @@ -35,6 +35,10 @@ class ToastViewModel: ObservableObject { @Published var showToast: Bool = false + @Published var indeterminateToastDescription: String? = nil + @Published var indeterminateCancelAction: (() -> ())? = nil + @Published var showIndeterminateToast: Bool = false + public func updateToastDescription(_ description: String, newToastType: ToastType? = nil) { if let newToastType { toastType = newToastType @@ -43,6 +47,24 @@ class ToastViewModel: ObservableObject { toastDescription = description } + public func updateIndeterminateToast(_ description: String, cancelAction: (() -> ())?) { + indeterminateToastDescription = description + + if let cancelAction { + indeterminateCancelAction = cancelAction + } + + if !showIndeterminateToast { + showIndeterminateToast = true + } + } + + public func hideIndeterminateToast() { + showIndeterminateToast = false + indeterminateToastDescription = "" + indeterminateCancelAction = nil + } + // Default the toast type to error since the majority of toasts are errors @Published var toastType: ToastType = .error } diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index aaa4787..d4e40c6 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -13,57 +13,89 @@ struct ContentView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var pluginManager: PluginManager + @EnvironmentObject var toastModel: ToastViewModel + + @State private var isEditingSearch = false + @State private var isSearching = false + @State private var searchText: String = "" var body: some View { NavView { - SearchResultsView() - .listStyle(.insetGrouped) - .navigationTitle("Search") - .navigationSearchBar { - SearchBar("Search", - text: $scrapingModel.searchText, - isEditing: $navModel.isEditingSearch, - onCommit: { - scrapingModel.searchResults = [] - scrapingModel.runningSearchTask = Task { - navModel.isSearching = true - navModel.showSearchProgress = true - - let sources = pluginManager.fetchInstalledSources() - await scrapingModel.scanSources(sources: sources) - - if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { - debridManager.clearIAValues() - - // Remove magnets that don't have a hash - let magnets = scrapingModel.searchResults.compactMap { - if let magnetHash = $0.magnet.hash { - return Magnet(hash: magnetHash, link: $0.magnet.link) - } else { - return nil - } - } - await debridManager.populateDebridIA(magnets) - } - - navModel.showSearchProgress = false - } - }) - .showsCancelButton(navModel.isEditingSearch || navModel.isSearching) - .onCancel { - scrapingModel.searchResults = [] - scrapingModel.runningSearchTask?.cancel() - scrapingModel.runningSearchTask = nil - navModel.isSearching = false - scrapingModel.searchText = "" - } + List { + ForEach(scrapingModel.searchResults, id: \.self) { result in + if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil { + SearchResultButtonView(result: result) + } } - .navigationSearchBarHiddenWhenScrolling(false) - .customScopeBar { - SearchFilterHeaderView() - .environmentObject(scrapingModel) - .environmentObject(debridManager) + } + .listStyle(.insetGrouped) + .inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 20 : -20) + .overlay { + if scrapingModel.searchResults.isEmpty && isSearching && scrapingModel.runningSearchTask == nil { + Text("No results found") } + } + .onChange(of: scrapingModel.searchResults) { _ in + // Cleans up any leftover search results in the event of an abrupt cancellation + if !isSearching { + scrapingModel.searchResults = [] + } + } + .onChange(of: navModel.selectedTab) { tab in + // Cancel the search if tab is switched while search is in progress + if tab != .search, scrapingModel.runningSearchTask != nil { + scrapingModel.searchResults = [] + scrapingModel.runningSearchTask?.cancel() + scrapingModel.runningSearchTask = nil + isSearching = false + searchText = "" + } + } + .navigationTitle("Search") + .navigationSearchBar { + SearchBar( + "Search", + text: $searchText, + isEditing: $isEditingSearch, + onCommit: { + if let runningSearchTask = scrapingModel.runningSearchTask, runningSearchTask.isCancelled { + scrapingModel.runningSearchTask = nil + return + } + + scrapingModel.runningSearchTask = Task { + isSearching = true + + let sources = pluginManager.fetchInstalledSources() + await scrapingModel.scanSources(sources: sources, searchText: searchText) + + if debridManager.enabledDebrids.count > 0, !scrapingModel.searchResults.isEmpty { + debridManager.clearIAValues() + + let magnets = scrapingModel.searchResults.map(\.magnet) + await debridManager.populateDebridIA(magnets) + } + + toastModel.hideIndeterminateToast() + scrapingModel.runningSearchTask = nil + } + } + ) + .showsCancelButton(isEditingSearch || isSearching) + .onCancel { + scrapingModel.searchResults = [] + scrapingModel.runningSearchTask?.cancel() + scrapingModel.runningSearchTask = nil + isSearching = false + searchText = "" + } + } + .navigationSearchBarHiddenWhenScrolling(false) + } + .customScopeBar { + SearchFilterHeaderView() + .environmentObject(scrapingModel) + .environmentObject(debridManager) } } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 6629c4b..89dfdd9 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -76,11 +76,6 @@ struct LibraryView: View { } } .navigationSearchBarHiddenWhenScrolling(false) - .customScopeBar { - LibraryPickerView() - .environmentObject(debridManager) - .environmentObject(navModel) - } .environment(\.editMode, $editMode) } .overlay { @@ -99,6 +94,11 @@ struct LibraryView: View { } } } + .customScopeBar { + LibraryPickerView() + .environmentObject(debridManager) + .environmentObject(navModel) + } .onChange(of: navModel.libraryPickerSelection) { _ in editMode = .inactive } diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 69b2bd7..7403048 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -171,17 +171,18 @@ struct MainView: View { .cornerRadius(10) } - if debridManager.showLoadingProgress { + if toastModel.showIndeterminateToast { VStack { - Text("Loading content") + Text(toastModel.indeterminateToastDescription ?? "Loading...") HStack { IndeterminateProgressView() - Button("Cancel") { - debridManager.currentDebridTask?.cancel() - debridManager.currentDebridTask = nil - debridManager.showLoadingProgress = false + if let cancelAction = toastModel.indeterminateCancelAction { + Button("Cancel") { + cancelAction() + toastModel.hideIndeterminateToast() + } } } } @@ -198,7 +199,7 @@ struct MainView: View { .foregroundColor(.clear) .frame(height: 60) } - .animation(.easeInOut(duration: 0.3), value: toastModel.showToast || debridManager.showLoadingProgress) + .animation(.easeInOut(duration: 0.3), value: toastModel.showToast || toastModel.showIndeterminateToast) } } } diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift deleted file mode 100644 index dc2b699..0000000 --- a/Ferrite/Views/SearchResultsView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// SearchResultsView.swift -// Ferrite -// -// Created by Brian Dashore on 7/11/22. -// - -import SwiftUI - -struct SearchResultsView: View { - @EnvironmentObject var scrapingModel: ScrapingViewModel - @EnvironmentObject var navModel: NavigationViewModel - @EnvironmentObject var debridManager: DebridManager - - var body: some View { - List { - ForEach(scrapingModel.searchResults, id: \.self) { result in - if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil { - SearchResultButtonView(result: result) - } - } - } - .listStyle(.insetGrouped) - .inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 20 : -20) - .overlay { - if scrapingModel.searchResults.isEmpty { - if navModel.showSearchProgress { - VStack(spacing: 5) { - ProgressView() - Text("Loading \(scrapingModel.currentSourceName ?? "")") - } - } else if navModel.isSearching, scrapingModel.runningSearchTask != nil { - Text("No results found") - } - } - } - .onChange(of: navModel.selectedTab) { tab in - // Cancel the search if tab is switched while search is in progress - if tab != .search, navModel.showSearchProgress { - scrapingModel.searchResults = [] - scrapingModel.runningSearchTask?.cancel() - scrapingModel.runningSearchTask = nil - navModel.isSearching = false - scrapingModel.searchText = "" - } - } - .onChange(of: scrapingModel.searchResults) { _ in - // Cleans up any leftover search results in the event of an abrupt cancellation - if !navModel.isSearching { - scrapingModel.searchResults = [] - } - } - } -} - -struct SearchResultsView_Previews: PreviewProvider { - static var previews: some View { - SearchResultsView() - } -} diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index b413b43..7497ff2 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -14,6 +14,7 @@ struct BatchChoiceView: View { let backgroundContext = PersistenceController.shared.backgroundContext + // TODO: Make this generic for IA(?) and add searchbar var body: some View { NavView { List { -- 2.45.2 From f8b6ea6ba78cec430ebe716b3f14898c3d58f75b Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 27 Feb 2023 13:05:46 -0500 Subject: [PATCH 13/18] Search: Add random text to the searchbar It's redundant to have the navgiation title of the Search view as "Search" and the searchbar to have the same title. Make the searchbar have randomized quotes (from a fixed set) that doesn't repeat cases. Maybe make this customizable in the future? Signed-off-by: kingbri --- Ferrite/Views/ContentView.swift | 41 +++++++++++++++++++++++++++++++- Ferrite/Views/SettingsView.swift | 5 ++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index d4e40c6..7089b7b 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -15,10 +15,23 @@ struct ContentView: View { @EnvironmentObject var pluginManager: PluginManager @EnvironmentObject var toastModel: ToastViewModel + @AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false + @State private var isEditingSearch = false @State private var isSearching = false @State private var searchText: String = "" + @State private var lastSearchTextIndex: Int = -1 + @State private var searchBarText: String = "Search" + let searchBarTextArray: [String] = [ + "What's on your mind?", + "Discover something interesting", + "Find an engaging show", + "Feeling adventurous?", + "Look for something new", + "The classics are a good idea" + ] + var body: some View { NavView { List { @@ -35,6 +48,11 @@ struct ContentView: View { Text("No results found") } } + .onChange(of: searchText) { newText in + if newText.isEmpty && isSearching { + searchBarText = getSearchBarText() + } + } .onChange(of: scrapingModel.searchResults) { _ in // Cleans up any leftover search results in the event of an abrupt cancellation if !isSearching { @@ -54,7 +72,7 @@ struct ContentView: View { .navigationTitle("Search") .navigationSearchBar { SearchBar( - "Search", + searchBarText, text: $searchText, isEditing: $isEditingSearch, onCommit: { @@ -88,6 +106,7 @@ struct ContentView: View { scrapingModel.runningSearchTask = nil isSearching = false searchText = "" + searchBarText = getSearchBarText() } } .navigationSearchBarHiddenWhenScrolling(false) @@ -97,6 +116,26 @@ struct ContentView: View { .environmentObject(scrapingModel) .environmentObject(debridManager) } + .backport.onAppear { + searchBarText = getSearchBarText() + } + } + + // Fetches random searchbar text if enabled, otherwise deinit the last case value + func getSearchBarText() -> String { + if usesRandomSearchText { + let num = Int.random(in: 0.. Date: Mon, 27 Feb 2023 15:05:43 -0500 Subject: [PATCH 14/18] Actions: Add release action Signed-off-by: kingbri --- .github/workflows/nightly.yml | 4 ++-- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 1cb3eb7..b429522 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -8,7 +8,7 @@ jobs: build: runs-on: macos-12 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Setup Xcode uses: maxim-lobanov/setup-xcode@v1 with: @@ -25,7 +25,7 @@ jobs: cp -r build/Ferrite.xcarchive/Products/Applications/Ferrite.app Payload zip -r Ferrite-iOS_nightly-${{ env.sha_short }}.ipa Payload - name: Upload artifacts - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa path: Ferrite-iOS_nightly-${{ env.sha_short }}.ipa diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1bc654d --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,36 @@ +name: Build and upload release ipa + +on: + release: + types: + - created + +jobs: + build: + runs-on: macos-12 + steps: + - uses: actions/checkout@v3 + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest + - name: Build + run: xcodebuild -scheme Ferrite -configuration Release archive -archivePath build/Ferrite.xcarchive CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO + env: + IS_NIGHTLY: NO + - name: Get app version + run: | + echo "app_version=$(/usr/libexec/plistbuddy -c Print:CFBundleShortVersionString: build/Ferrite.xcarchive/Products/Applications/Ferrite.app/Info.plist)" >> $GITHUB_ENV + - name: Package ipa + run: | + mkdir Payload + cp -r build/Ferrite.xcarchive/Products/Applications/Ferrite.app Payload + zip -r Ferrite-iOS_v${{ env.app_version }}.ipa Payload + - name: Create ipa zip + run: | + zip -j Ferrite-iOS_v${{ env.app_version }}.ipa.zip Ferrite-iOS_v${{ env.app_version }}.ipa + - name: Upload release + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/') + with: + files: Ferrite-iOS_v${{ env.app_version }}.ipa.zip -- 2.45.2 From 0661ed66f334c6008d468fd27f87f2a2f87d7f5d Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 28 Feb 2023 19:42:20 -0500 Subject: [PATCH 15/18] Search: Fix picker overlay and position iOS 14 requires the scope bar modifier to be on the first subview of the NavView. This is because a VStack wraps the content. A bug was that the segmented picker was being overlaid due to the scope bar modifier having an AppStorage call. The AppStorage call updated the modifier which for some reason added another HostingView. I am not sure why this happens, but avoid using AppStorage in the modifier to make sure this doesn't happen again. Signed-off-by: kingbri --- .../CommonViews/Modifiers/CustomScopeBar.swift | 13 +++++++------ Ferrite/Views/ContentView.swift | 10 +++++----- Ferrite/Views/LibraryView.swift | 10 +++++----- Ferrite/Views/PluginsView.swift | 8 ++++---- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift index 98626bf..1cb11b1 100644 --- a/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift +++ b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift @@ -9,24 +9,25 @@ import SwiftUI import Introspect struct CustomScopeBarModifier: ViewModifier { - @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true - let hostingContent: V @State private var hostingController: UIHostingController? - init(hostingContent: V) { - self.hostingContent = hostingContent + // Don't use AppStorage since it causes a view update + var autocorrectSearch: Bool { + UserDefaults.standard.bool(forKey: "Behavior.AutocorrectSearch") } func body(content: Content) -> some View { if #available(iOS 15, *) { content .backport.introspectSearchController { searchController in + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + + // MARK: One-time setup guard hostingController == nil else { return } searchController.hidesNavigationBarDuringPresentation = true - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none searchController.searchBar.showsScopeBar = true searchController.searchBar.scopeButtonTitles = [""] (searchController.searchBar.value(forKey: "_scopeBar") as? UIView)?.isHidden = true diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 7089b7b..36db8af 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -110,11 +110,11 @@ struct ContentView: View { } } .navigationSearchBarHiddenWhenScrolling(false) - } - .customScopeBar { - SearchFilterHeaderView() - .environmentObject(scrapingModel) - .environmentObject(debridManager) + .customScopeBar { + SearchFilterHeaderView() + .environmentObject(scrapingModel) + .environmentObject(debridManager) + } } .backport.onAppear { searchBarText = getSearchBarText() diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 89dfdd9..6629c4b 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -76,6 +76,11 @@ struct LibraryView: View { } } .navigationSearchBarHiddenWhenScrolling(false) + .customScopeBar { + LibraryPickerView() + .environmentObject(debridManager) + .environmentObject(navModel) + } .environment(\.editMode, $editMode) } .overlay { @@ -94,11 +99,6 @@ struct LibraryView: View { } } } - .customScopeBar { - LibraryPickerView() - .environmentObject(debridManager) - .environmentObject(navModel) - } .onChange(of: navModel.libraryPickerSelection) { _ in editMode = .inactive } diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift index 07a71aa..3f4b179 100644 --- a/Ferrite/Views/PluginsView.swift +++ b/Ferrite/Views/PluginsView.swift @@ -66,6 +66,10 @@ struct PluginsView: View { } } .navigationSearchBarHiddenWhenScrolling(false) + .customScopeBar { + PluginPickerView() + .environmentObject(navModel) + } } .overlay { if checkedForPlugins { @@ -83,10 +87,6 @@ struct PluginsView: View { ProgressView() } } - .customScopeBar { - PluginPickerView() - .environmentObject(navModel) - } } } -- 2.45.2 From f622b7af05a37997a7d316d48b03c83b0cada97f Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 1 Mar 2023 18:31:29 -0500 Subject: [PATCH 16/18] Actions: Fix default action settings Since actions use a new API, update default actions to use the same API rather than the legacy models. If an action is removed, a prompt will tell the user to change their default debrid/magnet action and default to the choice sheet. Also add extra UI fixes and cleanup. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 12 +- Ferrite/Models/SettingsModels.swift | 32 ----- Ferrite/ViewModels/NavigationViewModel.swift | 86 +------------- Ferrite/ViewModels/PluginManager.swift | 54 +++++++++ .../Library/Cloud/AllDebridCloudView.swift | 6 +- .../Library/Cloud/PremiumizeCloudView.swift | 6 +- .../Library/Cloud/RealDebridCloudView.swift | 11 +- .../Library/HistoryButtonView.swift | 13 ++- .../Plugin/Source/SourceSettingsView.swift | 13 ++- .../SearchResult/SearchResultButtonView.swift | 13 ++- .../Settings/DefaultActionPickerView.swift | 109 ++++++++++++++++++ .../Settings/DefaultActionsPickerViews.swift | 90 --------------- .../Settings/SettingsPluginListView.swift | 11 +- Ferrite/Views/LibraryView.swift | 32 ++--- Ferrite/Views/MainView.swift | 6 +- Ferrite/Views/PluginsView.swift | 32 ++--- Ferrite/Views/SettingsView.swift | 61 +++++----- .../Views/SheetViews/ActionChoiceView.swift | 7 ++ .../Views/SheetViews/BatchChoiceView.swift | 6 +- 19 files changed, 301 insertions(+), 299 deletions(-) delete mode 100644 Ferrite/Models/SettingsModels.swift create mode 100644 Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift delete mode 100644 Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index f6f5a18..e19c7f5 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -81,8 +81,7 @@ 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 */; }; 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; }; - 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */; }; - 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */; }; + 0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.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 */; }; @@ -204,8 +203,7 @@ 0C84F47C2895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataClass.swift"; sourceTree = ""; }; 0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = ""; }; - 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionsPickerViews.swift; sourceTree = ""; }; - 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; + 0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionPickerView.swift; sourceTree = ""; }; 0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = ""; }; 0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = ""; }; 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListEditorView.swift; sourceTree = ""; }; @@ -346,7 +344,6 @@ 0C422E7F293542F300486D65 /* PremiumizeModels.swift */, 0C0167DB29293FA900B65783 /* RealDebridModels.swift */, 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, - 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, ); @@ -438,7 +435,7 @@ 0C44E2AE28D52E8A007711AE /* BackupsView.swift */, 0CA05456288EE58200850554 /* SettingsPluginListView.swift */, 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */, - 0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */, + 0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */, 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */, ); path = Settings; @@ -763,7 +760,7 @@ 0CC389532970AD900066D06F /* Action+CoreDataClass.swift in Sources */, 0C03EB72296F619900162E9A /* PluginList+CoreDataProperties.swift in Sources */, 0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */, - 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, + 0C95D8D828A55B03005E22B3 /* DefaultActionPickerView.swift in Sources */, 0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, @@ -778,7 +775,6 @@ 0CA148D8288903F000DE2211 /* ActionChoiceView.swift in Sources */, 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, - 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, diff --git a/Ferrite/Models/SettingsModels.swift b/Ferrite/Models/SettingsModels.swift deleted file mode 100644 index 3f20e57..0000000 --- a/Ferrite/Models/SettingsModels.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SettingsModels.swift -// Ferrite -// -// Created by Brian Dashore on 8/11/22. -// - -import Foundation - -public enum DefaultMagnetActionType: Int, CaseIterable { - // Let the user choose - case none = 0 - - // Open in actions come first - case webtor = 1 - - // Sharing actions come last - case shareMagnet = 2 -} - -public enum DefaultDebridActionType: Int, CaseIterable { - // Let the user choose - case none = 0 - - // Open in actions come first - case outplayer = 1 - case vlc = 2 - case infuse = 3 - - // Sharing actions come last - case shareDownload = 4 -} diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index aed154a..45e4c57 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -8,16 +8,16 @@ import SwiftUI @MainActor -class NavigationViewModel: ObservableObject { +public class NavigationViewModel: ObservableObject { var toastModel: ToastViewModel? // Used between SearchResultsView and MagnetChoiceView - enum ChoiceSheetType: Identifiable { - var id: Int { + public enum ChoiceSheetType: Identifiable { + public var id: Int { hashValue } - case magnet + case action case batch case activity } @@ -66,82 +66,4 @@ class NavigationViewModel: ObservableObject { @Published var libraryPickerSelection: LibraryPickerSegment = .bookmarks @Published var pluginPickerSelection: PluginPickerSegment = .sources - - @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none - @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none - - // TODO: Fix for new Actions API - public func runDebridAction(urlString: String, _ action: DefaultDebridActionType? = nil) { - currentChoiceSheet = .magnet - /* - let selectedAction = action ?? defaultDebridAction - - switch selectedAction { - case .none: - currentChoiceSheet = .magnet - case .outplayer: - if let downloadUrl = URL(string: "outplayer://\(urlString)") { - UIApplication.shared.open(downloadUrl) - } else { - toastModel?.updateToastDescription("Could not create an Outplayer URL") - } - case .vlc: - if let downloadUrl = URL(string: "vlc://\(urlString)") { - UIApplication.shared.open(downloadUrl) - } else { - toastModel?.updateToastDescription("Could not create a VLC URL") - } - case .infuse: - if let downloadUrl = URL(string: "infuse://x-callback-url/play?url=\(urlString)") { - UIApplication.shared.open(downloadUrl) - } else { - toastModel?.updateToastDescription("Could not create a Infuse URL") - } - case .shareDownload: - if let downloadUrl = URL(string: urlString), currentChoiceSheet == nil { - activityItems = [downloadUrl] - currentChoiceSheet = .activity - } else { - toastModel?.updateToastDescription("Could not create object for sharing") - } - } - */ - } - - // TODO: Fix for new Actions API - public func runMagnetAction(magnet: Magnet?, _ action: DefaultMagnetActionType? = nil) { - currentChoiceSheet = .magnet - // Fall back to selected magnet if the provided magnet is nil - /* - let magnet = magnet ?? selectedMagnet - guard let magnetLink = magnet?.link else { - toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") - print("Magnet action error: The magnet link is invalid.") - - return - } - - let selectedAction = action ?? defaultMagnetAction - - switch selectedAction { - case .none: - currentChoiceSheet = .magnet - case .webtor: - if let url = URL(string: "https://webtor.io/#/show?magnet=\(magnetLink)") { - UIApplication.shared.open(url) - } else { - toastModel?.updateToastDescription("Could not create a WebTor URL") - } - case .shareMagnet: - if let magnetUrl = URL(string: magnetLink), - currentChoiceSheet == nil - { - activityItems = [magnetUrl] - currentChoiceSheet = .activity - } else { - toastModel?.updateToastDescription("Could not create object for sharing") - } - } - */ - } } diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 2bac4d6..f1fdd29 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -14,6 +14,8 @@ public class PluginManager: ObservableObject { @Published var availableSources: [SourceJson] = [] @Published var availableActions: [ActionJson] = [] + @Published var showBrokenDefaultActionAlert = false + @MainActor public func fetchPluginsFromUrl() async { let pluginListRequest = PluginList.fetchRequest() @@ -162,6 +164,58 @@ public class PluginManager: ObservableObject { } } + @MainActor + public func runDebridAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) { + let context = PersistenceController.shared.backgroundContext + + if + let defaultDebridActionName = UserDefaults.standard.string(forKey: "Actions.DefaultDebridName"), + let defaultDebridActionList = UserDefaults.standard.string(forKey: "Actions.DefaultDebridList") + { + let actionFetchRequest = Action.fetchRequest() + actionFetchRequest.fetchLimit = 1 + actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultDebridActionName, defaultDebridActionList) + + if let fetchedAction = try? context.fetch(actionFetchRequest).first { + runDeeplinkAction(fetchedAction, urlString: urlString) + } else { + currentChoiceSheet = .action + UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName") + UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList") + + showBrokenDefaultActionAlert.toggle() + } + } else { + currentChoiceSheet = .action + } + } + + @MainActor + public func runMagnetAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) { + let context = PersistenceController.shared.backgroundContext + + if + let defaultMagnetActionName = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetName"), + let defaultMagnetActionList = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetList") + { + let actionFetchRequest = Action.fetchRequest() + actionFetchRequest.fetchLimit = 1 + actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultMagnetActionName, defaultMagnetActionList) + + if let fetchedAction = try? context.fetch(actionFetchRequest).first { + runDeeplinkAction(fetchedAction, urlString: urlString) + } else { + currentChoiceSheet = .action + UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName") + UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList") + + showBrokenDefaultActionAlert.toggle() + } + } else { + currentChoiceSheet = .action + } + } + // The iOS version of Ferrite only runs deeplink actions @MainActor public func runDeeplinkAction(_ action: Action, urlString: String?) { diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index d6d6b77..fc8f0b5 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -10,6 +10,7 @@ import SwiftUI struct AllDebridCloudView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var pluginManager: PluginManager @Binding var searchText: String @@ -38,7 +39,10 @@ struct AllDebridCloudView: View { if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) - navModel.runDebridAction(urlString: debridManager.downloadUrl) + pluginManager.runDebridAction( + urlString: debridManager.downloadUrl, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } } } else { diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index 8217ea7..760cd10 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -11,6 +11,7 @@ import SwiftUIX struct PremiumizeCloudView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var pluginManager: PluginManager @Binding var searchText: String @@ -38,7 +39,10 @@ struct PremiumizeCloudView: View { performSave: true ) - navModel.runDebridAction(urlString: debridManager.downloadUrl) + pluginManager.runDebridAction( + urlString: debridManager.downloadUrl, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index d826375..9b4700c 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -10,6 +10,7 @@ import SwiftUI struct RealDebridCloudView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var pluginManager: PluginManager @Binding var searchText: String @@ -35,7 +36,10 @@ struct RealDebridCloudView: View { performSave: true ) - navModel.runDebridAction(urlString: debridManager.downloadUrl) + pluginManager.runDebridAction( + urlString: debridManager.downloadUrl, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } .backport.tint(.primary) } @@ -72,7 +76,10 @@ struct RealDebridCloudView: View { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) - navModel.runDebridAction(urlString: debridManager.downloadUrl) + pluginManager.runDebridAction( + urlString: debridManager.downloadUrl, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } } } else { diff --git a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift index 799cebe..34f793e 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift @@ -11,6 +11,7 @@ struct HistoryButtonView: View { @EnvironmentObject var toastModel: ToastViewModel @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var pluginManager: PluginManager let entry: HistoryEntry @@ -23,14 +24,20 @@ struct HistoryButtonView: View { if url.starts(with: "https://") { Task { debridManager.downloadUrl = url - navModel.runDebridAction(urlString: url) + pluginManager.runDebridAction( + urlString: url, + currentChoiceSheet: &navModel.currentChoiceSheet + ) - if navModel.currentChoiceSheet != .magnet { + if navModel.currentChoiceSheet != .action { debridManager.downloadUrl = "" } } } else { - navModel.runMagnetAction(magnet: Magnet(hash: nil, link: url)) + pluginManager.runMagnetAction( + urlString: url, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } } else { toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.") diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift index b882874..fa30ab4 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift @@ -12,6 +12,11 @@ struct SourceSettingsView: View { @EnvironmentObject var navModel: NavigationViewModel + @FetchRequest( + entity: PluginList.entity(), + sortDescriptors: [] + ) var pluginLists: FetchedResults + var body: some View { NavView { List { @@ -32,10 +37,12 @@ struct SourceSettingsView: View { Group { Text("ID: \(selectedSource.id)") - if let listId = selectedSource.listId { - Text("List ID: \(listId)") + if let pluginList = pluginLists.first(where: { $0.id == selectedSource.listId }) + { + Text("List: \(pluginList.name)") + Text("List ID: \(pluginList.id.uuidString)") } else { - Text("No list ID found. This source should be removed.") + Text("No plugin list found. This source should be removed.") } } .foregroundColor(.secondary) diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index 5a49bb7..19409b0 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -12,6 +12,7 @@ struct SearchResultButtonView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @EnvironmentObject var pluginManager: PluginManager var result: SearchResult @@ -42,9 +43,12 @@ struct SearchResultButtonView: View { performSave: true ) - navModel.runDebridAction(urlString: debridManager.downloadUrl) + pluginManager.runDebridAction( + urlString: debridManager.downloadUrl, + currentChoiceSheet: &navModel.currentChoiceSheet + ) - if navModel.currentChoiceSheet != .magnet { + if navModel.currentChoiceSheet != .action { debridManager.downloadUrl = "" } } @@ -68,7 +72,10 @@ struct SearchResultButtonView: View { performSave: true ) - navModel.runMagnetAction(magnet: result.magnet) + pluginManager.runMagnetAction( + urlString: result.magnet.link, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } } } label: { diff --git a/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift b/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift new file mode 100644 index 0000000..def50f6 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift @@ -0,0 +1,109 @@ +// +// DefaultActionsPickerViews.swift +// Ferrite +// +// Created by Brian Dashore on 8/11/22. +// + +import SwiftUI + +struct DefaultActionPickerView: View { + @EnvironmentObject var toastModel: ToastViewModel + + let actionRequirement: ActionRequirement + @Binding var defaultActionName: String? + @Binding var defaultActionList: String? + + @FetchRequest( + entity: Action.entity(), + sortDescriptors: [] + ) var actions: FetchedResults + + @FetchRequest( + entity: PluginList.entity(), + sortDescriptors: [] + ) var pluginLists: FetchedResults + + var body: some View { + List { + UserChoiceButton( + defaultActionName: $defaultActionName, + defaultActionList: $defaultActionList, + pluginLists: pluginLists + ) + + ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in + Button { + if let actionListId = action.listId?.uuidString { + defaultActionName = action.name + defaultActionList = actionListId + } else { + toastModel.updateToastDescription( + "Default action error: This action doesn't have a corresponding plugin list! Please uninstall the action" + ) + } + } label: { + HStack { + VStack(alignment: .leading, spacing: 5) { + Text(action.name) + + Group { + if let pluginList = pluginLists.first(where: { $0.id == action.listId }) { + Text("List: \(pluginList.name)") + + Text(pluginList.id.uuidString) + .font(.caption) + } else { + Text("No plugin list found") + .font(.caption) + } + } + .foregroundColor(.secondary) + } + Spacer() + + if + let defaultActionList, + action.listId?.uuidString == defaultActionList, + action.name == defaultActionName + { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + .backport.tint(.primary) + } + } + .listStyle(.insetGrouped) + .inlinedList(inset: -20) + .navigationTitle("Default \(actionRequirement == .debrid ? "debrid" : "magnet") action") + .navigationBarTitleDisplayMode(.inline) + } +} + +private struct UserChoiceButton: View { + @Binding var defaultActionName: String? + @Binding var defaultActionList: String? + var pluginLists: FetchedResults + + var body: some View { + Button { + defaultActionName = nil + defaultActionList = nil + } label: { + HStack { + Text("Let me choose") + Spacer() + + // Force "Let me choose" if the name OR list ID is nil + // Prevents any mismatches + if defaultActionName == nil || pluginLists.contains(where: { $0.id.uuidString == defaultActionList }) { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + .backport.tint(.primary) + } +} diff --git a/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift b/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift deleted file mode 100644 index ab7b52c..0000000 --- a/Ferrite/Views/ComponentViews/Settings/DefaultActionsPickerViews.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// DefaultActionsPickerViews.swift -// Ferrite -// -// Created by Brian Dashore on 8/11/22. -// - -import SwiftUI - -struct MagnetActionPickerView: View { - @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none - - var body: some View { - List { - ForEach(DefaultMagnetActionType.allCases, id: \.self) { action in - Button { - defaultMagnetAction = action - } label: { - HStack { - Text(fetchPickerChoiceName(choice: action)) - Spacer() - if action == defaultMagnetAction { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - } - } - .backport.tint(.primary) - } - } - .listStyle(.insetGrouped) - .inlinedList(inset: -20) - .navigationTitle("Default magnet action") - .navigationBarTitleDisplayMode(.inline) - } - - func fetchPickerChoiceName(choice: DefaultMagnetActionType) -> String { - switch choice { - case .none: - return "Let me choose" - case .webtor: - return "Open in Webtor" - case .shareMagnet: - return "Share magnet link" - } - } -} - -struct DebridActionPickerView: View { - @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none - - var body: some View { - List { - ForEach(DefaultDebridActionType.allCases, id: \.self) { action in - Button { - defaultDebridAction = action - } label: { - HStack { - Text(fetchPickerChoiceName(choice: action)) - Spacer() - if action == defaultDebridAction { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - } - } - .backport.tint(.primary) - } - } - .listStyle(.insetGrouped) - .inlinedList(inset: -20) - .navigationTitle("Default debrid action") - .navigationBarTitleDisplayMode(.inline) - } - - func fetchPickerChoiceName(choice: DefaultDebridActionType) -> String { - switch choice { - case .none: - return "Let me choose" - case .outplayer: - return "Open in Outplayer" - case .vlc: - return "Open in VLC" - case .infuse: - return "Open in Infuse" - case .shareDownload: - return "Share download link" - } - } -} diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift index 1f23b57..fe523ba 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsPluginListView.swift @@ -30,12 +30,13 @@ struct SettingsPluginListView: View { VStack(alignment: .leading, spacing: 5) { Text(pluginList.name) - Text(pluginList.author) - .foregroundColor(.gray) + Group { + Text(pluginList.author) - Text("ID: \(pluginList.id)") - .font(.caption) - .foregroundColor(.gray) + Text("ID: \(pluginList.id)") + .font(.caption) + } + .foregroundColor(.secondary) } .padding(.vertical, 2) .contextMenu { diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 6629c4b..ace53a1 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -44,6 +44,22 @@ struct LibraryView: View { DebridCloudView(searchText: $searchText) } } + .overlay { + switch navModel.libraryPickerSelection { + case .bookmarks: + if bookmarks.isEmpty { + EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") + } + case .history: + if history.isEmpty { + EmptyInstructionView(title: "No History", message: "Start watching to build history") + } + case .debridCloud: + if debridManager.selectedDebridType == nil { + EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") + } + } + } .navigationTitle("Library") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { @@ -83,22 +99,6 @@ struct LibraryView: View { } .environment(\.editMode, $editMode) } - .overlay { - switch navModel.libraryPickerSelection { - case .bookmarks: - if bookmarks.isEmpty { - EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") - } - case .history: - if history.isEmpty { - EmptyInstructionView(title: "No History", message: "Start watching to build history") - } - case .debridCloud: - if debridManager.selectedDebridType == nil { - EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") - } - } - } .onChange(of: navModel.libraryPickerSelection) { _ in editMode = .inactive } diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 7403048..233a5dc 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -51,7 +51,7 @@ struct MainView: View { } .sheet(item: $navModel.currentChoiceSheet) { item in switch item { - case .magnet: + case .action: ActionChoiceView() .environmentObject(debridManager) .environmentObject(scrapingModel) @@ -139,7 +139,9 @@ struct MainView: View { .backport.alert( isPresented: $showUpdateAlert, title: "Update available", - message: "Ferrite \(releaseVersionString) can be downloaded. \n\nThis alert can be disabled in Settings.", + message: + "Ferrite \(releaseVersionString) can be downloaded. \n\n" + + "This alert can be disabled in Settings.", buttons: [ .init("Download") { guard let releaseUrl = URL(string: releaseUrlString) else { diff --git a/Ferrite/Views/PluginsView.swift b/Ferrite/Views/PluginsView.swift index 3f4b179..14b29af 100644 --- a/Ferrite/Views/PluginsView.swift +++ b/Ferrite/Views/PluginsView.swift @@ -44,6 +44,22 @@ struct PluginsView: View { } } } + .overlay { + if checkedForPlugins { + switch navModel.pluginPickerSelection { + 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() + } + } .backport.onAppear { viewTask = Task { await pluginManager.fetchPluginsFromUrl() @@ -71,22 +87,6 @@ struct PluginsView: View { .environmentObject(navModel) } } - .overlay { - if checkedForPlugins { - switch navModel.pluginPickerSelection { - 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() - } - } } } diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 17912cd..1c38209 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -20,8 +20,11 @@ struct SettingsView: View { @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true - @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none - @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none + @AppStorage("Actions.DefaultDebridName") var defaultDebridActionName: String? + @AppStorage("Actions.DefaultDebridList") var defaultDebridActionList: String? + + @AppStorage("Actions.DefaultMagnetName") var defaultMagnetActionName: String? + @AppStorage("Actions.DefaultMagnetList") var defaultMagnetActionList: String? var body: some View { NavView { @@ -94,50 +97,40 @@ struct SettingsView: View { } Section(header: InlineHeader("Default actions")) { - if debridManager.enabledDebrids.count > 0 { + //if debridManager.enabledDebrids.count > 0 { NavigationLink( - destination: DebridActionPickerView(), + destination: DefaultActionPickerView( + actionRequirement: .debrid, + defaultActionName: $defaultDebridActionName, + defaultActionList: $defaultDebridActionList + ), label: { HStack { - Text("Default debrid action") + Text("Debrid action") Spacer() - Group { - switch defaultDebridAction { - case .none: - Text("User choice") - case .outplayer: - Text("Outplayer") - case .vlc: - Text("VLC") - case .infuse: - Text("Infuse") - case .shareDownload: - Text("Share") - } - } - .foregroundColor(.gray) + + // TODO: Maybe make this check for nil list as well? + Text(defaultDebridActionName.map { $0 } ?? "User choice") + .foregroundColor(.secondary) } } ) - } + //} NavigationLink( - destination: MagnetActionPickerView(), + destination: DefaultActionPickerView( + actionRequirement: .magnet, + defaultActionName: $defaultMagnetActionName, + defaultActionList: $defaultMagnetActionList + ), label: { HStack { - Text("Default magnet action") + Text("Magnet action") Spacer() - Group { - switch defaultMagnetAction { - case .none: - Text("User choice") - case .webtor: - Text("Webtor") - case .shareMagnet: - Text("Share") - } - } - .foregroundColor(.gray) + + // TODO: Maybe make this check for nil list as well? + Text(defaultMagnetActionName.map { $0 } ?? "User choice") + .foregroundColor(.secondary) } } ) diff --git a/Ferrite/Views/SheetViews/ActionChoiceView.swift b/Ferrite/Views/SheetViews/ActionChoiceView.swift index e24a3e3..d9b4839 100644 --- a/Ferrite/Views/SheetViews/ActionChoiceView.swift +++ b/Ferrite/Views/SheetViews/ActionChoiceView.swift @@ -112,6 +112,13 @@ struct ActionChoiceView: View { AppActivityView(activityItems: navModel.activityItems) } } + .backport.alert( + isPresented: $pluginManager.showBrokenDefaultActionAlert, + title: "Action not found", + message: + "The default action could not be run. The action choice sheet has been opened. \n\n" + + "Please check your default actions in Settings" + ) .onDisappear { debridManager.downloadUrl = "" navModel.selectedTitle = "" diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 7497ff2..1721adb 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -11,6 +11,7 @@ struct BatchChoiceView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var pluginManager: PluginManager let backgroundContext = PersistenceController.shared.backgroundContext @@ -83,7 +84,10 @@ struct BatchChoiceView: View { PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true) } - navModel.runDebridAction(urlString: debridManager.downloadUrl, nil) + pluginManager.runDebridAction( + urlString: debridManager.downloadUrl, + currentChoiceSheet: &navModel.currentChoiceSheet + ) } debridManager.clearSelectedDebridItems() -- 2.45.2 From 282783c46064ee7323c54e24a11575fc2ff58bbe Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 2 Mar 2023 11:06:57 -0500 Subject: [PATCH 17/18] Plugins: Fix list sync in view Plugin entries were not syncing properly inside the plugins view. Instead of adding events to update filtered lists, make it so that these filtered lists are updated on state changes. This is the intended method of reactive programming and removes complexity from filtering logic. Signed-off-by: kingbri --- Ferrite/Extensions/NotificationCenter.swift | 4 -- Ferrite/ViewModels/PluginManager.swift | 24 ++++++++---- .../Buttons/InstalledPluginButtonView.swift | 2 - .../Plugin/PluginListView.swift | 39 ++++++++----------- 4 files changed, 34 insertions(+), 35 deletions(-) diff --git a/Ferrite/Extensions/NotificationCenter.swift b/Ferrite/Extensions/NotificationCenter.swift index 63599a9..7bc0db5 100644 --- a/Ferrite/Extensions/NotificationCenter.swift +++ b/Ferrite/Extensions/NotificationCenter.swift @@ -11,8 +11,4 @@ extension Notification.Name { static var didDeleteBookmark: Notification.Name { Notification.Name("Deleted bookmark") } - - static var didDeletePlugin: Notification.Name { - Notification.Name("Deleted plugin") - } } diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index f1fdd29..b370b64 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -89,12 +89,16 @@ public class PluginManager: ObservableObject { } print("Plugin fetch error: \(error)") - } + } } - // Check if underlying type is Source or Action - func fetchFilteredPlugins(installedPlugins: FetchedResults

, searchText: String) -> [PJ] { - let availablePlugins: [PJ] = fetchCastedPlugins(PJ.self) + // forType required to guide generic inferences + func fetchFilteredPlugins( + forType: PJ.Type, + installedPlugins: FetchedResults

, + searchText: String + ) -> [PJ] { + let availablePlugins: [PJ] = fetchCastedPlugins(forType) return availablePlugins .filter { availablePlugin in @@ -112,13 +116,19 @@ public class PluginManager: ObservableObject { } } - func fetchUpdatedPlugins(installedPlugins: FetchedResults

, searchText: String) -> [PJ] { + func fetchUpdatedPlugins( + forType: PJ.Type, + installedPlugins: FetchedResults

, + searchText: String + ) -> [PJ] { var updatedPlugins: [PJ] = [] - let availablePlugins: [PJ] = fetchCastedPlugins(PJ.self) + let availablePlugins: [PJ] = fetchCastedPlugins(forType) for plugin in installedPlugins { if let availablePlugin = availablePlugins.first(where: { - plugin.listId == $0.listId && plugin.name == $0.name && plugin.author == $0.author + plugin.listId == $0.listId && + plugin.name == $0.name && + plugin.author == $0.author }), availablePlugin.version > plugin.version { diff --git a/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift index 0efdce4..b99061d 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift @@ -54,7 +54,6 @@ struct InstalledPluginButtonView: View { if #available(iOS 15.0, *) { Button(role: .destructive) { PersistenceController.shared.delete(installedPlugin, context: backgroundContext) - NotificationCenter.default.post(name: .didDeletePlugin, object: nil) } label: { Text("Remove") Image(systemName: "trash") @@ -62,7 +61,6 @@ struct InstalledPluginButtonView: View { } else { Button { PersistenceController.shared.delete(installedPlugin, context: backgroundContext) - NotificationCenter.default.post(name: .didDeletePlugin, object: nil) } label: { Text("Remove") Image(systemName: "trash") diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift index cc8232d..9c5c5f6 100644 --- a/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/PluginListView.swift @@ -4,7 +4,6 @@ // // Created by Brian Dashore on 7/24/22. // - import SwiftUI struct PluginListView: View { @@ -20,14 +19,19 @@ struct PluginListView: View { @State private var isEditingSearch = false @State private var isSearching = false - @State private var filteredUpdatedPlugins: [PJ] = [] - @State private var filteredAvailablePlugins: [PJ] = [] @State private var sourcePredicate: NSPredicate? var body: some View { DynamicFetchRequest(predicate: sourcePredicate) { (installedPlugins: FetchedResults

) in List { - if !filteredUpdatedPlugins.isEmpty { + if + let filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins( + forType: PJ.self, + installedPlugins: installedPlugins, + searchText: searchText + ), + !filteredUpdatedPlugins.isEmpty + { Section(header: InlineHeader("Updates")) { ForEach(filteredUpdatedPlugins, id: \.self) { (updatedPlugin: PJ) in PluginCatalogButtonView(availablePlugin: updatedPlugin, doUpsert: true) @@ -43,16 +47,17 @@ struct PluginListView: View { } } - if !filteredAvailablePlugins.isEmpty { + if + let filteredAvailablePlugins = pluginManager.fetchFilteredPlugins( + forType: PJ.self, + installedPlugins: installedPlugins, + searchText: searchText + ), + !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) - } + PluginCatalogButtonView(availablePlugin: availablePlugin, doUpsert: false) } } } @@ -65,18 +70,8 @@ struct PluginListView: View { .environmentObject(navModel) } } - .backport.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) - filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) - filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) - } - .onReceive(NotificationCenter.default.publisher(for: .didDeletePlugin)) { _ in - filteredAvailablePlugins = pluginManager.fetchFilteredPlugins(installedPlugins: installedPlugins, searchText: searchText) - filteredUpdatedPlugins = pluginManager.fetchUpdatedPlugins(installedPlugins: installedPlugins, searchText: searchText) } } } -- 2.45.2 From 8c8e9d02157e3d38bb7f97cae075eac607f456d2 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 2 Mar 2023 11:23:14 -0500 Subject: [PATCH 18/18] Ferrite: Bump version Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index e19c7f5..ec42081 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -971,7 +971,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.1; PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -1006,7 +1006,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.6.0; + MARKETING_VERSION = 0.6.1; PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; -- 2.45.2