diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 6f2e599..d7ebec3 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -72,6 +72,7 @@ 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 */; }; + 0C748EDA29D9256D0049B8BE /* Yams in Frameworks */ = {isa = PBXBuildFile; productRef = 0C748ED929D9256D0049B8BE /* Yams */; }; 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 */; }; @@ -299,6 +300,7 @@ 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, + 0C748EDA29D9256D0049B8BE /* Yams in Frameworks */, 0CDDDE052935235E006810B1 /* BetterSafariView in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -688,6 +690,7 @@ 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, 0CDDDE042935235E006810B1 /* BetterSafariView */, 0C448BE829A135F100F4E266 /* Introspect-Static */, + 0C748ED929D9256D0049B8BE /* Yams */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -725,6 +728,7 @@ 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, 0CDDDE032935235E006810B1 /* XCRemoteSwiftPackageReference "BetterSafariView" */, 0C448BE729A135F100F4E266 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, + 0C748ED829D9256D0049B8BE /* XCRemoteSwiftPackageReference "Yams" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -1152,6 +1156,14 @@ kind = branch; }; }; + 0C748ED829D9256D0049B8BE /* XCRemoteSwiftPackageReference "Yams" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/jpsim/Yams"; + requirement = { + kind = upToNextMinorVersion; + minimumVersion = 5.0.5; + }; + }; 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/SwiftyJSON/SwiftyJSON"; @@ -1199,6 +1211,11 @@ package = 0C64A4B5288903880079976D /* XCRemoteSwiftPackageReference "keychain-swift" */; productName = KeychainSwift; }; + 0C748ED929D9256D0049B8BE /* Yams */ = { + isa = XCSwiftPackageProductDependency; + package = 0C748ED829D9256D0049B8BE /* XCRemoteSwiftPackageReference "Yams" */; + productName = Yams; + }; 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */ = { isa = XCSwiftPackageProductDependency; package = 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */; diff --git a/Ferrite/Models/PluginModels.swift b/Ferrite/Models/PluginModels.swift index dfac824..83ae8a5 100644 --- a/Ferrite/Models/PluginModels.swift +++ b/Ferrite/Models/PluginModels.swift @@ -29,6 +29,7 @@ extension PluginManager { enum PluginManagerError: Error { case ListAddition(description: String) case ActionAddition(description: String) + case PluginFetch(description: String) } struct AvailablePlugins { diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 97036b2..dfa0b06 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import Yams public class PluginManager: ObservableObject { var logManager: LoggingManager? @@ -102,7 +103,18 @@ public class PluginManager: ObservableObject { let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) let (data, _) = try await URLSession.shared.data(for: request) - let pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + let pluginResponse: PluginListJson? + + // If the URL is a yaml file, decode as such. Otherwise assume legacy JSON + if url.pathExtension == "yaml" || url.pathExtension == "yml" { + pluginResponse = try YAMLDecoder().decode(PluginListJson.self, from: data) + } else { + pluginResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + } + + guard let pluginResponse else { + throw PluginManagerError.PluginFetch(description: "Could not decode plugin list data") + } if let sources = pluginResponse.sources { // Faster and more performant to map instead of a for loop @@ -748,25 +760,41 @@ public class PluginManager: ObservableObject { // Adds a plugin list // Can move this to PersistenceController if needed - public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws { + public func addPluginList(_ urlString: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws { let backgroundContext = PersistenceController.shared.backgroundContext - if url.isEmpty || !url.starts(with: "https://") && !url.starts(with: "http://") { + if urlString.isEmpty || !urlString.starts(with: "https://") && !urlString.starts(with: "http://") { throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.") } - let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: url)!)) - let rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + guard let url = URL(string: urlString) else { + throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.") + } + + let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url)) + + let rawResponse: PluginListJson? + + // If the URL is a yaml file, decode as such. Otherwise assume legacy JSON + if url.pathExtension == "yaml" || url.pathExtension == "yml" { + rawResponse = try YAMLDecoder().decode(PluginListJson.self, from: data) + } else { + rawResponse = try JSONDecoder().decode(PluginListJson.self, from: data) + } + + guard let rawResponse else { + throw PluginManagerError.ListAddition(description: "Could not decode the plugin list from URL \(urlString)") + } if let existingPluginList { - existingPluginList.urlString = url + existingPluginList.urlString = urlString existingPluginList.name = rawResponse.name existingPluginList.author = rawResponse.author try PersistenceController.shared.container.viewContext.save() } else { let pluginListRequest = PluginList.fetchRequest() - let urlPredicate = NSPredicate(format: "urlString == %@", url) + let urlPredicate = NSPredicate(format: "urlString == %@", urlString) let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", rawResponse.author, rawResponse.name) pluginListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate]) pluginListRequest.fetchLimit = 1 @@ -779,7 +807,7 @@ public class PluginManager: ObservableObject { let newPluginList = PluginList(context: backgroundContext) newPluginList.id = UUID() - newPluginList.urlString = url + newPluginList.urlString = urlString newPluginList.name = rawResponse.name newPluginList.author = rawResponse.author