From ff23a854ef0d2b2bccc7598b6cb62c8a25623ebc Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 5 Aug 2022 22:31:15 -0400 Subject: [PATCH] Sources: Add source updating and source list edits Sources can now be updated based on the repo ID. To preserve repo IDs across single URL links, the source lists can be edited and the ID is transferred over. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 22 ++++- Ferrite/ViewModels/NavigationViewModel.swift | 3 + Ferrite/ViewModels/SourceManager.swift | 55 ++++++----- Ferrite/ViewModels/ToastViewModel.swift | 2 +- .../SettingsSourceListView.swift | 28 ++++-- .../SettingsViews/SourceListEditorView.swift | 12 ++- .../SourceViews/InstalledSourceView.swift | 53 ++++++++++ .../Views/SourceViews/SourceCatalogView.swift | 35 +++++++ .../SourceSettingsView.swift | 19 ++-- .../SourceViews/SourceUpdateButtonView.swift | 35 +++++++ Ferrite/Views/SourcesView.swift | 96 ++++++++----------- 11 files changed, 266 insertions(+), 94 deletions(-) create mode 100644 Ferrite/Views/SourceViews/InstalledSourceView.swift create mode 100644 Ferrite/Views/SourceViews/SourceCatalogView.swift rename Ferrite/Views/{ => SourceViews}/SourceSettingsView.swift (81%) create mode 100644 Ferrite/Views/SourceViews/SourceUpdateButtonView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index b7c2294..82d4ffe 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -21,6 +21,9 @@ 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */; }; 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; }; + 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; }; + 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */; }; + 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */; }; 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; }; 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; }; 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */; }; @@ -74,6 +77,9 @@ 0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = ""; }; 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 = ""; }; + 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = ""; }; + 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceView.swift; sourceTree = ""; }; + 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogView.swift; 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 = ""; }; 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = ""; }; @@ -160,6 +166,17 @@ path = Models; sourceTree = ""; }; + 0C794B65289DAC9F00DD1CC8 /* SourceViews */ = { + isa = PBXGroup; + children = ( + 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, + 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */, + 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */, + 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */, + ); + path = SourceViews; + sourceTree = ""; + }; 0CA0545C288F7CB200850554 /* SettingsViews */ = { isa = PBXGroup; children = ( @@ -217,6 +234,7 @@ 0CA148EE2889061200DE2211 /* Views */ = { isa = PBXGroup; children = ( + 0C794B65289DAC9F00DD1CC8 /* SourceViews */, 0CA148F02889062700DE2211 /* RepresentableViews */, 0CA148C0288903F000DE2211 /* CommonViews */, 0CA0545C288F7CB200850554 /* SettingsViews */, @@ -229,7 +247,6 @@ 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, - 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, ); path = Views; @@ -381,6 +398,7 @@ 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, + 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0CF501F2289AE06A0099C785 /* SourceTracker+CoreDataClass.swift in Sources */, @@ -389,6 +407,7 @@ 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, + 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, @@ -397,6 +416,7 @@ 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, + 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */, 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */, diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index f5a38d5..5b4e2f2 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -23,4 +23,7 @@ class NavigationViewModel: ObservableObject { // Used between SourceListView and SourceSettingsView @Published var showSourceSettings: Bool = false @Published var selectedSource: Source? + + @Published var showSourceListEditor: Bool = false + @Published var selectedSourceList: SourceList? } diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index 724132b..5c7de64 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -45,21 +45,23 @@ public class SourceManager: ObservableObject { } } - public func installSource(sourceJson: SourceJson) { + public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) { let backgroundContext = PersistenceController.shared.backgroundContext - // If a source exists, don't add the new one + // If a source exists, don't add the new one unless upserting let existingSourceRequest = Source.fetchRequest() existingSourceRequest.predicate = NSPredicate(format: "name == %@", sourceJson.name) existingSourceRequest.fetchLimit = 1 - let existingSource = try? backgroundContext.fetch(existingSourceRequest).first - if existingSource != nil { - Task { @MainActor in - toastModel?.toastDescription = "Could not install source with name \(sourceJson.name) because it is already installed." + if let existingSource = try? backgroundContext.fetch(existingSourceRequest).first { + if doUpsert { + PersistenceController.shared.delete(existingSource, context: backgroundContext) + } else { + Task { @MainActor in + toastModel?.toastDescription = "Could not install source with name \(sourceJson.name) because it is already installed." + } + return } - - return } let newSource = Source(context: backgroundContext) @@ -218,7 +220,7 @@ public class SourceManager: ObservableObject { } @MainActor - public func addSourceList(sourceUrl: String) async -> Bool { + public func addSourceList(sourceUrl: String, existingSourceList: SourceList?) async -> Bool { let backgroundContext = PersistenceController.shared.backgroundContext if sourceUrl.isEmpty || URL(string: sourceUrl) == nil { @@ -232,22 +234,31 @@ public class SourceManager: ObservableObject { let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!)) let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data) - let sourceListRequest = SourceList.fetchRequest() - sourceListRequest.predicate = NSPredicate(format: "urlString == %@ OR author == %@", sourceUrl, rawResponse.author) - sourceListRequest.fetchLimit = 1 + if let existingSourceList = existingSourceList { + existingSourceList.urlString = sourceUrl + existingSourceList.name = rawResponse.name + existingSourceList.author = rawResponse.author + } 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.init(type: .or, subpredicates: [urlPredicate, infoPredicate]) + sourceListRequest.fetchLimit = 1 - if (try? backgroundContext.fetch(sourceListRequest).first) != nil { - urlErrorAlertText = "A source with the same URL or author exists. Please remove it and try again." - showUrlErrorAlert.toggle() - return false + 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 } - let newSourceUrl = SourceList(context: backgroundContext) - newSourceUrl.id = UUID() - newSourceUrl.urlString = sourceUrl - newSourceUrl.name = rawResponse.name - newSourceUrl.author = rawResponse.author - try backgroundContext.save() return true diff --git a/Ferrite/ViewModels/ToastViewModel.swift b/Ferrite/ViewModels/ToastViewModel.swift index a4cde3f..f88c022 100644 --- a/Ferrite/ViewModels/ToastViewModel.swift +++ b/Ferrite/ViewModels/ToastViewModel.swift @@ -1,5 +1,5 @@ // -// ErrorViewModel.swift +// ToastViewModel.swift // Ferrite // // Created by Brian Dashore on 7/19/22. diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index 9eb89fb..8781716 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -1,5 +1,5 @@ // -// SettingsSourceUrlView.swift +// SettingsSourceListView.swift // Ferrite // // Created by Brian Dashore on 7/25/22. @@ -10,27 +10,43 @@ import SwiftUI struct SettingsSourceListView: View { let backgroundContext = PersistenceController.shared.backgroundContext + @EnvironmentObject var navModel: NavigationViewModel + @FetchRequest( entity: SourceList.entity(), sortDescriptors: [] ) var sourceLists: FetchedResults @State private var presentSourceSheet = false + @State private var selectedSourceList: SourceList? var body: some View { List { ForEach(sourceLists, id: \.self) { sourceList in VStack(alignment: .leading, spacing: 5) { Text(sourceList.name) + + Text(sourceList.author) + .foregroundColor(.gray) + Text("ID: \(sourceList.id)") .font(.caption) .foregroundColor(.gray) } - } - .onDelete { offsets in - for index in offsets { - if let sourceUrl = sourceLists[safe: index] { - PersistenceController.shared.delete(sourceUrl, context: backgroundContext) + .contextMenu { + Button { + navModel.selectedSourceList = sourceList + presentSourceSheet.toggle() + } label: { + Text("Edit") + Image(systemName: "pencil") + } + + Button { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") } } } diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index c7e085e..004e3fe 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -10,6 +10,7 @@ import SwiftUI struct SourceListEditorView: View { @Environment(\.dismiss) var dismiss + @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var sourceManager: SourceManager let backgroundContext = PersistenceController.shared.backgroundContext @@ -26,6 +27,9 @@ struct SourceListEditorView: View { .autocapitalization(.none) } } + .onAppear { + sourceUrl = navModel.selectedSourceList?.urlString ?? "" + } .alert(isPresented: $sourceManager.showUrlErrorAlert) { Alert( title: Text("Error"), @@ -45,13 +49,19 @@ struct SourceListEditorView: View { ToolbarItem(placement: .navigationBarTrailing) { Button("Save") { Task { - if await sourceManager.addSourceList(sourceUrl: sourceUrl) { + if await sourceManager.addSourceList( + sourceUrl: sourceUrl, + existingSourceList: navModel.selectedSourceList + ) { dismiss() } } } } } + .onDisappear { + navModel.selectedSourceList = nil + } } } } diff --git a/Ferrite/Views/SourceViews/InstalledSourceView.swift b/Ferrite/Views/SourceViews/InstalledSourceView.swift new file mode 100644 index 0000000..429ab9e --- /dev/null +++ b/Ferrite/Views/SourceViews/InstalledSourceView.swift @@ -0,0 +1,53 @@ +// +// InstalledSourceView.swift +// Ferrite +// +// Created by Brian Dashore on 8/5/22. +// + +import SwiftUI + +struct InstalledSourceView: View { + let backgroundContext = PersistenceController.shared.backgroundContext + + @EnvironmentObject var navModel: NavigationViewModel + + @ObservedObject var installedSource: Source + + var body: some View { + Toggle(isOn: Binding( + get: { installedSource.enabled }, + set: { + installedSource.enabled = $0 + PersistenceController.shared.save() + } + )) { + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(installedSource.name) + Text("v\(installedSource.version)") + .foregroundColor(.secondary) + } + + Text("by \(installedSource.author)") + .foregroundColor(.secondary) + } + } + .contextMenu { + Button { + navModel.selectedSource = installedSource + navModel.showSourceSettings.toggle() + } label: { + Text("Settings") + Image(systemName: "gear") + } + + Button { + PersistenceController.shared.delete(installedSource, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } + } +} diff --git a/Ferrite/Views/SourceViews/SourceCatalogView.swift b/Ferrite/Views/SourceViews/SourceCatalogView.swift new file mode 100644 index 0000000..1ebbfe5 --- /dev/null +++ b/Ferrite/Views/SourceViews/SourceCatalogView.swift @@ -0,0 +1,35 @@ +// +// SourceCatalogButtonView.swift +// Ferrite +// +// Created by Brian Dashore on 8/5/22. +// + +import SwiftUI + +struct SourceCatalogButtonView: View { + @EnvironmentObject var sourceManager: SourceManager + + let availableSource: SourceJson + + var body: some View { + HStack { + VStack(alignment: .leading, spacing: 5) { + HStack { + Text(availableSource.name) + Text("v\(availableSource.version)") + .foregroundColor(.secondary) + } + + Text("by \(availableSource.author ?? "Unknown")") + .foregroundColor(.secondary) + } + + Spacer() + + Button("Install") { + sourceManager.installSource(sourceJson: availableSource) + } + } + } +} diff --git a/Ferrite/Views/SourceSettingsView.swift b/Ferrite/Views/SourceViews/SourceSettingsView.swift similarity index 81% rename from Ferrite/Views/SourceSettingsView.swift rename to Ferrite/Views/SourceViews/SourceSettingsView.swift index b54576e..78a1f7b 100644 --- a/Ferrite/Views/SourceSettingsView.swift +++ b/Ferrite/Views/SourceViews/SourceSettingsView.swift @@ -22,20 +22,23 @@ struct SourceSettingsView: View { Text(selectedSource.name) Text("v\(selectedSource.version)") + .foregroundColor(.secondary) } Text("by \(selectedSource.author)") .foregroundColor(.secondary) - if let listId = selectedSource.listId { - Text("List ID: \(listId)") - .font(.caption) - .foregroundColor(.secondary) - } else { - Text("No list ID found. This source should be removed.") - .font(.caption) - .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) } } diff --git a/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift b/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift new file mode 100644 index 0000000..9db6c0d --- /dev/null +++ b/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift @@ -0,0 +1,35 @@ +// +// 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) + } + + Spacer() + + Button("Update") { + sourceManager.installSource(sourceJson: updatedSource, doUpsert: true) + } + } + } +} diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 7c08608..2f6218d 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -18,48 +18,39 @@ struct SourcesView: View { sortDescriptors: [] ) var sources: FetchedResults - @State private var availableSourceLength = 0 + private var updatedSources: [SourceJson] { + var tempSources: [SourceJson] = [] + + for source in sources { + guard let availableSource = sourceManager.availableSources.first(where: { + source.listId == $0.listId && source.name == $0.name && source.author == $0.author + }) else { + continue + } + + if availableSource.version > source.version { + tempSources.append(availableSource) + } + } + + return tempSources + } var body: some View { NavView { List { + if !updatedSources.isEmpty { + Section("Updates") { + ForEach(updatedSources, id: \.self) { source in + SourceUpdateButtonView(updatedSource: source) + } + } + } + if !sources.isEmpty { Section("Installed") { ForEach(sources, id: \.self) { source in - Toggle(isOn: Binding( - get: { source.enabled }, - set: { - source.enabled = $0 - PersistenceController.shared.save() - } - )) { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(source.name) - Text("v\(source.version)") - .foregroundColor(.secondary) - } - - Text("by \(source.author)") - .foregroundColor(.secondary) - } - } - .contextMenu { - Button { - navModel.selectedSource = source - navModel.showSourceSettings.toggle() - } label: { - Text("Settings") - Image(systemName: "gear") - } - - Button { - PersistenceController.shared.delete(source, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") - } - } + InstalledSourceView(installedSource: source) } .sheet(isPresented: $navModel.showSourceSettings) { SourceSettingsView() @@ -67,30 +58,25 @@ struct SourcesView: View { } } - if sourceManager.availableSources.contains(where: { avail in - !sources.contains(where: { avail.name == $0.name }) + if sourceManager.availableSources.contains(where: { availableSource in + !sources.contains( + where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + } + ) }) { Section("Catalog") { ForEach(sourceManager.availableSources, id: \.self) { availableSource in - if !sources.contains(where: { availableSource.name == $0.name }) { - HStack { - VStack(alignment: .leading, spacing: 5) { - HStack { - Text(availableSource.name) - Text("v\(availableSource.version)") - .foregroundColor(.secondary) - } - - Text("by \(availableSource.author ?? "Unknown")") - .foregroundColor(.secondary) - } - - Spacer() - - Button("Install") { - sourceManager.installSource(sourceJson: availableSource) - } + if !sources.contains( + where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author } + ) { + SourceCatalogButtonView(availableSource: availableSource) } } }