Ferrite: Add bookmarks
Bookmarks are added through search results and can be accessed through the library. These can be moved and deleted within the list. Add a RealDebrid instant availability cache for bookmark IA status to not overwhelm the API. Instant availability results are fresh on every search results since the cache is cleared. Also don't require a source API object to be present for the API parser button in source settings. If a JSON parser exists for a source, allow the option to be presented. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
5d97c7511f
commit
2f870b9410
23 changed files with 635 additions and 154 deletions
|
|
@ -16,6 +16,8 @@
|
|||
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; };
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
|
||||
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
|
||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; };
|
||||
|
|
@ -25,6 +27,8 @@
|
|||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; };
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; };
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; };
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
|
||||
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; };
|
||||
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; };
|
||||
|
|
@ -70,6 +74,11 @@
|
|||
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 */; };
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23328C2658700616D3A /* LibraryView.swift */; };
|
||||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23628C2660700616D3A /* HistoryView.swift */; };
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23828C2660D00616D3A /* BookmarksView.swift */; };
|
||||
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */; };
|
||||
0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */; };
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; };
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||
|
|
@ -93,12 +102,16 @@
|
|||
0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.swift; sourceTree = "<group>"; };
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = "<group>"; };
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; };
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = "<group>"; };
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = "<group>"; };
|
||||
0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = "<group>"; };
|
||||
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.swift; sourceTree = "<group>"; };
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = "<group>"; };
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = "<group>"; };
|
||||
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = "<group>"; };
|
||||
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = "<group>"; };
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = "<group>"; };
|
||||
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -142,6 +155,11 @@
|
|||
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>"; };
|
||||
0CA3B23328C2658700616D3A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = "<group>"; };
|
||||
0CA3B23628C2660700616D3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||
0CA3B23828C2660D00616D3A /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = "<group>"; };
|
||||
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataProperties.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; };
|
||||
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -176,6 +194,8 @@
|
|||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */,
|
||||
0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */,
|
||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */,
|
||||
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */,
|
||||
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */,
|
||||
|
|
@ -201,6 +221,7 @@
|
|||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -258,6 +279,7 @@
|
|||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
||||
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */,
|
||||
);
|
||||
path = CommonViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -279,6 +301,7 @@
|
|||
0C32FB542890D1BF002BD219 /* UIApplication.swift */,
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||
0C78041C28BFB3EA001E8CA3 /* String.swift */,
|
||||
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -286,6 +309,7 @@
|
|||
0CA148EE2889061200DE2211 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */,
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */,
|
||||
0CA148F02889062700DE2211 /* RepresentableViews */,
|
||||
0CA148C0288903F000DE2211 /* CommonViews */,
|
||||
|
|
@ -301,6 +325,8 @@
|
|||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */,
|
||||
0CA3B23328C2658700616D3A /* LibraryView.swift */,
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -334,6 +360,15 @@
|
|||
path = API;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
|
||||
0CA3B23628C2660700616D3A /* HistoryView.swift */,
|
||||
);
|
||||
path = LibraryViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CAF1C5F286F5C0D00296F86 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -454,8 +489,10 @@
|
|||
0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */,
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */,
|
||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */,
|
||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||
|
|
@ -468,7 +505,10 @@
|
|||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
|
||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
||||
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
|
||||
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
||||
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||
|
|
@ -479,20 +519,24 @@
|
|||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */,
|
||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||
0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */,
|
||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
|
||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */,
|
||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
||||
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -248,9 +248,21 @@ public class RealDebrid {
|
|||
}
|
||||
}
|
||||
|
||||
availableHashes.append(RealDebridIA(hash: hash, files: files, batches: batches))
|
||||
// TTL: 5 minutes
|
||||
availableHashes.append(
|
||||
RealDebridIA(
|
||||
hash: hash,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300,
|
||||
files: files,
|
||||
batches: batches)
|
||||
)
|
||||
} else {
|
||||
availableHashes.append(RealDebridIA(hash: hash))
|
||||
availableHashes.append(
|
||||
RealDebridIA(
|
||||
hash: hash,
|
||||
expiryTimeStamp: Date().timeIntervalSince1970 + 300
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
25
Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift
Normal file
25
Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// Bookmark+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(Bookmark)
|
||||
public class Bookmark: NSManagedObject {
|
||||
func toSearchResult() -> SearchResult {
|
||||
SearchResult(
|
||||
title: title,
|
||||
source: source,
|
||||
size: size,
|
||||
magnetLink: magnetLink,
|
||||
magnetHash: magnetHash,
|
||||
seeders: seeders,
|
||||
leechers: leechers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
//
|
||||
// Bookmark+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/3/22.
|
||||
//
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
|
||||
extension Bookmark {
|
||||
|
||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<Bookmark> {
|
||||
return NSFetchRequest<Bookmark>(entityName: "Bookmark")
|
||||
}
|
||||
|
||||
@NSManaged public var leechers: String?
|
||||
@NSManaged public var magnetHash: String?
|
||||
@NSManaged public var magnetLink: String?
|
||||
@NSManaged public var seeders: String?
|
||||
@NSManaged public var size: String?
|
||||
@NSManaged public var source: String
|
||||
@NSManaged public var title: String?
|
||||
@NSManaged public var orderNum: Int16
|
||||
|
||||
}
|
||||
|
||||
extension Bookmark : Identifiable {
|
||||
|
||||
}
|
||||
|
|
@ -1,5 +1,26 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="21277" systemVersion="21F79" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||
<entity name="Bookmark" representedClassName="Bookmark" syncable="YES">
|
||||
<attribute name="leechers" optional="YES" attributeType="String"/>
|
||||
<attribute name="magnetHash" optional="YES" attributeType="String"/>
|
||||
<attribute name="magnetLink" optional="YES" attributeType="String"/>
|
||||
<attribute name="orderNum" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="seeders" optional="YES" attributeType="String"/>
|
||||
<attribute name="size" optional="YES" attributeType="String"/>
|
||||
<attribute name="source" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="History" representedClassName="History" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="date" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="dateString" optional="YES" attributeType="String"/>
|
||||
<relationship name="entries" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="HistoryEntry" inverseName="parentHistory" inverseEntity="HistoryEntry"/>
|
||||
</entity>
|
||||
<entity name="HistoryEntry" representedClassName="HistoryEntry" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="timeStamp" optional="YES" attributeType="Double" defaultValueString="0.0" usesScalarValueType="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<relationship name="parentHistory" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="History" inverseName="entries" inverseEntity="History"/>
|
||||
</entity>
|
||||
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||
<attribute name="baseUrl" optional="YES" attributeType="String"/>
|
||||
|
|
|
|||
14
Ferrite/Extensions/NotificationCenter.swift
Normal file
14
Ferrite/Extensions/NotificationCenter.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// NotificationCenter.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/3/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Notification.Name {
|
||||
static var didDeleteBookmark: Notification.Name {
|
||||
return Notification.Name("Deleted bookmark")
|
||||
}
|
||||
}
|
||||
|
|
@ -36,4 +36,11 @@ extension View {
|
|||
func inlinedList() -> some View {
|
||||
modifier(InlinedList())
|
||||
}
|
||||
|
||||
func conditionalContextMenu<InternalContent: View, ID: Hashable>(
|
||||
id: ID,
|
||||
@ViewBuilder _ internalContent: @escaping () -> InternalContent
|
||||
) -> some View {
|
||||
modifier(ConditionalContextMenu(internalContent, id: id))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,148 +11,149 @@ import Foundation
|
|||
// MARK: - device code endpoint
|
||||
|
||||
public struct DeviceCodeResponse: Codable {
|
||||
let deviceCode, userCode: String
|
||||
let interval, expiresIn: Int
|
||||
let verificationURL, directVerificationURL: String
|
||||
let deviceCode, userCode: String
|
||||
let interval, expiresIn: Int
|
||||
let verificationURL, directVerificationURL: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deviceCode = "device_code"
|
||||
case userCode = "user_code"
|
||||
case interval
|
||||
case expiresIn = "expires_in"
|
||||
case verificationURL = "verification_url"
|
||||
case directVerificationURL = "direct_verification_url"
|
||||
}
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case deviceCode = "device_code"
|
||||
case userCode = "user_code"
|
||||
case interval
|
||||
case expiresIn = "expires_in"
|
||||
case verificationURL = "verification_url"
|
||||
case directVerificationURL = "direct_verification_url"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - device credentials endpoint
|
||||
|
||||
public struct DeviceCredentialsResponse: Codable {
|
||||
let clientID, clientSecret: String?
|
||||
let clientID, clientSecret: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientID = "client_id"
|
||||
case clientSecret = "client_secret"
|
||||
}
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case clientID = "client_id"
|
||||
case clientSecret = "client_secret"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - token endpoint
|
||||
|
||||
public struct TokenResponse: Codable {
|
||||
let accessToken: String
|
||||
let expiresIn: Int
|
||||
let refreshToken, tokenType: String
|
||||
let accessToken: String
|
||||
let expiresIn: Int
|
||||
let refreshToken, tokenType: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case expiresIn = "expires_in"
|
||||
case refreshToken = "refresh_token"
|
||||
case tokenType = "token_type"
|
||||
}
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case expiresIn = "expires_in"
|
||||
case refreshToken = "refresh_token"
|
||||
case tokenType = "token_type"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - instantAvailability endpoint
|
||||
|
||||
// Thanks Skitty!
|
||||
public struct InstantAvailabilityResponse: Codable {
|
||||
var data: InstantAvailabilityData?
|
||||
var data: InstantAvailabilityData?
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
public init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
|
||||
if let data = try? container.decode(InstantAvailabilityData.self) {
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
if let data = try? container.decode(InstantAvailabilityData.self) {
|
||||
self.data = data
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InstantAvailabilityData: Codable {
|
||||
var rd: [[String: InstantAvailabilityInfo]]
|
||||
var rd: [[String: InstantAvailabilityInfo]]
|
||||
}
|
||||
|
||||
struct InstantAvailabilityInfo: Codable {
|
||||
var filename: String
|
||||
var filesize: Int
|
||||
var filename: String
|
||||
var filesize: Int
|
||||
}
|
||||
|
||||
// MARK: - Instant Availability client side structures
|
||||
|
||||
public struct RealDebridIA: Codable, Hashable {
|
||||
let hash: String
|
||||
var files: [RealDebridIAFile] = []
|
||||
var batches: [RealDebridIABatch] = []
|
||||
let hash: String
|
||||
let expiryTimeStamp: Double
|
||||
var files: [RealDebridIAFile] = []
|
||||
var batches: [RealDebridIABatch] = []
|
||||
}
|
||||
|
||||
public struct RealDebridIABatch: Codable, Hashable {
|
||||
let files: [RealDebridIABatchFile]
|
||||
let files: [RealDebridIABatchFile]
|
||||
}
|
||||
|
||||
public struct RealDebridIABatchFile: Codable, Hashable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
|
||||
public struct RealDebridIAFile: Codable, Hashable {
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
}
|
||||
|
||||
public enum RealDebridIAStatus: Codable, Hashable {
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
}
|
||||
|
||||
// MARK: - addMagnet endpoint
|
||||
|
||||
public struct AddMagnetResponse: Codable {
|
||||
let id: String
|
||||
let uri: String
|
||||
let id: String
|
||||
let uri: String
|
||||
}
|
||||
|
||||
// MARK: - torrentInfo endpoint
|
||||
|
||||
struct TorrentInfoResponse: Codable {
|
||||
let id, filename, originalFilename, hash: String
|
||||
let bytes, originalBytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let files: [TorrentInfoFile]
|
||||
let links: [String]
|
||||
let ended: String
|
||||
let id, filename, originalFilename, hash: String
|
||||
let bytes, originalBytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let files: [TorrentInfoFile]
|
||||
let links: [String]
|
||||
let ended: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename
|
||||
case originalFilename = "original_filename"
|
||||
case hash, bytes
|
||||
case originalBytes = "original_bytes"
|
||||
case host, split, progress, status, added, files, links, ended
|
||||
}
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename
|
||||
case originalFilename = "original_filename"
|
||||
case hash, bytes
|
||||
case originalBytes = "original_bytes"
|
||||
case host, split, progress, status, added, files, links, ended
|
||||
}
|
||||
}
|
||||
|
||||
struct TorrentInfoFile: Codable {
|
||||
let id: Int
|
||||
let path: String
|
||||
let bytes, selected: Int
|
||||
let id: Int
|
||||
let path: String
|
||||
let bytes, selected: Int
|
||||
}
|
||||
|
||||
// MARK: - unrestrictLink endpoint
|
||||
|
||||
struct UnrestrictLinkResponse: Codable {
|
||||
let id, filename, mimeType: String
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks, crc: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
let id, filename, mimeType: String
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks, crc: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, crc, download, streamable
|
||||
}
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, crc, download, streamable
|
||||
}
|
||||
}
|
||||
|
|
|
|||
18
Ferrite/Models/SearchModels.swift
Normal file
18
Ferrite/Models/SearchModels.swift
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
//
|
||||
// SearchModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct SearchResult: Hashable, Codable {
|
||||
let title: String?
|
||||
let source: String
|
||||
let size: String?
|
||||
let magnetLink: String?
|
||||
let magnetHash: String?
|
||||
let seeders: String?
|
||||
let leechers: String?
|
||||
}
|
||||
|
|
@ -10,8 +10,11 @@ import SwiftUI
|
|||
|
||||
@MainActor
|
||||
public class DebridManager: ObservableObject {
|
||||
// UI Variables
|
||||
// Linked classes
|
||||
var toastModel: ToastViewModel?
|
||||
let realDebrid: RealDebrid = .init()
|
||||
|
||||
// UI Variables
|
||||
@Published var showWebView: Bool = false
|
||||
@Published var showLoadingProgress: Bool = false
|
||||
|
||||
|
|
@ -19,8 +22,6 @@ public class DebridManager: ObservableObject {
|
|||
@Published var currentDebridTask: Task<Void, Never>?
|
||||
|
||||
// RealDebrid auth variables
|
||||
let realDebrid: RealDebrid = .init()
|
||||
|
||||
@Published var realDebridEnabled: Bool = false {
|
||||
didSet {
|
||||
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
|
||||
|
|
@ -31,7 +32,7 @@ public class DebridManager: ObservableObject {
|
|||
@Published var realDebridAuthUrl: String = ""
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridHashes: [RealDebridIA] = []
|
||||
@Published var realDebridIAValues: [RealDebridIA] = []
|
||||
@Published var realDebridDownloadUrl: String = ""
|
||||
@Published var selectedRealDebridItem: RealDebridIA?
|
||||
@Published var selectedRealDebridFile: RealDebridIAFile?
|
||||
|
|
@ -40,19 +41,30 @@ public class DebridManager: ObservableObject {
|
|||
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||
}
|
||||
|
||||
public func populateDebridHashes(_ searchResults: [SearchResult]) async {
|
||||
var hashes: [String] = []
|
||||
|
||||
for result in searchResults {
|
||||
if let hash = result.magnetHash {
|
||||
hashes.append(hash)
|
||||
}
|
||||
}
|
||||
|
||||
public func populateDebridHashes(_ resultHashes: [String]) async {
|
||||
do {
|
||||
let debridHashes = try await realDebrid.instantAvailability(magnetHashes: hashes)
|
||||
let now = Date()
|
||||
|
||||
realDebridHashes = debridHashes
|
||||
// If a hash isn't found in the IA, update it
|
||||
// If the hash is expired, remove it and update it
|
||||
let sendHashes = resultHashes.filter { hash in
|
||||
if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }) {
|
||||
if now.timeIntervalSince1970 > realDebridIAValues[IAIndex].expiryTimeStamp {
|
||||
realDebridIAValues.remove(at: IAIndex)
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if !sendHashes.isEmpty {
|
||||
let fetchedIAValues = try await realDebrid.instantAvailability(magnetHashes: sendHashes)
|
||||
|
||||
realDebridIAValues += fetchedIAValues
|
||||
}
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
|
||||
|
|
@ -69,7 +81,7 @@ public class DebridManager: ObservableObject {
|
|||
return .none
|
||||
}
|
||||
|
||||
guard let debridMatch = realDebridHashes.first(where: { result.magnetHash == $0.hash }) else {
|
||||
guard let debridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else {
|
||||
return .none
|
||||
}
|
||||
|
||||
|
|
@ -86,7 +98,7 @@ public class DebridManager: ObservableObject {
|
|||
return false
|
||||
}
|
||||
|
||||
if let realDebridItem = realDebridHashes.first(where: { magnetHash == $0.hash }) {
|
||||
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) {
|
||||
selectedRealDebridItem = realDebridItem
|
||||
return true
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ enum ViewTab {
|
|||
case search
|
||||
case sources
|
||||
case settings
|
||||
case library
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
@ -31,6 +32,8 @@ class NavigationViewModel: ObservableObject {
|
|||
@Published var isEditingSearch: Bool = false
|
||||
@Published var isSearching: Bool = false
|
||||
|
||||
@Published var selectedSearchResult: SearchResult?
|
||||
|
||||
@Published var hideNavigationBar = false
|
||||
|
||||
@Published var currentChoiceSheet: ChoiceSheetType?
|
||||
|
|
@ -86,11 +89,18 @@ class NavigationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func runMagnetAction(action: DefaultMagnetActionType?, searchResult: SearchResult) {
|
||||
public func runMagnetAction(_ action: DefaultMagnetActionType? = nil) {
|
||||
guard let searchResult = selectedSearchResult else {
|
||||
toastModel?.updateToastDescription("Magnet action error: A search result was not selected.")
|
||||
print("Magnet action error: A search result was not selected.")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let selectedAction = action ?? defaultMagnetAction
|
||||
|
||||
guard let magnetLink = searchResult.magnetLink else {
|
||||
toastModel?.toastDescription = "Could not run your action because the magnet link is invalid."
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("Magnet action error: The magnet link is invalid.")
|
||||
|
||||
return
|
||||
|
|
|
|||
|
|
@ -11,16 +11,6 @@ import SwiftSoup
|
|||
import SwiftUI
|
||||
import SwiftyJSON
|
||||
|
||||
public struct SearchResult: Hashable, Codable {
|
||||
let title: String?
|
||||
let source: String
|
||||
let size: String?
|
||||
let magnetLink: String?
|
||||
let magnetHash: String?
|
||||
let seeders: String?
|
||||
let leechers: String?
|
||||
}
|
||||
|
||||
class ScrapingViewModel: ObservableObject {
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
|
|
@ -31,7 +21,6 @@ class ScrapingViewModel: ObservableObject {
|
|||
@Published var runningSearchTask: Task<Void, Error>?
|
||||
@Published var searchResults: [SearchResult] = []
|
||||
@Published var searchText: String = ""
|
||||
@Published var selectedSearchResult: SearchResult?
|
||||
@Published var filteredSource: Source?
|
||||
@Published var currentSourceName: String?
|
||||
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ struct BatchChoiceView: View {
|
|||
Button(file.name) {
|
||||
debridManager.selectedRealDebridFile = file
|
||||
|
||||
if let searchResult = scrapingModel.selectedSearchResult {
|
||||
if let searchResult = navModel.selectedSearchResult {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file)
|
||||
|
||||
|
|
|
|||
39
Ferrite/Views/CommonViews/ConditionalContextMenu.swift
Normal file
39
Ferrite/Views/CommonViews/ConditionalContextMenu.swift
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
//
|
||||
// ConditionalContextMenu.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/3/22.
|
||||
//
|
||||
// Used as a workaround for iOS 15 not updating context views with conditional variables
|
||||
// A stateful ID is required for the contextMenu to update itself.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ConditionalContextMenu<InternalContent: View, ID: Hashable>: ViewModifier {
|
||||
let internalContent: () -> InternalContent
|
||||
let id: ID
|
||||
|
||||
init(@ViewBuilder _ internalContent: @escaping () -> InternalContent, id: ID) {
|
||||
self.internalContent = internalContent
|
||||
self.id = id
|
||||
}
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
.contextMenu {
|
||||
internalContent()
|
||||
}
|
||||
} else {
|
||||
content
|
||||
.background {
|
||||
Color.clear
|
||||
.contextMenu {
|
||||
internalContent()
|
||||
}
|
||||
.id(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -110,7 +110,11 @@ struct ContentView: View {
|
|||
await scrapingModel.scanSources(sources: sources)
|
||||
|
||||
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
|
||||
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
||||
debridManager.realDebridIAValues = []
|
||||
|
||||
await debridManager.populateDebridHashes(
|
||||
scrapingModel.searchResults.compactMap(\.magnetHash)
|
||||
)
|
||||
}
|
||||
|
||||
navModel.showSearchProgress = false
|
||||
|
|
|
|||
83
Ferrite/Views/LibraryView.swift
Normal file
83
Ferrite/Views/LibraryView.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
//
|
||||
// Library.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LibraryView: View {
|
||||
enum LibraryPickerSegment {
|
||||
case bookmarks
|
||||
case history
|
||||
}
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@FetchRequest(
|
||||
entity: Bookmark.entity(),
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true)
|
||||
]
|
||||
) var bookmarks: FetchedResults<Bookmark>
|
||||
|
||||
@State private var historyEmpty = true
|
||||
|
||||
@State private var selectedSegment: LibraryPickerSegment = .bookmarks
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
VStack(spacing: 0) {
|
||||
Picker("Segments", selection: $selectedSegment) {
|
||||
Text("Bookmarks").tag(LibraryPickerSegment.bookmarks)
|
||||
Text("History").tag(LibraryPickerSegment.history)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal)
|
||||
.padding(.top)
|
||||
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
BookmarksView(bookmarks: bookmarks)
|
||||
case .history:
|
||||
HistoryView()
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.overlay {
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
if bookmarks.isEmpty {
|
||||
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
|
||||
}
|
||||
case .history:
|
||||
if historyEmpty {
|
||||
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Library")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
EditButton()
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
.onChange(of: selectedSegment) { _ in
|
||||
editMode = .inactive
|
||||
}
|
||||
.onDisappear {
|
||||
editMode = .inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LibraryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LibraryView()
|
||||
}
|
||||
}
|
||||
67
Ferrite/Views/LibraryViews/BookmarksView.swift
Normal file
67
Ferrite/Views/LibraryViews/BookmarksView.swift
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
//
|
||||
// BookmarksView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BookmarksView: View {
|
||||
@Environment(\.verticalSizeClass) var verticalSizeClass
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var bookmarks: FetchedResults<Bookmark>
|
||||
|
||||
@State private var viewTask: Task<Void, Never>?
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if !bookmarks.isEmpty {
|
||||
List {
|
||||
ForEach(bookmarks, id: \.self) { bookmark in
|
||||
SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let bookmark = bookmarks[safe: index] {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onMove { (source, destination) in
|
||||
var changedBookmarks = bookmarks.map { $0 }
|
||||
|
||||
changedBookmarks.move(fromOffsets: source, toOffset: destination)
|
||||
|
||||
for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) {
|
||||
changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex)
|
||||
}
|
||||
|
||||
PersistenceController.shared.save()
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.onAppear {
|
||||
if realDebridEnabled {
|
||||
viewTask = Task {
|
||||
let hashes = bookmarks.compactMap { $0.magnetHash }
|
||||
await debridManager.populateDebridHashes(hashes)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
Ferrite/Views/LibraryViews/HistoryView.swift
Normal file
22
Ferrite/Views/LibraryViews/HistoryView.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
//
|
||||
// HistoryView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
var body: some View {
|
||||
ZStack {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryView()
|
||||
}
|
||||
}
|
||||
|
|
@ -23,7 +23,7 @@ struct MagnetChoiceView: View {
|
|||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
if realDebridEnabled, debridManager.matchSearchResult(result: scrapingModel.selectedSearchResult) != .none {
|
||||
if realDebridEnabled, debridManager.matchSearchResult(result: navModel.selectedSearchResult) != .none {
|
||||
Section(header: "Real Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl)
|
||||
|
|
@ -60,7 +60,7 @@ struct MagnetChoiceView: View {
|
|||
|
||||
Section(header: "Magnet options") {
|
||||
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = scrapingModel.selectedSearchResult?.magnetLink
|
||||
UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink
|
||||
showMagnetCopyAlert.toggle()
|
||||
}
|
||||
.alert(isPresented: $showMagnetCopyAlert) {
|
||||
|
|
@ -72,7 +72,7 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
|
||||
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
||||
if let result = scrapingModel.selectedSearchResult,
|
||||
if let result = navModel.selectedSearchResult,
|
||||
let magnetLink = result.magnetLink,
|
||||
let url = URL(string: magnetLink)
|
||||
{
|
||||
|
|
@ -82,9 +82,7 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
|
||||
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
||||
if let result = scrapingModel.selectedSearchResult {
|
||||
navModel.runMagnetAction(action: .webtor, searchResult: result)
|
||||
}
|
||||
navModel.runMagnetAction(.webtor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,12 @@ struct MainView: View {
|
|||
}
|
||||
.tag(ViewTab.search)
|
||||
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "book.closed")
|
||||
}
|
||||
.tag(ViewTab.library)
|
||||
|
||||
SourcesView()
|
||||
.tabItem {
|
||||
Label("Sources", systemImage: "doc.text")
|
||||
|
|
|
|||
109
Ferrite/Views/SearchResultButtonView.swift
Normal file
109
Ferrite/Views/SearchResultButtonView.swift
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
//
|
||||
// SearchResultButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// BUG: iOS 15 cannot refresh the context menu. Debating using swipe actions or adopting a workaround.
|
||||
struct SearchResultButtonView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
@State private var runOnce = false
|
||||
@State var existingBookmark: Bookmark? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
if debridManager.currentDebridTask == nil {
|
||||
navModel.selectedSearchResult = 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)
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
navModel.runMagnetAction()
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(result.title ?? "No title")
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.padding(.bottom, 5)
|
||||
.conditionalContextMenu(id: existingBookmark) {
|
||||
if let bookmark = existingBookmark {
|
||||
Button {
|
||||
PersistenceController.shared.delete(bookmark, context: backgroundContext)
|
||||
|
||||
// When the entity is deleted, let other instances know to remove that reference
|
||||
NotificationCenter.default.post(name: .didDeleteBookmark, object: nil)
|
||||
} label: {
|
||||
Text("Remove bookmark")
|
||||
Image(systemName: "bookmark.slash.fill")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
let newBookmark = Bookmark(context: backgroundContext)
|
||||
newBookmark.title = result.title
|
||||
newBookmark.source = result.source
|
||||
newBookmark.magnetHash = result.magnetHash
|
||||
newBookmark.magnetLink = result.magnetLink
|
||||
newBookmark.seeders = result.seeders
|
||||
newBookmark.leechers = result.leechers
|
||||
|
||||
existingBookmark = newBookmark
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
} label: {
|
||||
Text("Bookmark")
|
||||
Image(systemName: "bookmark")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SearchResultRDView(result: result)
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in
|
||||
existingBookmark = nil
|
||||
}
|
||||
.onAppear {
|
||||
// Only run a exists request if a bookmark isn't passed to the view
|
||||
if existingBookmark == nil && !runOnce {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
bookmarkRequest.predicate = NSPredicate(
|
||||
format: "title == %@ AND source == %@ AND magnetLink == %@ AND magnetHash = %@",
|
||||
result.title ?? "",
|
||||
result.source,
|
||||
result.magnetLink ?? "",
|
||||
result.magnetHash ?? ""
|
||||
)
|
||||
bookmarkRequest.fetchLimit = 1
|
||||
|
||||
if let fetchedBookmark = try? backgroundContext.fetch(bookmarkRequest).first {
|
||||
existingBookmark = fetchedBookmark
|
||||
}
|
||||
|
||||
runOnce = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,7 +9,6 @@ import SwiftUI
|
|||
|
||||
struct SearchResultsView: View {
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
|
@ -18,38 +17,7 @@ struct SearchResultsView: View {
|
|||
List {
|
||||
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
||||
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
||||
VStack(alignment: .leading) {
|
||||
Button {
|
||||
if debridManager.currentDebridTask == nil {
|
||||
scrapingModel.selectedSearchResult = 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)
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
navModel.runMagnetAction(action: nil, searchResult: result)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
Text(result.title ?? "No title")
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.padding(.bottom, 5)
|
||||
|
||||
SearchResultRDView(result: result)
|
||||
}
|
||||
SearchResultButtonView(result: result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ struct SourceSettingsMethodView: View {
|
|||
|
||||
var body: some View {
|
||||
Section(header: InlineHeader("Fetch method")) {
|
||||
if selectedSource.api != nil, selectedSource.jsonParser != nil {
|
||||
if selectedSource.jsonParser != nil {
|
||||
Button {
|
||||
selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue
|
||||
} label: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue