Plugins: Unify settings

Plugin settings used to only be available for installed sources.
Change this to display info about an installed plugin and add settings
depending on the plugin type.

For example, a source will have additional settings specified by its
own views.

Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-03-24 16:22:24 -04:00
parent 9f83ebfce0
commit 39a705717e
9 changed files with 275 additions and 219 deletions

View file

@ -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 = "<group>"; };
0C84F47D2895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceHtmlParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = "<group>"; };
0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = "<group>"; };
0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = "<group>"; };
0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = "<group>"; };
0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsMethodView.swift; sourceTree = "<group>"; };
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultActionPickerView.swift; sourceTree = "<group>"; };
0CA05456288EE58200850554 /* SettingsPluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsPluginListView.swift; sourceTree = "<group>"; };
0CA05458288EE9E600850554 /* PluginManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginManager.swift; sourceTree = "<group>"; };
@ -417,6 +425,7 @@
0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */,
0C5005512992B6750064606A /* PluginTagsView.swift */,
0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */,
0C8DC35129CE287E008A83AD /* PluginInfoView.swift */,
);
path = Plugin;
sourceTree = "<group>";
@ -475,6 +484,9 @@
isa = PBXGroup;
children = (
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */,
0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */,
0C8DC35729CE2ACA008A83AD /* SourceSettingsMethodView.swift */,
);
path = Source;
sourceTree = "<group>";
@ -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 */,

View file

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

View file

@ -10,10 +10,11 @@ import SwiftUI
struct InstalledPluginButtonView<P: Plugin>: 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<Bool>(
get: { installedPlugin.enabled },
@ -42,14 +43,12 @@ struct InstalledPluginButtonView<P: Plugin>: 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, *) {

View file

@ -27,6 +27,9 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: 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<P>) in
@ -49,7 +52,11 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: 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<P: Plugin, PJ: PluginJson>: View {
}
}
}
.sheet(isPresented: $navModel.showSourceSettings) {
if String(describing: P.self) == "Source" {
SourceSettingsView()
.environmentObject(navModel)
}
.sheet(isPresented: $showPluginOptions) {
PluginInfoView(selectedPlugin: $selectedPlugin)
}
}
}

View file

@ -0,0 +1,78 @@
//
// PluginInfoView.swift
// Ferrite
//
// Created by Brian Dashore on 3/24/23.
//
import SwiftUI
struct PluginInfoView<P: Plugin>: View {
@Environment(\.presentationMode) var presentationMode
@Binding var selectedPlugin: P?
@FetchRequest(
entity: PluginList.entity(),
sortDescriptors: []
) var pluginLists: FetchedResults<PluginList>
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()
}
}
}
}
}
}

View file

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

View file

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

View file

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

View file

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