From 6e95c6072cd954f6c27c2017daed76032d59b1b8 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 21 Mar 2023 13:49:19 -0400 Subject: [PATCH] Settings: Fix Default actions and Kodi Fix how default actions are picked and add in default app actions as options for both debrid and magnet defaults. Kodi shows the action choice sheet with the DisclosureGroup dropped down. The new Kodi server framework also wasn't implemented in the Kodi wrapper. Fix that. Finally, add some iOS 14 fixes and repair the autocorrect search setting to actually work. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 14 +++- Ferrite/API/KodiWrapper.swift | 12 +-- Ferrite/Models/SettingsModels.swift | 19 +++++ Ferrite/{Classes => Utils}/Application.swift | 0 Ferrite/Utils/CodableWrapper.swift | 36 ++++++++ Ferrite/ViewModels/NavigationViewModel.swift | 2 +- Ferrite/ViewModels/PluginManager.swift | 82 +++++++++---------- .../Modifiers/CustomScopeBar.swift | 12 --- .../Library/Cloud/AllDebridCloudView.swift | 2 +- .../Library/Cloud/PremiumizeCloudView.swift | 2 +- .../Library/Cloud/RealDebridCloudView.swift | 4 +- .../Library/HistoryButtonView.swift | 4 +- .../Plugin/PluginAggregateView.swift | 1 + .../SearchResult/SearchResultButtonView.swift | 4 +- .../Settings/DefaultActionPickerView.swift | 62 ++++++++------ .../Settings/SettingsDebridInfoView.swift | 1 + Ferrite/Views/ContentView.swift | 6 ++ Ferrite/Views/SettingsView.swift | 46 +++++++---- .../Views/SheetViews/ActionChoiceView.swift | 4 +- .../Views/SheetViews/BatchChoiceView.swift | 2 +- 20 files changed, 197 insertions(+), 118 deletions(-) create mode 100644 Ferrite/Models/SettingsModels.swift rename Ferrite/{Classes => Utils}/Application.swift (100%) create mode 100644 Ferrite/Utils/CodableWrapper.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 9635827..0bbbdfb 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; + 0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */; }; + 0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */; }; 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; }; 0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.swift */; }; @@ -153,6 +155,8 @@ 0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginAggregateView.swift; sourceTree = ""; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = ""; }; + 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = ""; }; + 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = ""; }; 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = ""; }; 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = ""; }; 0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = ""; }; @@ -369,6 +373,7 @@ 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */, + 0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */, ); path = Models; sourceTree = ""; @@ -414,12 +419,13 @@ path = Plugin; sourceTree = ""; }; - 0C44E2A628D4DDC6007711AE /* Classes */ = { + 0C44E2A628D4DDC6007711AE /* Utils */ = { isa = PBXGroup; children = ( 0C44E2A728D4DDDC007711AE /* Application.swift */, + 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */, ); - path = Classes; + path = Utils; sourceTree = ""; }; 0C44E2A928D4DFC4007711AE /* Modifiers */ = { @@ -494,7 +500,7 @@ 0C0D50E3288DFE6E0035ECC8 /* Models */, 0CA148EF2889061600DE2211 /* ViewModels */, 0CA148EE2889061200DE2211 /* Views */, - 0C44E2A628D4DDC6007711AE /* Classes */, + 0C44E2A628D4DDC6007711AE /* Utils */, 0C5005552992B9C20064606A /* Protocols */, 0CA148C8288903F000DE2211 /* Extensions */, 0CA148C5288903F000DE2211 /* Preview Content */, @@ -775,6 +781,7 @@ 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */, 0C50055A2992BA6A0064606A /* PluginTag+CoreDataClass.swift in Sources */, + 0C1A3E5229C8A7F500DA9730 /* SettingsModels.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */, 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, @@ -828,6 +835,7 @@ 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E7288903F000DE2211 /* LoggingManager.swift in Sources */, + 0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */, 0C6771FC29B3E0DB005D38D2 /* HybridSecureField.swift in Sources */, 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */, diff --git a/Ferrite/API/KodiWrapper.swift b/Ferrite/API/KodiWrapper.swift index f0ade8e..95bc23f 100644 --- a/Ferrite/API/KodiWrapper.swift +++ b/Ferrite/API/KodiWrapper.swift @@ -94,27 +94,21 @@ public class Kodi { } } - public func sendVideoUrl(urlString: String) async throws { - guard let baseUrl = UserDefaults.standard.string(forKey: "ExternalServices.KodiUrl") else { - throw KodiError.InvalidBaseUrl - } - + public func sendVideoUrl(urlString: String, server: KodiServer) async throws { if URL(string: urlString) == nil { throw KodiError.InvalidPlaybackUrl } - let username = UserDefaults.standard.string(forKey: "ExternalServices.KodiUsername") - let password = UserDefaults.standard.string(forKey: "ExternalServices.KodiPassword") let requestBody = RPCPayload( method: "Player.Open", params: Params(item: Item(file: urlString)) ) - var request = URLRequest(url: URL(string: "\(baseUrl)/jsonrpc")!) + var request = URLRequest(url: URL(string: "\(server.urlString)/jsonrpc")!) request.httpMethod = "POST" request.setValue("application/json", forHTTPHeaderField: "Content-Type") - if let username, let password { + if let username = server.username, let password = server.password { request.setValue("Basic \(Data("\(username):\(password)".utf8).base64EncodedString())", forHTTPHeaderField: "Authorization") } diff --git a/Ferrite/Models/SettingsModels.swift b/Ferrite/Models/SettingsModels.swift new file mode 100644 index 0000000..96320fc --- /dev/null +++ b/Ferrite/Models/SettingsModels.swift @@ -0,0 +1,19 @@ +// +// SettingsModels.swift +// Ferrite +// +// Created by Brian Dashore on 3/20/23. +// + +import Foundation + +enum DefaultAction: Codable, CaseIterable, Hashable { + static var allCases: [DefaultAction] { + [.none, .share, .kodi, .custom(name: "", listId: "")] + } + + case none + case share + case kodi + case custom(name: String, listId: String) +} diff --git a/Ferrite/Classes/Application.swift b/Ferrite/Utils/Application.swift similarity index 100% rename from Ferrite/Classes/Application.swift rename to Ferrite/Utils/Application.swift diff --git a/Ferrite/Utils/CodableWrapper.swift b/Ferrite/Utils/CodableWrapper.swift new file mode 100644 index 0000000..1b45754 --- /dev/null +++ b/Ferrite/Utils/CodableWrapper.swift @@ -0,0 +1,36 @@ +// +// CodableWrapper.swift +// Ferrite +// +// Created by Brian Dashore on 3/20/23. +// +// From https://forums.swift.org/t/rawrepresentable-conformance-leads-to-crash/51912/4 +// Prevents recursion when using Codable with RawRepresentable without needing manual conformance + +import Foundation + +struct CodableWrapper { + var value: Value +} + +extension CodableWrapper: RawRepresentable { + var rawValue: String { + guard + let data = try? JSONEncoder().encode(value), + let string = String(data: data, encoding: .utf8) + else { + return "" + } + return string + } + + init?(rawValue: String) { + guard + let data = rawValue.data(using: .utf8), + let decoded = try? JSONDecoder().decode(Value.self, from: data) + else { + return nil + } + value = decoded + } +} diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index e710a47..749632f 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -48,7 +48,7 @@ public class NavigationViewModel: ObservableObject { @Published var selectedTitle: String = "" @Published var selectedBatchTitle: String = "" - @Published var hideNavigationBar = false + @Published var kodiExpanded: Bool = false @Published var currentChoiceSheet: ChoiceSheetType? var activityItems: [Any] = [] diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index 5dfc965..201703a 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -258,57 +258,51 @@ public class PluginManager: ObservableObject { } @MainActor - public func runDebridAction(urlString: String?, navModel: NavigationViewModel) { + public func runDefaultAction(urlString: String?, navModel: NavigationViewModel) { let context = PersistenceController.shared.backgroundContext - if - let defaultDebridActionName = UserDefaults.standard.string(forKey: "Actions.DefaultDebridName"), - let defaultDebridActionList = UserDefaults.standard.string(forKey: "Actions.DefaultDebridList") - { - let actionFetchRequest = Action.fetchRequest() - actionFetchRequest.fetchLimit = 1 - actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultDebridActionName, defaultDebridActionList) - - if let fetchedAction = try? context.fetch(actionFetchRequest).first { - runDeeplinkAction(fetchedAction, urlString: urlString) - } else { - navModel.currentChoiceSheet = .action - UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName") - 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" - showActionErrorAlert.toggle() - } - } else { - navModel.currentChoiceSheet = .action + guard let urlString else { + logManager?.error("Default action: Could not run because the URL is invalid") + return } - } - @MainActor - public func runMagnetAction(urlString: String?, navModel: NavigationViewModel) { - let context = PersistenceController.shared.backgroundContext + let defaultsKey: String + // Assume this is a magnet link + if urlString.starts(with: "magnet") { + defaultsKey = "Actions.DefaultMagnet" + } else { + defaultsKey = "Actions.DefaultDebrid" + } if - let defaultMagnetActionName = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetName"), - let defaultMagnetActionList = UserDefaults.standard.string(forKey: "Actions.DefaultMagnetList") + let rawValue = UserDefaults.standard.string(forKey: defaultsKey), + let defaultAction = CodableWrapper(rawValue: rawValue)?.value { - let actionFetchRequest = Action.fetchRequest() - actionFetchRequest.fetchLimit = 1 - actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", defaultMagnetActionName, defaultMagnetActionList) - - if let fetchedAction = try? context.fetch(actionFetchRequest).first { - runDeeplinkAction(fetchedAction, urlString: urlString) - } else { + switch defaultAction { + case .none: navModel.currentChoiceSheet = .action - UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName") - UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList") + case .share: + navModel.activityItems = [urlString] + navModel.currentChoiceSheet = .activity + case .kodi: + navModel.kodiExpanded = true + navModel.currentChoiceSheet = .action + case .custom(let name, let listId): + let actionFetchRequest = Action.fetchRequest() + actionFetchRequest.fetchLimit = 1 + actionFetchRequest.predicate = NSPredicate(format: "name == %@ AND listId == %@", name, listId) - actionErrorAlertMessage = - "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() + if let fetchedAction = try? context.fetch(actionFetchRequest).first { + runDeeplinkAction(fetchedAction, urlString: urlString) + } else { + navModel.currentChoiceSheet = .action + UserDefaults.standard.set(CodableWrapper(value: .none).rawValue, forKey: "Actions.DefaultDebrid") + + actionErrorAlertMessage = + "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 { navModel.currentChoiceSheet = .action @@ -340,7 +334,7 @@ public class PluginManager: ObservableObject { } @MainActor - public func sendToKodi(urlString: String?) async { + public func sendToKodi(urlString: String?, server: KodiServer) async { guard let urlString else { actionErrorAlertMessage = "Could not send URL to Kodi since there is no playback URL to send" showActionErrorAlert.toggle() @@ -351,7 +345,7 @@ public class PluginManager: ObservableObject { } do { - try await kodi.sendVideoUrl(urlString: urlString) + try await kodi.sendVideoUrl(urlString: urlString, server: server) actionSuccessAlertMessage = "Your URL should be playing on Kodi" showActionSuccessAlert.toggle() diff --git a/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift index dc2da73..65f14ef 100644 --- a/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift +++ b/Ferrite/Views/CommonViews/Modifiers/CustomScopeBar.swift @@ -12,18 +12,10 @@ struct CustomScopeBarModifier: ViewModifier { let hostingContent: V @State private var hostingController: UIHostingController? - // Don't use AppStorage since it causes a view update - var autocorrectSearch: Bool { - UserDefaults.standard.bool(forKey: "Behavior.AutocorrectSearch") - } - func body(content: Content) -> some View { if #available(iOS 15, *) { content .backport.introspectSearchController { searchController in - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none - // MARK: One-time setup guard hostingController == nil else { return } @@ -64,10 +56,6 @@ struct CustomScopeBarModifier: ViewModifier { content Spacer() } - .backport.introspectSearchController { searchController in - searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no - searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none - } } } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index a1928aa..eec82e8 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -39,7 +39,7 @@ struct AllDebridCloudView: View { if !debridManager.downloadUrl.isEmpty { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel ) diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index c5d6b93..f49b128 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -39,7 +39,7 @@ struct PremiumizeCloudView: View { performSave: true ) - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel ) diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 14bd2d9..e796da3 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -36,7 +36,7 @@ struct RealDebridCloudView: View { performSave: true ) - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel ) @@ -76,7 +76,7 @@ struct RealDebridCloudView: View { historyInfo.url = debridManager.downloadUrl PersistenceController.shared.createHistory(historyInfo, performSave: true) - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel ) diff --git a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift index 7cda667..7f9d10e 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift @@ -24,7 +24,7 @@ struct HistoryButtonView: View { if url.starts(with: "https://") { Task { debridManager.downloadUrl = url - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: url, navModel: navModel ) @@ -34,7 +34,7 @@ struct HistoryButtonView: View { } } } else { - pluginManager.runMagnetAction( + pluginManager.runDefaultAction( urlString: url, navModel: navModel ) diff --git a/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift b/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift index 4b0a5bc..76293f7 100644 --- a/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/PluginAggregateView.swift @@ -80,6 +80,7 @@ struct PluginAggregateView: View { .onChange(of: installedPlugins.count) { newCount in pluginsEmpty = newCount == 0 } + .id(UUID()) } } } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index a825628..89361a4 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -43,7 +43,7 @@ struct SearchResultButtonView: View { performSave: true ) - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel ) @@ -72,7 +72,7 @@ struct SearchResultButtonView: View { performSave: true ) - pluginManager.runMagnetAction( + pluginManager.runDefaultAction( urlString: result.magnet.link, navModel: navModel ) diff --git a/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift b/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift index c863724..e4a59ca 100644 --- a/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift +++ b/Ferrite/Views/ComponentViews/Settings/DefaultActionPickerView.swift @@ -11,8 +11,8 @@ struct DefaultActionPickerView: View { @EnvironmentObject var logManager: LoggingManager let actionRequirement: ActionRequirement - @Binding var defaultActionName: String? - @Binding var defaultActionList: String? + + @Binding var defaultAction: DefaultAction @FetchRequest( entity: Action.entity(), @@ -24,19 +24,25 @@ struct DefaultActionPickerView: View { sortDescriptors: [] ) var pluginLists: FetchedResults + @FetchRequest( + entity: KodiServer.entity(), + sortDescriptors: [] + ) var kodiServers: FetchedResults + var body: some View { List { - UserChoiceButton( - defaultActionName: $defaultActionName, - defaultActionList: $defaultActionList, - pluginLists: pluginLists - ) + DefaultChoiceButton(defaultAction: $defaultAction, selectedOption: .none) + DefaultChoiceButton(defaultAction: $defaultAction, selectedOption: .share) + if actionRequirement == .debrid && !kodiServers.isEmpty { + DefaultChoiceButton(defaultAction: $defaultAction, selectedOption: .kodi) + } + + // Handle custom here ForEach(actions.filter { $0.requires.contains(actionRequirement.rawValue) }, id: \.id) { action in Button { if let actionListId = action.listId?.uuidString { - defaultActionName = action.name - defaultActionList = actionListId + defaultAction = .custom(name: action.name, listId: actionListId) } else { logManager.error( "Default action: This action doesn't have a corresponding plugin list! Please uninstall the action" @@ -63,9 +69,9 @@ struct DefaultActionPickerView: View { Spacer() if - let defaultActionList, - action.listId?.uuidString == defaultActionList, - action.name == defaultActionName + case let .custom(name, listId) = defaultAction, + action.listId?.uuidString == listId, + action.name == name { Image(systemName: "checkmark") .foregroundColor(.blue) @@ -77,28 +83,24 @@ struct DefaultActionPickerView: View { } .listStyle(.insetGrouped) .inlinedList(inset: -20) - .navigationTitle("Default \(actionRequirement == .debrid ? "debrid" : "magnet") action") + .navigationTitle("Default \(actionRequirement == .debrid ? "Debrid" : "Magnet") Action") .navigationBarTitleDisplayMode(.inline) } } -private struct UserChoiceButton: View { - @Binding var defaultActionName: String? - @Binding var defaultActionList: String? - var pluginLists: FetchedResults +private struct DefaultChoiceButton: View { + @Binding var defaultAction: DefaultAction + let selectedOption: DefaultAction var body: some View { Button { - defaultActionName = nil - defaultActionList = nil + defaultAction = selectedOption } label: { HStack { - Text("Let me choose") + Text(fetchButtonName()) Spacer() - // Force "Let me choose" if the name OR list ID is nil - // Prevents any mismatches - if defaultActionName == nil || pluginLists.contains(where: { $0.id.uuidString == defaultActionList }) { + if defaultAction == selectedOption { Image(systemName: "checkmark") .foregroundColor(.blue) } @@ -106,4 +108,18 @@ private struct UserChoiceButton: View { } .backport.tint(.primary) } + + func fetchButtonName() -> String { + switch selectedOption { + case .none: + return "Let me choose" + case .share: + return "Share link" + case .kodi: + return "Open in Kodi" + case .custom(_, _): + // This should not be called + return "Custom button" + } + } } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index b2fb787..0872a2a 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -44,6 +44,7 @@ struct SettingsDebridInfoView: View { } } } + .listStyle(.insetGrouped) .navigationTitle(debridType.toString()) .navigationBarTitleDisplayMode(.inline) } diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index e8d57a1..c56c508 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -15,6 +15,7 @@ struct ContentView: View { @EnvironmentObject var pluginManager: PluginManager @EnvironmentObject var logManager: LoggingManager + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch: Bool = false @AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText: Bool = false @State private var isEditingSearch = false @@ -110,6 +111,11 @@ struct ContentView: View { searchBarText = getSearchBarText() } } + .autocorrectionDisabled(!autocorrectSearch) + .backport.introspectSearchController { searchController in + // TODO: Replace with SwiftUI autocapitalization modifier + searchController.searchBar.autocapitalizationType = .none + } .navigationSearchBarHiddenWhenScrolling(false) .customScopeBar { SearchFilterHeaderView() diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 302bff9..e0ab6a9 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -27,14 +27,12 @@ struct SettingsView: View { @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true - @AppStorage("Actions.DefaultDebridName") var defaultDebridActionName: String? - @AppStorage("Actions.DefaultDebridList") var defaultDebridActionList: String? - - @AppStorage("Actions.DefaultMagnetName") var defaultMagnetActionName: String? - @AppStorage("Actions.DefaultMagnetList") var defaultMagnetActionList: String? + @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: CodableWrapper = .init(value: .none) + @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: CodableWrapper = .init(value: .none) @AppStorage("Debug.ShowErrorToasts") var showErrorToasts = true + var body: some View { NavView { Form { @@ -85,17 +83,26 @@ struct SettingsView: View { NavigationLink( destination: DefaultActionPickerView( actionRequirement: .debrid, - defaultActionName: $defaultDebridActionName, - defaultActionList: $defaultDebridActionList + defaultAction: $defaultDebridAction.value ), label: { HStack { Text("Debrid action") Spacer() - // TODO: Maybe make this check for nil list as well? - Text(defaultDebridActionName.map { $0 } ?? "User choice") - .foregroundColor(.secondary) + Group { + switch defaultDebridAction.value { + case .none: + Text("User choice") + case .share: + Text("Share") + case .kodi: + Text("Kodi") + case .custom(let name, _): + Text(name) + } + } + .foregroundColor(.secondary) } } ) @@ -104,17 +111,26 @@ struct SettingsView: View { NavigationLink( destination: DefaultActionPickerView( actionRequirement: .magnet, - defaultActionName: $defaultMagnetActionName, - defaultActionList: $defaultMagnetActionList + defaultAction: $defaultMagnetAction.value ), label: { HStack { Text("Magnet action") Spacer() - // TODO: Maybe make this check for nil list as well? - Text(defaultMagnetActionName.map { $0 } ?? "User choice") - .foregroundColor(.secondary) + Group { + switch defaultMagnetAction.value { + case .none: + Text("User choice") + case .share: + Text("Share") + case .kodi: + Text("Kodi") + case .custom(let name, _): + Text(name) + } + } + .foregroundColor(.secondary) } } ) diff --git a/Ferrite/Views/SheetViews/ActionChoiceView.swift b/Ferrite/Views/SheetViews/ActionChoiceView.swift index bb91655..d9b323d 100644 --- a/Ferrite/Views/SheetViews/ActionChoiceView.swift +++ b/Ferrite/Views/SheetViews/ActionChoiceView.swift @@ -57,11 +57,11 @@ struct ActionChoiceView: View { } if !kodiServers.isEmpty { - DisclosureGroup("Open in Kodi") { + DisclosureGroup("Open in Kodi", isExpanded: $navModel.kodiExpanded) { ForEach(kodiServers, id: \.self) { server in Button { Task { - await pluginManager.sendToKodi(urlString: debridManager.downloadUrl) + await pluginManager.sendToKodi(urlString: debridManager.downloadUrl, server: server) } } label: { KodiServerView(server: server) diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index fa4c36d..c22c359 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -84,7 +84,7 @@ struct BatchChoiceView: View { PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true) } - pluginManager.runDebridAction( + pluginManager.runDefaultAction( urlString: debridManager.downloadUrl, navModel: navModel )