From 69d2f6babec5e14b47fdc4d895a035f62256264b Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 19 Mar 2023 21:24:24 -0400 Subject: [PATCH] Actions: Add OS-specific checks Plugin lists are universal across clients and OS-specific cases should be appropriately handled. OSes can now be added to the deeplink action case and Ferrite scans for either iOS-specific or fallthrough actions. There must be 1 action that corresponds to an OS and/or 1 fallback case, otherwise the action will not show in the plugins list. Also remove some extraneous files. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 -- Ferrite/Classes/Application.swift | 10 +++- Ferrite/Classes/Logger.swift | 65 --------------------- Ferrite/Models/ActionModels.swift | 71 +++++++++++++++++++++-- Ferrite/Models/PluginModels.swift | 1 + Ferrite/Models/SourceModels.swift | 6 +- Ferrite/Protocols/Plugin.swift | 6 +- Ferrite/ViewModels/LoggingManager.swift | 2 +- Ferrite/ViewModels/PluginManager.swift | 75 ++++++++++++++++++------- 9 files changed, 137 insertions(+), 103 deletions(-) delete mode 100644 Ferrite/Classes/Logger.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index da9c436..9635827 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -52,7 +52,6 @@ 0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */; }; 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; }; 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; - 0C5708E929B8E61C00BE07F9 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708E829B8E61C00BE07F9 /* Logger.swift */; }; 0C5708EB29B8F89300BE07F9 /* SettingsLogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */; }; 0C572D4C2993FC2A003EEC05 /* ViewDidAppearHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */; }; 0C572D4E299403B7003EEC05 /* ViewDidAppear.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */; }; @@ -188,7 +187,6 @@ 0C50B7CF299DF63C00A9FA3C /* UIDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIDevice.swift; sourceTree = ""; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; - 0C5708E829B8E61C00BE07F9 /* Logger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Logger.swift; sourceTree = ""; }; 0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLogView.swift; sourceTree = ""; }; 0C572D4B2993FC2A003EEC05 /* ViewDidAppearHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppearHandler.swift; sourceTree = ""; }; 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppear.swift; sourceTree = ""; }; @@ -420,7 +418,6 @@ isa = PBXGroup; children = ( 0C44E2A728D4DDDC007711AE /* Application.swift */, - 0C5708E829B8E61C00BE07F9 /* Logger.swift */, ); path = Classes; sourceTree = ""; @@ -788,7 +785,6 @@ 0C5005522992B6750064606A /* PluginTagsView.swift in Sources */, 0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */, 0CE66B3A28E640D200F69346 /* Backport.swift in Sources */, - 0C5708E929B8E61C00BE07F9 /* Logger.swift in Sources */, 0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */, 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */, 0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */, diff --git a/Ferrite/Classes/Application.swift b/Ferrite/Classes/Application.swift index 37b0723..dff3856 100644 --- a/Ferrite/Classes/Application.swift +++ b/Ferrite/Classes/Application.swift @@ -12,8 +12,16 @@ import Foundation public class Application { static let shared = Application() + // OS name for Plugins to read. Lowercase for ease of use + let os = "ios" + + // Minimum OS version that Ferrite runs on let minVersion = OperatingSystemVersion(majorVersion: 14, minorVersion: 0, patchVersion: 0) + // Grabs the current user's OS version + let osVersion: OperatingSystemVersion = ProcessInfo().operatingSystemVersion + + // Application version and build variables var appVersion: String { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0" } @@ -34,6 +42,4 @@ public class Application { } #endif } - - let osVersion: OperatingSystemVersion = ProcessInfo().operatingSystemVersion } diff --git a/Ferrite/Classes/Logger.swift b/Ferrite/Classes/Logger.swift deleted file mode 100644 index 8d7c485..0000000 --- a/Ferrite/Classes/Logger.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Logger.swift -// Ferrite -// -// Created by Brian Dashore on 3/8/23. -// - -import Foundation - -public class Logger { - var messageArray: [Log] = [] - - struct Log: Hashable { - let level: LogLevel - let description: String - let timeStamp: Date = .init() - - func toMessage() -> String { - "[\(level.rawValue)]: \(description)" - } - } - - enum LogLevel: String, Identifiable { - var id: Int { - hashValue - } - - case info = "INFO" - case warn = "WARN" - case error = "ERROR" - } - - func info(_ message: String) { - let log = Log( - level: .info, - description: message - ) - - messageArray.append(log) - - print("LOG: \(log.toMessage())") - } - - func warn(_ message: String) { - let log = Log( - level: .warn, - description: message - ) - - messageArray.append(log) - - print("LOG: \(log.toMessage())") - } - - func error(_ message: String) { - let log = Log( - level: .error, - description: message - ) - - messageArray.append(log) - - print("LOG: \(log.toMessage())") - } -} diff --git a/Ferrite/Models/ActionModels.swift b/Ferrite/Models/ActionModels.swift index 1e6267f..25888c0 100644 --- a/Ferrite/Models/ActionModels.swift +++ b/Ferrite/Models/ActionModels.swift @@ -12,10 +12,73 @@ public struct ActionJson: Codable, Hashable, PluginJson { public let version: Int16 let minVersion: String? let requires: [ActionRequirement] - let deeplink: String? - public var author: String? - public var listId: UUID? - public var tags: [PluginTagJson]? + let deeplink: [DeeplinkActionJson]? + public let author: String? + public let listId: UUID? + public let tags: [PluginTagJson]? + + public init( + name: String, + version: Int16, + minVersion: String?, + requires: [ActionRequirement], + deeplink: [DeeplinkActionJson]?, + author: String?, + listId: UUID?, + tags: [PluginTagJson]? + ) { + self.name = name + self.version = version + self.minVersion = minVersion + self.requires = requires + self.deeplink = deeplink + self.author = author + self.listId = listId + self.tags = tags + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.version = try container.decode(Int16.self, forKey: .version) + self.minVersion = try container.decodeIfPresent(String.self, forKey: .minVersion) + self.requires = try container.decode([ActionRequirement].self, forKey: .requires) + self.author = try container.decodeIfPresent(String.self, forKey: .author) + self.listId = try container.decodeIfPresent(UUID.self, forKey: .listId) + self.tags = try container.decodeIfPresent([PluginTagJson].self, forKey: .tags) + + if let deeplinkString = try? container.decode(String.self, forKey: .deeplink) { + self.deeplink = [DeeplinkActionJson(os: [], scheme: deeplinkString)] + } else if let deeplinkAction = try? container.decode([DeeplinkActionJson].self, forKey: .deeplink) { + self.deeplink = deeplinkAction + } else { + self.deeplink = nil + } + } +} + +public struct DeeplinkActionJson: Codable, Hashable { + let os: [String] + let scheme: String + + init(os: [String], scheme: String) { + self.os = os + self.scheme = scheme + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + + if let os = try? container.decode(String.self, forKey: .os) { + self.os = [os] + } else if let os = try? container.decode([String].self, forKey: .os) { + self.os = os + } else { + self.os = [] + } + + self.scheme = try container.decode(String.self, forKey: .scheme) + } } public extension ActionJson { diff --git a/Ferrite/Models/PluginModels.swift b/Ferrite/Models/PluginModels.swift index afe79fe..dfac824 100644 --- a/Ferrite/Models/PluginModels.swift +++ b/Ferrite/Models/PluginModels.swift @@ -28,6 +28,7 @@ public struct PluginTagJson: Codable, Hashable, Sendable { extension PluginManager { enum PluginManagerError: Error { case ListAddition(description: String) + case ActionAddition(description: String) } struct AvailablePlugins { diff --git a/Ferrite/Models/SourceModels.swift b/Ferrite/Models/SourceModels.swift index 103e260..2adf110 100644 --- a/Ferrite/Models/SourceModels.swift +++ b/Ferrite/Models/SourceModels.swift @@ -24,9 +24,9 @@ public struct SourceJson: Codable, Hashable, Sendable, PluginJson { let jsonParser: SourceJsonParserJson? let rssParser: SourceRssParserJson? let htmlParser: SourceHtmlParserJson? - public var author: String? - public var listId: UUID? - public var tags: [PluginTagJson]? + public let author: String? + public let listId: UUID? + public let tags: [PluginTagJson]? } public extension SourceJson { diff --git a/Ferrite/Protocols/Plugin.swift b/Ferrite/Protocols/Plugin.swift index 961e187..08e70be 100644 --- a/Ferrite/Protocols/Plugin.swift +++ b/Ferrite/Protocols/Plugin.swift @@ -28,8 +28,8 @@ extension Plugin { public protocol PluginJson: Hashable { var name: String { get } var version: Int16 { get } - var author: String? { get set } - var listId: UUID? { get set } - var tags: [PluginTagJson]? { get set } + var author: String? { get } + var listId: UUID? { get } + var tags: [PluginTagJson]? { get } func getTags() -> [PluginTagJson] } diff --git a/Ferrite/ViewModels/LoggingManager.swift b/Ferrite/ViewModels/LoggingManager.swift index f128b63..bdce46f 100644 --- a/Ferrite/ViewModels/LoggingManager.swift +++ b/Ferrite/ViewModels/LoggingManager.swift @@ -52,7 +52,7 @@ class LoggingManager: ObservableObject { @Published var showToast: Bool = false // Default the toast type to error since the majority of toasts are errors - @Published var toastType: Logger.LogLevel = .error + @Published var toastType: LogLevel = .error var showErrorToasts: Bool { UserDefaults.standard.bool(forKey: "Debug.ShowErrorToasts") } diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 6f979a1..9229615 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -132,13 +132,17 @@ public class PluginManager: ObservableObject { if let actions = pluginResponse.actions { tempActions += actions.compactMap { inputJson in - if checkAppVersion(minVersion: inputJson.minVersion) { + if + let deeplink = inputJson.deeplink, + checkAppVersion(minVersion: inputJson.minVersion), + let filteredDeeplinks = getFilteredDeeplinks(deeplink) + { return ActionJson( name: inputJson.name, version: inputJson.version, minVersion: inputJson.minVersion, requires: inputJson.requires, - deeplink: inputJson.deeplink, + deeplink: filteredDeeplinks, author: pluginList.author, listId: pluginList.id, tags: inputJson.tags @@ -152,6 +156,27 @@ public class PluginManager: ObservableObject { return AvailablePlugins(availableSources: tempSources, availableActions: tempActions) } + // Checks if a deeplink action is present and if there's a single action for the OS (or fallback) + func getFilteredDeeplinks(_ deeplinks: [DeeplinkActionJson]) -> [DeeplinkActionJson]? { + let osArray = deeplinks.filter { deeplink in + deeplink.os.contains(where: { $0.lowercased() == Application.shared.os.lowercased() }) + } + + if osArray.count == 1 { + return osArray + } else { + let universalArray = deeplinks.filter { deeplink in + deeplink.os.isEmpty + } + + if universalArray.count == 1 { + return universalArray + } else { + return nil + } + } + } + // forType required to guide generic inferences func fetchFilteredPlugins(forType: PJ.Type, installedPlugins: FetchedResults, @@ -163,8 +188,8 @@ public class PluginManager: ObservableObject { .filter { availablePlugin in let pluginExists = installedPlugins.contains(where: { availablePlugin.name == $0.name && - availablePlugin.listId == $0.listId && - availablePlugin.author == $0.author + availablePlugin.listId == $0.listId && + availablePlugin.author == $0.author }) if searchText.isEmpty { @@ -185,10 +210,10 @@ public class PluginManager: ObservableObject { for plugin in installedPlugins { if let availablePlugin = availablePlugins.first(where: { plugin.listId == $0.listId && - plugin.name == $0.name && - plugin.author == $0.author + plugin.name == $0.name && + plugin.author == $0.author }), - availablePlugin.version > plugin.version + availablePlugin.version > plugin.version { updatedPlugins.append(availablePlugin) } @@ -252,8 +277,8 @@ public class PluginManager: ObservableObject { UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList") actionErrorAlertMessage = - "The default action could not be run. The action choice sheet has been opened. \n\n" + - "Please check your default actions in Settings" + "The default action could not be run. The action choice sheet has been opened. \n\n" + + "Please check your default actions in Settings" showActionErrorAlert.toggle() } } else { @@ -281,8 +306,8 @@ public class PluginManager: ObservableObject { UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList") actionErrorAlertMessage = - "The default action could not be run. The action choice sheet has been opened. \n\n" + - "Please check your default actions in Settings" + "The default action could not be run. The action choice sheet has been opened. \n\n" + + "Please check your default actions in Settings" showActionErrorAlert.toggle() } } else { @@ -296,9 +321,9 @@ public class PluginManager: ObservableObject { guard let deeplink = action.deeplink, let urlString else { actionErrorAlertMessage = "Could not run action: \(action.name) since there is no deeplink to execute. Contact the action dev!" showActionErrorAlert.toggle() - - print("Could not run action: \(action.name) since there is no deeplink to execute.") - + + logManager?.error("Could not run action: \(action.name) since there is no deeplink to execute.") + return } @@ -309,8 +334,8 @@ public class PluginManager: ObservableObject { } else { actionErrorAlertMessage = "Could not run action: \(action.name) because the created deeplink was invalid. Contact the action dev!" showActionErrorAlert.toggle() - - print("Could not run action: \(action.name) because the created deeplink (\(String(describing: playbackUrl))) was invalid") + + logManager?.error("Could not run action: \(action.name) because the created deeplink (\(String(describing: playbackUrl))) was invalid") } } @@ -320,7 +345,7 @@ public class PluginManager: ObservableObject { actionErrorAlertMessage = "Could not send URL to Kodi since there is no playback URL to send" showActionErrorAlert.toggle() - print("Could not send URL to Kodi since there is no playback URL to send") + logManager?.error("Kodi action: Could not send URL to Kodi since there is no playback URL to send") return } @@ -330,11 +355,13 @@ public class PluginManager: ObservableObject { actionSuccessAlertMessage = "Your URL should be playing on Kodi" showActionSuccessAlert.toggle() + + logManager?.info("URL \(urlString) is playing on Kodi") } catch { actionErrorAlertMessage = "Kodi Error: \(error)" showActionErrorAlert.toggle() - - print("Kodi action error: \(error)") + + logManager?.error("Kodi action: \(error)") } } @@ -351,7 +378,7 @@ public class PluginManager: ObservableObject { return } - guard let deeplink = actionJson.deeplink else { + guard let deeplinks = actionJson.deeplink else { await logManager?.error("Action addition: only deeplink actions can be added to Ferrite iOS. Please contact the action dev!") return } @@ -389,7 +416,13 @@ public class PluginManager: ObservableObject { } } - newAction.deeplink = deeplink + // Only one deeplink is left in this action JSON because of the previous filtering logic + guard let deeplinkJson = deeplinks.first else { + await logManager?.error("Action addition: No deeplink was present in action with name \(actionJson.name). Contact the action dev!") + + return + } + newAction.deeplink = deeplinkJson.scheme do { try backgroundContext.save()