diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index f85a249..081ab6e 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -93,6 +93,10 @@ 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 */; }; + 0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */; }; + 0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; }; + 0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */; }; + 0C8DC35829CE2ACA008A83AD /* SourceSettingsMethodView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.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 */; }; @@ -228,6 +232,10 @@ 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 = ""; }; + 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = ""; }; + 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = ""; }; + 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = ""; }; + 0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsMethodView.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 = ""; }; @@ -417,6 +425,7 @@ 0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */, 0C5005512992B6750064606A /* PluginTagsView.swift */, 0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */, + 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */, ); path = Plugin; sourceTree = ""; @@ -475,6 +484,9 @@ isa = PBXGroup; children = ( 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, + 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */, + 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */, + 0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.swift */, ); path = Source; sourceTree = ""; @@ -773,6 +785,7 @@ 0C6771FA29B3D1AE005D38D2 /* KodiModels.swift in Sources */, 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, + 0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */, @@ -824,6 +837,7 @@ 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, 0C794B69289DACC800DD1CC8 /* InstalledPluginButtonView.swift in Sources */, + 0C8DC35829CE2ACA008A83AD /* SourceSettingsMethodView.swift in Sources */, 0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0C6771FE29B521F1005D38D2 /* SettingsDebridInfoView.swift in Sources */, @@ -869,7 +883,9 @@ 0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */, 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, + 0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */, 0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */, + 0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */, 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, 0C0974B029CCAAAF006DE7A3 /* OperatingSystemVersion.swift in Sources */, 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 749632f..1056d99 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -58,11 +58,6 @@ public class NavigationViewModel: ObservableObject { @Published var selectedTab: ViewTab = .search - // TODO: Maybe move these to their own StateObjects? - // Used between SourceListView and SourceSettingsView - @Published var showSourceSettings: Bool = false - var selectedSource: Source? - // Used between service views and editor views in Settings @Published var selectedPluginList: PluginList? @Published var selectedKodiServer: KodiServer? diff --git a/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift index d35f6ab..30f6be8 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Buttons/InstalledPluginButtonView.swift @@ -10,10 +10,11 @@ import SwiftUI struct InstalledPluginButtonView: View { let backgroundContext = PersistenceController.shared.backgroundContext - @EnvironmentObject var navModel: NavigationViewModel - @ObservedObject var installedPlugin: P + @Binding var showPluginOptions: Bool + @Binding var selectedPlugin: P? + var body: some View { Toggle(isOn: Binding( get: { installedPlugin.enabled }, @@ -42,14 +43,12 @@ struct InstalledPluginButtonView: View { .padding(.vertical, 2) } .contextMenu { - if let installedSource = installedPlugin as? Source { - Button { - navModel.selectedSource = installedSource - navModel.showSourceSettings.toggle() - } label: { - Text("Settings") - Image(systemName: "gear") - } + Button { + selectedPlugin = installedPlugin + showPluginOptions.toggle() + } label: { + Text("Options") + Image(systemName: "gear") } if #available(iOS 15.0, *) { diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift index 19252ac..08c2ab6 100644 --- a/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift @@ -27,6 +27,9 @@ struct PluginAggregateView: View { @State private var sourcePredicate: NSPredicate? + @State private var showPluginOptions = false + @State private var selectedPlugin: P? + var body: some View { ZStack { DynamicFetchRequest(predicate: sourcePredicate) { (installedPlugins: FetchedResults

) in @@ -49,7 +52,11 @@ struct PluginAggregateView: View { if !installedPlugins.isEmpty { Section(header: InlineHeader("Installed")) { ForEach(installedPlugins, id: \.self) { installedPlugin in - InstalledPluginButtonView(installedPlugin: installedPlugin) + InstalledPluginButtonView( + installedPlugin: installedPlugin, + showPluginOptions: $showPluginOptions, + selectedPlugin: $selectedPlugin + ) } } } @@ -82,11 +89,8 @@ struct PluginAggregateView: View { } } } - .sheet(isPresented: $navModel.showSourceSettings) { - if String(describing: P.self) == "Source" { - SourceSettingsView() - .environmentObject(navModel) - } + .sheet(isPresented: $showPluginOptions) { + PluginInfoView(selectedPlugin: $selectedPlugin) } } } diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginInfoView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginInfoView.swift new file mode 100644 index 0000000..0d5b5c5 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/PluginInfoView.swift @@ -0,0 +1,78 @@ +// +// PluginInfoView.swift +// Ferrite +// +// Created by Brian Dashore on 3/24/23. +// + +import SwiftUI + +struct PluginInfoView: View { + @Environment(\.presentationMode) var presentationMode + + @Binding var selectedPlugin: P? + + @FetchRequest( + entity: PluginList.entity(), + sortDescriptors: [] + ) var pluginLists: FetchedResults + + var body: some View { + NavView { + List { + if let selectedPlugin { + Section(header: InlineHeader("Info")) { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 5) { + HStack(spacing: 5) { + Text(selectedPlugin.name) + + Text("v\(selectedPlugin.version)") + .foregroundColor(.secondary) + } + + Text("by \(selectedPlugin.author)") + .foregroundColor(.secondary) + + Group { + Text("ID: \(selectedPlugin.id)") + + if let pluginList = pluginLists.first(where: { $0.id == selectedPlugin.listId }) + { + Text("List: \(pluginList.name)") + Text("List ID: \(pluginList.id.uuidString)") + } else { + Text("No plugin list found. This source should be removed.") + } + } + .foregroundColor(.secondary) + .font(.caption) + } + + if let tags = selectedPlugin.getTags(), !tags.isEmpty { + PluginTagsView(tags: tags) + } + } + .padding(.vertical, 2) + } + + if let selectedSource = selectedPlugin as? Source { + SourceSettingsView(selectedSource: selectedSource) + } + } + } + .listStyle(.insetGrouped) + .onDisappear { + PersistenceController.shared.save() + } + .navigationTitle("Plugin Options") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + presentationMode.wrappedValue.dismiss() + } + } + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsApiView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsApiView.swift new file mode 100644 index 0000000..30ada8b --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsApiView.swift @@ -0,0 +1,54 @@ +// +// SourceSettingsApiView.swift +// Ferrite +// +// Created by Brian Dashore on 3/24/23. +// + +import SwiftUI + +struct SourceSettingsApiView: View { + @ObservedObject var selectedSourceApi: SourceApi + + @State private var tempClientId: String = "" + @State private var tempClientSecret: String = "" + + enum Field { + case secure, plain + } + + var body: some View { + Section( + header: InlineHeader("API credentials"), + footer: Text("Grab the required API credentials from the website. A client secret can be an API token.") + ) { + if let clientId = selectedSourceApi.clientId, clientId.dynamic { + TextField("Client ID", text: $tempClientId, onEditingChanged: { isFocused in + if !isFocused { + clientId.value = tempClientId + clientId.timeStamp = Date() + } + }) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .backport.onAppear { + tempClientId = clientId.value ?? "" + } + } + + if let clientSecret = selectedSourceApi.clientSecret, clientSecret.dynamic { + TextField("Token", text: $tempClientSecret, onEditingChanged: { isFocused in + if !isFocused { + clientSecret.value = tempClientSecret + clientSecret.timeStamp = Date() + } + }) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .backport.onAppear { + tempClientSecret = clientSecret.value ?? "" + } + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsBaseUrlView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsBaseUrlView.swift new file mode 100644 index 0000000..dadcc97 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsBaseUrlView.swift @@ -0,0 +1,36 @@ +// +// SourceSettingsBaseUrlView.swift +// Ferrite +// +// Created by Brian Dashore on 3/24/23. +// + +import SwiftUI + +struct SourceSettingsBaseUrlView: View { + @ObservedObject var selectedSource: Source + + @State private var tempBaseUrl: String = "" + var body: some View { + Section( + header: InlineHeader("Base URL"), + footer: Text("Enter the base URL of your server.") + ) { + TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in + if !isFocused { + if tempBaseUrl.last == "/" { + selectedSource.baseUrl = String(tempBaseUrl.dropLast()) + } else { + selectedSource.baseUrl = tempBaseUrl + } + } + }) + .keyboardType(.URL) + .autocorrectionDisabled(true) + .autocapitalization(.none) + .backport.onAppear { + tempBaseUrl = selectedSource.baseUrl ?? "" + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsMethodView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsMethodView.swift new file mode 100644 index 0000000..ca64b7a --- /dev/null +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsMethodView.swift @@ -0,0 +1,62 @@ +// +// SourceSettingsMethodView.swift +// Ferrite +// +// Created by Brian Dashore on 3/24/23. +// + +import SwiftUI + +struct SourceSettingsMethodView: View { + @ObservedObject var selectedSource: Source + + var body: some View { + Section(header: InlineHeader("Fetch method")) { + if selectedSource.jsonParser != nil { + Button { + selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue + } label: { + HStack { + Text("Website API") + Spacer() + if SourcePreferredParser.siteApi.rawValue == selectedSource.preferredParser { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + } + + if selectedSource.rssParser != nil { + Button { + selectedSource.preferredParser = SourcePreferredParser.rss.rawValue + } label: { + HStack { + Text("RSS") + Spacer() + if SourcePreferredParser.rss.rawValue == selectedSource.preferredParser { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + } + + if selectedSource.htmlParser != nil { + Button { + selectedSource.preferredParser = SourcePreferredParser.scraping.rawValue + } label: { + HStack { + Text("Web scraping") + Spacer() + if SourcePreferredParser.scraping.rawValue == selectedSource.preferredParser { + Image(systemName: "checkmark") + .foregroundColor(.blue) + } + } + } + } + } + .backport.tint(.primary) + } +} diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift index f402137..cc7a3a9 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift @@ -8,207 +8,19 @@ import SwiftUI struct SourceSettingsView: View { - @Environment(\.presentationMode) var presentationMode - - @EnvironmentObject var navModel: NavigationViewModel - - @FetchRequest( - entity: PluginList.entity(), - sortDescriptors: [] - ) var pluginLists: FetchedResults - - var body: some View { - NavView { - List { - if let selectedSource = navModel.selectedSource { - Section(header: InlineHeader("Info")) { - 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 pluginList = pluginLists.first(where: { $0.id == selectedSource.listId }) - { - Text("List: \(pluginList.name)") - Text("List ID: \(pluginList.id.uuidString)") - } else { - Text("No plugin list found. This source should be removed.") - } - } - .foregroundColor(.secondary) - .font(.caption) - } - - if let tags = selectedSource.getTags(), !tags.isEmpty { - PluginTagsView(tags: tags) - } - } - .padding(.vertical, 2) - } - - if selectedSource.dynamicBaseUrl { - SourceSettingsBaseUrlView(selectedSource: selectedSource) - } - - if let sourceApi = selectedSource.api, - sourceApi.clientId?.dynamic ?? false || sourceApi.clientSecret?.dynamic ?? false - { - SourceSettingsApiView(selectedSourceApi: sourceApi) - } - - SourceSettingsMethodView(selectedSource: selectedSource) - } - } - .listStyle(.insetGrouped) - .onDisappear { - PersistenceController.shared.save() - } - .navigationTitle("Source Settings") - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - } - } - } -} - -struct SourceSettingsBaseUrlView: View { - @ObservedObject var selectedSource: Source - - @State private var tempBaseUrl: String = "" - var body: some View { - Section( - header: InlineHeader("Base URL"), - footer: Text("Enter the base URL of your server.") - ) { - TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in - if !isFocused { - if tempBaseUrl.last == "/" { - selectedSource.baseUrl = String(tempBaseUrl.dropLast()) - } else { - selectedSource.baseUrl = tempBaseUrl - } - } - }) - .keyboardType(.URL) - .autocorrectionDisabled(true) - .autocapitalization(.none) - .backport.onAppear { - tempBaseUrl = selectedSource.baseUrl ?? "" - } - } - } -} - -struct SourceSettingsApiView: View { - @ObservedObject var selectedSourceApi: SourceApi - - @State private var tempClientId: String = "" - @State private var tempClientSecret: String = "" - - enum Field { - case secure, plain - } - - var body: some View { - Section( - header: InlineHeader("API credentials"), - footer: Text("Grab the required API credentials from the website. A client secret can be an API token.") - ) { - if let clientId = selectedSourceApi.clientId, clientId.dynamic { - TextField("Client ID", text: $tempClientId, onEditingChanged: { isFocused in - if !isFocused { - clientId.value = tempClientId - clientId.timeStamp = Date() - } - }) - .autocorrectionDisabled(true) - .autocapitalization(.none) - .backport.onAppear { - tempClientId = clientId.value ?? "" - } - } - - if let clientSecret = selectedSourceApi.clientSecret, clientSecret.dynamic { - TextField("Token", text: $tempClientSecret, onEditingChanged: { isFocused in - if !isFocused { - clientSecret.value = tempClientSecret - clientSecret.timeStamp = Date() - } - }) - .autocorrectionDisabled(true) - .autocapitalization(.none) - .backport.onAppear { - tempClientSecret = clientSecret.value ?? "" - } - } - } - } -} - -struct SourceSettingsMethodView: View { @ObservedObject var selectedSource: Source var body: some View { - Section(header: InlineHeader("Fetch method")) { - if selectedSource.jsonParser != nil { - Button { - selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue - } label: { - HStack { - Text("Website API") - Spacer() - if SourcePreferredParser.siteApi.rawValue == selectedSource.preferredParser { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - } - } - } - - if selectedSource.rssParser != nil { - Button { - selectedSource.preferredParser = SourcePreferredParser.rss.rawValue - } label: { - HStack { - Text("RSS") - Spacer() - if SourcePreferredParser.rss.rawValue == selectedSource.preferredParser { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - } - } - } - - if selectedSource.htmlParser != nil { - Button { - selectedSource.preferredParser = SourcePreferredParser.scraping.rawValue - } label: { - HStack { - Text("Web scraping") - Spacer() - if SourcePreferredParser.scraping.rawValue == selectedSource.preferredParser { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - } - } - } + if selectedSource.dynamicBaseUrl { + SourceSettingsBaseUrlView(selectedSource: selectedSource) } - .backport.tint(.primary) + + if let sourceApi = selectedSource.api, + sourceApi.clientId?.dynamic ?? false || sourceApi.clientSecret?.dynamic ?? false + { + SourceSettingsApiView(selectedSourceApi: sourceApi) + } + + SourceSettingsMethodView(selectedSource: selectedSource) } }