Ferrite: Decouple torrent sources
These sources will be converted to be more flexible with JavaScript in the future. The source catalog is populated by adding a source list in settings then installing a source from the catalog. Sources can be enabled or disabled when using Ferrite. Signed-off-by: kingbri <bdashore3@gmail.com>
This commit is contained in:
parent
ac42936afb
commit
1eef8202ca
22 changed files with 686 additions and 52 deletions
|
|
@ -7,9 +7,16 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C0D50E1288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */; };
|
||||
0C0D50E2288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourceListView.swift */; };
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
||||
0C90E32C2888E5D000C0BC89 /* ActivityView in Frameworks */ = {isa = PBXBuildFile; productRef = 0C90E32B2888E5D000C0BC89 /* ActivityView */; };
|
||||
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */; };
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA05458288EE9E600850554 /* SourceManager.swift */; };
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */; };
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BB288903F000DE2211 /* SettingsView.swift */; };
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BC288903F000DE2211 /* LoginWebView.swift */; };
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */; };
|
||||
|
|
@ -33,10 +40,21 @@
|
|||
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 */; };
|
||||
0CBC7702288DE4400054BE44 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */; };
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
|
||||
0CE37ABA288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */; };
|
||||
0CE37ABB288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */; };
|
||||
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSource+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSource+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* SourceListView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListView.swift; sourceTree = "<group>"; };
|
||||
0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsSourceUrlView.swift; sourceTree = "<group>"; };
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceManager.swift; sourceTree = "<group>"; };
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceListEditorView.swift; sourceTree = "<group>"; };
|
||||
0CA148BB288903F000DE2211 /* SettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LoginWebView.swift; sourceTree = "<group>"; };
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MagnetChoiceView.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -60,6 +78,10 @@
|
|||
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>"; };
|
||||
0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
|
||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||
0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSourceUrl+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TorrentSourceUrl+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -78,11 +100,42 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CE37AB8288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift */,
|
||||
0CE37AB9288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift */,
|
||||
0C0D50DF288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift */,
|
||||
0C0D50E0288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148C4288903F000DE2211 /* RealDebridModels.swift */,
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA05456288EE58200850554 /* SettingsSourceUrlView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */,
|
||||
);
|
||||
path = SettingsViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA148BA288903F000DE2211 /* Ferrite */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CBC7703288DE7E90054BE44 /* DataManagement */,
|
||||
0CA148F12889066000DE2211 /* API */,
|
||||
0CA148EF2889061600DE2211 /* Models */,
|
||||
0C0D50E3288DFE6E0035ECC8 /* Models */,
|
||||
0CA148EF2889061600DE2211 /* ViewModels */,
|
||||
0CA148EE2889061200DE2211 /* Views */,
|
||||
0CA148C8288903F000DE2211 /* Extensions */,
|
||||
0CA148C5288903F000DE2211 /* Preview Content */,
|
||||
|
|
@ -124,6 +177,7 @@
|
|||
children = (
|
||||
0CA148F02889062700DE2211 /* RepresentableViews */,
|
||||
0CA148C0288903F000DE2211 /* CommonViews */,
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D1288903F000DE2211 /* MainView.swift */,
|
||||
|
|
@ -132,19 +186,21 @@
|
|||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
0C0D50E6288DFF850035ECC8 /* SourceListView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA148EF2889061600DE2211 /* Models */ = {
|
||||
0CA148EF2889061600DE2211 /* ViewModels */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148CD288903F000DE2211 /* DebridManager.swift */,
|
||||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */,
|
||||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */,
|
||||
);
|
||||
path = Models;
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA148F02889062700DE2211 /* RepresentableViews */ = {
|
||||
|
|
@ -159,7 +215,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
0CA148C4288903F000DE2211 /* RealDebridModels.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -180,6 +235,16 @@
|
|||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CBC7703288DE7E90054BE44 /* DataManagement */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C0D50DE288DF72D0035ECC8 /* Classes */,
|
||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */,
|
||||
0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */,
|
||||
);
|
||||
path = DataManagement;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
|
@ -262,9 +327,14 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0CE37ABB288E4AE900428C2D /* TorrentSourceUrl+CoreDataProperties.swift in Sources */,
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* SourceListView.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
||||
|
|
@ -272,16 +342,22 @@
|
|||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */,
|
||||
0CE37ABA288E4AE900428C2D /* TorrentSourceUrl+CoreDataClass.swift in Sources */,
|
||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||
0C0D50E1288DF7700035ECC8 /* TorrentSource+CoreDataClass.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0CA05457288EE58200850554 /* SettingsSourceUrlView.swift in Sources */,
|
||||
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
||||
0CBC7702288DE4400054BE44 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
0CA148D9288903F000DE2211 /* CardView.swift in Sources */,
|
||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
|
||||
0C0D50E2288DF7700035ECC8 /* TorrentSource+CoreDataProperties.swift in Sources */,
|
||||
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -545,6 +621,19 @@
|
|||
productName = SwiftSoup;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
0CBC7700288DE4400054BE44 /* FerriteDB.xcdatamodeld */ = {
|
||||
isa = XCVersionGroup;
|
||||
children = (
|
||||
0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */,
|
||||
);
|
||||
currentVersion = 0CBC7701288DE4400054BE44 /* FerriteDB.xcdatamodel */;
|
||||
path = FerriteDB.xcdatamodeld;
|
||||
sourceTree = "<group>";
|
||||
versionGroupType = wrapper.xcdatamodel;
|
||||
};
|
||||
/* End XCVersionGroup section */
|
||||
};
|
||||
rootObject = 0CAF1C60286F5C0D00296F86 /* Project object */;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ public class RealDebrid: ObservableObject {
|
|||
keychain.set(rawResponse.accessToken, forKey: "RealDebrid.AccessToken")
|
||||
keychain.set(rawResponse.refreshToken, forKey: "RealDebrid.RefreshToken")
|
||||
|
||||
let accessTimestamp = Date().timeIntervalSince1970 + Double(rawResponse.expiresIn)
|
||||
let accessTimestamp = Date().timeIntervalSince1970 + (Double(rawResponse.expiresIn))
|
||||
UserDefaults.standard.set(accessTimestamp, forKey: "RealDebrid.AccessTokenStamp")
|
||||
|
||||
// Set AppStorage variable
|
||||
|
|
@ -161,7 +161,6 @@ public class RealDebrid: ObservableObject {
|
|||
if Date().timeIntervalSince1970 > accessTokenStamp {
|
||||
do {
|
||||
if let refreshToken = keychain.get("RealDebrid.RefreshToken") {
|
||||
print("Refresh token found")
|
||||
try await getTokens(deviceCode: refreshToken)
|
||||
}
|
||||
} catch {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// TorrentSource+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(TorrentSource)
|
||||
public class TorrentSource: NSManagedObject {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// TorrentSource+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension TorrentSource {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<TorrentSource> {
|
||||
return NSFetchRequest<TorrentSource>(entityName: "TorrentSource")
|
||||
}
|
||||
|
||||
@NSManaged public var enabled: Bool
|
||||
@NSManaged public var linkQuery: String
|
||||
@NSManaged public var name: String?
|
||||
@NSManaged public var rowQuery: String
|
||||
@NSManaged public var sizeQuery: String?
|
||||
@NSManaged public var titleQuery: String?
|
||||
@NSManaged public var url: String
|
||||
|
||||
}
|
||||
|
||||
extension TorrentSource : Identifiable {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
//
|
||||
// TorrentSourceUrl+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
@objc(TorrentSourceUrl)
|
||||
public class TorrentSourceUrl: NSManagedObject {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// TorrentSourceUrl+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension TorrentSourceUrl {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<TorrentSourceUrl> {
|
||||
return NSFetchRequest<TorrentSourceUrl>(entityName: "TorrentSourceUrl")
|
||||
}
|
||||
|
||||
@NSManaged public var urlString: String
|
||||
@NSManaged public var repoName: String?
|
||||
@NSManaged public var repoAuthor: String?
|
||||
|
||||
}
|
||||
|
||||
extension TorrentSourceUrl : Identifiable {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21256.2" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="TorrentSource" representedClassName="TorrentSource" syncable="YES">
|
||||
<attribute name="enabled" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<attribute name="linkQuery" attributeType="String"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="rowQuery" attributeType="String"/>
|
||||
<attribute name="sizeQuery" optional="YES" attributeType="String"/>
|
||||
<attribute name="titleQuery" optional="YES" attributeType="String"/>
|
||||
<attribute name="url" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="TorrentSourceUrl" representedClassName="TorrentSourceUrl" syncable="YES">
|
||||
<attribute name="repoAuthor" optional="YES" attributeType="String"/>
|
||||
<attribute name="repoName" optional="YES" attributeType="String"/>
|
||||
<attribute name="urlString" attributeType="String"/>
|
||||
</entity>
|
||||
</model>
|
||||
81
Ferrite/DataManagement/PersistenceController.swift
Normal file
81
Ferrite/DataManagement/PersistenceController.swift
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
//
|
||||
// PersistenceController.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import CoreData
|
||||
|
||||
// No iCloud until finalized sources
|
||||
struct PersistenceController {
|
||||
static let shared = PersistenceController()
|
||||
|
||||
// Coredata storage
|
||||
let container: NSPersistentContainer
|
||||
|
||||
// Background context for writes
|
||||
let backgroundContext: NSManagedObjectContext
|
||||
|
||||
// Coredata load
|
||||
init(inMemory: Bool = false) {
|
||||
container = NSPersistentContainer(name: "FerriteDB")
|
||||
|
||||
if inMemory {
|
||||
container.persistentStoreDescriptions.first?.url = URL(fileURLWithPath: "/dev/null")
|
||||
}
|
||||
|
||||
guard let description = container.persistentStoreDescriptions.first else {
|
||||
fatalError("CoreData: Failed to find a persistent store description")
|
||||
}
|
||||
|
||||
description.setOption(true as NSNumber, forKey: NSPersistentHistoryTrackingKey)
|
||||
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
|
||||
|
||||
container.viewContext.automaticallyMergesChangesFromParent = true
|
||||
container.viewContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
try? container.viewContext.setQueryGenerationFrom(.current)
|
||||
|
||||
backgroundContext = container.newBackgroundContext()
|
||||
backgroundContext.automaticallyMergesChangesFromParent = true
|
||||
backgroundContext.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy
|
||||
try? backgroundContext.setQueryGenerationFrom(.current)
|
||||
|
||||
container.loadPersistentStores { _, error in
|
||||
if let error = error {
|
||||
fatalError("Error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func save(_ context: NSManagedObjectContext? = nil) {
|
||||
let context = context ?? container.viewContext
|
||||
|
||||
if context.hasChanges {
|
||||
do {
|
||||
try context.save()
|
||||
} catch {
|
||||
debugPrint("Error in CoreData saving! \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// By default, delete objects using the ViewContext unless specified
|
||||
func delete(_ object: NSManagedObject, context: NSManagedObjectContext? = nil) {
|
||||
let context = context ?? container.viewContext
|
||||
|
||||
if context != container.viewContext {
|
||||
let wrappedObject = try? context.existingObject(with: object.objectID)
|
||||
|
||||
if let backgroundObject = wrappedObject {
|
||||
context.delete(backgroundObject)
|
||||
save(context)
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
container.viewContext.delete(object)
|
||||
save()
|
||||
}
|
||||
}
|
||||
|
|
@ -9,10 +9,13 @@ import SwiftUI
|
|||
|
||||
@main
|
||||
struct FerriteApp: App {
|
||||
let persistenceController = PersistenceController.shared
|
||||
|
||||
@StateObject var scrapingModel: ScrapingViewModel = .init()
|
||||
@StateObject var toastModel: ToastViewModel = .init()
|
||||
@StateObject var debridManager: DebridManager = .init()
|
||||
@StateObject var navigationModel: NavigationViewModel = .init()
|
||||
@StateObject var sourceManager: SourceManager = .init()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
|
|
@ -20,11 +23,14 @@ struct FerriteApp: App {
|
|||
.onAppear {
|
||||
scrapingModel.toastModel = toastModel
|
||||
debridManager.toastModel = toastModel
|
||||
sourceManager.toastModel = toastModel
|
||||
}
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(toastModel)
|
||||
.environmentObject(navigationModel)
|
||||
.environmentObject(sourceManager)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
29
Ferrite/Models/SourceModels.swift
Normal file
29
Ferrite/Models/SourceModels.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// SourceModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct SourceJson: Codable {
|
||||
let repoName: String?
|
||||
let repoAuthor: String?
|
||||
let sources: [TorrentSourceJson]
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case repoName = "name"
|
||||
case repoAuthor = "author"
|
||||
case sources = "sources"
|
||||
}
|
||||
}
|
||||
|
||||
public struct TorrentSourceJson: Codable, Hashable {
|
||||
let name: String?
|
||||
let url: String
|
||||
let rowQuery: String
|
||||
let linkQuery: String
|
||||
let titleQuery: String?
|
||||
let sizeQuery: String?
|
||||
}
|
||||
|
|
@ -17,45 +17,39 @@ public struct SearchResult: Hashable, Codable {
|
|||
let magnetHash: String?
|
||||
}
|
||||
|
||||
public struct TorrentSource: Hashable, Codable {
|
||||
let name: String
|
||||
let url: String
|
||||
let rowQuery: String
|
||||
let linkQuery: String
|
||||
let titleQuery: String
|
||||
let sizeQuery: String
|
||||
}
|
||||
|
||||
class ScrapingViewModel: ObservableObject {
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
// Link the toast view model for single-directional communication
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
// Decopule this in the future
|
||||
let sources = [
|
||||
// TorrentSource(
|
||||
// name: "Nyaa",
|
||||
// url: "https://nyaa.si",
|
||||
// rowQuery: ".torrent-list tbody tr",
|
||||
// linkQuery: "td:nth-child(3) > a:nth-child(2))",
|
||||
// titleQuery: "td:nth-child(2) > a:last-child"
|
||||
// ),
|
||||
TorrentSource(
|
||||
name: "AnimeTosho",
|
||||
url: "https://animetosho.org/search?q=",
|
||||
rowQuery: "#content .home_list_entry",
|
||||
linkQuery: ".links > a:nth-child(4)",
|
||||
titleQuery: ".link",
|
||||
sizeQuery: ".size"
|
||||
)
|
||||
]
|
||||
|
||||
@Published var searchResults: [SearchResult] = []
|
||||
@Published var debridHashes: [String] = []
|
||||
@Published var searchText: String = ""
|
||||
@Published var selectedSearchResult: SearchResult?
|
||||
|
||||
@MainActor
|
||||
public func scanSources(sources: FetchedResults<TorrentSource>) async {
|
||||
if sources.isEmpty {
|
||||
print("Sources empty")
|
||||
}
|
||||
|
||||
var tempResults: [SearchResult] = []
|
||||
|
||||
for source in sources {
|
||||
if source.enabled {
|
||||
guard let html = await fetchWebsiteHtml(source: source) else {
|
||||
continue
|
||||
}
|
||||
|
||||
let sourceResults = await scrapeWebsite(source: source, html: html)
|
||||
tempResults += sourceResults
|
||||
}
|
||||
}
|
||||
|
||||
searchResults = tempResults
|
||||
}
|
||||
|
||||
// Fetches the HTML body for the source website
|
||||
@MainActor
|
||||
public func fetchWebsiteHtml(source: TorrentSource) async -> String? {
|
||||
|
|
@ -88,7 +82,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
// Returns results to UI
|
||||
// Results must have a link and title, but other parameters aren't required
|
||||
@MainActor
|
||||
public func scrapeWebsite(source: TorrentSource, html: String) async {
|
||||
public func scrapeWebsite(source: TorrentSource, html: String) async -> [SearchResult] {
|
||||
var tempResults: [SearchResult] = []
|
||||
var hashes: [String] = []
|
||||
|
||||
|
|
@ -110,15 +104,24 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
let magnetHash = fetchMagnetHash(magnetLink: href)
|
||||
|
||||
let title = try row.select(source.titleQuery).first()
|
||||
let titleText = try title?.text()
|
||||
var title: String?
|
||||
|
||||
let size = try row.select(source.sizeQuery).first()
|
||||
// Some sources may use last-child, but SwiftSoup doesn't support it
|
||||
if let titleQuery = source.titleQuery {
|
||||
if titleQuery.contains("last-child") {
|
||||
let newTitleQuery = titleQuery.replacingOccurrences(of: ":last-child", with: "")
|
||||
title = try row.select(newTitleQuery).last()?.text()
|
||||
} else {
|
||||
title = try row.select(titleQuery).first()?.text()
|
||||
}
|
||||
}
|
||||
|
||||
let size = try row.select(source.sizeQuery ?? "").first()
|
||||
let sizeText = try size?.text()
|
||||
|
||||
let result = SearchResult(
|
||||
title: titleText ?? "No title provided",
|
||||
source: source.name,
|
||||
title: title ?? "No title",
|
||||
source: source.name ?? "N/A",
|
||||
size: sizeText ?? "?B",
|
||||
magnetLink: href,
|
||||
magnetHash: magnetHash
|
||||
|
|
@ -132,10 +135,12 @@ class ScrapingViewModel: ObservableObject {
|
|||
tempResults.append(result)
|
||||
}
|
||||
|
||||
searchResults = tempResults
|
||||
return tempResults
|
||||
} catch {
|
||||
toastModel?.toastDescription = "Error while scraping: \(error)"
|
||||
print("Error while scraping: \(error)")
|
||||
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
76
Ferrite/ViewModels/SourceManager.swift
Normal file
76
Ferrite/ViewModels/SourceManager.swift
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
//
|
||||
// SourceViewModel.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class SourceManager: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var availableSources: [TorrentSourceJson] = []
|
||||
|
||||
@MainActor
|
||||
public func fetchSourcesFromUrl() async {
|
||||
let sourceUrlRequest = TorrentSourceUrl.fetchRequest()
|
||||
do {
|
||||
let sourceUrls = try PersistenceController.shared.backgroundContext.fetch(sourceUrlRequest)
|
||||
var tempSourceUrls: [TorrentSourceJson] = []
|
||||
|
||||
for sourceUrl in sourceUrls {
|
||||
guard let url = URL(string: sourceUrl.urlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: url))
|
||||
let sourceResponse = try JSONDecoder().decode(SourceJson.self, from: data)
|
||||
|
||||
tempSourceUrls += sourceResponse.sources
|
||||
}
|
||||
|
||||
availableSources = tempSourceUrls
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
public func installSource(sourceJson: TorrentSourceJson) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// If a source exists, don't add the new one
|
||||
if let name = sourceJson.name {
|
||||
let existingSourceRequest = TorrentSource.fetchRequest()
|
||||
existingSourceRequest.predicate = NSPredicate(format: "name == %@", name)
|
||||
existingSourceRequest.fetchLimit = 1
|
||||
|
||||
let existingSource = try? backgroundContext.fetch(existingSourceRequest).first
|
||||
if existingSource != nil {
|
||||
Task { @MainActor in
|
||||
toastModel?.toastDescription = "Could not install source \(sourceJson.name ?? "Unknown source") because it is already installed."
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
let newTorrentSource = TorrentSource(context: backgroundContext)
|
||||
newTorrentSource.name = sourceJson.name
|
||||
newTorrentSource.url = sourceJson.url
|
||||
newTorrentSource.rowQuery = sourceJson.rowQuery
|
||||
newTorrentSource.linkQuery = sourceJson.linkQuery
|
||||
newTorrentSource.titleQuery = sourceJson.titleQuery
|
||||
newTorrentSource.sizeQuery = sourceJson.sizeQuery
|
||||
|
||||
newTorrentSource.enabled = true
|
||||
|
||||
do {
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
Task{ @MainActor in
|
||||
toastModel?.toastDescription = error.localizedDescription
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -13,6 +13,11 @@ struct ContentView: View {
|
|||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
@FetchRequest(
|
||||
entity: TorrentSource.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<TorrentSource>
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
VStack {
|
||||
|
|
@ -21,17 +26,8 @@ struct ContentView: View {
|
|||
.searchable(text: $scrapingModel.searchText)
|
||||
.onSubmit(of: .search) {
|
||||
Task {
|
||||
for source in scrapingModel.sources {
|
||||
guard let html = await scrapingModel.fetchWebsiteHtml(source: source) else {
|
||||
continue
|
||||
}
|
||||
|
||||
await scrapingModel.scrapeWebsite(source: source, html: html)
|
||||
|
||||
if realDebridEnabled {
|
||||
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
||||
}
|
||||
}
|
||||
await scrapingModel.scanSources(sources: sources)
|
||||
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import SwiftUI
|
|||
|
||||
enum Tab {
|
||||
case search
|
||||
case sources
|
||||
case settings
|
||||
}
|
||||
|
||||
|
|
@ -24,6 +25,13 @@ struct MainView: View {
|
|||
Label("Search", systemImage: "magnifyingglass")
|
||||
}
|
||||
.tag(Tab.search)
|
||||
|
||||
SourceListView()
|
||||
.tabItem {
|
||||
Label("Sources", systemImage: "doc.text")
|
||||
}
|
||||
.tag(Tab.sources)
|
||||
|
||||
SettingsView()
|
||||
.tabItem {
|
||||
Label("Settings", systemImage: "gear")
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ import SwiftUI
|
|||
struct SettingsView: View {
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
@State private var isProcessing = false
|
||||
|
|
@ -36,6 +38,10 @@ struct SettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section("Source management") {
|
||||
NavigationLink("Source lists", destination: SettingsSourceListView())
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $debridManager.showWebView) {
|
||||
LoginWebView(url: URL(string: debridManager.realDebridAuthUrl)!)
|
||||
|
|
|
|||
58
Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift
Normal file
58
Ferrite/Views/SettingsViews/SettingsSourceUrlView.swift
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
//
|
||||
// SettingsSourceUrlView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsSourceListView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@FetchRequest(
|
||||
entity: TorrentSourceUrl.entity(),
|
||||
sortDescriptors: []
|
||||
) var sourceUrls: FetchedResults<TorrentSourceUrl>
|
||||
|
||||
@State private var presentSourceSheet = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
ForEach(sourceUrls, id: \.self) { sourceUrl in
|
||||
Text(sourceUrl.repoName ?? "Unknown repo")
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let sourceUrl = sourceUrls[safe: index] {
|
||||
PersistenceController.shared.delete(sourceUrl, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $presentSourceSheet) {
|
||||
if #available(iOS 16, *) {
|
||||
SourceListEditorView()
|
||||
.presentationDetents([.medium])
|
||||
} else {
|
||||
SourceListEditorView()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Source lists")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
presentSourceSheet.toggle()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SettingsSourceListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SettingsSourceListView()
|
||||
}
|
||||
}
|
||||
97
Ferrite/Views/SettingsViews/SourceListEditorView.swift
Normal file
97
Ferrite/Views/SettingsViews/SourceListEditorView.swift
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
//
|
||||
// SourceListEditorView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SourceListEditorView: View {
|
||||
@Environment(\.dismiss) var dismiss
|
||||
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@State private var sourceUrl = ""
|
||||
@State private var urlErrorAlertText = ""
|
||||
@State private var showUrlErrorAlert = false
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Enter URL", text: $sourceUrl)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showUrlErrorAlert) {
|
||||
Alert(
|
||||
title: Text("Error"),
|
||||
message: Text(urlErrorAlertText),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
.navigationTitle("Editing source list")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
Button("Cancel") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
// Placing this function in the SourceManager causes the view to break on error. Place it here for now.
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Save") {
|
||||
Task {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
if sourceUrl.isEmpty || URL(string: sourceUrl) == nil {
|
||||
urlErrorAlertText = "The provided source list is invalid. Please check if the URL is formatted properly."
|
||||
showUrlErrorAlert.toggle()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let sourceUrlRequest = TorrentSourceUrl.fetchRequest()
|
||||
sourceUrlRequest.predicate = NSPredicate(format: "urlString == %@", sourceUrl)
|
||||
sourceUrlRequest.fetchLimit = 1
|
||||
|
||||
if let existingSourceUrl = try? backgroundContext.fetch(sourceUrlRequest).first {
|
||||
print("Existing source URL found")
|
||||
PersistenceController.shared.delete(existingSourceUrl, context: backgroundContext)
|
||||
}
|
||||
|
||||
let newSourceUrl = TorrentSourceUrl(context: backgroundContext)
|
||||
newSourceUrl.urlString = sourceUrl
|
||||
|
||||
do {
|
||||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
||||
if let rawResponse = try? JSONDecoder().decode(SourceJson.self, from: data) {
|
||||
newSourceUrl.repoName = rawResponse.repoName
|
||||
}
|
||||
|
||||
try backgroundContext.save()
|
||||
} catch {
|
||||
urlErrorAlertText = error.localizedDescription
|
||||
showUrlErrorAlert.toggle()
|
||||
}
|
||||
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceListEditorView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SourceListEditorView()
|
||||
}
|
||||
}
|
||||
79
Ferrite/Views/SourceListView.swift
Normal file
79
Ferrite/Views/SourceListView.swift
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
//
|
||||
// SourceListView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SourceListView: View {
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@FetchRequest(
|
||||
entity: TorrentSource.entity(),
|
||||
sortDescriptors: []
|
||||
) var sources: FetchedResults<TorrentSource>
|
||||
|
||||
@State private var availableSourceLength = 0
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
if !sources.isEmpty {
|
||||
Section("Installed") {
|
||||
ForEach(sources, id: \.self) { source in
|
||||
Toggle(isOn: Binding<Bool>(
|
||||
get: { source.enabled },
|
||||
set: {
|
||||
source.enabled = $0
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
})) {
|
||||
Text(source.name ?? "Unknown Source")
|
||||
}
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let source = sources[safe: index] {
|
||||
PersistenceController.shared.delete(source, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sourceManager.availableSources.contains(where: { avail in
|
||||
!sources.contains(where: { avail.name == $0.name })
|
||||
}) {
|
||||
Section("Catalog") {
|
||||
ForEach(sourceManager.availableSources, id: \.self) { availableSource in
|
||||
if !sources.contains(where: { availableSource.name == $0.name }) {
|
||||
HStack {
|
||||
Text(availableSource.name ?? "Unnamed source")
|
||||
|
||||
Spacer()
|
||||
|
||||
Button("Install") {
|
||||
sourceManager.installSource(sourceJson: availableSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.task {
|
||||
await sourceManager.fetchSourcesFromUrl()
|
||||
}
|
||||
.navigationTitle("Sources")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SourceListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SourceListView()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue