mirror of
https://github.com/Ferrite-iOS/Ferrite.git
synced 2026-04-21 00:42:07 +00:00
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 */; };
|
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; };
|
||||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
||||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.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 */; };
|
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; };
|
||||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; };
|
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 */; };
|
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 */; };
|
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; };
|
||||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
|
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; };
|
||||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.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 */; };
|
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; };
|
||||||
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; };
|
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; };
|
||||||
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; };
|
0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; };
|
||||||
|
|
@ -70,6 +74,11 @@
|
||||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D1288903F000DE2211 /* MainView.swift */; };
|
0CA148E9288903F000DE2211 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D1288903F000DE2211 /* MainView.swift */; };
|
||||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; };
|
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; };
|
||||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.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 */; };
|
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; };
|
||||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -176,6 +194,8 @@
|
||||||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */,
|
||||||
|
0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */,
|
||||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */,
|
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */,
|
||||||
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */,
|
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */,
|
||||||
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */,
|
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */,
|
||||||
|
|
@ -201,6 +221,7 @@
|
||||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||||
|
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -258,6 +279,7 @@
|
||||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
||||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
||||||
|
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */,
|
||||||
);
|
);
|
||||||
path = CommonViews;
|
path = CommonViews;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -279,6 +301,7 @@
|
||||||
0C32FB542890D1BF002BD219 /* UIApplication.swift */,
|
0C32FB542890D1BF002BD219 /* UIApplication.swift */,
|
||||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||||
0C78041C28BFB3EA001E8CA3 /* String.swift */,
|
0C78041C28BFB3EA001E8CA3 /* String.swift */,
|
||||||
|
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -286,6 +309,7 @@
|
||||||
0CA148EE2889061200DE2211 /* Views */ = {
|
0CA148EE2889061200DE2211 /* Views */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
0CA3B23528C265FD00616D3A /* LibraryViews */,
|
||||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */,
|
0C794B65289DAC9F00DD1CC8 /* SourceViews */,
|
||||||
0CA148F02889062700DE2211 /* RepresentableViews */,
|
0CA148F02889062700DE2211 /* RepresentableViews */,
|
||||||
0CA148C0288903F000DE2211 /* CommonViews */,
|
0CA148C0288903F000DE2211 /* CommonViews */,
|
||||||
|
|
@ -301,6 +325,8 @@
|
||||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
||||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||||
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */,
|
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */,
|
||||||
|
0CA3B23328C2658700616D3A /* LibraryView.swift */,
|
||||||
|
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||||
);
|
);
|
||||||
path = Views;
|
path = Views;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
|
@ -334,6 +360,15 @@
|
||||||
path = API;
|
path = API;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
0CA3B23528C265FD00616D3A /* LibraryViews */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
|
||||||
|
0CA3B23628C2660700616D3A /* HistoryView.swift */,
|
||||||
|
);
|
||||||
|
path = LibraryViews;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
0CAF1C5F286F5C0D00296F86 = {
|
0CAF1C5F286F5C0D00296F86 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
|
@ -454,8 +489,10 @@
|
||||||
0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */,
|
0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */,
|
||||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
|
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
|
||||||
|
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */,
|
||||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
0CA148DB288903F000DE2211 /* NavView.swift in Sources */,
|
||||||
|
0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */,
|
||||||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */,
|
||||||
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */,
|
||||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */,
|
||||||
|
|
@ -468,7 +505,10 @@
|
||||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */,
|
||||||
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
|
0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */,
|
||||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */,
|
||||||
|
0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */,
|
||||||
|
0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */,
|
||||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */,
|
||||||
|
0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */,
|
||||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
||||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||||
|
|
@ -479,20 +519,24 @@
|
||||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */,
|
||||||
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */,
|
||||||
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */,
|
||||||
|
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */,
|
||||||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
||||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||||
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */,
|
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */,
|
||||||
|
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
||||||
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
0CA148E6288903F000DE2211 /* WebView.swift in Sources */,
|
||||||
|
0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */,
|
||||||
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
|
0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */,
|
||||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */,
|
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */,
|
||||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
||||||
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */,
|
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */,
|
||||||
|
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift 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 {
|
} 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"?>
|
<?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="">
|
<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">
|
<entity name="Source" representedClassName="Source" syncable="YES">
|
||||||
<attribute name="author" attributeType="String" defaultValueString=""/>
|
<attribute name="author" attributeType="String" defaultValueString=""/>
|
||||||
<attribute name="baseUrl" optional="YES" attributeType="String"/>
|
<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 {
|
func inlinedList() -> some View {
|
||||||
modifier(InlinedList())
|
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
|
// MARK: - device code endpoint
|
||||||
|
|
||||||
public struct DeviceCodeResponse: Codable {
|
public struct DeviceCodeResponse: Codable {
|
||||||
let deviceCode, userCode: String
|
let deviceCode, userCode: String
|
||||||
let interval, expiresIn: Int
|
let interval, expiresIn: Int
|
||||||
let verificationURL, directVerificationURL: String
|
let verificationURL, directVerificationURL: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case deviceCode = "device_code"
|
case deviceCode = "device_code"
|
||||||
case userCode = "user_code"
|
case userCode = "user_code"
|
||||||
case interval
|
case interval
|
||||||
case expiresIn = "expires_in"
|
case expiresIn = "expires_in"
|
||||||
case verificationURL = "verification_url"
|
case verificationURL = "verification_url"
|
||||||
case directVerificationURL = "direct_verification_url"
|
case directVerificationURL = "direct_verification_url"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - device credentials endpoint
|
// MARK: - device credentials endpoint
|
||||||
|
|
||||||
public struct DeviceCredentialsResponse: Codable {
|
public struct DeviceCredentialsResponse: Codable {
|
||||||
let clientID, clientSecret: String?
|
let clientID, clientSecret: String?
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case clientID = "client_id"
|
case clientID = "client_id"
|
||||||
case clientSecret = "client_secret"
|
case clientSecret = "client_secret"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - token endpoint
|
// MARK: - token endpoint
|
||||||
|
|
||||||
public struct TokenResponse: Codable {
|
public struct TokenResponse: Codable {
|
||||||
let accessToken: String
|
let accessToken: String
|
||||||
let expiresIn: Int
|
let expiresIn: Int
|
||||||
let refreshToken, tokenType: String
|
let refreshToken, tokenType: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case accessToken = "access_token"
|
case accessToken = "access_token"
|
||||||
case expiresIn = "expires_in"
|
case expiresIn = "expires_in"
|
||||||
case refreshToken = "refresh_token"
|
case refreshToken = "refresh_token"
|
||||||
case tokenType = "token_type"
|
case tokenType = "token_type"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - instantAvailability endpoint
|
// MARK: - instantAvailability endpoint
|
||||||
|
|
||||||
// Thanks Skitty!
|
// Thanks Skitty!
|
||||||
public struct InstantAvailabilityResponse: Codable {
|
public struct InstantAvailabilityResponse: Codable {
|
||||||
var data: InstantAvailabilityData?
|
var data: InstantAvailabilityData?
|
||||||
|
|
||||||
public init(from decoder: Decoder) throws {
|
public init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
|
|
||||||
if let data = try? container.decode(InstantAvailabilityData.self) {
|
if let data = try? container.decode(InstantAvailabilityData.self) {
|
||||||
self.data = data
|
self.data = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstantAvailabilityData: Codable {
|
struct InstantAvailabilityData: Codable {
|
||||||
var rd: [[String: InstantAvailabilityInfo]]
|
var rd: [[String: InstantAvailabilityInfo]]
|
||||||
}
|
}
|
||||||
|
|
||||||
struct InstantAvailabilityInfo: Codable {
|
struct InstantAvailabilityInfo: Codable {
|
||||||
var filename: String
|
var filename: String
|
||||||
var filesize: Int
|
var filesize: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Instant Availability client side structures
|
// MARK: - Instant Availability client side structures
|
||||||
|
|
||||||
public struct RealDebridIA: Codable, Hashable {
|
public struct RealDebridIA: Codable, Hashable {
|
||||||
let hash: String
|
let hash: String
|
||||||
var files: [RealDebridIAFile] = []
|
let expiryTimeStamp: Double
|
||||||
var batches: [RealDebridIABatch] = []
|
var files: [RealDebridIAFile] = []
|
||||||
|
var batches: [RealDebridIABatch] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RealDebridIABatch: Codable, Hashable {
|
public struct RealDebridIABatch: Codable, Hashable {
|
||||||
let files: [RealDebridIABatchFile]
|
let files: [RealDebridIABatchFile]
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RealDebridIABatchFile: Codable, Hashable {
|
public struct RealDebridIABatchFile: Codable, Hashable {
|
||||||
let id: Int
|
let id: Int
|
||||||
let fileName: String
|
let fileName: String
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct RealDebridIAFile: Codable, Hashable {
|
public struct RealDebridIAFile: Codable, Hashable {
|
||||||
let name: String
|
let name: String
|
||||||
let batchIndex: Int
|
let batchIndex: Int
|
||||||
let batchFileIndex: Int
|
let batchFileIndex: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum RealDebridIAStatus: Codable, Hashable {
|
public enum RealDebridIAStatus: Codable, Hashable {
|
||||||
case full
|
case full
|
||||||
case partial
|
case partial
|
||||||
case none
|
case none
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - addMagnet endpoint
|
// MARK: - addMagnet endpoint
|
||||||
|
|
||||||
public struct AddMagnetResponse: Codable {
|
public struct AddMagnetResponse: Codable {
|
||||||
let id: String
|
let id: String
|
||||||
let uri: String
|
let uri: String
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - torrentInfo endpoint
|
// MARK: - torrentInfo endpoint
|
||||||
|
|
||||||
struct TorrentInfoResponse: Codable {
|
struct TorrentInfoResponse: Codable {
|
||||||
let id, filename, originalFilename, hash: String
|
let id, filename, originalFilename, hash: String
|
||||||
let bytes, originalBytes: Int
|
let bytes, originalBytes: Int
|
||||||
let host: String
|
let host: String
|
||||||
let split, progress: Int
|
let split, progress: Int
|
||||||
let status, added: String
|
let status, added: String
|
||||||
let files: [TorrentInfoFile]
|
let files: [TorrentInfoFile]
|
||||||
let links: [String]
|
let links: [String]
|
||||||
let ended: String
|
let ended: String
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, filename
|
case id, filename
|
||||||
case originalFilename = "original_filename"
|
case originalFilename = "original_filename"
|
||||||
case hash, bytes
|
case hash, bytes
|
||||||
case originalBytes = "original_bytes"
|
case originalBytes = "original_bytes"
|
||||||
case host, split, progress, status, added, files, links, ended
|
case host, split, progress, status, added, files, links, ended
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TorrentInfoFile: Codable {
|
struct TorrentInfoFile: Codable {
|
||||||
let id: Int
|
let id: Int
|
||||||
let path: String
|
let path: String
|
||||||
let bytes, selected: Int
|
let bytes, selected: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - unrestrictLink endpoint
|
// MARK: - unrestrictLink endpoint
|
||||||
|
|
||||||
struct UnrestrictLinkResponse: Codable {
|
struct UnrestrictLinkResponse: Codable {
|
||||||
let id, filename, mimeType: String
|
let id, filename, mimeType: String
|
||||||
let filesize: Int
|
let filesize: Int
|
||||||
let link: String
|
let link: String
|
||||||
let host: String
|
let host: String
|
||||||
let hostIcon: String
|
let hostIcon: String
|
||||||
let chunks, crc: Int
|
let chunks, crc: Int
|
||||||
let download: String
|
let download: String
|
||||||
let streamable: Int
|
let streamable: Int
|
||||||
|
|
||||||
enum CodingKeys: String, CodingKey {
|
enum CodingKeys: String, CodingKey {
|
||||||
case id, filename, mimeType, filesize, link, host
|
case id, filename, mimeType, filesize, link, host
|
||||||
case hostIcon = "host_icon"
|
case hostIcon = "host_icon"
|
||||||
case chunks, crc, download, streamable
|
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
|
@MainActor
|
||||||
public class DebridManager: ObservableObject {
|
public class DebridManager: ObservableObject {
|
||||||
// UI Variables
|
// Linked classes
|
||||||
var toastModel: ToastViewModel?
|
var toastModel: ToastViewModel?
|
||||||
|
let realDebrid: RealDebrid = .init()
|
||||||
|
|
||||||
|
// UI Variables
|
||||||
@Published var showWebView: Bool = false
|
@Published var showWebView: Bool = false
|
||||||
@Published var showLoadingProgress: Bool = false
|
@Published var showLoadingProgress: Bool = false
|
||||||
|
|
||||||
|
|
@ -19,8 +22,6 @@ public class DebridManager: ObservableObject {
|
||||||
@Published var currentDebridTask: Task<Void, Never>?
|
@Published var currentDebridTask: Task<Void, Never>?
|
||||||
|
|
||||||
// RealDebrid auth variables
|
// RealDebrid auth variables
|
||||||
let realDebrid: RealDebrid = .init()
|
|
||||||
|
|
||||||
@Published var realDebridEnabled: Bool = false {
|
@Published var realDebridEnabled: Bool = false {
|
||||||
didSet {
|
didSet {
|
||||||
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
|
UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled")
|
||||||
|
|
@ -31,7 +32,7 @@ public class DebridManager: ObservableObject {
|
||||||
@Published var realDebridAuthUrl: String = ""
|
@Published var realDebridAuthUrl: String = ""
|
||||||
|
|
||||||
// RealDebrid fetch variables
|
// RealDebrid fetch variables
|
||||||
@Published var realDebridHashes: [RealDebridIA] = []
|
@Published var realDebridIAValues: [RealDebridIA] = []
|
||||||
@Published var realDebridDownloadUrl: String = ""
|
@Published var realDebridDownloadUrl: String = ""
|
||||||
@Published var selectedRealDebridItem: RealDebridIA?
|
@Published var selectedRealDebridItem: RealDebridIA?
|
||||||
@Published var selectedRealDebridFile: RealDebridIAFile?
|
@Published var selectedRealDebridFile: RealDebridIAFile?
|
||||||
|
|
@ -40,19 +41,30 @@ public class DebridManager: ObservableObject {
|
||||||
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled")
|
||||||
}
|
}
|
||||||
|
|
||||||
public func populateDebridHashes(_ searchResults: [SearchResult]) async {
|
public func populateDebridHashes(_ resultHashes: [String]) async {
|
||||||
var hashes: [String] = []
|
|
||||||
|
|
||||||
for result in searchResults {
|
|
||||||
if let hash = result.magnetHash {
|
|
||||||
hashes.append(hash)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
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 {
|
} catch {
|
||||||
let error = error as NSError
|
let error = error as NSError
|
||||||
|
|
||||||
|
|
@ -69,7 +81,7 @@ public class DebridManager: ObservableObject {
|
||||||
return .none
|
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
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -86,7 +98,7 @@ public class DebridManager: ObservableObject {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let realDebridItem = realDebridHashes.first(where: { magnetHash == $0.hash }) {
|
if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) {
|
||||||
selectedRealDebridItem = realDebridItem
|
selectedRealDebridItem = realDebridItem
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ enum ViewTab {
|
||||||
case search
|
case search
|
||||||
case sources
|
case sources
|
||||||
case settings
|
case settings
|
||||||
|
case library
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
|
@ -31,6 +32,8 @@ class NavigationViewModel: ObservableObject {
|
||||||
@Published var isEditingSearch: Bool = false
|
@Published var isEditingSearch: Bool = false
|
||||||
@Published var isSearching: Bool = false
|
@Published var isSearching: Bool = false
|
||||||
|
|
||||||
|
@Published var selectedSearchResult: SearchResult?
|
||||||
|
|
||||||
@Published var hideNavigationBar = false
|
@Published var hideNavigationBar = false
|
||||||
|
|
||||||
@Published var currentChoiceSheet: ChoiceSheetType?
|
@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
|
let selectedAction = action ?? defaultMagnetAction
|
||||||
|
|
||||||
guard let magnetLink = searchResult.magnetLink else {
|
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.")
|
print("Magnet action error: The magnet link is invalid.")
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -11,16 +11,6 @@ import SwiftSoup
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import SwiftyJSON
|
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 {
|
class ScrapingViewModel: ObservableObject {
|
||||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||||
|
|
||||||
|
|
@ -31,7 +21,6 @@ class ScrapingViewModel: ObservableObject {
|
||||||
@Published var runningSearchTask: Task<Void, Error>?
|
@Published var runningSearchTask: Task<Void, Error>?
|
||||||
@Published var searchResults: [SearchResult] = []
|
@Published var searchResults: [SearchResult] = []
|
||||||
@Published var searchText: String = ""
|
@Published var searchText: String = ""
|
||||||
@Published var selectedSearchResult: SearchResult?
|
|
||||||
@Published var filteredSource: Source?
|
@Published var filteredSource: Source?
|
||||||
@Published var currentSourceName: String?
|
@Published var currentSourceName: String?
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ struct BatchChoiceView: View {
|
||||||
Button(file.name) {
|
Button(file.name) {
|
||||||
debridManager.selectedRealDebridFile = file
|
debridManager.selectedRealDebridFile = file
|
||||||
|
|
||||||
if let searchResult = scrapingModel.selectedSearchResult {
|
if let searchResult = navModel.selectedSearchResult {
|
||||||
debridManager.currentDebridTask = Task {
|
debridManager.currentDebridTask = Task {
|
||||||
await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file)
|
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)
|
await scrapingModel.scanSources(sources: sources)
|
||||||
|
|
||||||
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
|
if realDebridEnabled, !scrapingModel.searchResults.isEmpty {
|
||||||
await debridManager.populateDebridHashes(scrapingModel.searchResults)
|
debridManager.realDebridIAValues = []
|
||||||
|
|
||||||
|
await debridManager.populateDebridHashes(
|
||||||
|
scrapingModel.searchResults.compactMap(\.magnetHash)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
navModel.showSearchProgress = false
|
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 {
|
var body: some View {
|
||||||
NavView {
|
NavView {
|
||||||
Form {
|
Form {
|
||||||
if realDebridEnabled, debridManager.matchSearchResult(result: scrapingModel.selectedSearchResult) != .none {
|
if realDebridEnabled, debridManager.matchSearchResult(result: navModel.selectedSearchResult) != .none {
|
||||||
Section(header: "Real Debrid options") {
|
Section(header: "Real Debrid options") {
|
||||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||||
navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl)
|
navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl)
|
||||||
|
|
@ -60,7 +60,7 @@ struct MagnetChoiceView: View {
|
||||||
|
|
||||||
Section(header: "Magnet options") {
|
Section(header: "Magnet options") {
|
||||||
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") {
|
||||||
UIPasteboard.general.string = scrapingModel.selectedSearchResult?.magnetLink
|
UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink
|
||||||
showMagnetCopyAlert.toggle()
|
showMagnetCopyAlert.toggle()
|
||||||
}
|
}
|
||||||
.alert(isPresented: $showMagnetCopyAlert) {
|
.alert(isPresented: $showMagnetCopyAlert) {
|
||||||
|
|
@ -72,7 +72,7 @@ struct MagnetChoiceView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") {
|
||||||
if let result = scrapingModel.selectedSearchResult,
|
if let result = navModel.selectedSearchResult,
|
||||||
let magnetLink = result.magnetLink,
|
let magnetLink = result.magnetLink,
|
||||||
let url = URL(string: magnetLink)
|
let url = URL(string: magnetLink)
|
||||||
{
|
{
|
||||||
|
|
@ -82,9 +82,7 @@ struct MagnetChoiceView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") {
|
||||||
if let result = scrapingModel.selectedSearchResult {
|
navModel.runMagnetAction(.webtor)
|
||||||
navModel.runMagnetAction(action: .webtor, searchResult: result)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,12 @@ struct MainView: View {
|
||||||
}
|
}
|
||||||
.tag(ViewTab.search)
|
.tag(ViewTab.search)
|
||||||
|
|
||||||
|
LibraryView()
|
||||||
|
.tabItem {
|
||||||
|
Label("Library", systemImage: "book.closed")
|
||||||
|
}
|
||||||
|
.tag(ViewTab.library)
|
||||||
|
|
||||||
SourcesView()
|
SourcesView()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Sources", systemImage: "doc.text")
|
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 {
|
struct SearchResultsView: View {
|
||||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||||
@EnvironmentObject var debridManager: DebridManager
|
|
||||||
@EnvironmentObject var navModel: NavigationViewModel
|
@EnvironmentObject var navModel: NavigationViewModel
|
||||||
|
|
||||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||||
|
|
@ -18,38 +17,7 @@ struct SearchResultsView: View {
|
||||||
List {
|
List {
|
||||||
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
ForEach(scrapingModel.searchResults, id: \.self) { result in
|
||||||
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil {
|
||||||
VStack(alignment: .leading) {
|
SearchResultButtonView(result: result)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ struct SourceSettingsMethodView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Section(header: InlineHeader("Fetch method")) {
|
Section(header: InlineHeader("Fetch method")) {
|
||||||
if selectedSource.api != nil, selectedSource.jsonParser != nil {
|
if selectedSource.jsonParser != nil {
|
||||||
Button {
|
Button {
|
||||||
selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue
|
selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue
|
||||||
} label: {
|
} label: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue