mirror of
https://github.com/Ferrite-iOS/Ferrite.git
synced 2026-04-20 16:32:09 +00:00
Debrid: Add loading indicator and fix iOS <14.5 issues
When a search result is selected, there is usually a delay due to the debrid dance of API routes for grabbing a download link to stream. Add a loading indicator and prevent any other tasks from loading unless the user cancels it. iOS 14.5 was a huge update which added many QoL SwiftUI changes that are consistent to modern iOS versions. However, Ferrite supports iOS versions less than 14.5, mainly 14.3. More fixes had to be added to make sure UI is consistent across all OS versions. Signed-off-by: kingbri <bdashore3@gmail.com>
This commit is contained in:
parent
49010a270e
commit
1761f8dfb4
21 changed files with 376 additions and 233 deletions
|
|
@ -65,6 +65,7 @@
|
|||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D1288903F000DE2211 /* MainView.swift */; };
|
||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; };
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; };
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; };
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||
|
|
@ -126,6 +127,7 @@
|
|||
0CA148D1288903F000DE2211 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = "<group>"; };
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = "<group>"; };
|
||||
0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -228,6 +230,7 @@
|
|||
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */,
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */,
|
||||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */,
|
||||
);
|
||||
path = CommonViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -421,6 +424,7 @@
|
|||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -18,9 +18,7 @@ public enum RealDebridError: Error {
|
|||
case AuthQuery(description: String)
|
||||
}
|
||||
|
||||
public class RealDebrid: ObservableObject {
|
||||
var parentManager: DebridManager?
|
||||
|
||||
public class RealDebrid {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let keychain = KeychainSwift()
|
||||
|
||||
|
|
@ -31,7 +29,7 @@ public class RealDebrid: ObservableObject {
|
|||
var authTask: Task<Void, Error>?
|
||||
|
||||
// Fetches the device code from RD
|
||||
public func getVerificationInfo() async throws -> String {
|
||||
public func getVerificationInfo() async throws -> DeviceCodeResponse {
|
||||
var urlComponents = URLComponents(string: "\(baseAuthUrl)/device/code")!
|
||||
urlComponents.queryItems = [
|
||||
URLQueryItem(name: "client_id", value: openSourceClientId),
|
||||
|
|
@ -47,22 +45,7 @@ public class RealDebrid: ObservableObject {
|
|||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
let rawResponse = try jsonDecoder.decode(DeviceCodeResponse.self, from: data)
|
||||
|
||||
// Spawn a separate process to get the device code
|
||||
Task {
|
||||
do {
|
||||
try await getDeviceCredentials(deviceCode: rawResponse.deviceCode)
|
||||
} catch {
|
||||
print("Authentication error in \(#function): \(error)")
|
||||
authTask?.cancel()
|
||||
|
||||
Task { @MainActor in
|
||||
parentManager?.toastModel?.toastDescription = "Authentication error in \(#function): \(error)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rawResponse.directVerificationURL
|
||||
return rawResponse
|
||||
} catch {
|
||||
print("Couldn't get the new client creds!")
|
||||
throw RealDebridError.AuthQuery(description: error.localizedDescription)
|
||||
|
|
@ -82,32 +65,36 @@ public class RealDebrid: ObservableObject {
|
|||
}
|
||||
|
||||
let request = URLRequest(url: url)
|
||||
try await getDeviceCredentialsInternal(urlRequest: request, deviceCode: deviceCode)
|
||||
}
|
||||
|
||||
// Timer to poll RD api for credentials
|
||||
func getDeviceCredentialsInternal(urlRequest: URLRequest, deviceCode: String) async throws {
|
||||
// Timer to poll RD API for credentials
|
||||
authTask = Task {
|
||||
var count = 0
|
||||
|
||||
while count < 20 {
|
||||
let (data, _) = try await URLSession.shared.data(for: urlRequest)
|
||||
if Task.isCancelled {
|
||||
throw RealDebridError.AuthQuery(description: "Token request cancelled.")
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: request)
|
||||
|
||||
// We don't care if this fails
|
||||
let rawResponse = try? self.jsonDecoder.decode(DeviceCredentialsResponse.self, from: data)
|
||||
|
||||
// If there's a client ID from the response, end the task successfully
|
||||
if let clientId = rawResponse?.clientID, let clientSecret = rawResponse?.clientSecret {
|
||||
UserDefaults.standard.set(clientId, forKey: "RealDebrid.ClientId")
|
||||
keychain.set(clientSecret, forKey: "RealDebrid.ClientSecret")
|
||||
|
||||
try await getTokens(deviceCode: deviceCode)
|
||||
|
||||
break
|
||||
return
|
||||
} else {
|
||||
try await Task.sleep(seconds: 5)
|
||||
count += 1
|
||||
}
|
||||
}
|
||||
|
||||
throw RealDebridError.AuthQuery(description: "Could not fetch the client ID and secret in time. Try logging in again.")
|
||||
}
|
||||
|
||||
if case let .failure(error) = await authTask?.result {
|
||||
|
|
@ -148,11 +135,6 @@ public class RealDebrid: ObservableObject {
|
|||
|
||||
let accessTimestamp = Date().timeIntervalSince1970 + Double(rawResponse.expiresIn)
|
||||
UserDefaults.standard.set(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
|
||||
|
||||
// Set AppStorage variable
|
||||
Task { @MainActor in
|
||||
parentManager?.realDebridEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchToken() async -> String? {
|
||||
|
|
@ -186,10 +168,6 @@ public class RealDebrid: ObservableObject {
|
|||
|
||||
keychain.delete("RealDebrid.AccessToken")
|
||||
}
|
||||
|
||||
Task { @MainActor in
|
||||
parentManager?.realDebridEnabled = false
|
||||
}
|
||||
}
|
||||
|
||||
// Wrapper request function which matches the responses and returns data
|
||||
|
|
@ -331,6 +309,14 @@ public class RealDebrid: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
// Deletes a torrent download from RD
|
||||
public func deleteTorrent(debridID: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!)
|
||||
request.httpMethod = "DELETE"
|
||||
|
||||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
// Downloads link from selectFiles for playback
|
||||
public func unrestrictLink(debridDownloadLink: String) async throws -> String {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/unrestrict/link")!)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum DefaultMagnetActionType: Int {
|
||||
public enum DefaultMagnetActionType: Int, CaseIterable {
|
||||
// Let the user choose
|
||||
case none = 0
|
||||
|
||||
|
|
@ -18,7 +18,7 @@ public enum DefaultMagnetActionType: Int {
|
|||
case shareMagnet = 2
|
||||
}
|
||||
|
||||
public enum DefaultDebridActionType: Int {
|
||||
public enum DefaultDebridActionType: Int, CaseIterable {
|
||||
// Let the user choose
|
||||
case none = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ public struct SourceJson: Codable, Hashable {
|
|||
}
|
||||
|
||||
public enum SourcePreferredParser: Int16, CaseIterable {
|
||||
case none = 0
|
||||
// case none = 0
|
||||
case scraping = 1
|
||||
case rss = 2
|
||||
case siteApi = 3
|
||||
|
|
|
|||
|
|
@ -8,24 +8,35 @@
|
|||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
public class DebridManager: ObservableObject {
|
||||
// UI Variables
|
||||
var toastModel: ToastViewModel?
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showLoadingProgress: Bool = false
|
||||
|
||||
// RealDebrid variables
|
||||
// Service agnostic variables
|
||||
@Published var currentDebridTask: Task<Void, Never>?
|
||||
|
||||
// RealDebrid auth variables
|
||||
let realDebrid: RealDebrid = .init()
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
@Published var realDebridHashes: [RealDebridIA] = []
|
||||
@Published var realDebridEnabled: Bool = false {
|
||||
didSet {
|
||||
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
|
||||
}
|
||||
}
|
||||
@Published var realDebridAuthProcessing: Bool = false
|
||||
@Published var realDebridAuthUrl: String = ""
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridHashes: [RealDebridIA] = []
|
||||
@Published var realDebridDownloadUrl: String = ""
|
||||
@Published var selectedRealDebridItem: RealDebridIA?
|
||||
@Published var selectedRealDebridFile: RealDebridIAFile?
|
||||
|
||||
init() {
|
||||
realDebrid.parentManager = self
|
||||
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||
}
|
||||
|
||||
public func populateDebridHashes(_ searchResults: [SearchResult]) async {
|
||||
|
|
@ -40,16 +51,12 @@ public class DebridManager: ObservableObject {
|
|||
do {
|
||||
let debridHashes = try await realDebrid.instantAvailability(magnetHashes: hashes)
|
||||
|
||||
Task { @MainActor in
|
||||
realDebridHashes = debridHashes
|
||||
}
|
||||
realDebridHashes = debridHashes
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
let error = error as NSError
|
||||
let error = error as NSError
|
||||
|
||||
if error.code != -999 {
|
||||
toastModel?.toastDescription = "RealDebrid hash error: \(error)"
|
||||
}
|
||||
if error.code != -999 {
|
||||
toastModel?.updateToastDescription("RealDebrid hash error: \(error)")
|
||||
}
|
||||
|
||||
print("RealDebrid hash error: \(error)")
|
||||
|
|
@ -72,10 +79,9 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
@MainActor
|
||||
public func setSelectedRdResult(result: SearchResult) -> Bool {
|
||||
guard let magnetHash = result.magnetHash else {
|
||||
toastModel?.toastDescription = "Could not find the torrent magnet hash"
|
||||
toastModel?.updateToastDescription("Could not find the torrent magnet hash")
|
||||
return false
|
||||
}
|
||||
|
||||
|
|
@ -83,40 +89,61 @@ public class DebridManager: ObservableObject {
|
|||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not find the associated RealDebrid entry for magnet hash \(magnetHash)"
|
||||
toastModel?.updateToastDescription("Could not find the associated RealDebrid entry for magnet hash \(magnetHash)")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public func authenticateRd() async {
|
||||
do {
|
||||
let url = try await realDebrid.getVerificationInfo()
|
||||
realDebridAuthProcessing = true
|
||||
let verificationResponse = try await realDebrid.getVerificationInfo()
|
||||
|
||||
Task { @MainActor in
|
||||
realDebridAuthUrl = url
|
||||
showWebView.toggle()
|
||||
}
|
||||
realDebridAuthUrl = verificationResponse.directVerificationURL
|
||||
showWebView.toggle()
|
||||
|
||||
try await realDebrid.getDeviceCredentials(deviceCode: verificationResponse.deviceCode)
|
||||
|
||||
realDebridEnabled = true
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "RealDebrid Authentication error: \(error)"
|
||||
}
|
||||
toastModel?.updateToastDescription("RealDebrid authentication error: \(error)")
|
||||
realDebrid.authTask?.cancel()
|
||||
|
||||
print(error)
|
||||
print("RealDebrid authentication error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func logoutRd() async {
|
||||
do {
|
||||
try await realDebrid.deleteTokens()
|
||||
realDebridEnabled = false
|
||||
realDebridAuthProcessing = false
|
||||
} catch {
|
||||
toastModel?.updateToastDescription("RealDebrid logout error: \(error)")
|
||||
|
||||
print("RealDebrid logout error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func fetchRdDownload(searchResult: SearchResult, iaFile: RealDebridIAFile? = nil) async {
|
||||
defer {
|
||||
currentDebridTask = nil
|
||||
showLoadingProgress = false
|
||||
}
|
||||
|
||||
showLoadingProgress = true
|
||||
|
||||
guard let magnetLink = searchResult.magnetLink else {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Could not run your action because the magnet link is invalid."
|
||||
}
|
||||
print("RD error: Invalid magnet link")
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("RealDebrid error: Invalid magnet link")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var realDebridId: String?
|
||||
|
||||
do {
|
||||
let realDebridId = try await realDebrid.addMagnet(magnetLink: magnetLink)
|
||||
realDebridId = try await realDebrid.addMagnet(magnetLink: magnetLink)
|
||||
|
||||
var fileIds: [Int] = []
|
||||
|
||||
|
|
@ -128,23 +155,34 @@ public class DebridManager: ObservableObject {
|
|||
fileIds = iaBatchFromFile.files.map(\.id)
|
||||
}
|
||||
|
||||
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
|
||||
if let realDebridId = realDebridId {
|
||||
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
|
||||
|
||||
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile == nil ? 0 : iaFile?.batchFileIndex)
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile?.batchFileIndex ?? 0)
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
let downloadUrlTask = Task { @MainActor in
|
||||
realDebridDownloadUrl = downloadLink
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
|
||||
}
|
||||
|
||||
// Prevent a race condition when setting the published variable
|
||||
await downloadUrlTask.value
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "RealDebrid download error: \(error)"
|
||||
let error = error as NSError
|
||||
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
|
||||
default:
|
||||
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
|
||||
}
|
||||
|
||||
print(error)
|
||||
// Delete the torrent download if it exists
|
||||
if let realDebridId = realDebridId {
|
||||
try? await realDebrid.deleteTorrent(debridID: realDebridId)
|
||||
}
|
||||
|
||||
showLoadingProgress = false
|
||||
|
||||
print("RealDebrid download error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ class NavigationViewModel: ObservableObject {
|
|||
|
||||
case magnet
|
||||
case batch
|
||||
case activity
|
||||
}
|
||||
|
||||
@Published var isEditingSearch: Bool = false
|
||||
|
|
@ -34,7 +35,9 @@ class NavigationViewModel: ObservableObject {
|
|||
|
||||
@Published var currentChoiceSheet: ChoiceSheetType?
|
||||
@Published var activityItems: [Any] = []
|
||||
@Published var showActivityView: Bool = false
|
||||
|
||||
// Used to show the activity sheet in the share menu
|
||||
@Published var showLocalActivitySheet = false
|
||||
|
||||
@Published var selectedTab: ViewTab = .search
|
||||
@Published var showSearchProgress: Bool = false
|
||||
|
|
@ -59,26 +62,26 @@ class NavigationViewModel: ObservableObject {
|
|||
if let downloadUrl = URL(string: "outplayer://\(urlString)") {
|
||||
UIApplication.shared.open(downloadUrl)
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not create an Outplayer URL"
|
||||
toastModel?.updateToastDescription("Could not create an Outplayer URL")
|
||||
}
|
||||
case .vlc:
|
||||
if let downloadUrl = URL(string: "vlc://\(urlString)") {
|
||||
UIApplication.shared.open(downloadUrl)
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not create a VLC URL"
|
||||
toastModel?.updateToastDescription("Could not create a VLC URL")
|
||||
}
|
||||
case .infuse:
|
||||
if let downloadUrl = URL(string: "infuse://x-callback-url/play?url=\(urlString)") {
|
||||
UIApplication.shared.open(downloadUrl)
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not create a Infuse URL"
|
||||
toastModel?.updateToastDescription("Could not create a Infuse URL")
|
||||
}
|
||||
case .shareDownload:
|
||||
if let downloadUrl = URL(string: urlString), currentChoiceSheet == nil {
|
||||
activityItems = [downloadUrl]
|
||||
showActivityView.toggle()
|
||||
currentChoiceSheet = .activity
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not create object for sharing"
|
||||
toastModel?.updateToastDescription("Could not create object for sharing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -100,16 +103,16 @@ class NavigationViewModel: ObservableObject {
|
|||
if let url = URL(string: "https://webtor.io/#/show?magnet=\(magnetLink)") {
|
||||
UIApplication.shared.open(url)
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not create a WebTor URL"
|
||||
toastModel?.updateToastDescription("Could not create a WebTor URL")
|
||||
}
|
||||
case .shareMagnet:
|
||||
if let magnetUrl = URL(string: magnetLink),
|
||||
currentChoiceSheet == nil
|
||||
{
|
||||
activityItems = [magnetUrl]
|
||||
showActivityView.toggle()
|
||||
currentChoiceSheet = .activity
|
||||
} else {
|
||||
toastModel?.toastDescription = "Could not create object for sharing"
|
||||
toastModel?.updateToastDescription("Could not create object for sharing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,12 +36,13 @@ class ScrapingViewModel: ObservableObject {
|
|||
@Published var currentSourceName: String?
|
||||
|
||||
@MainActor
|
||||
func updateSearchResults(newResults: [SearchResult]) {
|
||||
searchResults = newResults
|
||||
}
|
||||
|
||||
public func scanSources(sources: [Source]) async {
|
||||
if sources.isEmpty {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastType = .info
|
||||
toastModel?.toastDescription = "There are no sources to search!"
|
||||
}
|
||||
await toastModel?.updateToastDescription("There are no sources to search!", newToastType: .info)
|
||||
|
||||
print("There are no sources to search!")
|
||||
return
|
||||
|
|
@ -51,10 +52,12 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
for source in sources {
|
||||
if source.enabled {
|
||||
currentSourceName = source.name
|
||||
Task { @MainActor in
|
||||
currentSourceName = source.name
|
||||
}
|
||||
|
||||
guard let baseUrl = source.baseUrl else {
|
||||
toastModel?.toastDescription = "The base URL could not be found for source \(source.name)"
|
||||
await toastModel?.updateToastDescription("The base URL could not be found for source \(source.name)")
|
||||
|
||||
print("The base URL could not be found for source \(source.name)")
|
||||
continue
|
||||
|
|
@ -64,7 +67,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let preferredParser = SourcePreferredParser(rawValue: source.preferredParser) ?? .none
|
||||
|
||||
guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
||||
toastModel?.toastDescription = "Could not process search query, invalid characters present."
|
||||
await toastModel?.updateToastDescription("Could not process search query, invalid characters present.")
|
||||
print("Could not process search query, invalid characters present")
|
||||
|
||||
continue
|
||||
|
|
@ -110,7 +113,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
if let data = data,
|
||||
let rss = String(data: data, encoding: .utf8)
|
||||
{
|
||||
let sourceResults = scrapeRss(source: source, rss: rss)
|
||||
let sourceResults = await scrapeRss(source: source, rss: rss)
|
||||
tempResults += sourceResults
|
||||
}
|
||||
}
|
||||
|
|
@ -154,7 +157,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
)
|
||||
|
||||
if let data = data {
|
||||
let sourceResults = scrapeJson(source: source, jsonData: data)
|
||||
let sourceResults = await scrapeJson(source: source, jsonData: data)
|
||||
tempResults += sourceResults
|
||||
}
|
||||
}
|
||||
|
|
@ -169,7 +172,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
return
|
||||
}
|
||||
|
||||
searchResults = tempResults
|
||||
await updateSearchResults(newResults: tempResults)
|
||||
}
|
||||
|
||||
// Checks the base URL for any website data then iterates through the fallback URLs
|
||||
|
|
@ -232,7 +235,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
public func fetchApiCredential(urlString: String, credential: SourceApiCredential) async -> String? {
|
||||
guard let url = URL(string: urlString) else {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "This token URL is invalid."
|
||||
toastModel?.updateToastDescription("This token URL is invalid.")
|
||||
}
|
||||
print("Token url \(urlString) is invalid!")
|
||||
|
||||
|
|
@ -259,17 +262,15 @@ class ScrapingViewModel: ObservableObject {
|
|||
} catch {
|
||||
let error = error as NSError
|
||||
|
||||
Task { @MainActor in
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.toastType = .info
|
||||
toastModel?.toastDescription = "Search cancelled"
|
||||
case -1001:
|
||||
toastModel?.toastDescription = "Credentials request timed out"
|
||||
default:
|
||||
toastModel?.toastDescription = "Error in fetching an API credential \(error)"
|
||||
}
|
||||
switch error.code {
|
||||
case -999:
|
||||
await toastModel?.updateToastDescription("Search cancelled", newToastType: .info)
|
||||
case -1001:
|
||||
await toastModel?.updateToastDescription("Credentials request timed out")
|
||||
default:
|
||||
await toastModel?.updateToastDescription("Error in fetching an API credential \(error)")
|
||||
}
|
||||
|
||||
print("Error in fetching an API credential \(error)")
|
||||
|
||||
return nil
|
||||
|
|
@ -279,9 +280,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
// Fetches the data for a URL
|
||||
public func fetchWebsiteData(urlString: String) async -> Data? {
|
||||
guard let url = URL(string: urlString) else {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Source doesn't contain a valid URL, contact the source dev!"
|
||||
}
|
||||
await toastModel?.updateToastDescription("Source doesn't contain a valid URL, contact the source dev!")
|
||||
|
||||
print("Source doesn't contain a valid URL, contact the source dev!")
|
||||
|
||||
|
|
@ -296,24 +295,22 @@ class ScrapingViewModel: ObservableObject {
|
|||
} catch {
|
||||
let error = error as NSError
|
||||
|
||||
Task { @MainActor in
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.toastType = .info
|
||||
toastModel?.toastDescription = "Search cancelled"
|
||||
case -1001:
|
||||
toastModel?.toastDescription = "Data request timed out. Trying fallback URLs if present."
|
||||
default:
|
||||
toastModel?.toastDescription = "Error in fetching website data \(error)"
|
||||
}
|
||||
switch error.code {
|
||||
case -999:
|
||||
await toastModel?.updateToastDescription("Search cancelled", newToastType: .info)
|
||||
case -1001:
|
||||
await toastModel?.updateToastDescription("Data request timed out. Trying fallback URLs if present.")
|
||||
default:
|
||||
await toastModel?.updateToastDescription("Error in fetching website data \(error)")
|
||||
}
|
||||
|
||||
print("Error in fetching data \(error)")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
public func scrapeJson(source: Source, jsonData: Data) -> [SearchResult] {
|
||||
public func scrapeJson(source: Source, jsonData: Data) async -> [SearchResult] {
|
||||
var tempResults: [SearchResult] = []
|
||||
|
||||
guard let jsonParser = source.jsonParser else {
|
||||
|
|
@ -332,9 +329,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
} catch {
|
||||
if let api = source.api {
|
||||
Task { @MainActor in
|
||||
cleanApiCreds(api: api)
|
||||
}
|
||||
await cleanApiCreds(api: api)
|
||||
|
||||
print("JSON parsing error, couldn't fetch results: \(error)")
|
||||
}
|
||||
|
|
@ -342,9 +337,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
// If there are no results and the client secret isn't dynamic, just clear out the token
|
||||
if let api = source.api, jsonResults.isEmpty {
|
||||
Task { @MainActor in
|
||||
cleanApiCreds(api: api)
|
||||
}
|
||||
await cleanApiCreds(api: api)
|
||||
|
||||
print("JSON results were empty!")
|
||||
}
|
||||
|
|
@ -470,7 +463,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
// RSS feed scraper
|
||||
public func scrapeRss(source: Source, rss: String) -> [SearchResult] {
|
||||
public func scrapeRss(source: Source, rss: String) async -> [SearchResult] {
|
||||
var tempResults: [SearchResult] = []
|
||||
|
||||
guard let rssParser = source.rssParser else {
|
||||
|
|
@ -483,9 +476,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let document = try SwiftSoup.parse(rss, "", Parser.xmlParser())
|
||||
items = try document.getElementsByTag("item")
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "RSS scraping error, couldn't fetch items: \(error)"
|
||||
}
|
||||
await toastModel?.updateToastDescription("RSS scraping error, couldn't fetch items: \(error)")
|
||||
print("RSS scraping error, couldn't fetch items: \(error)")
|
||||
|
||||
return tempResults
|
||||
|
|
@ -640,9 +631,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let document = try SwiftSoup.parse(html)
|
||||
rows = try document.select(htmlParser.rows)
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Scraping error, couldn't fetch rows: \(error)"
|
||||
}
|
||||
await toastModel?.updateToastDescription("Scraping error, couldn't fetch rows: \(error)")
|
||||
print("Scraping error, couldn't fetch rows: \(error)")
|
||||
|
||||
return tempResults
|
||||
|
|
@ -775,9 +764,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
tempResults.append(result)
|
||||
}
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Scraping error: \(error)"
|
||||
}
|
||||
await toastModel?.updateToastDescription("Scraping error: \(error)")
|
||||
print("Scraping error: \(error)")
|
||||
|
||||
continue
|
||||
|
|
@ -880,8 +867,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
return magnetLinkArray.joined()
|
||||
}
|
||||
|
||||
@MainActor
|
||||
func cleanApiCreds(api: SourceApi) {
|
||||
func cleanApiCreds(api: SourceApi) async {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let hasCredentials = api.clientId != nil || api.clientSecret != nil
|
||||
|
|
@ -926,7 +912,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
toastModel?.toastDescription = responseArray.joined()
|
||||
await toastModel?.updateToastDescription(responseArray.joined())
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -92,16 +92,14 @@ public class SourceManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) {
|
||||
public func installSource(sourceJson: SourceJson, doUpsert: Bool = false) async {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// If there's no base URL and it isn't dynamic, return before any transactions occur
|
||||
let dynamicBaseUrl = sourceJson.dynamicBaseUrl ?? false
|
||||
|
||||
if !dynamicBaseUrl, sourceJson.baseUrl == nil {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Not adding this source because base URL parameters are malformed. Please contact the source dev."
|
||||
}
|
||||
await toastModel?.updateToastDescription("Not adding this source because base URL parameters are malformed. Please contact the source dev.")
|
||||
|
||||
print("Not adding this source because base URL parameters are malformed")
|
||||
return
|
||||
|
|
@ -116,9 +114,7 @@ public class SourceManager: ObservableObject {
|
|||
if doUpsert {
|
||||
PersistenceController.shared.delete(existingSource, context: backgroundContext)
|
||||
} else {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Could not install source with name \(sourceJson.name) because it is already installed."
|
||||
}
|
||||
await toastModel?.updateToastDescription("Could not install source with name \(sourceJson.name) because it is already installed.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
@ -166,9 +162,7 @@ public class SourceManager: ObservableObject {
|
|||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = error.localizedDescription
|
||||
}
|
||||
await toastModel?.updateToastDescription(error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -35,6 +35,14 @@ class ToastViewModel: ObservableObject {
|
|||
|
||||
@Published var showToast: Bool = false
|
||||
|
||||
public func updateToastDescription(_ description: String, newToastType: ToastType? = nil) {
|
||||
if let newToastType = newToastType {
|
||||
toastType = newToastType
|
||||
}
|
||||
|
||||
toastDescription = description
|
||||
}
|
||||
|
||||
// Default the toast type to error since the majority of toasts are errors
|
||||
@Published var toastType: ToastType = .error
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ struct BatchChoiceView: View {
|
|||
debridManager.selectedRealDebridFile = file
|
||||
|
||||
if let searchResult = scrapingModel.selectedSearchResult {
|
||||
Task {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
|
|
|
|||
44
Ferrite/Views/CommonViews/IndeterminateProgressView.swift
Normal file
44
Ferrite/Views/CommonViews/IndeterminateProgressView.swift
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// IndeterminateProgressView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/26/22.
|
||||
//
|
||||
// Inspired by https://daringsnowball.net/articles/indeterminate-linear-progress-view/
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct IndeterminateProgressView: View {
|
||||
@State private var offset: CGFloat = 0
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { reader in
|
||||
Rectangle()
|
||||
.foregroundColor(.gray.opacity(0.15))
|
||||
.overlay(
|
||||
Rectangle()
|
||||
.foregroundColor(Color.accentColor)
|
||||
.frame(width: reader.size.width * 0.26, height: 6)
|
||||
.clipShape(Capsule())
|
||||
|
||||
.offset(x: -reader.size.width * 0.6, y: 0)
|
||||
.offset(x: reader.size.width * 1.2 * self.offset, y: 0)
|
||||
.animation(.default.repeatForever().speed(0.5), value: self.offset)
|
||||
.onAppear{
|
||||
withAnimation {
|
||||
self.offset = 1
|
||||
}
|
||||
}
|
||||
)
|
||||
.clipShape(Capsule())
|
||||
}
|
||||
.frame(height: 4, alignment: .center)
|
||||
}
|
||||
}
|
||||
|
||||
struct IndeterminateProgressView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
IndeterminateProgressView()
|
||||
}
|
||||
}
|
||||
|
|
@ -85,18 +85,17 @@ struct ContentView: View {
|
|||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
case .activity:
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
.presentationDetents([.medium, .large])
|
||||
} else {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
}
|
||||
.sheet(isPresented: $navModel.showActivityView) {
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
.presentationDetents([.medium])
|
||||
} else {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search",
|
||||
|
|
|
|||
|
|
@ -17,10 +17,8 @@ struct MagnetChoiceView: View {
|
|||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
@State private var showActivityView = false
|
||||
@State private var showLinkCopyAlert = false
|
||||
@State private var showMagnetCopyAlert = false
|
||||
@State private var activityItems: [Any] = []
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
|
|
@ -53,8 +51,8 @@ struct MagnetChoiceView: View {
|
|||
|
||||
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
|
||||
if let url = URL(string: debridManager.realDebridDownloadUrl) {
|
||||
activityItems = [url]
|
||||
navModel.showActivityView.toggle()
|
||||
navModel.activityItems = [url]
|
||||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -78,8 +76,8 @@ struct MagnetChoiceView: View {
|
|||
let magnetLink = result.magnetLink,
|
||||
let url = URL(string: magnetLink)
|
||||
{
|
||||
activityItems = [url]
|
||||
navModel.showActivityView.toggle()
|
||||
navModel.activityItems = [url]
|
||||
navModel.showLocalActivitySheet.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -90,12 +88,12 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $navModel.showActivityView) {
|
||||
.sheet(isPresented: $navModel.showLocalActivitySheet) {
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: activityItems)
|
||||
.presentationDetents([.medium])
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
.presentationDetents([.medium, .large])
|
||||
} else {
|
||||
AppActivityView(activityItems: activityItems)
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Link actions")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import SwiftUIX
|
|||
struct MainView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var body: some View {
|
||||
TabView(selection: $navModel.selectedTab) {
|
||||
|
|
@ -52,11 +53,34 @@ struct MainView: View {
|
|||
.cornerRadius(10)
|
||||
}
|
||||
|
||||
if debridManager.showLoadingProgress {
|
||||
VStack {
|
||||
Text("Loading content")
|
||||
|
||||
HStack {
|
||||
IndeterminateProgressView()
|
||||
|
||||
Button("Cancel") {
|
||||
debridManager.currentDebridTask?.cancel()
|
||||
debridManager.currentDebridTask = nil
|
||||
debridManager.showLoadingProgress = false
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(12)
|
||||
.font(.caption)
|
||||
.background {
|
||||
VisualEffectBlurView(blurStyle: .systemThinMaterial)
|
||||
}
|
||||
.cornerRadius(10)
|
||||
.frame(width: 200)
|
||||
}
|
||||
|
||||
Rectangle()
|
||||
.foregroundColor(.clear)
|
||||
.frame(height: 60)
|
||||
}
|
||||
.animation(.easeInOut(duration: 0.3), value: toastModel.showToast)
|
||||
.animation(.easeInOut(duration: 0.3), value: toastModel.showToast || debridManager.showLoadingProgress)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,23 +20,25 @@ struct SearchResultsView: View {
|
|||
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
scrapingModel.selectedSearchResult = result
|
||||
if debridManager.currentDebridTask == nil {
|
||||
scrapingModel.selectedSearchResult = result
|
||||
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
Task {
|
||||
await debridManager.fetchRdDownload(searchResult: result)
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: result)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
navModel.runMagnetAction(action: nil, searchResult: result)
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
navModel.runMagnetAction(action: nil, searchResult: result)
|
||||
}
|
||||
} label: {
|
||||
Text(result.title ?? "No title")
|
||||
|
|
|
|||
|
|
@ -13,12 +13,9 @@ struct SettingsView: View {
|
|||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none
|
||||
|
||||
@State private var isProcessing = false
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
|
|
@ -28,16 +25,15 @@ struct SettingsView: View {
|
|||
Spacer()
|
||||
Button {
|
||||
Task {
|
||||
if realDebridEnabled {
|
||||
try? await debridManager.realDebrid.deleteTokens()
|
||||
} else if !isProcessing {
|
||||
if debridManager.realDebridEnabled {
|
||||
await debridManager.logoutRd()
|
||||
} else if !debridManager.realDebridAuthProcessing {
|
||||
await debridManager.authenticateRd()
|
||||
isProcessing = true
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(realDebridEnabled ? "Logout" : (isProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(realDebridEnabled ? .red : .blue)
|
||||
Text(debridManager.realDebridEnabled ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.realDebridEnabled ? .red : .blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -47,7 +43,7 @@ struct SettingsView: View {
|
|||
}
|
||||
|
||||
Section(header: "Default actions") {
|
||||
if realDebridEnabled {
|
||||
if debridManager.realDebridEnabled {
|
||||
NavigationLink(
|
||||
destination: DebridActionPickerView(),
|
||||
label: {
|
||||
|
|
|
|||
|
|
@ -12,20 +12,37 @@ struct MagnetActionPickerView: View {
|
|||
|
||||
var body: some View {
|
||||
List {
|
||||
Picker(selection: $defaultMagnetAction, label: EmptyView()) {
|
||||
Text("Let me choose")
|
||||
.tag(DefaultMagnetActionType.none)
|
||||
Text("Open in Webtor")
|
||||
.tag(DefaultMagnetActionType.webtor)
|
||||
Text("Share magnet link")
|
||||
.tag(DefaultMagnetActionType.shareMagnet)
|
||||
ForEach(DefaultMagnetActionType.allCases, id: \.self) { action in
|
||||
Button {
|
||||
defaultMagnetAction = action
|
||||
} label: {
|
||||
HStack {
|
||||
Text(fetchPickerChoiceName(choice: action))
|
||||
Spacer()
|
||||
if action == defaultMagnetAction {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("Default magnet action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
func fetchPickerChoiceName(choice: DefaultMagnetActionType) -> String {
|
||||
switch choice {
|
||||
case .none:
|
||||
return "Let me choose"
|
||||
case .webtor:
|
||||
return "Open in Webtor"
|
||||
case .shareMagnet:
|
||||
return "Share magnet link"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct DebridActionPickerView: View {
|
||||
|
|
@ -33,22 +50,39 @@ struct DebridActionPickerView: View {
|
|||
|
||||
var body: some View {
|
||||
List {
|
||||
Picker(selection: $defaultDebridAction, label: EmptyView()) {
|
||||
Text("Let me choose")
|
||||
.tag(DefaultDebridActionType.none)
|
||||
Text("Open in Outplayer")
|
||||
.tag(DefaultDebridActionType.outplayer)
|
||||
Text("Open in VLC")
|
||||
.tag(DefaultDebridActionType.vlc)
|
||||
Text("Open in Infuse")
|
||||
.tag(DefaultDebridActionType.infuse)
|
||||
Text("Share download link")
|
||||
.tag(DefaultDebridActionType.shareDownload)
|
||||
ForEach(DefaultDebridActionType.allCases, id: \.self) { action in
|
||||
Button {
|
||||
defaultDebridAction = action
|
||||
} label: {
|
||||
HStack {
|
||||
Text(fetchPickerChoiceName(choice: action))
|
||||
Spacer()
|
||||
if action == defaultDebridAction {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("Default debrid action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
func fetchPickerChoiceName(choice: DefaultDebridActionType) -> String {
|
||||
switch choice {
|
||||
case .none:
|
||||
return "Let me choose"
|
||||
case .outplayer:
|
||||
return "Open in Outplayer"
|
||||
case .vlc:
|
||||
return "Open in VLC"
|
||||
case .infuse:
|
||||
return "Open in Infuse"
|
||||
case .shareDownload:
|
||||
return "Share download link"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ struct SettingsSourceListView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.sheet(isPresented: $presentSourceSheet) {
|
||||
if #available(iOS 16, *) {
|
||||
SourceListEditorView()
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ struct SourceCatalogButtonView: View {
|
|||
Spacer()
|
||||
|
||||
Button("Install") {
|
||||
sourceManager.installSource(sourceJson: availableSource)
|
||||
Task {
|
||||
await sourceManager.installSource(sourceJson: availableSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -144,31 +144,53 @@ struct SourceSettingsApiView: View {
|
|||
struct SourceSettingsMethodView: View {
|
||||
@ObservedObject var selectedSource: Source
|
||||
|
||||
@State private var selectedTempParser: SourcePreferredParser = .none
|
||||
|
||||
var body: some View {
|
||||
Picker("Fetch method", selection: $selectedTempParser) {
|
||||
if selectedSource.jsonParser != nil {
|
||||
Text("Website API")
|
||||
.tag(SourcePreferredParser.siteApi)
|
||||
Section(header: Text("Fetch method")) {
|
||||
if selectedSource.api != nil, selectedSource.jsonParser != nil {
|
||||
Button {
|
||||
selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Website API")
|
||||
Spacer()
|
||||
if SourcePreferredParser.siteApi.rawValue == selectedSource.preferredParser {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedSource.rssParser != nil {
|
||||
Text("RSS")
|
||||
.tag(SourcePreferredParser.rss)
|
||||
Button {
|
||||
selectedSource.preferredParser = SourcePreferredParser.rss.rawValue
|
||||
} label: {
|
||||
HStack {
|
||||
Text("RSS")
|
||||
Spacer()
|
||||
if SourcePreferredParser.rss.rawValue == selectedSource.preferredParser {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if selectedSource.htmlParser != nil {
|
||||
Text("Web scraping")
|
||||
.tag(SourcePreferredParser.scraping)
|
||||
Button {
|
||||
selectedSource.preferredParser = SourcePreferredParser.scraping.rawValue
|
||||
} label: {
|
||||
HStack {
|
||||
Text("Web scraping")
|
||||
Spacer()
|
||||
if SourcePreferredParser.scraping.rawValue == selectedSource.preferredParser {
|
||||
Image(systemName: "checkmark")
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
.onAppear {
|
||||
selectedTempParser = SourcePreferredParser(rawValue: selectedSource.preferredParser) ?? .none
|
||||
}
|
||||
.onChange(of: selectedTempParser) { _ in
|
||||
selectedSource.preferredParser = selectedTempParser.rawValue
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,9 @@ struct SourceUpdateButtonView: View {
|
|||
Spacer()
|
||||
|
||||
Button("Update") {
|
||||
sourceManager.installSource(sourceJson: updatedSource, doUpsert: true)
|
||||
Task {
|
||||
await sourceManager.installSource(sourceJson: updatedSource, doUpsert: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue