Ferrite: Add Kodi support
Adds support for playing links on a preset Kodi server. This is less featured than the Ferrite companion, but should still work without a problem. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
438e48be66
commit
b8799be896
15 changed files with 290 additions and 26 deletions
|
|
@ -55,6 +55,10 @@
|
||||||
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
0C5FCB05296744F300849E87 /* AllDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */; };
|
||||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
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 */; };
|
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
|
||||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
||||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C6C7C9A2931521B002DF910 /* AllDebridWrapper.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 = "<group>"; };
|
0C572D4D299403B7003EEC05 /* ViewDidAppear.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewDidAppear.swift; sourceTree = "<group>"; };
|
||||||
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
|
0C57D4CB289032ED008534E8 /* SearchResultInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultInfoView.swift; sourceTree = "<group>"; };
|
||||||
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
|
0C5FCB04296744F300849E87 /* AllDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridCloudView.swift; sourceTree = "<group>"; };
|
||||||
|
0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiWrapper.swift; sourceTree = "<group>"; };
|
||||||
|
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsKodiView.swift; sourceTree = "<group>"; };
|
||||||
|
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiModels.swift; sourceTree = "<group>"; };
|
||||||
|
0C6771FB29B3E0DB005D38D2 /* HybridSecureField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HybridSecureField.swift; sourceTree = "<group>"; };
|
||||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
|
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
|
||||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
|
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
|
||||||
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
|
0C6C7C9A2931521B002DF910 /* AllDebridWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllDebridWrapper.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -346,6 +354,7 @@
|
||||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
|
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
|
||||||
|
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -437,6 +446,7 @@
|
||||||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
||||||
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */,
|
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */,
|
||||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||||
|
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */,
|
||||||
);
|
);
|
||||||
path = Settings;
|
path = Settings;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -475,6 +485,7 @@
|
||||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||||
0C2D9652299316CC00A504B6 /* Tag.swift */,
|
0C2D9652299316CC00A504B6 /* Tag.swift */,
|
||||||
|
0C6771FB29B3E0DB005D38D2 /* HybridSecureField.swift */,
|
||||||
);
|
);
|
||||||
path = CommonViews;
|
path = CommonViews;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -554,6 +565,7 @@
|
||||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
||||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */,
|
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */,
|
||||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||||
|
0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */,
|
||||||
);
|
);
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -715,6 +727,7 @@
|
||||||
files = (
|
files = (
|
||||||
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
|
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
|
||||||
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */,
|
0C03EB71296F619900162E9A /* PluginList+CoreDataClass.swift in Sources */,
|
||||||
|
0C6771FA29B3D1AE005D38D2 /* KodiModels.swift in Sources */,
|
||||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
||||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
|
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
|
||||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||||
|
|
@ -777,6 +790,7 @@
|
||||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
||||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||||
|
0C6771FC29B3E0DB005D38D2 /* HybridSecureField.swift in Sources */,
|
||||||
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
|
0C6C7C9D29315292002DF910 /* AllDebridModels.swift in Sources */,
|
||||||
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
|
0C6C7C9B2931521B002DF910 /* AllDebridWrapper.swift in Sources */,
|
||||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||||
|
|
@ -803,6 +817,7 @@
|
||||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||||
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
0C50055B2992BA6A0064606A /* PluginTag+CoreDataProperties.swift in Sources */,
|
||||||
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */,
|
||||||
|
0C6771F429B3B4FD005D38D2 /* KodiWrapper.swift in Sources */,
|
||||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */,
|
||||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||||
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
0C422E80293542F300486D65 /* PremiumizeModels.swift in Sources */,
|
||||||
|
|
@ -815,6 +830,7 @@
|
||||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
||||||
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
0CE1C4182981E8D700418F20 /* Plugin.swift in Sources */,
|
||||||
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
0CEC8AB2299B3B57007BFE8F /* LibraryPickerView.swift in Sources */,
|
||||||
|
0C6771F629B3B602005D38D2 /* SettingsKodiView.swift in Sources */,
|
||||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
||||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||||
|
|
|
||||||
51
Ferrite/API/KodiWrapper.swift
Normal file
51
Ferrite/API/KodiWrapper.swift
Normal file
|
|
@ -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).")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Ferrite/Models/KodiModels.swift
Normal file
35
Ferrite/Models/KodiModels.swift
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,11 +10,16 @@ import SwiftUI
|
||||||
|
|
||||||
public class PluginManager: ObservableObject {
|
public class PluginManager: ObservableObject {
|
||||||
var toastModel: ToastViewModel?
|
var toastModel: ToastViewModel?
|
||||||
|
let kodi: Kodi = .init()
|
||||||
|
|
||||||
@Published var availableSources: [SourceJson] = []
|
@Published var availableSources: [SourceJson] = []
|
||||||
@Published var availableActions: [ActionJson] = []
|
@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
|
@MainActor
|
||||||
public func fetchPluginsFromUrl() async {
|
public func fetchPluginsFromUrl() async {
|
||||||
|
|
@ -173,7 +178,7 @@ public class PluginManager: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func runDebridAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) {
|
public func runDebridAction(urlString: String?, navModel: NavigationViewModel) {
|
||||||
let context = PersistenceController.shared.backgroundContext
|
let context = PersistenceController.shared.backgroundContext
|
||||||
|
|
||||||
if
|
if
|
||||||
|
|
@ -187,19 +192,22 @@ public class PluginManager: ObservableObject {
|
||||||
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
|
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
|
||||||
runDeeplinkAction(fetchedAction, urlString: urlString)
|
runDeeplinkAction(fetchedAction, urlString: urlString)
|
||||||
} else {
|
} else {
|
||||||
currentChoiceSheet = .action
|
navModel.currentChoiceSheet = .action
|
||||||
UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName")
|
UserDefaults.standard.set(nil, forKey: "Actions.DefaultDebridName")
|
||||||
UserDefaults.standard.set(nil, forKey: "Action.DefaultDebridList")
|
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 {
|
} else {
|
||||||
currentChoiceSheet = .action
|
navModel.currentChoiceSheet = .action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public func runMagnetAction(urlString: String?, currentChoiceSheet: inout NavigationViewModel.ChoiceSheetType?) {
|
public func runMagnetAction(urlString: String?, navModel: NavigationViewModel) {
|
||||||
let context = PersistenceController.shared.backgroundContext
|
let context = PersistenceController.shared.backgroundContext
|
||||||
|
|
||||||
if
|
if
|
||||||
|
|
@ -213,14 +221,17 @@ public class PluginManager: ObservableObject {
|
||||||
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
|
if let fetchedAction = try? context.fetch(actionFetchRequest).first {
|
||||||
runDeeplinkAction(fetchedAction, urlString: urlString)
|
runDeeplinkAction(fetchedAction, urlString: urlString)
|
||||||
} else {
|
} else {
|
||||||
currentChoiceSheet = .action
|
navModel.currentChoiceSheet = .action
|
||||||
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName")
|
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetName")
|
||||||
UserDefaults.standard.set(nil, forKey: "Actions.DefaultMagnetList")
|
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 {
|
} else {
|
||||||
currentChoiceSheet = .action
|
navModel.currentChoiceSheet = .action
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -228,7 +239,9 @@ public class PluginManager: ObservableObject {
|
||||||
@MainActor
|
@MainActor
|
||||||
public func runDeeplinkAction(_ action: Action, urlString: String?) {
|
public func runDeeplinkAction(_ action: Action, urlString: String?) {
|
||||||
guard let deeplink = action.deeplink, let urlString else {
|
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.")
|
print("Could not run action: \(action.name) since there is no deeplink to execute.")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
@ -239,11 +252,37 @@ public class PluginManager: ObservableObject {
|
||||||
if let playbackUrl {
|
if let playbackUrl {
|
||||||
UIApplication.shared.open(playbackUrl)
|
UIApplication.shared.open(playbackUrl)
|
||||||
} else {
|
} 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")
|
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 {
|
public func installAction(actionJson: ActionJson?, doUpsert: Bool = false) async {
|
||||||
guard let actionJson else {
|
guard let actionJson else {
|
||||||
await toastModel?.updateToastDescription("Action addition error: No action present. Contact the app dev!")
|
await toastModel?.updateToastDescription("Action addition error: No action present. Contact the app dev!")
|
||||||
|
|
|
||||||
34
Ferrite/Views/CommonViews/HybridSecureField.swift
Normal file
34
Ferrite/Views/CommonViews/HybridSecureField.swift
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -41,7 +41,7 @@ struct AllDebridCloudView: View {
|
||||||
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
PersistenceController.shared.createHistory(historyInfo, performSave: true)
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: debridManager.downloadUrl,
|
urlString: debridManager.downloadUrl,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ struct PremiumizeCloudView: View {
|
||||||
|
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: debridManager.downloadUrl,
|
urlString: debridManager.downloadUrl,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,7 @@ struct RealDebridCloudView: View {
|
||||||
|
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: debridManager.downloadUrl,
|
urlString: debridManager.downloadUrl,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.backport.tint(.primary)
|
.backport.tint(.primary)
|
||||||
|
|
@ -78,7 +78,7 @@ struct RealDebridCloudView: View {
|
||||||
|
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: debridManager.downloadUrl,
|
urlString: debridManager.downloadUrl,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ struct HistoryButtonView: View {
|
||||||
debridManager.downloadUrl = url
|
debridManager.downloadUrl = url
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: url,
|
urlString: url,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
|
|
||||||
if navModel.currentChoiceSheet != .action {
|
if navModel.currentChoiceSheet != .action {
|
||||||
|
|
@ -36,7 +36,7 @@ struct HistoryButtonView: View {
|
||||||
} else {
|
} else {
|
||||||
pluginManager.runMagnetAction(
|
pluginManager.runMagnetAction(
|
||||||
urlString: url,
|
urlString: url,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,7 @@ struct SourceSettingsApiView: View {
|
||||||
clientId.timeStamp = Date()
|
clientId.timeStamp = Date()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.backport.onAppear {
|
.backport.onAppear {
|
||||||
tempClientId = clientId.value ?? ""
|
tempClientId = clientId.value ?? ""
|
||||||
|
|
@ -146,6 +147,7 @@ struct SourceSettingsApiView: View {
|
||||||
clientSecret.timeStamp = Date()
|
clientSecret.timeStamp = Date()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
.autocorrectionDisabled(true)
|
||||||
.autocapitalization(.none)
|
.autocapitalization(.none)
|
||||||
.backport.onAppear {
|
.backport.onAppear {
|
||||||
tempClientSecret = clientSecret.value ?? ""
|
tempClientSecret = clientSecret.value ?? ""
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ struct SearchResultButtonView: View {
|
||||||
|
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: debridManager.downloadUrl,
|
urlString: debridManager.downloadUrl,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
|
|
||||||
if navModel.currentChoiceSheet != .action {
|
if navModel.currentChoiceSheet != .action {
|
||||||
|
|
@ -74,7 +74,7 @@ struct SearchResultButtonView: View {
|
||||||
|
|
||||||
pluginManager.runMagnetAction(
|
pluginManager.runMagnetAction(
|
||||||
urlString: result.magnet.link,
|
urlString: result.magnet.link,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
Ferrite/Views/ComponentViews/Settings/SettingsKodiView.swift
Normal file
61
Ferrite/Views/ComponentViews/Settings/SettingsKodiView.swift
Normal file
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,8 @@ struct SettingsView: View {
|
||||||
|
|
||||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||||
|
|
||||||
|
@AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = ""
|
||||||
|
|
||||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||||
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText = false
|
@AppStorage("Behavior.UsesRandomSearchText") var usesRandomSearchText = false
|
||||||
|
|
||||||
|
|
@ -29,7 +31,7 @@ struct SettingsView: View {
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
Form {
|
Form {
|
||||||
Section(header: InlineHeader("Debrid Services")) {
|
Section(header: InlineHeader("Debrid services")) {
|
||||||
HStack {
|
HStack {
|
||||||
Text("RealDebrid")
|
Text("RealDebrid")
|
||||||
Spacer()
|
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")) {
|
Section(header: InlineHeader("Behavior")) {
|
||||||
Toggle(isOn: $autocorrectSearch) {
|
Toggle(isOn: $autocorrectSearch) {
|
||||||
Text("Autocorrect search")
|
Text("Autocorrect search")
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ struct ActionChoiceView: View {
|
||||||
sortDescriptors: []
|
sortDescriptors: []
|
||||||
) var actions: FetchedResults<Action>
|
) var actions: FetchedResults<Action>
|
||||||
|
|
||||||
|
@AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = ""
|
||||||
|
|
||||||
@State private var showLinkCopyAlert = false
|
@State private var showLinkCopyAlert = false
|
||||||
@State private var showMagnetCopyAlert = 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") {
|
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||||
UIPasteboard.general.string = debridManager.downloadUrl
|
UIPasteboard.general.string = debridManager.downloadUrl
|
||||||
showLinkCopyAlert.toggle()
|
showLinkCopyAlert.toggle()
|
||||||
|
|
@ -113,11 +123,14 @@ struct ActionChoiceView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backport.alert(
|
.backport.alert(
|
||||||
isPresented: $pluginManager.showBrokenDefaultActionAlert,
|
isPresented: $pluginManager.showActionSuccessAlert,
|
||||||
title: "Action not found",
|
title: "Action successful",
|
||||||
message:
|
message: pluginManager.actionSuccessAlertMessage
|
||||||
"The default action could not be run. The action choice sheet has been opened. \n\n" +
|
)
|
||||||
"Please check your default actions in Settings"
|
.backport.alert(
|
||||||
|
isPresented: $pluginManager.showActionErrorAlert,
|
||||||
|
title: "Action error",
|
||||||
|
message: pluginManager.actionErrorAlertMessage
|
||||||
)
|
)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
debridManager.downloadUrl = ""
|
debridManager.downloadUrl = ""
|
||||||
|
|
|
||||||
|
|
@ -86,7 +86,7 @@ struct BatchChoiceView: View {
|
||||||
|
|
||||||
pluginManager.runDebridAction(
|
pluginManager.runDebridAction(
|
||||||
urlString: debridManager.downloadUrl,
|
urlString: debridManager.downloadUrl,
|
||||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
navModel: navModel
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue