Kodi: Add multi-server support
Multiple servers can be added to Ferrite to playback from any Kodi server that the user wants. This also adds the ability to have friendly names which makes it easier to select what server to play on. Each server shows the user whether it's online or not through Kodi's JSONRPC ping method. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
d2d7d7364f
commit
20c55316b0
25 changed files with 611 additions and 198 deletions
|
|
@ -13,7 +13,7 @@
|
|||
0C0755C6293424A200ECA142 /* DebridLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C5293424A200ECA142 /* DebridLabelView.swift */; };
|
||||
0C0755C8293425B500ECA142 /* DebridManagerModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0755C7293425B500ECA142 /* DebridManagerModels.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* PluginListView.swift */; };
|
||||
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 */; };
|
||||
0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; };
|
||||
|
|
@ -26,6 +26,9 @@
|
|||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
|
||||
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD43E29B6968D006429DB /* KodiEditorView.swift */; };
|
||||
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD44029B6ACD9006429DB /* KodiServer+CoreDataClass.swift */; };
|
||||
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3DD44129B6ACD9006429DB /* KodiServer+CoreDataProperties.swift */; };
|
||||
0C3E00D0296F4DB200ECECB2 /* ActionModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */; };
|
||||
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */; };
|
||||
0C3E00D8296F5B9A00ECECB2 /* PluginModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */; };
|
||||
|
|
@ -118,6 +121,7 @@
|
|||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; };
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||
0CAF9319296399190050812A /* PremiumizeCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CAF9318296399190050812A /* PremiumizeCloudView.swift */; };
|
||||
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB0AB5E29BD2A200015422C /* KodiServerView.swift */; };
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; };
|
||||
|
|
@ -147,7 +151,7 @@
|
|||
0C0755C5293424A200ECA142 /* DebridLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridLabelView.swift; sourceTree = "<group>"; };
|
||||
0C0755C7293425B500ECA142 /* DebridManagerModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridManagerModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* PluginListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginListView.swift; sourceTree = "<group>"; };
|
||||
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>"; };
|
||||
0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -159,6 +163,9 @@
|
|||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; };
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; };
|
||||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = "<group>"; };
|
||||
0C3DD43E29B6968D006429DB /* KodiEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiEditorView.swift; sourceTree = "<group>"; };
|
||||
0C3DD44029B6ACD9006429DB /* KodiServer+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KodiServer+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C3DD44129B6ACD9006429DB /* KodiServer+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "KodiServer+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C3E00CF296F4DB200ECECB2 /* ActionModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionModels.swift; sourceTree = "<group>"; };
|
||||
0C3E00D1296F4FD200ECECB2 /* PluginsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginsView.swift; sourceTree = "<group>"; };
|
||||
0C3E00D7296F5B9A00ECECB2 /* PluginModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginModels.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -247,6 +254,7 @@
|
|||
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = "<group>"; };
|
||||
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0CAF9318296399190050812A /* PremiumizeCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PremiumizeCloudView.swift; sourceTree = "<group>"; };
|
||||
0CB0AB5E29BD2A200015422C /* KodiServerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KodiServerView.swift; sourceTree = "<group>"; };
|
||||
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -321,6 +329,8 @@
|
|||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C3DD44029B6ACD9006429DB /* KodiServer+CoreDataClass.swift */,
|
||||
0C3DD44129B6ACD9006429DB /* KodiServer+CoreDataProperties.swift */,
|
||||
0C5005582992BA6A0064606A /* PluginTag+CoreDataClass.swift */,
|
||||
0C5005592992BA6A0064606A /* PluginTag+CoreDataProperties.swift */,
|
||||
0CC389512970AD900066D06F /* Action+CoreDataClass.swift */,
|
||||
|
|
@ -375,12 +385,31 @@
|
|||
path = Cloud;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C3DD43C29B69664006429DB /* Kodi */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */,
|
||||
0C3DD43E29B6968D006429DB /* KodiEditorView.swift */,
|
||||
0CB0AB5E29BD2A200015422C /* KodiServerView.swift */,
|
||||
);
|
||||
path = Kodi;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C3DD43D29B69672006429DB /* PluginList */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
||||
);
|
||||
path = PluginList;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C3E00D4296F560800ECECB2 /* Plugin */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C794B65289DAC9F00DD1CC8 /* Source */,
|
||||
0C0D50E6288DFF850035ECC8 /* PluginListView.swift */,
|
||||
0C0D50E6288DFF850035ECC8 /* PluginAggregateView.swift */,
|
||||
0C5005512992B6750064606A /* PluginTagsView.swift */,
|
||||
0CD5F1FC299C083B00476DDB /* PluginPickerView.swift */,
|
||||
);
|
||||
|
|
@ -448,12 +477,11 @@
|
|||
0CA0545C288F7CB200850554 /* Settings */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C3DD43D29B69672006429DB /* PluginList */,
|
||||
0C3DD43C29B69664006429DB /* Kodi */,
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsPluginListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* PluginListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionPickerView.swift */,
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */,
|
||||
0C6771F529B3B602005D38D2 /* SettingsKodiView.swift */,
|
||||
0C6771FD29B521F1005D38D2 /* SettingsDebridInfoView.swift */,
|
||||
0C5708EA29B8F89300BE07F9 /* SettingsLogView.swift */,
|
||||
);
|
||||
|
|
@ -758,10 +786,12 @@
|
|||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||
0C5005522992B6750064606A /* PluginTagsView.swift in Sources */,
|
||||
0CB0AB5F29BD2A200015422C /* KodiServerView.swift in Sources */,
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
||||
0C5708E929B8E61C00BE07F9 /* Logger.swift in Sources */,
|
||||
0C42B5962932F2D5008057A0 /* DebridPickerView.swift in Sources */,
|
||||
0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */,
|
||||
0C3DD44229B6ACD9006429DB /* KodiServer+CoreDataClass.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||
|
|
@ -771,7 +801,7 @@
|
|||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C50B7D0299DF63C00A9FA3C /* UIDevice.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* PluginListView.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* PluginAggregateView.swift in Sources */,
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||
0CD72E17293D9928001A7EA4 /* Array.swift in Sources */,
|
||||
0CD5F1FB299BEFBE00476DDB /* CustomScopeBar.swift in Sources */,
|
||||
|
|
@ -847,7 +877,9 @@
|
|||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0CEC8AAE299B31B6007BFE8F /* SearchFilterHeaderView.swift in Sources */,
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||
0C3DD43F29B6968D006429DB /* KodiEditorView.swift in Sources */,
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
|
||||
0C3DD44329B6ACD9006429DB /* KodiServer+CoreDataProperties.swift in Sources */,
|
||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
||||
0CD5F1FD299C083B00476DDB /* PluginPickerView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -10,6 +10,91 @@ import Foundation
|
|||
public class Kodi {
|
||||
let encoder = JSONEncoder()
|
||||
|
||||
// Used to add server to CoreData. Not part of API
|
||||
public func addServer(
|
||||
urlString: String,
|
||||
friendlyName: String?,
|
||||
username: String?,
|
||||
password: String?,
|
||||
existingServer: KodiServer? = nil
|
||||
) throws {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if !urlString.starts(with: "http://") && !urlString.starts(with: "https://") {
|
||||
throw KodiError.ServerAddition(description: "Could not add Kodi server because the URL is invalid.")
|
||||
}
|
||||
|
||||
var name: String = ""
|
||||
if let friendlyName {
|
||||
name = friendlyName
|
||||
} else {
|
||||
var components = URLComponents(string: urlString)
|
||||
components?.scheme = nil
|
||||
components?.path = ""
|
||||
|
||||
guard let cleanedName = components?.url?.description.dropFirst(2) else {
|
||||
throw KodiError.ServerAddition(description: "An invalid friendly name for this Kodi server was generated.")
|
||||
}
|
||||
|
||||
name = String(cleanedName)
|
||||
}
|
||||
|
||||
if existingServer == nil {
|
||||
let existingServerRequest = KodiServer.fetchRequest()
|
||||
existingServerRequest.fetchLimit = 1
|
||||
|
||||
// If a server with the same name or URL exists, error out
|
||||
let namePredicate = NSPredicate(format: "name == %@", name)
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", urlString)
|
||||
existingServerRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [namePredicate, urlPredicate])
|
||||
|
||||
if (try? backgroundContext.fetch(existingServerRequest).first) != nil {
|
||||
throw KodiError.ServerAddition(description: "An existing kodi server with the same name or URL was found. Please try editing an existing server instead.")
|
||||
}
|
||||
}
|
||||
|
||||
let newServerObject = existingServer ?? KodiServer(context: backgroundContext)
|
||||
|
||||
newServerObject.urlString = urlString
|
||||
newServerObject.name = name
|
||||
|
||||
if let username, let password {
|
||||
newServerObject.username = username
|
||||
newServerObject.password = password
|
||||
}
|
||||
|
||||
try backgroundContext.save()
|
||||
}
|
||||
|
||||
public func ping(server: KodiServer) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(server.urlString)/jsonrpc")!)
|
||||
request.httpMethod = "POST"
|
||||
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
let requestBody = RPCPayload(
|
||||
method: "JSONRPC.Ping",
|
||||
params: nil
|
||||
)
|
||||
|
||||
if let username = server.username, let password = server.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 for server \(server.name) 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).")
|
||||
}
|
||||
}
|
||||
|
||||
public func sendVideoUrl(urlString: String) async throws {
|
||||
guard let baseUrl = UserDefaults.standard.string(forKey: "ExternalServices.KodiUrl") else {
|
||||
throw KodiError.InvalidBaseUrl
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// KodiServer+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 3/6/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(KodiServer)
|
||||
public class KodiServer: NSManagedObject {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// KodiServer+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 3/6/23.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension KodiServer {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<KodiServer> {
|
||||
return NSFetchRequest<KodiServer>(entityName: "KodiServer")
|
||||
}
|
||||
|
||||
@NSManaged public var urlString: String
|
||||
@NSManaged public var name: String
|
||||
@NSManaged public var username: String?
|
||||
@NSManaged public var password: String?
|
||||
|
||||
}
|
||||
|
||||
extension KodiServer : Identifiable {
|
||||
|
||||
}
|
||||
|
|
@ -34,6 +34,12 @@
|
|||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<relationship name="parentHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="History" inverseName="entries" inverseEntity="History"/>
|
||||
</entity>
|
||||
<entity name="KodiServer" representedClassName="KodiServer" syncable="YES">
|
||||
<attribute name="name" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="password" optional="YES" attributeType="String"/>
|
||||
<attribute name="urlString" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="username" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="PluginList" representedClassName="PluginList" syncable="YES">
|
||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="id" attributeType="UUID" usesScalarValueType="NO"/>
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import Foundation
|
|||
|
||||
extension Kodi {
|
||||
enum KodiError: Error {
|
||||
case ServerAddition(description: String)
|
||||
case InvalidBaseUrl
|
||||
case InvalidPlaybackUrl
|
||||
case InvalidPostBody
|
||||
|
|
|
|||
|
|
@ -58,11 +58,14 @@ public class NavigationViewModel: ObservableObject {
|
|||
|
||||
@Published var selectedTab: ViewTab = .search
|
||||
|
||||
// TODO: Maybe move these to their own StateObjects?
|
||||
// Used between SourceListView and SourceSettingsView
|
||||
@Published var showSourceSettings: Bool = false
|
||||
var selectedSource: Source?
|
||||
|
||||
@Published var showSourceListEditor: Bool = false
|
||||
// Used between service views and editor views in Settings
|
||||
@Published var selectedPluginList: PluginList?
|
||||
@Published var selectedKodiServer: KodiServer?
|
||||
|
||||
@Published var libraryPickerSelection: LibraryPickerSegment = .bookmarks
|
||||
@Published var pluginPickerSelection: PluginPickerSegment = .sources
|
||||
|
|
|
|||
|
|
@ -728,7 +728,7 @@ public class PluginManager: ObservableObject {
|
|||
public func addPluginList(_ url: String, isSheet: Bool = false, existingPluginList: PluginList? = nil) async throws {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if url.isEmpty || URL(string: url) == nil {
|
||||
if url.isEmpty || !url.starts(with: "https://") && !url.starts(with: "http://") {
|
||||
throw PluginManagerError.ListAddition(description: "The provided source list is invalid. Please check if the URL is formatted properly.")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ struct ListRowLinkView: View {
|
|||
Image(systemName: "arrow.up.forward.app.fill")
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.trailing, -5)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ struct ListRowButtonView: View {
|
|||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, -5)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -66,10 +68,10 @@ struct ListRowTextView: View {
|
|||
|
||||
if let rightText {
|
||||
Text(rightText)
|
||||
} else {
|
||||
Image(systemName: rightSymbol!)
|
||||
.foregroundColor(.gray)
|
||||
} else if let rightSymbol {
|
||||
Image(systemName: rightSymbol)
|
||||
}
|
||||
}
|
||||
.padding(.trailing, -5)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ struct BookmarksView: View {
|
|||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@Binding var searchText: String
|
||||
@Binding var bookmarksEmpty: Bool
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
@State private var bookmarkPredicate: NSPredicate?
|
||||
|
|
@ -54,6 +55,8 @@ struct BookmarksView: View {
|
|||
.listStyle(.insetGrouped)
|
||||
.inlinedList(inset: Application.shared.osVersion.majorVersion > 14 ? 15 : -25)
|
||||
.backport.onAppear {
|
||||
bookmarksEmpty = bookmarks.isEmpty
|
||||
|
||||
if debridManager.enabledDebrids.count > 0 {
|
||||
viewTask = Task {
|
||||
let magnets = bookmarks.compactMap {
|
||||
|
|
@ -70,6 +73,9 @@ struct BookmarksView: View {
|
|||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
.onChange(of: bookmarks.count) { newCount in
|
||||
bookmarksEmpty = newCount == 0
|
||||
}
|
||||
}
|
||||
.backport.onAppear {
|
||||
applyPredicate()
|
||||
|
|
|
|||
|
|
@ -10,9 +10,15 @@ import SwiftUI
|
|||
struct HistoryView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
var history: FetchedResults<History>
|
||||
@FetchRequest(
|
||||
entity: History.entity(),
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \History.date, ascending: false)
|
||||
]
|
||||
) var history: FetchedResults<History>
|
||||
|
||||
@Binding var searchText: String
|
||||
@Binding var historyEmpty: Bool
|
||||
|
||||
@State private var historyPredicate: NSPredicate?
|
||||
|
||||
|
|
@ -28,8 +34,12 @@ struct HistoryView: View {
|
|||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.backport.onAppear {
|
||||
historyEmpty = history.isEmpty
|
||||
applyPredicate()
|
||||
}
|
||||
.onChange(of: history.count) { newCount in
|
||||
historyEmpty = newCount == 0
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
applyPredicate()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
//
|
||||
import SwiftUI
|
||||
|
||||
struct PluginListView<P: Plugin, PJ: PluginJson>: View {
|
||||
struct PluginAggregateView<P: Plugin, PJ: PluginJson>: View {
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
|
|
@ -15,6 +15,7 @@ struct PluginListView<P: Plugin, PJ: PluginJson>: View {
|
|||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@Binding var searchText: String
|
||||
@Binding var pluginsEmpty: Bool
|
||||
|
||||
@State private var isEditingSearch = false
|
||||
@State private var isSearching = false
|
||||
|
|
@ -70,9 +71,15 @@ struct PluginListView<P: Plugin, PJ: PluginJson>: View {
|
|||
.environmentObject(navModel)
|
||||
}
|
||||
}
|
||||
.backport.onAppear {
|
||||
pluginsEmpty = installedPlugins.isEmpty
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
|
||||
}
|
||||
.onChange(of: installedPlugins.count) { newCount in
|
||||
pluginsEmpty = newCount == 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -104,6 +104,8 @@ struct SourceSettingsBaseUrlView: View {
|
|||
}
|
||||
})
|
||||
.keyboardType(.URL)
|
||||
.autocorrectionDisabled(true)
|
||||
.autocapitalization(.none)
|
||||
.backport.onAppear {
|
||||
tempBaseUrl = selectedSource.baseUrl ?? ""
|
||||
}
|
||||
|
|
|
|||
111
Ferrite/Views/ComponentViews/Settings/Kodi/KodiEditorView.swift
Normal file
111
Ferrite/Views/ComponentViews/Settings/Kodi/KodiEditorView.swift
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
//
|
||||
// KodiEditorView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 3/6/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct KodiEditorView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var logManager: LoggingManager
|
||||
|
||||
@State private var loadedSelectedServer = false
|
||||
|
||||
@State private var serverUrl: String = ""
|
||||
@State private var friendlyName: String = ""
|
||||
@State private var username: String = ""
|
||||
@State private var password: String = ""
|
||||
|
||||
@State private var showErrorAlert = false
|
||||
@State private var errorAlertText: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
Group {
|
||||
Section(
|
||||
header: InlineHeader("URL"),
|
||||
footer: Text("Must follow the format http(s)://<ip>:<port>")
|
||||
) {
|
||||
TextField("Enter URL", text: $serverUrl)
|
||||
.keyboardType(.URL)
|
||||
}
|
||||
|
||||
Section(
|
||||
header: InlineHeader("Friendly name"),
|
||||
footer: Text("Defaults to the URL if not provided")
|
||||
) {
|
||||
TextField("Friendly name", text: $friendlyName)
|
||||
}
|
||||
|
||||
Section(
|
||||
header: InlineHeader("Credentials"),
|
||||
footer: Text("Only use for clients with authentication")
|
||||
) {
|
||||
TextField("Username", text: $username)
|
||||
|
||||
HybridSecureField(text: $password)
|
||||
}
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
.autocapitalization(.none)
|
||||
.id(loadedSelectedServer)
|
||||
}
|
||||
.backport.onAppear {
|
||||
if let selectedKodiServer = navModel.selectedKodiServer {
|
||||
serverUrl = selectedKodiServer.urlString
|
||||
friendlyName = selectedKodiServer.name
|
||||
username = selectedKodiServer.username ?? ""
|
||||
password = selectedKodiServer.password ?? ""
|
||||
|
||||
loadedSelectedServer.toggle()
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $showErrorAlert,
|
||||
title: "Error",
|
||||
message: errorAlertText
|
||||
)
|
||||
.navigationTitle("Editing Kodi Server")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
do {
|
||||
try pluginManager.kodi.addServer(
|
||||
urlString: serverUrl,
|
||||
friendlyName: friendlyName.isEmpty ? nil : friendlyName,
|
||||
username: username.isEmpty ? nil : username,
|
||||
password: password.isEmpty ? nil : password,
|
||||
existingServer: navModel.selectedKodiServer
|
||||
)
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} catch {
|
||||
logManager.error("Editing Kodi server: \(error)", showToast: false)
|
||||
errorAlertText = error.localizedDescription
|
||||
showErrorAlert.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct KodiEditorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
KodiEditorView()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// KodiServerView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 3/11/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct KodiServerView: View {
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var logManager: LoggingManager
|
||||
|
||||
var server: KodiServer
|
||||
|
||||
@State private var isActive = false
|
||||
@State private var pingInProgress = false
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
Text(server.name)
|
||||
Spacer()
|
||||
|
||||
if pingInProgress {
|
||||
ProgressView()
|
||||
} else {
|
||||
Circle()
|
||||
.foregroundColor(isActive ? .green : .red)
|
||||
.frame(width: 10, height: 10)
|
||||
}
|
||||
}
|
||||
.backport.onAppear {
|
||||
viewTask = Task {
|
||||
pingInProgress = true
|
||||
|
||||
do {
|
||||
try await pluginManager.kodi.ping(server: server)
|
||||
isActive = true
|
||||
} catch {
|
||||
logManager.error("Kodi server \(server.name): \(error)", showToast: false)
|
||||
isActive = false
|
||||
}
|
||||
|
||||
pingInProgress = false
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
//
|
||||
// SettingsKodiView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 3/4/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsKodiView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
// TODO: Change to var in v0.7
|
||||
@FetchRequest(
|
||||
entity: KodiServer.entity(),
|
||||
sortDescriptors: []
|
||||
) var kodiServers: FetchedResults<KodiServer>
|
||||
|
||||
@State private var presentEditSheet = false
|
||||
|
||||
var body: some View {
|
||||
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("Servers"),
|
||||
footer: Text("Edit a server by holding it and accessing the context menu")
|
||||
) {
|
||||
if kodiServers.isEmpty {
|
||||
Text("Add a server using the + button in the top-right")
|
||||
} else {
|
||||
ForEach(kodiServers, id: \.self) { server in
|
||||
KodiServerView(server: server)
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedKodiServer = server
|
||||
presentEditSheet.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
Image(systemName: "pencil")
|
||||
}
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(server, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(server, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let server = kodiServers[safe: index] {
|
||||
PersistenceController.shared.delete(server, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.sheet(isPresented: $presentEditSheet) {
|
||||
KodiEditorView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
.navigationTitle("Kodi")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
navModel.selectedKodiServer = nil
|
||||
presentEditSheet.toggle()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -12,17 +12,18 @@ struct PluginListEditorView: View {
|
|||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var logManager: LoggingManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@State var selectedPluginList: PluginList?
|
||||
|
||||
@State private var sourceUrlSet = false
|
||||
@State private var showUrlErrorAlert = false
|
||||
|
||||
@State private var pluginListUrl: String = ""
|
||||
@State private var urlErrorAlertText: String = ""
|
||||
|
||||
@State private var loadedSelectedList = false
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
|
|
@ -30,11 +31,13 @@ struct PluginListEditorView: View {
|
|||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.conditionalId(sourceUrlSet)
|
||||
.id(loadedSelectedList)
|
||||
}
|
||||
.backport.onAppear {
|
||||
pluginListUrl = selectedPluginList?.urlString ?? ""
|
||||
sourceUrlSet = true
|
||||
if let selectedList = navModel.selectedPluginList {
|
||||
pluginListUrl = selectedList.urlString
|
||||
loadedSelectedList.toggle()
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $showUrlErrorAlert,
|
||||
|
|
@ -54,9 +57,14 @@ struct PluginListEditorView: View {
|
|||
Button("Save") {
|
||||
Task {
|
||||
do {
|
||||
try await pluginManager.addPluginList(pluginListUrl, existingPluginList: selectedPluginList)
|
||||
try await pluginManager.addPluginList(
|
||||
pluginListUrl,
|
||||
existingPluginList: navModel.selectedPluginList
|
||||
)
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
} catch {
|
||||
logManager.error("Editing plugin list: \(error)", showToast: false)
|
||||
urlErrorAlertText = error.localizedDescription
|
||||
showUrlErrorAlert.toggle()
|
||||
}
|
||||
|
|
@ -67,9 +75,3 @@ struct PluginListEditorView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct PluginListEditorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
PluginListEditorView()
|
||||
}
|
||||
}
|
||||
|
|
@ -17,8 +17,7 @@ struct SettingsPluginListView: View {
|
|||
sortDescriptors: []
|
||||
) var pluginLists: FetchedResults<PluginList>
|
||||
|
||||
@State private var presentSourceSheet = false
|
||||
@State private var selectedPluginList: PluginList?
|
||||
@State private var presentEditSheet = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
|
@ -29,10 +28,10 @@ struct SettingsPluginListView: View {
|
|||
ForEach(pluginLists, id: \.self) { pluginList in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(pluginList.name)
|
||||
|
||||
|
||||
Group {
|
||||
Text(pluginList.author)
|
||||
|
||||
|
||||
Text("ID: \(pluginList.id)")
|
||||
.font(.caption)
|
||||
}
|
||||
|
|
@ -41,13 +40,13 @@ struct SettingsPluginListView: View {
|
|||
.padding(.vertical, 2)
|
||||
.contextMenu {
|
||||
Button {
|
||||
selectedPluginList = pluginList
|
||||
presentSourceSheet.toggle()
|
||||
navModel.selectedPluginList = pluginList
|
||||
presentEditSheet.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
Image(systemName: "pencil")
|
||||
}
|
||||
|
||||
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(pluginList, context: backgroundContext)
|
||||
|
|
@ -77,12 +76,13 @@ struct SettingsPluginListView: View {
|
|||
.inlinedList(inset: -20)
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $presentSourceSheet) {
|
||||
.sheet(isPresented: $presentEditSheet) {
|
||||
if #available(iOS 16, *) {
|
||||
PluginListEditorView(selectedPluginList: selectedPluginList)
|
||||
PluginListEditorView()
|
||||
.presentationDetents([.medium])
|
||||
} else {
|
||||
PluginListEditorView(selectedPluginList: selectedPluginList)
|
||||
PluginListEditorView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Plugin Lists")
|
||||
|
|
@ -90,7 +90,8 @@ struct SettingsPluginListView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
presentSourceSheet.toggle()
|
||||
navModel.selectedPluginList = nil
|
||||
presentEditSheet.toggle()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
|
|
@ -13,40 +13,38 @@ struct SettingsDebridInfoView: View {
|
|||
let debridType: DebridType
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
Section(header: InlineHeader("Description")) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("\(debridType.toString()) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.")
|
||||
List {
|
||||
Section(header: InlineHeader("Description")) {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text("\(debridType.toString()) is a debrid service that is used for unrestricting downloads and media playback. You must pay to access the service.")
|
||||
|
||||
Link("Website", destination: URL(string: debridType.website()) ?? URL(string: "https://kingbri.dev/ferrite")!)
|
||||
}
|
||||
}
|
||||
|
||||
Section(
|
||||
header: InlineHeader("Login status"),
|
||||
footer: Text("A WebView will show up to prompt you for credentials")
|
||||
) {
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
await debridManager.logoutDebrid(debridType: debridType)
|
||||
} else if !debridManager.getAuthProcessingBool(debridType: debridType) {
|
||||
await debridManager.authenticateDebrid(debridType: debridType)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(
|
||||
debridManager.enabledDebrids.contains(debridType)
|
||||
? "Logout"
|
||||
: (debridManager.getAuthProcessingBool(debridType: debridType) ? "Processing" : "Login")
|
||||
)
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue)
|
||||
}
|
||||
Link("Website", destination: URL(string: debridType.website()) ?? URL(string: "https://kingbri.dev/ferrite")!)
|
||||
}
|
||||
}
|
||||
|
||||
Section(
|
||||
header: InlineHeader("Login status"),
|
||||
footer: Text("A WebView will show up to prompt you for credentials")
|
||||
) {
|
||||
Button {
|
||||
Task {
|
||||
if debridManager.enabledDebrids.contains(debridType) {
|
||||
await debridManager.logoutDebrid(debridType: debridType)
|
||||
} else if !debridManager.getAuthProcessingBool(debridType: debridType) {
|
||||
await debridManager.authenticateDebrid(debridType: debridType)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(
|
||||
debridManager.enabledDebrids.contains(debridType)
|
||||
? "Logout"
|
||||
: (debridManager.getAuthProcessingBool(debridType: debridType) ? "Processing" : "Login")
|
||||
)
|
||||
.foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue)
|
||||
}
|
||||
}
|
||||
.navigationTitle(debridType.toString())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
.navigationTitle(debridType.toString())
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,61 +0,0 @@
|
|||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,51 +11,49 @@ struct SettingsLogView: View {
|
|||
@EnvironmentObject var logManager: LoggingManager
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
ForEach($logManager.messageArray, id: \.self) { $log in
|
||||
Text(log.toMessage())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(log.isExpanded ? nil : 5)
|
||||
.onTapGesture {
|
||||
log.isExpanded.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.backport.alert(
|
||||
isPresented: $logManager.showLogExportedAlert,
|
||||
title: "Success",
|
||||
message: "Log successfully exported in Ferrite's logs folder"
|
||||
)
|
||||
.navigationTitle("Logs")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
Button {
|
||||
logManager.exportLogs()
|
||||
} label: {
|
||||
Label("Export", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
Button(role: .destructive) {
|
||||
logManager.messageArray = []
|
||||
} label: {
|
||||
Label("Clear session logs", systemImage: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
logManager.messageArray = []
|
||||
} label: {
|
||||
Label("Clear session logs", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
List {
|
||||
ForEach($logManager.messageArray, id: \.self) { $log in
|
||||
Text(log.toMessage())
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
.lineLimit(log.isExpanded ? nil : 5)
|
||||
.onTapGesture {
|
||||
log.isExpanded.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.backport.alert(
|
||||
isPresented: $logManager.showLogExportedAlert,
|
||||
title: "Success",
|
||||
message: "Log successfully exported in Ferrite's logs folder"
|
||||
)
|
||||
.navigationTitle("Logs")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Menu {
|
||||
Button {
|
||||
logManager.exportLogs()
|
||||
} label: {
|
||||
Label("Export", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
Button(role: .destructive) {
|
||||
logManager.messageArray = []
|
||||
} label: {
|
||||
Label("Clear session logs", systemImage: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
logManager.messageArray = []
|
||||
} label: {
|
||||
Label("Clear session logs", systemImage: "trash")
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: "ellipsis")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,17 +12,8 @@ struct LibraryView: View {
|
|||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: Bookmark.entity(),
|
||||
sortDescriptors: []
|
||||
) var bookmarks: FetchedResults<Bookmark>
|
||||
|
||||
@FetchRequest(
|
||||
entity: History.entity(),
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \History.date, ascending: false)
|
||||
]
|
||||
) var history: FetchedResults<History>
|
||||
@State private var bookmarksEmpty = false
|
||||
@State private var historyEmpty = false
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
|
|
@ -37,9 +28,9 @@ struct LibraryView: View {
|
|||
ZStack {
|
||||
switch navModel.libraryPickerSelection {
|
||||
case .bookmarks:
|
||||
BookmarksView(searchText: $searchText)
|
||||
BookmarksView(searchText: $searchText, bookmarksEmpty: $bookmarksEmpty)
|
||||
case .history:
|
||||
HistoryView(history: history, searchText: $searchText)
|
||||
HistoryView(searchText: $searchText, historyEmpty: $historyEmpty)
|
||||
case .debridCloud:
|
||||
DebridCloudView(searchText: $searchText)
|
||||
}
|
||||
|
|
@ -47,11 +38,11 @@ struct LibraryView: View {
|
|||
.overlay {
|
||||
switch navModel.libraryPickerSelection {
|
||||
case .bookmarks:
|
||||
if bookmarks.isEmpty {
|
||||
if bookmarksEmpty {
|
||||
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
|
||||
}
|
||||
case .history:
|
||||
if history.isEmpty {
|
||||
if historyEmpty {
|
||||
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
||||
}
|
||||
case .debridCloud:
|
||||
|
|
|
|||
|
|
@ -12,18 +12,24 @@ struct PluginsView: View {
|
|||
@EnvironmentObject var pluginManager: PluginManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
/*
|
||||
@FetchRequest(
|
||||
entity: Source.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
*/
|
||||
|
||||
/*
|
||||
@FetchRequest(
|
||||
entity: Action.entity(),
|
||||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
*/
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
||||
@State private var installedSourcesEmpty = false
|
||||
@State private var installedActionsEmpty = false
|
||||
@State private var checkedForPlugins = false
|
||||
|
||||
@State private var isEditingSearch = false
|
||||
|
|
@ -38,9 +44,15 @@ struct PluginsView: View {
|
|||
if checkedForPlugins {
|
||||
switch navModel.pluginPickerSelection {
|
||||
case .sources:
|
||||
PluginListView<Source, SourceJson>(searchText: $searchText)
|
||||
PluginAggregateView<Source, SourceJson>(
|
||||
searchText: $searchText,
|
||||
pluginsEmpty: $installedSourcesEmpty
|
||||
)
|
||||
case .actions:
|
||||
PluginListView<Action, ActionJson>(searchText: $searchText)
|
||||
PluginAggregateView<Action, ActionJson>(
|
||||
searchText: $searchText,
|
||||
pluginsEmpty: $installedActionsEmpty
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -48,11 +60,11 @@ struct PluginsView: View {
|
|||
if checkedForPlugins {
|
||||
switch navModel.pluginPickerSelection {
|
||||
case .sources:
|
||||
if sources.isEmpty, pluginManager.availableSources.isEmpty {
|
||||
if installedSourcesEmpty, pluginManager.availableSources.isEmpty {
|
||||
EmptyInstructionView(title: "No Sources", message: "Add a plugin list in Settings")
|
||||
}
|
||||
case .actions:
|
||||
if actions.isEmpty, pluginManager.availableActions.isEmpty {
|
||||
if installedActionsEmpty, pluginManager.availableActions.isEmpty {
|
||||
EmptyInstructionView(title: "No Actions", message: "Add a plugin list in Settings")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,6 +15,11 @@ struct SettingsView: View {
|
|||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@FetchRequest(
|
||||
entity: KodiServer.entity(),
|
||||
sortDescriptors: []
|
||||
) var kodiServers: FetchedResults<KodiServer>
|
||||
|
||||
@AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = ""
|
||||
|
||||
@AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true
|
||||
|
|
@ -55,7 +60,7 @@ struct SettingsView: View {
|
|||
HStack {
|
||||
Text("Kodi")
|
||||
Spacer()
|
||||
Text(kodiUrl.isEmpty ? "Disabled" : "Enabled")
|
||||
Text(kodiServers.isEmpty ? "Disabled" : "Enabled")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -21,7 +21,10 @@ struct ActionChoiceView: View {
|
|||
sortDescriptors: []
|
||||
) var actions: FetchedResults<Action>
|
||||
|
||||
@AppStorage("ExternalServices.KodiUrl") var kodiUrl: String = ""
|
||||
@FetchRequest(
|
||||
entity: KodiServer.entity(),
|
||||
sortDescriptors: []
|
||||
) var kodiServers: FetchedResults<KodiServer>
|
||||
|
||||
@State private var showLinkCopyAlert = false
|
||||
@State private var showMagnetCopyAlert = false
|
||||
|
|
@ -53,12 +56,20 @@ struct ActionChoiceView: View {
|
|||
}
|
||||
}
|
||||
|
||||
if !kodiUrl.isEmpty {
|
||||
ListRowButtonView("Open in Kodi", systemImage: "arrow.up.forward.app.fill") {
|
||||
Task {
|
||||
await pluginManager.sendToKodi(urlString: debridManager.downloadUrl)
|
||||
if !kodiServers.isEmpty {
|
||||
DisclosureGroup("Open in Kodi") {
|
||||
ForEach(kodiServers, id: \.self) { server in
|
||||
Button {
|
||||
Task {
|
||||
await pluginManager.sendToKodi(urlString: debridManager.downloadUrl)
|
||||
}
|
||||
} label: {
|
||||
KodiServerView(server: server)
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.backport.tint(.secondary)
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||
|
|
|
|||
Loading…
Reference in a new issue