diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index ec42081..30a9729 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -55,6 +55,10 @@ 0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; }; 0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; }; 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; + 0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */; }; + 0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */; }; + 0C6771FA29B3D1AE005D38D2 /* KodiModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */; }; + 0C6771FC29B3E0DB005D38D2 /* HybridSecureField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6771FB29B3E0DB005D38D2 /* HybridSecureField.swift */; }; 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; }; 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; }; 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */; }; @@ -178,6 +182,10 @@ 0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppear.swift; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = ""; }; 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = ""; }; + 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiWrapper.swift; sourceTree = ""; }; + 0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKodiView.swift; sourceTree = ""; }; + 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiModels.swift; sourceTree = ""; }; + 0C6771FB29B3E0DB005D38D2 /* HybridSecureField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridSecureField.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; 0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = ""; }; 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = ""; }; @@ -346,6 +354,7 @@ 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */, + 0C6771F929B3D1AE005D38D2 /* KodiModels.swift */, ); path = Models; sourceTree = ""; @@ -437,6 +446,7 @@ 0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */, 0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */, 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */, + 0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */, ); path = Settings; sourceTree = ""; @@ -475,6 +485,7 @@ 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, 0C32FB562890D1F2002BD219 /* ListRowViews.swift */, 0C2D9652299316CC00A504B6 /* Tag.swift */, + 0C6771FB29B3E0DB005D38D2 /* HybridSecureField.swift */, ); path = CommonViews; sourceTree = ""; @@ -554,6 +565,7 @@ 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */, 0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */, 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */, + 0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */, ); path = API; sourceTree = ""; @@ -715,6 +727,7 @@ files = ( 0C7ED14328D65518009E29AD /* FileManager.swift in Sources */, 0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */, + 0C6771FA29B3D1AE005D38D2 /* KodiModels.swift in Sources */, 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, @@ -777,6 +790,7 @@ 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, + 0C6771FC29B3E0DB005D38D2 /* HybridSecureField.swift in Sources */, 0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */, 0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, @@ -803,6 +817,7 @@ 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */, 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */, + 0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */, 0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, 0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */, @@ -815,6 +830,7 @@ 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, 0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */, 0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */, + 0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, 0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */, diff --git a/Ferrite/API/KodiWrapper.swift b/Ferrite/API/KodiWrapper.swift new file mode 100644 index 0000000..e8b72ed --- /dev/null +++ b/Ferrite/API/KodiWrapper.swift @@ -0,0 +1,51 @@ +// +// KodiWrapper.swift +// Ferrite +// +// Created by Brian Dashore on 3/4/23. +// + +import Foundation + +public class Kodi { + let encoder = JSONEncoder() + + public func sendVideoUrl(urlString: String) async throws { + guard let baseUrl = UserDefaults.standard.string(forKey: "ExternalServices.KodiUrl") else { + throw KodiError.InvalidBaseUrl + } + + 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")!) + request.httpMethod = "POST" + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + + if let username, let password { + request.setValue("Basic \(Data("\(username):\(password)".utf8).base64EncodedString())", forHTTPHeaderField: "Authorization") + } + + request.httpBody = try encoder.encode(requestBody) + + let (_, response) = try await URLSession.shared.data(for: request) + + guard let response = response as? HTTPURLResponse else { + throw KodiError.FailedRequest(description: "No HTTP response given") + } + + if response.statusCode == 401 { + throw KodiError.FailedRequest(description: "Your Kodi account details are invalid. Please check your credentials in Settings > Kodi.") + } else if response.statusCode <= 200, response.statusCode >= 299 { + throw KodiError.FailedRequest(description: "The Kodi request failed with status code \(response.statusCode).") + } + } +} diff --git a/Ferrite/Models/KodiModels.swift b/Ferrite/Models/KodiModels.swift new file mode 100644 index 0000000..323d096 --- /dev/null +++ b/Ferrite/Models/KodiModels.swift @@ -0,0 +1,35 @@ +// +// KodiModels.swift +// Ferrite +// +// Created by Brian Dashore on 3/4/23. +// + +import Foundation + +extension Kodi { + enum KodiError: Error { + case InvalidBaseUrl + case InvalidPlaybackUrl + case InvalidPostBody + case FailedRequest(description: String) + } + + // MARK: - RPC payload + struct RPCPayload: Encodable { + let jsonrpc: String = "2.0" + let id: String = "1" + let method: String + let params: Params? + } + + // MARK: - RPC Params + struct Params: Codable { + let item: Item + } + + // MARK: - RPC Item + struct Item: Codable { + let file: String + } +} diff --git a/Ferrite/ViewModels/PluginManager.swift b/Ferrite/ViewModels/PluginManager.swift index ce9472b..b94ea16 100644 --- a/Ferrite/ViewModels/PluginManager.swift +++ b/Ferrite/ViewModels/PluginManager.swift @@ -10,11 +10,16 @@ import SwiftUI public class PluginManager: ObservableObject { var toastModel: ToastViewModel? + let kodi: Kodi = .init() @Published var availableSources: [SourceJson] = [] @Published var availableActions: [ActionJson] = [] - @Published var showBrokenDefaultActionAlert = false + @Published var showActionErrorAlert = false + @Published var actionErrorAlertMessage: String = "" + + @Published var showActionSuccessAlert = false + @Published var actionSuccessAlertMessage: String = "" @MainActor public func fetchPluginsFromUrl() async { @@ -173,7 +178,7 @@ public class PluginManager: ObservableObject { } @MainActor - public func runDebridAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) { + public func runDebridAction(urlString: String?, navModel: NavigationViewModel) { let context = PersistenceController.shared.backgroundContext if @@ -187,19 +192,22 @@ public class PluginManager: ObservableObject { if let fetchedAction = try? context.fetch(actionFetchRequest).first { runDeeplinkAction(fetchedAction, urlString: urlString) } else { - currentChoiceSheet = .action + navModel.currentChoiceSheet = .action UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName") UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList") - showBrokenDefaultActionAlert.toggle() + 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 { - currentChoiceSheet = .action + navModel.currentChoiceSheet = .action } } @MainActor - public func runMagnetAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) { + public func runMagnetAction(urlString: String?, navModel: NavigationViewModel) { let context = PersistenceController.shared.backgroundContext if @@ -213,14 +221,17 @@ public class PluginManager: ObservableObject { if let fetchedAction = try? context.fetch(actionFetchRequest).first { runDeeplinkAction(fetchedAction, urlString: urlString) } else { - currentChoiceSheet = .action + navModel.currentChoiceSheet = .action UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName") UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList") - showBrokenDefaultActionAlert.toggle() + 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 { - currentChoiceSheet = .action + navModel.currentChoiceSheet = .action } } @@ -228,7 +239,9 @@ public class PluginManager: ObservableObject { @MainActor public func runDeeplinkAction(_ action: Action, urlString: String?) { guard let deeplink = action.deeplink, let urlString else { - toastModel?.updateToastDescription("Could not run action: \(action.name) since there is no deeplink to execute. Contact the action dev!") + 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.") return @@ -239,11 +252,37 @@ public class PluginManager: ObservableObject { if let playbackUrl { UIApplication.shared.open(playbackUrl) } else { - toastModel?.updateToastDescription("Could not run action: \(action.name) because the created deeplink was invalid. Contact the action dev!") + 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") } } + @MainActor + public func sendToKodi(urlString: String?) async { + guard let urlString else { + 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") + + return + } + + do { + try await kodi.sendVideoUrl(urlString: urlString) + + actionSuccessAlertMessage = "Your URL should be playing on Kodi" + showActionSuccessAlert.toggle() + } catch { + actionErrorAlertMessage = "Kodi Error: \(error)" + showActionErrorAlert.toggle() + + print("Kodi action error: \(error)") + } + } + public func installAction(actionJson: ActionJson?, doUpsert: Bool = false) async { guard let actionJson else { await toastModel?.updateToastDescription("Action addition error: No action present. Contact the app dev!") diff --git a/Ferrite/Views/CommonViews/HybridSecureField.swift b/Ferrite/Views/CommonViews/HybridSecureField.swift new file mode 100644 index 0000000..de077eb --- /dev/null +++ b/Ferrite/Views/CommonViews/HybridSecureField.swift @@ -0,0 +1,34 @@ +// +// HybridSecureField.swift +// Ferrite +// +// Created by Brian Dashore on 3/4/23. +// + +import SwiftUI + +struct HybridSecureField: View { + @Binding var text: String + @State private var showPassword = false + + var body: some View { + HStack { + Group { + if showPassword { + TextField("Password", text: $text) + } else { + SecureField("Password", text: $text) + } + } + .autocorrectionDisabled(true) + .autocapitalization(.none) + + Button { + showPassword.toggle() + } label: { + Image(systemName: self.showPassword ? "eye.slash.fill" : "eye.fill") + .foregroundColor(.secondary) + } + } + } +} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index fc8f0b5..a1928aa 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -41,7 +41,7 @@ struct AllDebridCloudView: View { PersistenceController.shared.createHistory(historyInfo, performSave: true) pluginManager.runDebridAction( urlString: debridManager.downloadUrl, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index 760cd10..c5d6b93 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -41,7 +41,7 @@ struct PremiumizeCloudView: View { pluginManager.runDebridAction( urlString: debridManager.downloadUrl, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 9b4700c..14bd2d9 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -38,7 +38,7 @@ struct RealDebridCloudView: View { pluginManager.runDebridAction( urlString: debridManager.downloadUrl, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) } .backport.tint(.primary) @@ -78,7 +78,7 @@ struct RealDebridCloudView: View { pluginManager.runDebridAction( urlString: debridManager.downloadUrl, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) } } diff --git a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift index 34f793e..7713c68 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift @@ -26,7 +26,7 @@ struct HistoryButtonView: View { debridManager.downloadUrl = url pluginManager.runDebridAction( urlString: url, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) if navModel.currentChoiceSheet != .action { @@ -36,7 +36,7 @@ struct HistoryButtonView: View { } else { pluginManager.runMagnetAction( urlString: url, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) } } else { diff --git a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift index fa30ab4..8a42270 100644 --- a/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift +++ b/Ferrite/Views/ComponentViews/Plugin/Source/SourceSettingsView.swift @@ -133,6 +133,7 @@ struct SourceSettingsApiView: View { clientId.timeStamp = Date() } }) + .autocorrectionDisabled(true) .autocapitalization(.none) .backport.onAppear { tempClientId = clientId.value ?? "" @@ -146,6 +147,7 @@ struct SourceSettingsApiView: View { clientSecret.timeStamp = Date() } }) + .autocorrectionDisabled(true) .autocapitalization(.none) .backport.onAppear { tempClientSecret = clientSecret.value ?? "" diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift index 19409b0..a825628 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultButtonView.swift @@ -45,7 +45,7 @@ struct SearchResultButtonView: View { pluginManager.runDebridAction( urlString: debridManager.downloadUrl, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) if navModel.currentChoiceSheet != .action { @@ -74,7 +74,7 @@ struct SearchResultButtonView: View { pluginManager.runMagnetAction( urlString: result.magnet.link, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) } } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsKodiView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsKodiView.swift new file mode 100644 index 0000000..5268fce --- /dev/null +++ b/Ferrite/Views/ComponentViews/Settings/SettingsKodiView.swift @@ -0,0 +1,61 @@ +// +// SettingsKodiView.swift +// Ferrite +// +// Created by Brian Dashore on 3/4/23. +// + +import SwiftUI + +struct SettingsKodiView: View { + @AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = "" + @AppStorage("ExternalServices.KodiUsername") var kodiUsername: String = "" + @AppStorage("ExternalServices.KodiPassword") var kodiPassword: String = "" + + @State private var showPassword = false + + var body: some View { + NavView { + List { + Section(header: InlineHeader("Description")) { + VStack(alignment: .leading, spacing: 10) { + Text("Kodi is an external application that is used to manage a local media library and playback.") + + Link("Website", destination: URL(string: "https://kodi.tv")!) + } + } + + Section( + header: InlineHeader("Base URL"), + footer: Text("Enter your Kodi server's http URL here including the port.") + ) { + TextField("http://...", text: $kodiUrl, onEditingChanged: { isFocused in + if !isFocused && kodiUrl.last == "/" { + kodiUrl = String(kodiUrl.dropLast()) + } + }) + .keyboardType(.URL) + .autocorrectionDisabled(true) + .autocapitalization(.none) + } + + Section( + header: InlineHeader("Credentials"), + footer: Text("Enter your kodi username and password here (if applicable)") + ) { + TextField("Username", text: $kodiUsername) + + HybridSecureField(text: $kodiPassword) + } + } + .navigationTitle("Kodi") + .navigationBarTitleDisplayMode(.inline) + } + } +} + +struct SettingsKodiView_Previews: PreviewProvider { + static var previews: some View { + SettingsKodiView() + } +} diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 1d4316f..2c6f6da 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -15,6 +15,8 @@ struct SettingsView: View { let backgroundContext = PersistenceController.shared.backgroundContext + @AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = "" + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true @AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText = false @@ -29,7 +31,7 @@ struct SettingsView: View { var body: some View { NavView { Form { - Section(header: InlineHeader("Debrid Services")) { + Section(header: InlineHeader("Debrid services")) { HStack { Text("RealDebrid") Spacer() @@ -82,6 +84,17 @@ struct SettingsView: View { } } + Section(header: InlineHeader("Playback services")) { + NavigationLink(destination: SettingsKodiView(), label: { + HStack { + Text("Kodi") + Spacer() + Text(kodiUrl.isEmpty ? "Disabled" : "Enabled") + .foregroundColor(.secondary) + } + }) + } + Section(header: InlineHeader("Behavior")) { Toggle(isOn: $autocorrectSearch) { Text("Autocorrect search") diff --git a/Ferrite/Views/SheetViews/ActionChoiceView.swift b/Ferrite/Views/SheetViews/ActionChoiceView.swift index f854377..8feecd5 100644 --- a/Ferrite/Views/SheetViews/ActionChoiceView.swift +++ b/Ferrite/Views/SheetViews/ActionChoiceView.swift @@ -21,6 +21,8 @@ struct ActionChoiceView: View { sortDescriptors: [] ) var actions: FetchedResults + @AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = "" + @State private var showLinkCopyAlert = false @State private var showMagnetCopyAlert = false @@ -51,6 +53,14 @@ struct ActionChoiceView: View { } } + if !kodiUrl.isEmpty { + ListRowButtonView("Open in Kodi", systemImage: "arrow.up.forward.app.fill") { + Task { + await pluginManager.sendToKodi(urlString: debridManager.downloadUrl) + } + } + } + ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { UIPasteboard.general.string = debridManager.downloadUrl showLinkCopyAlert.toggle() @@ -113,11 +123,14 @@ struct ActionChoiceView: View { } } .backport.alert( - isPresented: $pluginManager.showBrokenDefaultActionAlert, - title: "Action not found", - message: - "The default action could not be run. The action choice sheet has been opened. \n\n" + - "Please check your default actions in Settings" + isPresented: $pluginManager.showActionSuccessAlert, + title: "Action successful", + message: pluginManager.actionSuccessAlertMessage + ) + .backport.alert( + isPresented: $pluginManager.showActionErrorAlert, + title: "Action error", + message: pluginManager.actionErrorAlertMessage ) .onDisappear { debridManager.downloadUrl = "" diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 1721adb..fa4c36d 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -86,7 +86,7 @@ struct BatchChoiceView: View { pluginManager.runDebridAction( urlString: debridManager.downloadUrl, - currentChoiceSheet: &navModel.currentChoiceSheet + navModel: navModel ) }