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 */; };
|
||||
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 = "<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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
|
|
@ -346,6 +354,7 @@
|
|||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */,
|
||||
0C6771F929B3D1AE005D38D2 /* KodiModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -437,6 +446,7 @@
|
|||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */,
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -475,6 +485,7 @@
|
|||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||
0C2D9652299316CC00A504B6 /* Tag.swift */,
|
||||
0C6771FB29B3E0DB005D38D2 /* HybridSecureField.swift */,
|
||||
);
|
||||
path = CommonViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -554,6 +565,7 @@
|
|||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
||||
0C422E7D293542EA00486D65 /* PremiumizeWrapper.swift */,
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
0C6771F329B3B4FD005D38D2 /* KodiWrapper.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -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 */,
|
||||
|
|
|
|||
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 {
|
||||
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!")
|
||||
|
|
|
|||
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)
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ struct PremiumizeCloudView: View {
|
|||
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 ?? ""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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
|
||||
|
||||
@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")
|
||||
|
|
|
|||
|
|
@ -21,6 +21,8 @@ struct ActionChoiceView: View {
|
|||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
|
||||
@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 = ""
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ struct BatchChoiceView: View {
|
|||
|
||||
pluginManager.runDebridAction(
|
||||
urlString: debridManager.downloadUrl,
|
||||
currentChoiceSheet: &navModel.currentChoiceSheet
|
||||
navModel: navModel
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue