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 <bdashore3@proton.me>
This commit is contained in:
kingbri 2024-06-05 12:33:11 -04:00
parent b80f8900b7
commit 07731e7b00
19 changed files with 272 additions and 122 deletions

View file

@ -96,6 +96,7 @@
0C84FCE729E4B61A00B0DFE4 /* FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */; }; 0C84FCE729E4B61A00B0DFE4 /* FilterModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */; };
0C84FCE929E5ADEF00B0DFE4 /* FilterAmountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */; }; 0C84FCE929E5ADEF00B0DFE4 /* FilterAmountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */; };
0C871BDF29994D9D005279AC /* FilterLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C871BDE29994D9D005279AC /* FilterLabelView.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 */; }; 0C8DC35229CE287E008A83AD /* PluginInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */; };
0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; }; 0C8DC35429CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */; };
0C8DC35629CE2ABF008A83AD /* SourceSettingsApiView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.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 = "<group>"; }; 0C84FCE629E4B61A00B0DFE4 /* FilterModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterModels.swift; sourceTree = "<group>"; };
0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAmountLabelView.swift; sourceTree = "<group>"; }; 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterAmountLabelView.swift; sourceTree = "<group>"; };
0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = "<group>"; }; 0C871BDE29994D9D005279AC /* FilterLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterLabelView.swift; sourceTree = "<group>"; };
0C8AE2472C0FFB6600701675 /* Store.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Store.swift; sourceTree = "<group>"; };
0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = "<group>"; }; 0C8DC35129CE287E008A83AD /* PluginInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PluginInfoView.swift; sourceTree = "<group>"; };
0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = "<group>"; }; 0C8DC35329CE2AB5008A83AD /* SourceSettingsBaseUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsBaseUrlView.swift; sourceTree = "<group>"; };
0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = "<group>"; }; 0C8DC35529CE2ABF008A83AD /* SourceSettingsApiView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsApiView.swift; sourceTree = "<group>"; };
@ -457,6 +459,7 @@
0C44E2A728D4DDDC007711AE /* Application.swift */, 0C44E2A728D4DDDC007711AE /* Application.swift */,
0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */, 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */,
0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */, 0CD0265629FEFBF900A83D25 /* FerriteKeychain.swift */,
0C8AE2472C0FFB6600701675 /* Store.swift */,
); );
path = Utils; path = Utils;
sourceTree = "<group>"; sourceTree = "<group>";
@ -858,6 +861,7 @@
0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* PluginCatalogButtonView.swift in Sources */,
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
0C8AE2482C0FFB6600701675 /* Store.swift in Sources */,
0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */, 0C3E00D2296F4FD200ECECB2 /* PluginsView.swift in Sources */,
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,

View file

