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 <bdashore3@proton.me>
This commit is contained in:
kingbri 2023-03-21 13:49:19 -04:00
parent edfba1c62e
commit 6e95c6072c
20 changed files with 197 additions and 118 deletions

View file

@ -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 = "<group>"; };
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModels.swift; sourceTree = "<group>"; };
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = "<group>"; };
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = "<group>"; };
0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
@ -369,6 +373,7 @@
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */,
0C1A3E5129C8A7F500DA9730 /* SettingsModels.swift */,
);
path = Models;
sourceTree = "<group>";
@ -414,12 +419,13 @@
path = Plugin;
sourceTree = "<group>";
};
0C44E2A628D4DDC6007711AE /* Classes */ = {
0C44E2A628D4DDC6007711AE /* Utils */ = {
isa = PBXGroup;
children = (
0C44E2A728D4DDDC007711AE /* Application.swift */,
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */,
);
path = Classes;
path = Utils;
sourceTree = "<group>";
};
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 */,

View file

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

View file

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

View file

@ -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<Value: Codable> {
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
}
}

View file

@ -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] = []

View file

@ -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<DefaultAction>(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<DefaultAction>(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()

View file

@ -12,18 +12,10 @@ struct CustomScopeBarModifier<V: View>: ViewModifier {
let hostingContent: V
@State private var hostingController: UIHostingController<V>?
// 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<V: View>: ViewModifier {
content
Spacer()
}
.backport.introspectSearchController { searchController in
searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no
searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none
}
}
}
}

View file

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

View file

@ -39,7 +39,7 @@ struct PremiumizeCloudView: View {
performSave: true
)
pluginManager.runDebridAction(
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)

View file

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

View file

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

View file

@ -80,6 +80,7 @@ struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
.onChange(of: installedPlugins.count) { newCount in
pluginsEmpty = newCount == 0
}
.id(UUID())
}
}
}

View file

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

View file

@ -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<PluginList>
@FetchRequest(
entity: KodiServer.entity(),
sortDescriptors: []
) var kodiServers: FetchedResults<KodiServer>
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<PluginList>
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"
}
}
}

View file

@ -44,6 +44,7 @@ struct SettingsDebridInfoView: View {
}
}
}
.listStyle(.insetGrouped)
.navigationTitle(debridType.toString())
.navigationBarTitleDisplayMode(.inline)
}

View file

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

View file

@ -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<DefaultAction> = .init(value: .none)
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: CodableWrapper<DefaultAction> = .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)
}
}
)

View file

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

View file

@ -84,7 +84,7 @@ struct BatchChoiceView: View {
PersistenceController.shared.createHistory(selectedHistoryInfo, performSave: true)
}
pluginManager.runDebridAction(
pluginManager.runDefaultAction(
urlString: debridManager.downloadUrl,
navModel: navModel
)