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 <bdashore3@gmail.com>
This commit is contained in:
kingbri 2022-08-05 22:31:15 -04:00
parent 1eb4bbb59a
commit ff23a854ef
11 changed files with 266 additions and 94 deletions

View file

@ -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 = "<group>"; };
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; };
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = "<group>"; };
0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceView.swift; sourceTree = "<group>"; };
0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogView.swift; sourceTree = "<group>"; };
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; };
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; };
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
@ -160,6 +166,17 @@
path = Models;
sourceTree = "<group>";
};
0C794B65289DAC9F00DD1CC8 /* SourceViews */ = {
isa = PBXGroup;
children = (
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */,
0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */,
0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */,
);
path = SourceViews;
sourceTree = "<group>";
};
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 */,

View file

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

View file

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

View file

@ -1,5 +1,5 @@
//
// ErrorViewModel.swift
// ToastViewModel.swift
// Ferrite
//
// Created by Brian Dashore on 7/19/22.

View file

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

View file

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

View file

@ -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<Bool>(
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")
}
}
}
}

View file

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

View file

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

View file

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

View file

@ -18,48 +18,39 @@ struct SourcesView: View {
sortDescriptors: []
) var sources: FetchedResults<Source>
@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<Bool>(
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)
}
}
}