From 07731e7b0099190effbfa6b6fcb1901bd64d3688 Mon Sep 17 00:00:00 2001 From: kingbri Date: Wed, 5 Jun 2024 12:33:11 -0400 Subject: [PATCH] Debrid: Migrate more components to the protocol Protocols can't be used in ObservedObjects. Observable in iOS 17 and up solves this, but Ferrite targets iOS 16 and up, so add a type-erased StateObject which supports protocols. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 + Ferrite/API/AllDebridWrapper.swift | 14 +- Ferrite/API/PremiumizeWrapper.swift | 12 +- Ferrite/API/RealDebridWrapper.swift | 23 +-- Ferrite/Models/DebridModels.swift | 6 + Ferrite/Protocols/Debrid.swift | 6 +- Ferrite/Utils/Store.swift | 148 ++++++++++++++++++ Ferrite/ViewModels/DebridManager.swift | 61 +++----- Ferrite/ViewModels/ScrapingViewModel.swift | 2 +- .../Debrid/DebridLabelView.swift | 38 ++--- .../Filters/SelectedDebridFilterView.swift | 16 +- .../Library/Cloud/AllDebridCloudView.swift | 2 +- .../Library/Cloud/RealDebridCloudView.swift | 2 +- .../SearchResult/SearchFilterHeaderView.swift | 2 +- .../SearchResult/SearchResultInfoView.swift | 4 +- .../Settings/SettingsDebridInfoView.swift | 32 ++-- Ferrite/Views/LibraryView.swift | 4 +- Ferrite/Views/SettingsView.swift | 8 +- .../Views/SheetViews/BatchChoiceView.swift | 10 +- 19 files changed, 272 insertions(+), 122 deletions(-) create mode 100644 Ferrite/Utils/Store.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index ed626ca..f957fd7 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -96,6 +96,7 @@ 0C84FCE729E4B61A00B0DFE4 /* FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */; }; 0C84FCE929E5ADEF00B0DFE4 /* FilterAmountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */; }; 0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.swift */; }; + 0C8AE2482C0FFB6600701675 /* Store.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8AE2472C0FFB6600701675 /* Store.swift */; }; 0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */; }; 0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; }; 0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */; }; @@ -244,6 +245,7 @@ 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterModels.swift; sourceTree = ""; }; 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAmountLabelView.swift; sourceTree = ""; }; 0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = ""; }; + 0C8AE2472C0FFB6600701675 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = ""; }; 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = ""; }; 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = ""; }; 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = ""; }; @@ -457,6 +459,7 @@ 0C44E2A728D4DDDC007711AE /* Application.swift */, 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */, 0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */, + 0C8AE2472C0FFB6600701675 /* Store.swift */, ); path = Utils; sourceTree = ""; @@ -858,6 +861,7 @@ 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, + 0C8AE2482C0FFB6600701675 /* Store.swift in Sources */, 0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, diff --git a/Ferrite/API/AllDebridWrapper.swift b/Ferrite/API/AllDebridWrapper.swift index 34268fa..0be235f 100644 --- a/Ferrite/API/AllDebridWrapper.swift +++ b/Ferrite/API/AllDebridWrapper.swift @@ -8,10 +8,10 @@ import Foundation // TODO: Fix errors -public class AllDebrid: PollingDebridSource { - public let id = "AllDebrid" - public let abbreviation = "AD" - public let website = "https://alldebrid.com" +public class AllDebrid: PollingDebridSource, ObservableObject { + public let id = DebridInfo( + name: "AllDebrid", abbreviation: "AD", website: "https://alldebrid.com" + ) public var authTask: Task? public var authProcessing: Bool = false @@ -178,7 +178,7 @@ public class AllDebrid: PollingDebridSource { return DebridIA( magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), - source: self.id, + source: self.id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -292,7 +292,7 @@ public class AllDebrid: PollingDebridSource { cloudTorrents = rawResponse.magnets.map { magnetResponse in DebridCloudTorrent( torrentId: String(magnetResponse.id), - source: self.id, + source: self.id.name, fileName: magnetResponse.filename, status: magnetResponse.status, hash: magnetResponse.hash, @@ -325,7 +325,7 @@ public class AllDebrid: PollingDebridSource { // The link is also the ID cloudDownloads = rawResponse.links.map { link in DebridCloudDownload( - downloadId: link.link, source: self.id, fileName: link.filename, link: link.link + downloadId: link.link, source: self.id.name, fileName: link.filename, link: link.link ) } diff --git a/Ferrite/API/PremiumizeWrapper.swift b/Ferrite/API/PremiumizeWrapper.swift index 9fcd234..939386f 100644 --- a/Ferrite/API/PremiumizeWrapper.swift +++ b/Ferrite/API/PremiumizeWrapper.swift @@ -7,10 +7,10 @@ import Foundation -public class Premiumize: OAuthDebridSource { - public let id = "Premiumize" - public let abbreviation = "PM" - public let website = "https://premiumize.me" +public class Premiumize: OAuthDebridSource, ObservableObject { + public let id = DebridInfo( + name: "Premiumize", abbreviation: "PM", website: "https://premiumize.me" + ) @Published public var authProcessing: Bool = false public var isLoggedIn: Bool { @@ -195,7 +195,7 @@ public class Premiumize: OAuthDebridSource { return DebridIA( magnet: magnet, - source: id, + source: id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -300,7 +300,7 @@ public class Premiumize: OAuthDebridSource { // The "link" is the ID for Premiumize cloudDownloads = rawResponse.files.map { file in - DebridCloudDownload(downloadId: file.id, source: self.id, fileName: file.name, link: file.id) + DebridCloudDownload(downloadId: file.id, source: self.id.name, fileName: file.name, link: file.id) } return cloudDownloads diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index e6f46de..7204cee 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -7,10 +7,10 @@ import Foundation -public class RealDebrid: PollingDebridSource { - public let id = "RealDebrid" - public let abbreviation = "RD" - public let website = "https://real-debrid.com" +public class RealDebrid: PollingDebridSource, ObservableObject { + public let id = DebridInfo( + name: "RealDebrid", abbreviation: "RD", website: "https://real-debrid.com" + ) public var authTask: Task? @Published public var authProcessing: Bool = false @@ -20,9 +20,14 @@ public class RealDebrid: PollingDebridSource { FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil } - @Published public var IAValues: [DebridIA] = [] + @Published public var IAValues: [DebridIA] = [] { + willSet { + self.objectWillChange.send() + } + } @Published public var cloudDownloads: [DebridCloudDownload] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = [] + var cloudTTL: Double = 0.0 let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseApiUrl = "https://api.real-debrid.com/rest/1.0" @@ -282,7 +287,7 @@ public class RealDebrid: PollingDebridSource { IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: id, + source: id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files ) @@ -291,7 +296,7 @@ public class RealDebrid: PollingDebridSource { IAValues.append( DebridIA( magnet: Magnet(hash: hash, link: nil), - source: id, + source: id.name, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: [] ) @@ -422,7 +427,7 @@ public class RealDebrid: PollingDebridSource { cloudTorrents = rawResponse.map { response in DebridCloudTorrent( torrentId: response.id, - source: self.id, + source: self.id.name, fileName: response.filename, status: response.status, hash: response.hash, @@ -448,7 +453,7 @@ public class RealDebrid: PollingDebridSource { let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) cloudDownloads = rawResponse.map { response in - DebridCloudDownload(downloadId: response.id, source: self.id, fileName: response.filename, link: response.download) + DebridCloudDownload(downloadId: response.id, source: self.id.name, fileName: response.filename, link: response.download) } return cloudDownloads diff --git a/Ferrite/Models/DebridModels.swift b/Ferrite/Models/DebridModels.swift index 3eb4e5f..0a4316d 100644 --- a/Ferrite/Models/DebridModels.swift +++ b/Ferrite/Models/DebridModels.swift @@ -7,6 +7,12 @@ import Foundation +public struct DebridInfo: Hashable, Sendable { + let name: String + let abbreviation: String + let website: String +} + public struct DebridIA: Hashable, Sendable { let magnet: Magnet let source: String diff --git a/Ferrite/Protocols/Debrid.swift b/Ferrite/Protocols/Debrid.swift index 8af5adf..3a842cb 100644 --- a/Ferrite/Protocols/Debrid.swift +++ b/Ferrite/Protocols/Debrid.swift @@ -7,11 +7,9 @@ import Foundation -public protocol DebridSource: ObservableObject { +public protocol DebridSource: AnyObservableObject { // ID of the service - var id: String { get } - var abbreviation: String { get } - var website: String { get } + var id: DebridInfo { get } // Auth variables var authProcessing: Bool { get set } diff --git a/Ferrite/Utils/Store.swift b/Ferrite/Utils/Store.swift new file mode 100644 index 0000000..a663fe6 --- /dev/null +++ b/Ferrite/Utils/Store.swift @@ -0,0 +1,148 @@ +// +// Store.swift +// Ferrite +// +// +// Originally created by William Baker on 09/06/2022. +// https://github.com/Tiny-Home-Consulting/Dependiject/blob/master/Dependiject/Store.swift +// Copyright (c) 2022 Tiny Home Consulting LLC. All rights reserved. +// +// Combined together by Brian Dashore +// +// TODO: Replace with Observable when minVersion >= iOS 17 +// + +import SwiftUI +import Combine + +class ErasedObservableObject: ObservableObject { + let objectWillChange: AnyPublisher + + init(objectWillChange: AnyPublisher) { + self.objectWillChange = objectWillChange + } + + static func empty() -> ErasedObservableObject { + .init(objectWillChange: Empty().eraseToAnyPublisher()) + } +} + +public protocol AnyObservableObject: AnyObject { + var objectWillChange: ObservableObjectPublisher { get } +} + +// The generic type names were chosen to match the SwiftUI equivalents: +// - ObjectType from StateObject and ObservedObject +// - Subject from ObservedObject.Wrapper.subscript(dynamicMember:) +// - S from Publisher.receive(on:options:) + +/// A property wrapper used to wrap injected observable objects. +/// +/// This is similar to SwiftUI's +/// [`StateObject`](https://developer.apple.com/documentation/swiftui/stateobject), but without +/// compile-time type restrictions. The lack of compile-time restrictions means that `ObjectType` +/// may be a protocol rather than a class. +/// +/// - Important: At runtime, the wrapped value must conform to ``AnyObservableObject``. +/// +/// To pass properties of the observable object down the view hierarchy as bindings, use the +/// projected value: +/// ```swift +/// struct ExampleView: View { +/// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self) +/// +/// var body: some View { +/// TextField("username", text: $viewModel.username) +/// } +/// } +/// ``` +/// Not all injected objects need this property wrapper. See the example projects for examples each +/// way. +@propertyWrapper +public struct Store { + /// The underlying object being stored. + public let wrappedValue: ObjectType + + // See https://github.com/Tiny-Home-Consulting/Dependiject/issues/38 + fileprivate var _observableObject: ObservedObject + + @MainActor internal var observableObject: ErasedObservableObject { + return _observableObject.wrappedValue + } + + /// A projected value which has the same properties as the wrapped value, but presented as + /// bindings. + /// + /// Use this to pass bindings down the view hierarchy: + /// ```swift + /// struct ExampleView: View { + /// @Store var viewModel = Factory.shared.resolve(ViewModelProtocol.self) + /// + /// var body: some View { + /// TextField("username", text: $viewModel.username) + /// } + /// } + /// ``` + public var projectedValue: Wrapper { + return Wrapper(self) + } + + /// Create a stored value on a custom scheduler. + /// + /// Use this init to schedule updates on a specific scheduler other than `DispatchQueue.main`. + public init( + wrappedValue: ObjectType, + on scheduler: S, + schedulerOptions: S.SchedulerOptions? = nil + ) { + self.wrappedValue = wrappedValue + + if let observable = wrappedValue as? AnyObservableObject { + let objectWillChange = observable.objectWillChange + .receive(on: scheduler, options: schedulerOptions) + .eraseToAnyPublisher() + self._observableObject = .init(initialValue: .init(objectWillChange: objectWillChange)) + } else { + assertionFailure( + "Only use the Store property wrapper with objects conforming to AnyObservableObject." + ) + self._observableObject = .init(initialValue: .empty()) + } + } + + /// Create a stored value which publishes on the main thread. + /// + /// To control when updates are published, see ``init(wrappedValue:on:schedulerOptions:)``. + public init(wrappedValue: ObjectType) { + self.init(wrappedValue: wrappedValue, on: DispatchQueue.main) + } + + /// An equivalent to SwiftUI's + /// [`ObservedObject.Wrapper`](https://developer.apple.com/documentation/swiftui/observedobject/wrapper) + /// type. + @dynamicMemberLookup + public struct Wrapper { + private var store: Store + + internal init(_ store: Store) { + self.store = store + } + + /// Returns a binding to the resulting value of a given key path. + public subscript( + dynamicMember keyPath: ReferenceWritableKeyPath + ) -> Binding { + return Binding { + self.store.wrappedValue[keyPath: keyPath] + } set: { + self.store.wrappedValue[keyPath: keyPath] = $0 + } + } + } +} + +extension Store: DynamicProperty { + public nonisolated mutating func update() { + _observableObject.update() + } +} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 6e30bcf..677125f 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -16,7 +16,7 @@ public class DebridManager: ObservableObject { @Published var allDebrid: AllDebrid = .init() @Published var premiumize: Premiumize = .init() - lazy var debridSources: [any DebridSource] = [realDebrid, allDebrid, premiumize] + lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize] // UI Variables @Published var showWebView: Bool = false @@ -26,6 +26,12 @@ public class DebridManager: ObservableObject { debridSources.contains { $0.isLoggedIn } } + @Published var selectedDebridId: DebridInfo? + + func debridSourceFromName(_ name: String? = nil) -> DebridSource? { + debridSources.first { $0.id.name == name ?? selectedDebridId?.name } + } + // Service agnostic variables @Published var enabledDebrids: Set = [] { didSet { @@ -106,12 +112,16 @@ public class DebridManager: ObservableObject { // If a UserDefaults integer isn't set, it's usually 0 let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService") - selectedDebridType = DebridType(rawValue: rawPreferredService) + let legacyPreferredService = DebridType(rawValue: rawPreferredService) + let preferredDebridSource = self.debridSourceFromName(legacyPreferredService?.toString()) + selectedDebridId = preferredDebridSource?.id // If a user has one logged in service, automatically set the preferred service to that one + /* if enabledDebrids.count == 1 { selectedDebridType = enabledDebrids.first } + */ } // TODO: Remove this after v0.6.0 @@ -255,38 +265,13 @@ public class DebridManager: ObservableObject { return .none } - switch selectedDebridType { - case .realDebrid: - guard let realDebridMatch = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { - return .none - } + let selectedSource = debridSourceFromName() - if realDebridMatch.files.count > 1 { - return .partial - } else { - return .full - } - case .allDebrid: - guard let allDebridMatch = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { - return .none - } - - if allDebridMatch.files.count > 1 { - return .partial - } else { - return .full - } - case .premiumize: - guard let premiumizeMatch = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) else { - return .none - } - - if premiumizeMatch.files.count > 1 { - return .partial - } else { - return .full - } - case .none: + if let selectedSource, + let match = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash }) + { + return match.files.count > 1 ? .partial : .full + } else { return .none } } @@ -297,8 +282,8 @@ public class DebridManager: ObservableObject { return false } - switch selectedDebridType { - case .realDebrid: + switch selectedDebridId?.name { + case .some("RealDebrid"): if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedRealDebridItem = realDebridItem return true @@ -306,7 +291,7 @@ public class DebridManager: ObservableObject { logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)") return false } - case .allDebrid: + case .some("AllDebrid"): if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedAllDebridItem = allDebridItem return true @@ -314,7 +299,7 @@ public class DebridManager: ObservableObject { logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)") return false } - case .premiumize: + case .some("Premiumize"): if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) { selectedPremiumizeItem = premiumizeItem return true @@ -322,7 +307,7 @@ public class DebridManager: ObservableObject { logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)") return false } - case .none: + default: return false } } diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 13c65a0..afdde3d 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -114,7 +114,7 @@ class ScrapingViewModel: ObservableObject { var failedSourceNames: [String] = [] for await (requestResult, sourceName) in group { if let requestResult { - if await !debridManager.hasEnabledDebrids { + if await debridManager.hasEnabledDebrids { await debridManager.populateDebridIA(requestResult.magnets) } diff --git a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift index e3fd2b0..ce47955 100644 --- a/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift +++ b/Ferrite/Views/ComponentViews/Debrid/DebridLabelView.swift @@ -8,38 +8,40 @@ import SwiftUI struct DebridLabelView: View { - @EnvironmentObject var debridManager: DebridManager + @Store var debridSource: DebridSource @State var cloudLinks: [String] = [] + @State var tagColor: Color = .red var magnet: Magnet? var body: some View { - if let selectedDebridType = debridManager.selectedDebridType { - Tag( - name: selectedDebridType.toString(abbreviated: true), - color: getTagColor(), - horizontalPadding: 5, - verticalPadding: 3 - ) + Tag( + name: debridSource.id.abbreviation, + color: tagColor, + horizontalPadding: 5, + verticalPadding: 3 + ) + .onAppear { + tagColor = getTagColor() + } + .onChange(of: debridSource.IAValues) { _ in + tagColor = getTagColor() } } func getTagColor() -> Color { if let magnet, cloudLinks.isEmpty { - switch debridManager.matchMagnetHash(magnet) { - case .full: - return Color.green - case .partial: - return Color.orange - case .none: - return Color.red + guard let match = debridSource.IAValues.first(where: { magnet.hash == $0.magnet.hash }) else { + return .red } + + return match.files.count > 1 ? .orange : .green } else if cloudLinks.count == 1 { - return Color.green + return .green } else if cloudLinks.count > 1 { - return Color.orange + return .orange } else { - return Color.red + return .red } } } diff --git a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift index 9a98c72..1ecb106 100644 --- a/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift +++ b/Ferrite/Views/ComponentViews/Filters/SelectedDebridFilterView.swift @@ -15,23 +15,23 @@ struct SelectedDebridFilterView: View { var body: some View { Menu { Button { - debridManager.selectedDebridType = nil + debridManager.selectedDebridId = nil } label: { Text("None") - if debridManager.selectedDebridType == nil { + if debridManager.selectedDebridId == nil { Image(systemName: "checkmark") } } - ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in - if debridManager.enabledDebrids.contains(debridType) { + ForEach(debridManager.debridSources, id: \.id) { debridSource in + if debridSource.isLoggedIn { Button { - debridManager.selectedDebridType = debridType + debridManager.selectedDebridId = debridSource.id } label: { - Text(debridType.toString()) + Text(debridSource.id.name) - if debridManager.selectedDebridType == debridType { + if debridManager.selectedDebridId == debridSource.id { Image(systemName: "checkmark") } } @@ -40,6 +40,6 @@ struct SelectedDebridFilterView: View { } label: { label } - .id(debridManager.selectedDebridType) + .id(debridManager.selectedDebridId) } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index 0f342b5..ee72b05 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -102,7 +102,7 @@ struct AllDebridCloudView: View { HStack { Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - DebridLabelView(cloudLinks: cloudTorrent.links) + //DebridLabelView(cloudLinks: cloudTorrent.links) } .font(.caption) } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index af74379..b05e764 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -103,7 +103,7 @@ struct RealDebridCloudView: View { HStack { Text(cloudTorrent.status.capitalizingFirstLetter()) Spacer() - DebridLabelView(cloudLinks: cloudTorrent.links) + //DebridLabelView(cloudLinks: cloudTorrent.links) } .font(.caption) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift index 8797a1a..b66fe14 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -53,7 +53,7 @@ struct SearchFilterHeaderView: View { SelectedDebridFilterView { FilterLabelView( - name: debridManager.selectedDebridType?.toString(), + name: debridManager.selectedDebridId?.name, fallbackName: "Debrid" ) } diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift index d42371d..547e8a3 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultInfoView.swift @@ -30,7 +30,9 @@ struct SearchResultInfoView: View { Text(size) } - DebridLabelView(magnet: result.magnet) + if let debridSource = debridManager.debridSourceFromName() { + DebridLabelView(debridSource: debridSource, magnet: result.magnet) + } } .font(.caption) } diff --git a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift index cebd792..8d13af5 100644 --- a/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift +++ b/Ferrite/Views/ComponentViews/Settings/SettingsDebridInfoView.swift @@ -10,7 +10,7 @@ import SwiftUI struct SettingsDebridInfoView: View { @EnvironmentObject var debridManager: DebridManager - let debridType: DebridType + @Store var debridSource: DebridSource @State private var apiKeyTempText: String = "" @@ -18,9 +18,9 @@ struct SettingsDebridInfoView: View { 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.") + Text("\(debridSource.id.name) 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")!) + Link("Website", destination: URL(string: debridSource.id.website) ?? URL(string: "https://kingbri.dev/ferrite")!) } } @@ -30,21 +30,21 @@ struct SettingsDebridInfoView: View { ) { Button { Task { - if debridManager.enabledDebrids.contains(debridType) { - await debridManager.logoutDebrid(debridType: debridType) - } else if !debridManager.authProcessing(debridType) { - await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) + if debridSource.isLoggedIn { + //await debridManager.logoutDebrid(debridType: debridType) + } else if !debridSource.authProcessing { + //await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) } - apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" } } label: { Text( - debridManager.enabledDebrids.contains(debridType) + debridSource.isLoggedIn ? "Logout" - : (debridManager.authProcessing(debridType) ? "Processing" : "Login") + : (debridSource.authProcessing ? "Processing" : "Login") ) - .foregroundColor(debridManager.enabledDebrids.contains(debridType) ? .red : .blue) + .foregroundColor(debridSource.isLoggedIn ? .red : .blue) } } @@ -57,22 +57,22 @@ struct SettingsDebridInfoView: View { onCommit: { Task { if !apiKeyTempText.isEmpty { - await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) - apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + //await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) + //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" } } } ) - .fieldDisabled(debridManager.enabledDebrids.contains(debridType)) + .fieldDisabled(debridSource.isLoggedIn) } .onAppear { Task { - apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" + //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" } } } .listStyle(.insetGrouped) - .navigationTitle(debridType.toString()) + .navigationTitle(debridSource.id.name) .navigationBarTitleDisplayMode(.inline) } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index fd5a87a..35d5f51 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -53,7 +53,7 @@ struct LibraryView: View { EmptyInstructionView(title: "No History", message: "Start watching to build history") } case .debridCloud: - if debridManager.selectedDebridType == nil { + if debridManager.selectedDebridId == nil { EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") } } @@ -69,7 +69,7 @@ struct LibraryView: View { switch navModel.libraryPickerSelection { case .bookmarks, .debridCloud: SelectedDebridFilterView { - Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid") + Text(debridManager.selectedDebridId?.abbreviation ?? "Debrid") } .transaction { $0.animation = .none diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 8914583..fc23cf4 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -46,14 +46,14 @@ struct SettingsView: View { NavView { Form { Section(header: InlineHeader("Debrid services")) { - ForEach(DebridType.allCases, id: \.self) { debridType in + ForEach(debridManager.debridSources, id: \.id) { (debridSource: DebridSource) in NavigationLink { - SettingsDebridInfoView(debridType: debridType) + SettingsDebridInfoView(debridSource: debridSource) } label: { HStack { - Text(debridType.toString()) + Text(debridSource.id.name) Spacer() - Text(debridManager.enabledDebrids.contains(debridType) ? "Enabled" : "Disabled") + Text(debridSource.isLoggedIn ? "Enabled" : "Disabled") .foregroundColor(.secondary) } } diff --git a/Ferrite/Views/SheetViews/BatchChoiceView.swift b/Ferrite/Views/SheetViews/BatchChoiceView.swift index 7d2d3f4..13c402b 100644 --- a/Ferrite/Views/SheetViews/BatchChoiceView.swift +++ b/Ferrite/Views/SheetViews/BatchChoiceView.swift @@ -23,8 +23,8 @@ struct BatchChoiceView: View { var body: some View { NavView { List { - switch debridManager.selectedDebridType { - case .realDebrid: + switch debridManager.selectedDebridId?.name { + case .some("RealDebrid"): ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { Button(file.name) { @@ -34,7 +34,7 @@ struct BatchChoiceView: View { } } } - case .allDebrid: + case .some("AllDebrid"): ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { Button(file.name) { @@ -44,7 +44,7 @@ struct BatchChoiceView: View { } } } - case .premiumize: + case .some("Premiumize"): ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { Button(file.name) { @@ -54,7 +54,7 @@ struct BatchChoiceView: View { } } } - case .none: + default: EmptyView() } }