@ -8,10 +8,10 @@
import Foundation import Foundation
// TODO: Fix errors // TODO: Fix errors
public class AllDebrid: PollingDebridSource { public class AllDebrid: PollingDebridSource, ObservableObject {
public let id = "AllDebrid" public let id = DebridInfo(
public let abbreviation = "AD" name: "AllDebrid", abbreviation: "AD", website: "https://alldebrid.com"
public let website = "https://alldebrid.com" )
public var authTask: Task<Void, Error>? public var authTask: Task<Void, Error>?
public var authProcessing: Bool = false public var authProcessing: Bool = false
@ -178,7 +178,7 @@ public class AllDebrid: PollingDebridSource {
return DebridIA( return DebridIA(
magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet), magnet: Magnet(hash: magnetResp.hash, link: magnetResp.magnet),
source: self.id, source: self.id.name,
expiryTimeStamp: Date().timeIntervalSince1970 + 300, expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files files: files
) )
@ -292,7 +292,7 @@ public class AllDebrid: PollingDebridSource {
cloudTorrents = rawResponse.magnets.map { magnetResponse in cloudTorrents = rawResponse.magnets.map { magnetResponse in
DebridCloudTorrent( DebridCloudTorrent(
torrentId: String(magnetResponse.id), torrentId: String(magnetResponse.id),
source: self.id, source: self.id.name,
fileName: magnetResponse.filename, fileName: magnetResponse.filename,
status: magnetResponse.status, status: magnetResponse.status,
hash: magnetResponse.hash, hash: magnetResponse.hash,
@ -325,7 +325,7 @@ public class AllDebrid: PollingDebridSource {
// The link is also the ID // The link is also the ID
cloudDownloads = rawResponse.links.map { link in cloudDownloads = rawResponse.links.map { link in
DebridCloudDownload( 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
) )
} }

View file

@ -7,10 +7,10 @@
import Foundation import Foundation
public class Premiumize: OAuthDebridSource { public class Premiumize: OAuthDebridSource, ObservableObject {
public let id = "Premiumize" public let id = DebridInfo(
public let abbreviation = "PM" name: "Premiumize", abbreviation: "PM", website: "https://premiumize.me"
public let website = "https://premiumize.me" )
@Published public var authProcessing: Bool = false @Published public var authProcessing: Bool = false
public var isLoggedIn: Bool { public var isLoggedIn: Bool {
@ -195,7 +195,7 @@ public class Premiumize: OAuthDebridSource {
return DebridIA( return DebridIA(
magnet: magnet, magnet: magnet,
source: id, source: id.name,
expiryTimeStamp: Date().timeIntervalSince1970 + 300, expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files files: files
) )
@ -300,7 +300,7 @@ public class Premiumize: OAuthDebridSource {
// The "link" is the ID for Premiumize // The "link" is the ID for Premiumize
cloudDownloads = rawResponse.files.map { file in 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 return cloudDownloads

View file

@ -7,10 +7,10 @@
import Foundation import Foundation
public class RealDebrid: PollingDebridSource { public class RealDebrid: PollingDebridSource, ObservableObject {
public let id = "RealDebrid" public let id = DebridInfo(
public let abbreviation = "RD" name: "RealDebrid", abbreviation: "RD", website: "https://real-debrid.com"
public let website = "https://real-debrid.com" )
public var authTask: Task<Void, Error>? public var authTask: Task<Void, Error>?
@Published public var authProcessing: Bool = false @Published public var authProcessing: Bool = false
@ -20,9 +20,14 @@ public class RealDebrid: PollingDebridSource {
FerriteKeychain.shared.get("RealDebrid.AccessToken") != nil 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 cloudDownloads: [DebridCloudDownload] = []
@Published public var cloudTorrents: [DebridCloudTorrent] = [] @Published public var cloudTorrents: [DebridCloudTorrent] = []
var cloudTTL: Double = 0.0
let baseAuthUrl = "https://api.real-debrid.com/oauth/v2" let baseAuthUrl = "https://api.real-debrid.com/oauth/v2"
let baseApiUrl = "https://api.real-debrid.com/rest/1.0" let baseApiUrl = "https://api.real-debrid.com/rest/1.0"
@ -282,7 +287,7 @@ public class RealDebrid: PollingDebridSource {
IAValues.append( IAValues.append(
DebridIA( DebridIA(
magnet: Magnet(hash: hash, link: nil), magnet: Magnet(hash: hash, link: nil),
source: id, source: id.name,
expiryTimeStamp: Date().timeIntervalSince1970 + 300, expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: files files: files
) )
@ -291,7 +296,7 @@ public class RealDebrid: PollingDebridSource {
IAValues.append( IAValues.append(
DebridIA( DebridIA(
magnet: Magnet(hash: hash, link: nil), magnet: Magnet(hash: hash, link: nil),
source: id, source: id.name,
expiryTimeStamp: Date().timeIntervalSince1970 + 300, expiryTimeStamp: Date().timeIntervalSince1970 + 300,
files: [] files: []
) )
@ -422,7 +427,7 @@ public class RealDebrid: PollingDebridSource {
cloudTorrents = rawResponse.map { response in cloudTorrents = rawResponse.map { response in
DebridCloudTorrent( DebridCloudTorrent(
torrentId: response.id, torrentId: response.id,
source: self.id, source: self.id.name,
fileName: response.filename, fileName: response.filename,
status: response.status, status: response.status,
hash: response.hash, hash: response.hash,
@ -448,7 +453,7 @@ public class RealDebrid: PollingDebridSource {
let data = try await performRequest(request: &request, requestName: #function) let data = try await performRequest(request: &request, requestName: #function)
let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data)
cloudDownloads = rawResponse.map { response in 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 return cloudDownloads

View file

@ -7,6 +7,12 @@
import Foundation import Foundation
public struct DebridInfo: Hashable, Sendable {
let name: String
let abbreviation: String
let website: String
}
public struct DebridIA: Hashable, Sendable { public struct DebridIA: Hashable, Sendable {
let magnet: Magnet let magnet: Magnet
let source: String let source: String

View file

@ -7,11 +7,9 @@
import Foundation import Foundation
public protocol DebridSource: ObservableObject { public protocol DebridSource: AnyObservableObject {
// ID of the service // ID of the service
var id: String { get } var id: DebridInfo { get }
var abbreviation: String { get }
var website: String { get }
// Auth variables // Auth variables
var authProcessing: Bool { get set } var authProcessing: Bool { get set }

148
Ferrite/Utils/Store.swift Normal file
View file

@ -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<Void, Never>
init(objectWillChange: AnyPublisher<Void, Never>) {
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<ObjectType> and ObservedObject<ObjectType>
// - Subject from ObservedObject.Wrapper.subscript<Subject>(dynamicMember:)
// - S from Publisher.receive<S>(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<ObjectType> {
/// The underlying object being stored.
public let wrappedValue: ObjectType
// See https://github.com/Tiny-Home-Consulting/Dependiject/issues/38
fileprivate var _observableObject: ObservedObject<ErasedObservableObject>
@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<S: Scheduler>(
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<ObjectType>) {
self.store = store
}
/// Returns a binding to the resulting value of a given key path.
public subscript<Subject>(
dynamicMember keyPath: ReferenceWritableKeyPath<ObjectType, Subject>
) -> Binding<Subject> {
return Binding {
self.store.wrappedValue[keyPath: keyPath]
} set: {
self.store.wrappedValue[keyPath: keyPath] = $0
}
}
}
}
extension Store: DynamicProperty {
public nonisolated mutating func update() {
_observableObject.update()
}
}

View file

@ -16,7 +16,7 @@ public class DebridManager: ObservableObject {
@Published var allDebrid: AllDebrid = .init() @Published var allDebrid: AllDebrid = .init()
@Published var premiumize: Premiumize = .init() @Published var premiumize: Premiumize = .init()
lazy var debridSources: [any DebridSource] = [realDebrid, allDebrid, premiumize] lazy var debridSources: [DebridSource] = [realDebrid, allDebrid, premiumize]
// UI Variables // UI Variables
@Published var showWebView: Bool = false @Published var showWebView: Bool = false
@ -26,6 +26,12 @@ public class DebridManager: ObservableObject {
debridSources.contains { $0.isLoggedIn } 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 // Service agnostic variables
@Published var enabledDebrids: Set<DebridType> = [] { @Published var enabledDebrids: Set<DebridType> = [] {
didSet { didSet {
@ -106,12 +112,16 @@ public class DebridManager: ObservableObject {
// If a UserDefaults integer isn't set, it's usually 0 // If a UserDefaults integer isn't set, it's usually 0
let rawPreferredService = UserDefaults.standard.integer(forKey: "Debrid.PreferredService") 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 a user has one logged in service, automatically set the preferred service to that one
/*
if enabledDebrids.count == 1 { if enabledDebrids.count == 1 {
selectedDebridType = enabledDebrids.first selectedDebridType = enabledDebrids.first
} }
*/
} }
// TODO: Remove this after v0.6.0 // TODO: Remove this after v0.6.0
@ -255,38 +265,13 @@ public class DebridManager: ObservableObject {
return .none return .none
} }
switch selectedDebridType { let selectedSource = debridSourceFromName()
case .realDebrid:
guard let realDebridMatch = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) else {
return .none
}
if realDebridMatch.files.count > 1 { if let selectedSource,
return .partial let match = selectedSource.IAValues.first(where: { magnetHash == $0.magnet.hash })
} else { {
return .full return match.files.count > 1 ? .partial : .full
} } else {
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:
return .none return .none
} }
} }
@ -297,8 +282,8 @@ public class DebridManager: ObservableObject {
return false return false
} }
switch selectedDebridType { switch selectedDebridId?.name {
case .realDebrid: case .some("RealDebrid"):
if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { if let realDebridItem = realDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedRealDebridItem = realDebridItem selectedRealDebridItem = realDebridItem
return true return true
@ -306,7 +291,7 @@ public class DebridManager: ObservableObject {
logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)") logManager?.error("DebridManager: Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
return false return false
} }
case .allDebrid: case .some("AllDebrid"):
if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) { if let allDebridItem = allDebrid.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedAllDebridItem = allDebridItem selectedAllDebridItem = allDebridItem
return true return true
@ -314,7 +299,7 @@ public class DebridManager: ObservableObject {
logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)") logManager?.error("DebridManager: Could not find the associated AllDebrid entry for magnet hash \(magnetHash)")
return false return false
} }
case .premiumize: case .some("Premiumize"):
if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) { if let premiumizeItem = premiumize.IAValues.first(where: { magnetHash == $0.magnet.hash }) {
selectedPremiumizeItem = premiumizeItem selectedPremiumizeItem = premiumizeItem
return true return true
@ -322,7 +307,7 @@ public class DebridManager: ObservableObject {
logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)") logManager?.error("DebridManager: Could not find the associated Premiumize entry for magnet hash \(magnetHash)")
return false return false
} }
case .none: default:
return false return false
} }
} }

View file

@ -114,7 +114,7 @@ class ScrapingViewModel: ObservableObject {
var failedSourceNames: [String] = [] var failedSourceNames: [String] = []
for await (requestResult, sourceName) in group { for await (requestResult, sourceName) in group {
if let requestResult { if let requestResult {
if await !debridManager.hasEnabledDebrids { if await debridManager.hasEnabledDebrids {
await debridManager.populateDebridIA(requestResult.magnets) await debridManager.populateDebridIA(requestResult.magnets)
} }

View file

@ -8,38 +8,40 @@
import SwiftUI import SwiftUI
struct DebridLabelView: View { struct DebridLabelView: View {
@EnvironmentObject var debridManager: DebridManager @Store var debridSource: DebridSource
@State var cloudLinks: [String] = [] @State var cloudLinks: [String] = []
@State var tagColor: Color = .red
var magnet: Magnet? var magnet: Magnet?
var body: some View { var body: some View {
if let selectedDebridType = debridManager.selectedDebridType { Tag(
Tag( name: debridSource.id.abbreviation,
name: selectedDebridType.toString(abbreviated: true), color: tagColor,
color: getTagColor(), horizontalPadding: 5,
horizontalPadding: 5, verticalPadding: 3
verticalPadding: 3 )
) .onAppear {
tagColor = getTagColor()
}
.onChange(of: debridSource.IAValues) { _ in
tagColor = getTagColor()
} }
} }
func getTagColor() -> Color { func getTagColor() -> Color {
if let magnet, cloudLinks.isEmpty { if let magnet, cloudLinks.isEmpty {
switch debridManager.matchMagnetHash(magnet) { guard let match = debridSource.IAValues.first(where: { magnet.hash == $0.magnet.hash }) else {
case .full: return .red
return Color.green
case .partial:
return Color.orange
case .none:
return Color.red
} }
return match.files.count > 1 ? .orange : .green
} else if cloudLinks.count == 1 { } else if cloudLinks.count == 1 {
return Color.green return .green
} else if cloudLinks.count > 1 { } else if cloudLinks.count > 1 {
return Color.orange return .orange
} else { } else {
return Color.red return .red
} }
} }
} }

View file

@ -15,23 +15,23 @@ struct SelectedDebridFilterView<Content: View>: View {
var body: some View { var body: some View {
Menu { Menu {
Button { Button {
debridManager.selectedDebridType = nil debridManager.selectedDebridId = nil
} label: { } label: {
Text("None") Text("None")
if debridManager.selectedDebridType == nil { if debridManager.selectedDebridId == nil {
Image(systemName: "checkmark") Image(systemName: "checkmark")
} }
} }
ForEach(DebridType.allCases, id: \.self) { (debridType: DebridType) in ForEach(debridManager.debridSources, id: \.id) { debridSource in
if debridManager.enabledDebrids.contains(debridType) { if debridSource.isLoggedIn {
Button { Button {
debridManager.selectedDebridType = debridType debridManager.selectedDebridId = debridSource.id
} label: { } label: {
Text(debridType.toString()) Text(debridSource.id.name)
if debridManager.selectedDebridType == debridType { if debridManager.selectedDebridId == debridSource.id {
Image(systemName: "checkmark") Image(systemName: "checkmark")
} }
} }
@ -40,6 +40,6 @@ struct SelectedDebridFilterView<Content: View>: View {
} label: { } label: {
label label
} }
.id(debridManager.selectedDebridType) .id(debridManager.selectedDebridId)
} }
} }

View file

@ -102,7 +102,7 @@ struct AllDebridCloudView: View {
HStack { HStack {
Text(cloudTorrent.status.capitalizingFirstLetter()) Text(cloudTorrent.status.capitalizingFirstLetter())
Spacer() Spacer()
DebridLabelView(cloudLinks: cloudTorrent.links) //DebridLabelView(cloudLinks: cloudTorrent.links)
} }
.font(.caption) .font(.caption)
} }

View file

@ -103,7 +103,7 @@ struct RealDebridCloudView: View {
HStack { HStack {
Text(cloudTorrent.status.capitalizingFirstLetter()) Text(cloudTorrent.status.capitalizingFirstLetter())
Spacer() Spacer()
DebridLabelView(cloudLinks: cloudTorrent.links) //DebridLabelView(cloudLinks: cloudTorrent.links)
} }
.font(.caption) .font(.caption)
} }

View file

@ -53,7 +53,7 @@ struct SearchFilterHeaderView: View {
SelectedDebridFilterView { SelectedDebridFilterView {
FilterLabelView( FilterLabelView(
name: debridManager.selectedDebridType?.toString(), name: debridManager.selectedDebridId?.name,
fallbackName: "Debrid" fallbackName: "Debrid"
) )
} }

View file

@ -30,7 +30,9 @@ struct SearchResultInfoView: View {
Text(size) Text(size)
} }
DebridLabelView(magnet: result.magnet) if let debridSource = debridManager.debridSourceFromName() {
DebridLabelView(debridSource: debridSource, magnet: result.magnet)
}
} }
.font(.caption) .font(.caption)
} }

View file

@ -10,7 +10,7 @@ import SwiftUI
struct SettingsDebridInfoView: View { struct SettingsDebridInfoView: View {
@EnvironmentObject var debridManager: DebridManager @EnvironmentObject var debridManager: DebridManager
let debridType: DebridType @Store var debridSource: DebridSource
@State private var apiKeyTempText: String = "" @State private var apiKeyTempText: String = ""
@ -18,9 +18,9 @@ struct SettingsDebridInfoView: View {
List { List {
Section(header: InlineHeader("Description")) { Section(header: InlineHeader("Description")) {
VStack(alignment: .leading, spacing: 10) { 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 { Button {
Task { Task {
if debridManager.enabledDebrids.contains(debridType) { if debridSource.isLoggedIn {
await debridManager.logoutDebrid(debridType: debridType) //await debridManager.logoutDebrid(debridType: debridType)
} else if !debridManager.authProcessing(debridType) { } else if !debridSource.authProcessing {
await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil) //await debridManager.authenticateDebrid(debridType: debridType, apiKey: nil)
} }
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
} }
} label: { } label: {
Text( Text(
debridManager.enabledDebrids.contains(debridType) debridSource.isLoggedIn
? "Logout" ? "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: { onCommit: {
Task { Task {
if !apiKeyTempText.isEmpty { if !apiKeyTempText.isEmpty {
await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText) //await debridManager.authenticateDebrid(debridType: debridType, apiKey: apiKeyTempText)
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
} }
} }
} }
) )
.fieldDisabled(debridManager.enabledDebrids.contains(debridType)) .fieldDisabled(debridSource.isLoggedIn)
} }
.onAppear { .onAppear {
Task { Task {
apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? "" //apiKeyTempText = await debridManager.getManualAuthKey(debridType) ?? ""
} }
} }
} }
.listStyle(.insetGrouped) .listStyle(.insetGrouped)
.navigationTitle(debridType.toString()) .navigationTitle(debridSource.id.name)
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
} }
} }

View file

@ -53,7 +53,7 @@ struct LibraryView: View {
EmptyInstructionView(title: "No History", message: "Start watching to build history") EmptyInstructionView(title: "No History", message: "Start watching to build history")
} }
case .debridCloud: case .debridCloud:
if debridManager.selectedDebridType == nil { if debridManager.selectedDebridId == nil {
EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service") EmptyInstructionView(title: "Cloud Unavailable", message: "Listing is not available for this service")
} }
} }
@ -69,7 +69,7 @@ struct LibraryView: View {
switch navModel.libraryPickerSelection { switch navModel.libraryPickerSelection {
case .bookmarks, .debridCloud: case .bookmarks, .debridCloud:
SelectedDebridFilterView { SelectedDebridFilterView {
Text(debridManager.selectedDebridType?.toString(abbreviated: true) ?? "Debrid") Text(debridManager.selectedDebridId?.abbreviation ?? "Debrid")
} }
.transaction { .transaction {
$0.animation = .none $0.animation = .none

View file

@ -46,14 +46,14 @@ struct SettingsView: View {
NavView { NavView {
Form { Form {
Section(header: InlineHeader("Debrid services")) { Section(header: InlineHeader("Debrid services")) {
ForEach(DebridType.allCases, id: \.self) { debridType in ForEach(debridManager.debridSources, id: \.id) { (debridSource: DebridSource) in
NavigationLink { NavigationLink {
SettingsDebridInfoView(debridType: debridType) SettingsDebridInfoView(debridSource: debridSource)
} label: { } label: {
HStack { HStack {
Text(debridType.toString()) Text(debridSource.id.name)
Spacer() Spacer()
Text(debridManager.enabledDebrids.contains(debridType) ? "Enabled" : "Disabled") Text(debridSource.isLoggedIn ? "Enabled" : "Disabled")
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }

View file

@ -23,8 +23,8 @@ struct BatchChoiceView: View {
var body: some View { var body: some View {
NavView { NavView {
List { List {
switch debridManager.selectedDebridType { switch debridManager.selectedDebridId?.name {
case .realDebrid: case .some("RealDebrid"):
ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in ForEach(debridManager.selectedRealDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) { Button(file.name) {
@ -34,7 +34,7 @@ struct BatchChoiceView: View {
} }
} }
} }
case .allDebrid: case .some("AllDebrid"):
ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in ForEach(debridManager.selectedAllDebridItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) { Button(file.name) {
@ -44,7 +44,7 @@ struct BatchChoiceView: View {
} }
} }
} }
case .premiumize: case .some("Premiumize"):
ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in ForEach(debridManager.selectedPremiumizeItem?.files ?? [], id: \.self) { file in
if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty { if file.name.lowercased().contains(searchText.lowercased()) || searchText.isEmpty {
Button(file.name) { Button(file.name) {
@ -54,7 +54,7 @@ struct BatchChoiceView: View {
} }
} }
} }
case .none: default:
EmptyView() EmptyView()
} }
} }