v0.5 #11
67 changed files with 2565 additions and 525 deletions
|
|
@ -7,23 +7,34 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; };
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; };
|
||||
0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; };
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; };
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; };
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; };
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; };
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
||||
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 */; };
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */; };
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; };
|
||||
0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */; };
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2A728D4DDDC007711AE /* Application.swift */; };
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AC28D51C63007711AE /* BackupManager.swift */; };
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44E2AE28D52E8A007711AE /* BackupsView.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 */; };
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; };
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; };
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; };
|
||||
0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */; };
|
||||
0C64A4B4288903680079976D /* Base32 in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B3288903680079976D /* Base32 */; };
|
||||
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 */; };
|
||||
|
|
@ -31,13 +42,15 @@
|
|||
0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; };
|
||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; };
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; };
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */; };
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */; };
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */; };
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */; };
|
||||
0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; };
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; };
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; };
|
||||
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */; };
|
||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7C128528DAA3CD00381CD1 /* URL.swift */; };
|
||||
0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7D11FD28AA03FE00ED92DB /* View.swift */; };
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7ED14028D61BBA009E29AD /* BackupModels.swift */; };
|
||||
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C7ED14228D65518009E29AD /* FileManager.swift */; };
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F4752895BE680074B7C9 /* FerriteDB.xcdatamodeld */; };
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */; };
|
||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */; };
|
||||
|
|
@ -56,7 +69,6 @@
|
|||
0CA148DB288903F000DE2211 /* NavView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C1288903F000DE2211 /* NavView.swift */; };
|
||||
0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; };
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */; };
|
||||
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C4288903F000DE2211 /* RealDebridModels.swift */; };
|
||||
0CA148DF288903F000DE2211 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C6288903F000DE2211 /* Preview Assets.xcassets */; };
|
||||
0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C7288903F000DE2211 /* FerriteApp.swift */; };
|
||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C9288903F000DE2211 /* Collection.swift */; };
|
||||
|
|
@ -69,41 +81,68 @@
|
|||
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 */; };
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; };
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; };
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; };
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; };
|
||||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; };
|
||||
0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; };
|
||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; };
|
||||
0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; };
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; };
|
||||
0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; };
|
||||
0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; };
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */; };
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */; };
|
||||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; };
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE66B3928E640D200F69346 /* Backport.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = "<group>"; };
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = "<group>"; };
|
||||
0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = "<group>"; };
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = "<group>"; };
|
||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.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>"; };
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = "<group>"; };
|
||||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.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>"; };
|
||||
0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = "<group>"; };
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = "<group>"; };
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.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>"; };
|
||||
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+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>"; };
|
||||
0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = "<group>"; };
|
||||
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceView.swift; sourceTree = "<group>"; };
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogView.swift; sourceTree = "<group>"; };
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogButtonView.swift; sourceTree = "<group>"; };
|
||||
0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAccentColor.swift; sourceTree = "<group>"; };
|
||||
0C7C128528DAA3CD00381CD1 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = "<group>"; };
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = "<group>"; };
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupModels.swift; sourceTree = "<group>"; };
|
||||
0C7ED14228D65518009E29AD /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = "<group>"; };
|
||||
0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = "<group>"; };
|
||||
0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
|
|
@ -122,7 +161,6 @@
|
|||
0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = "<group>"; };
|
||||
0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = "<group>"; };
|
||||
0CA148C4288903F000DE2211 /* RealDebridModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = "<group>"; };
|
||||
0CA148C6288903F000DE2211 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; };
|
||||
0CA148C7288903F000DE2211 /* FerriteApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FerriteApp.swift; sourceTree = "<group>"; };
|
||||
0CA148C9288903F000DE2211 /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = "<group>"; };
|
||||
|
|
@ -135,13 +173,26 @@
|
|||
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>"; };
|
||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.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>"; };
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = "<group>"; };
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = "<group>"; };
|
||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = "<group>"; };
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = "<group>"; };
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = "<group>"; };
|
||||
0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||
0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = "<group>"; };
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = "<group>"; };
|
||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = "<group>"; };
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = "<group>"; };
|
||||
0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
|
|
@ -153,6 +204,7 @@
|
|||
0C64A4B4288903680079976D /* Base32 in Frameworks */,
|
||||
0C4CFC462897030D00AD9FAD /* Regex in Frameworks */,
|
||||
0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */,
|
||||
0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */,
|
||||
0C64A4B7288903880079976D /* KeychainSwift in Frameworks */,
|
||||
0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */,
|
||||
);
|
||||
|
|
@ -164,6 +216,10 @@
|
|||
0C0D50DE288DF72D0035ECC8 /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */,
|
||||
0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */,
|
||||
0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */,
|
||||
0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */,
|
||||
0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */,
|
||||
0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */,
|
||||
0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */,
|
||||
|
|
@ -185,21 +241,60 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148C4288903F000DE2211 /* RealDebridModels.swift */,
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0C0167DB29293FA900B65783 /* RealDebridModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */,
|
||||
0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2A628D4DDC6007711AE /* Classes */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2A728D4DDDC007711AE /* Application.swift */,
|
||||
);
|
||||
path = Classes;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2A928D4DFC4007711AE /* Modifiers */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */,
|
||||
0CB6516228C5A57300DCA721 /* ConditionalId.swift */,
|
||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
|
||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
||||
);
|
||||
path = Modifiers;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */,
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */,
|
||||
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */,
|
||||
);
|
||||
path = Buttons;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C44E2AB28D4E126007711AE /* SearchResultViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */,
|
||||
);
|
||||
path = SearchResultViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AA28D4E09B007711AE /* Buttons */,
|
||||
0C733286289C4C820058D1FE /* SourceSettingsView.swift */,
|
||||
0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */,
|
||||
0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */,
|
||||
0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */,
|
||||
);
|
||||
path = SourceViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -207,6 +302,7 @@
|
|||
0CA0545C288F7CB200850554 /* SettingsViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsSourceListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
|
||||
|
|
@ -224,6 +320,7 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */,
|
||||
0CA148EF2889061600DE2211 /* ViewModels */,
|
||||
0CA148EE2889061200DE2211 /* Views */,
|
||||
0C44E2A628D4DDC6007711AE /* Classes */,
|
||||
0CA148C8288903F000DE2211 /* Extensions */,
|
||||
0CA148C5288903F000DE2211 /* Preview Content */,
|
||||
0CA148C7288903F000DE2211 /* FerriteApp.swift */,
|
||||
|
|
@ -236,11 +333,15 @@
|
|||
0CA148C0288903F000DE2211 /* CommonViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2A928D4DFC4007711AE /* Modifiers */,
|
||||
0CE66B3928E640D200F69346 /* Backport.swift */,
|
||||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */,
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
||||
0CA148C1288903F000DE2211 /* NavView.swift */,
|
||||
0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */,
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */,
|
||||
0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */,
|
||||
0CB6516928C5B4A600DCA721 /* InlineHeader.swift */,
|
||||
0C32FB562890D1F2002BD219 /* ListRowViews.swift */,
|
||||
);
|
||||
path = CommonViews;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -258,10 +359,13 @@
|
|||
children = (
|
||||
0CA148C9288903F000DE2211 /* Collection.swift */,
|
||||
0CA148CA288903F000DE2211 /* Data.swift */,
|
||||
0CA148CB288903F000DE2211 /* Task.swift */,
|
||||
0C32FB542890D1BF002BD219 /* UIApplication.swift */,
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||
0CA429F728C5098D000D0610 /* DateFormatter.swift */,
|
||||
0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */,
|
||||
0C78041C28BFB3EA001E8CA3 /* String.swift */,
|
||||
0CA148CB288903F000DE2211 /* Task.swift */,
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||
0C7ED14228D65518009E29AD /* FileManager.swift */,
|
||||
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -269,21 +373,22 @@
|
|||
0CA148EE2889061200DE2211 /* Views */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */,
|
||||
0C794B65289DAC9F00DD1CC8 /* SourceViews */,
|
||||
0CA148F02889062700DE2211 /* RepresentableViews */,
|
||||
0CA148C0288903F000DE2211 /* CommonViews */,
|
||||
0C44E2AB28D4E126007711AE /* SearchResultViews */,
|
||||
0CA0545C288F7CB200850554 /* SettingsViews */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */,
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D1288903F000DE2211 /* MainView.swift */,
|
||||
0CA148D4288903F000DE2211 /* ContentView.swift */,
|
||||
0CA148D3288903F000DE2211 /* SearchResultsView.swift */,
|
||||
0CA3B23328C2658700616D3A /* LibraryView.swift */,
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
||||
0CA148BB288903F000DE2211 /* SettingsView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0CA148BC288903F000DE2211 /* LoginWebView.swift */,
|
||||
0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */,
|
||||
0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */,
|
||||
0C0D50E6288DFF850035ECC8 /* SourcesView.swift */,
|
||||
0C32FB522890D19D002BD219 /* AboutView.swift */,
|
||||
0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -296,6 +401,7 @@
|
|||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */,
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -311,12 +417,23 @@
|
|||
0CA148F12889066000DE2211 /* API */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */,
|
||||
0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */,
|
||||
);
|
||||
path = API;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CA3B23528C265FD00616D3A /* LibraryViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0CA3B23828C2660D00616D3A /* BookmarksView.swift */,
|
||||
0CA3B23628C2660700616D3A /* HistoryView.swift */,
|
||||
0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */,
|
||||
0C12D43C28CC332A000195BF /* HistoryButtonView.swift */,
|
||||
);
|
||||
path = LibraryViews;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
0CAF1C5F286F5C0D00296F86 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
|
@ -366,6 +483,7 @@
|
|||
0C4CFC452897030D00AD9FAD /* Regex */,
|
||||
0C7376EF28A97D1400D60918 /* SwiftUIX */,
|
||||
0C7506D628B1AC9A008BEE38 /* SwiftyJSON */,
|
||||
0CB6516728C5A5EC00DCA721 /* Introspect */,
|
||||
);
|
||||
productName = Torrenter;
|
||||
productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */;
|
||||
|
|
@ -402,6 +520,7 @@
|
|||
0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */,
|
||||
0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */,
|
||||
0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */,
|
||||
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */,
|
||||
);
|
||||
productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */;
|
||||
projectDirPath = "";
|
||||
|
|
@ -430,61 +549,86 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
||||
0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */,
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||
0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */,
|
||||
0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */,
|
||||
0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */,
|
||||
0CD5E78928CD932B001BF684 /* DisabledAppearance.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 */,
|
||||
0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */,
|
||||
0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */,
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */,
|
||||
0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */,
|
||||
0CE66B3A28E640D200F69346 /* Backport.swift in Sources */,
|
||||
0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */,
|
||||
0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
0CA148E9288903F000DE2211 /* MainView.swift in Sources */,
|
||||
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 */,
|
||||
0C44E2A828D4DDDC007711AE /* Application.swift in Sources */,
|
||||
0C7ED14128D61BBA009E29AD /* BackupModels.swift in Sources */,
|
||||
0CA148EC288903F000DE2211 /* ContentView.swift in Sources */,
|
||||
0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */,
|
||||
0C44E2AF28D52E8A007711AE /* BackupsView.swift in Sources */,
|
||||
0CA148E1288903F000DE2211 /* Collection.swift in Sources */,
|
||||
0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */,
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */,
|
||||
0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */,
|
||||
0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */,
|
||||
0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */,
|
||||
0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */,
|
||||
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 */,
|
||||
0C0167DC29293FA900B65783 /* RealDebridModels.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 */,
|
||||
0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */,
|
||||
0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */,
|
||||
0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */,
|
||||
0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */,
|
||||
0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */,
|
||||
0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */,
|
||||
0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */,
|
||||
0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */,
|
||||
0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */,
|
||||
0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */,
|
||||
0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */,
|
||||
0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */,
|
||||
0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */,
|
||||
0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */,
|
||||
0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */,
|
||||
0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */,
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */,
|
||||
0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */,
|
||||
0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */,
|
||||
0C7C128628DAA3CD00381CD1 /* URL.swift in Sources */,
|
||||
0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */,
|
||||
0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */,
|
||||
0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */,
|
||||
|
|
@ -615,7 +759,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8A74DBQ6S3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -623,6 +767,7 @@
|
|||
INFOPLIST_FILE = Ferrite/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ferrite;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
|
|
@ -633,10 +778,11 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
|
@ -648,7 +794,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 9;
|
||||
CURRENT_PROJECT_VERSION = 10;
|
||||
DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\"";
|
||||
DEVELOPMENT_TEAM = 8A74DBQ6S3;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -656,6 +802,7 @@
|
|||
INFOPLIST_FILE = Ferrite/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = Ferrite;
|
||||
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment";
|
||||
INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
|
||||
|
|
@ -666,10 +813,11 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
MARKETING_VERSION = 0.4.0;
|
||||
MARKETING_VERSION = 0.5.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_STRICT_CONCURRENCY = minimal;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
|
|
@ -747,6 +895,14 @@
|
|||
minimumVersion = 2.0.0;
|
||||
};
|
||||
};
|
||||
0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/siteline/SwiftUI-Introspect";
|
||||
requirement = {
|
||||
branch = master;
|
||||
kind = branch;
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
|
|
@ -780,6 +936,11 @@
|
|||
package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */;
|
||||
productName = SwiftSoup;
|
||||
};
|
||||
0CB6516728C5A5EC00DCA721 /* Introspect */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */;
|
||||
productName = Introspect;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ public enum RealDebridError: Error {
|
|||
case InvalidResponse
|
||||
case InvalidToken
|
||||
case EmptyData
|
||||
case EmptyTorrents
|
||||
case FailedRequest(description: String)
|
||||
case AuthQuery(description: String)
|
||||
}
|
||||
|
|
@ -248,9 +249,22 @@ 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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -294,21 +308,33 @@ public class RealDebrid {
|
|||
try await performRequest(request: &request, requestName: #function)
|
||||
}
|
||||
|
||||
// Fetches the info of a torrent
|
||||
// Gets the info of a torrent from a given ID
|
||||
public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data)
|
||||
|
||||
// Error out if no index is provided
|
||||
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1] {
|
||||
// Let the user know if a torrent is downloading
|
||||
if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" {
|
||||
return torrentLink
|
||||
} else if rawResponse.status == "downloading" || rawResponse.status == "queued" {
|
||||
throw RealDebridError.EmptyTorrents
|
||||
} else {
|
||||
throw RealDebridError.EmptyData
|
||||
}
|
||||
}
|
||||
|
||||
// Gets the user's torrent library
|
||||
public func userTorrents() async throws -> [UserTorrentsResponse] {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data)
|
||||
|
||||
return rawResponse
|
||||
}
|
||||
|
||||
// Deletes a torrent download from RD
|
||||
public func deleteTorrent(debridID: String) async throws {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!)
|
||||
|
|
@ -333,4 +359,14 @@ public class RealDebrid {
|
|||
|
||||
return rawResponse.download
|
||||
}
|
||||
|
||||
// Gets the user's downloads
|
||||
public func userDownloads() async throws -> [UserDownloadsResponse] {
|
||||
var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!)
|
||||
|
||||
let data = try await performRequest(request: &request, requestName: #function)
|
||||
let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data)
|
||||
|
||||
return rawResponse
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
//
|
||||
// UIApplication.swift
|
||||
// Application.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
// Created by Brian Dashore on 9/16/22.
|
||||
//
|
||||
// A thread-safe UIApplication alternative for specifying app properties
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Foundation
|
||||
|
||||
public class Application {
|
||||
static let shared = Application()
|
||||
|
||||
// Extensions to get the version/build number for AboutView
|
||||
extension UIApplication {
|
||||
var appVersion: String {
|
||||
Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0"
|
||||
}
|
||||
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,27 @@
|
|||
//
|
||||
// Bookmark+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/3/22.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public extension Bookmark {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Bookmark> {
|
||||
NSFetchRequest<Bookmark>(entityName: "Bookmark")
|
||||
}
|
||||
|
||||
@NSManaged var leechers: String?
|
||||
@NSManaged var magnetHash: String?
|
||||
@NSManaged var magnetLink: String?
|
||||
@NSManaged var seeders: String?
|
||||
@NSManaged var size: String?
|
||||
@NSManaged var source: String
|
||||
@NSManaged var title: String?
|
||||
@NSManaged var orderNum: Int16
|
||||
}
|
||||
|
||||
extension Bookmark: Identifiable {}
|
||||
13
Ferrite/DataManagement/Classes/History+CoreDataClass.swift
Normal file
13
Ferrite/DataManagement/Classes/History+CoreDataClass.swift
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// History+CoreDataClass.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/4/22.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
@objc(History)
|
||||
public class History: NSManagedObject {}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// History+CoreDataProperties.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/4/22.
|
||||
//
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import Foundation
|
||||
|
||||
public extension History {
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<History> {
|
||||
NSFetchRequest<History>(entityName: "History")
|
||||
}
|
||||
|
||||
@NSManaged var date: Date?
|
||||
@NSManaged var dateString: String?
|
||||
@NSManaged var entries: NSSet?
|
||||
|
||||
internal var entryArray: [HistoryEntry] {
|
||||
let entrySet = entries as? Set<HistoryEntry> ?? []
|
||||
|
||||
return entrySet.sorted {
|
||||
$0.timeStamp > $1.timeStamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Generated accessors for entries
|
||||
|
||||
public extension History {
|
||||
@objc(addEntriesObject:)
|
||||
@NSManaged func addToEntries(_ value: HistoryEntry)
|
||||
|
||||
@objc(removeEntriesObject:)
|
||||
@NSManaged func removeFromEntries(_ value: HistoryEntry)
|
||||
|
||||
@objc(addEntries:)
|
||||
@NSManaged func addToEntries(_ values: NSSet)
|
||||
|
||||
@objc(removeEntries:)
|
||||
@NSManaged func removeFromEntries(_ values: NSSet)
|
||||
}
|
||||
|
||||
extension History: Identifiable {}
|
||||
|
|
@ -1,5 +1,28 @@
|
|||
<?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="21512" systemVersion="21G83" 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">
|
||||
<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="source" optional="YES" attributeType="String"/>
|
||||
<attribute name="subName" 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"/>
|
||||
|
|
|
|||
|
|
@ -7,9 +7,21 @@
|
|||
|
||||
import CoreData
|
||||
|
||||
enum HistoryDeleteRange {
|
||||
case day
|
||||
case week
|
||||
case month
|
||||
case allTime
|
||||
}
|
||||
|
||||
enum HistoryDeleteError: Error {
|
||||
case noDate(String)
|
||||
case unknown(String)
|
||||
}
|
||||
|
||||
// No iCloud until finalized sources
|
||||
struct PersistenceController {
|
||||
static var shared = PersistenceController()
|
||||
static let shared = PersistenceController()
|
||||
|
||||
// Coredata storage
|
||||
let container: NSPersistentContainer
|
||||
|
|
@ -33,7 +45,7 @@ struct PersistenceController {
|
|||
description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey)
|
||||
|
||||
container.loadPersistentStores { _, error in
|
||||
if let error = error {
|
||||
if let error {
|
||||
fatalError("CoreData init error: \(error)")
|
||||
}
|
||||
}
|
||||
|
|
@ -78,4 +90,130 @@ struct PersistenceController {
|
|||
container.viewContext.delete(object)
|
||||
save()
|
||||
}
|
||||
|
||||
func createBookmark(_ bookmarkJson: BookmarkJson) {
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
bookmarkRequest.predicate = NSPredicate(
|
||||
format: "source == %@ AND title == %@ AND magnetLink == %@",
|
||||
bookmarkJson.source,
|
||||
bookmarkJson.title ?? "",
|
||||
bookmarkJson.magnetLink ?? ""
|
||||
)
|
||||
|
||||
if (try? backgroundContext.fetch(bookmarkRequest).first) != nil {
|
||||
return
|
||||
}
|
||||
|
||||
let newBookmark = Bookmark(context: backgroundContext)
|
||||
|
||||
newBookmark.title = bookmarkJson.title
|
||||
newBookmark.source = bookmarkJson.source
|
||||
newBookmark.magnetHash = bookmarkJson.magnetHash
|
||||
newBookmark.magnetLink = bookmarkJson.magnetLink
|
||||
newBookmark.seeders = bookmarkJson.seeders
|
||||
newBookmark.leechers = bookmarkJson.leechers
|
||||
}
|
||||
|
||||
func createHistory(entryJson: HistoryEntryJson, date: Double?) {
|
||||
let historyDate = date.map { Date(timeIntervalSince1970: $0) } ?? Date()
|
||||
let historyDateString = DateFormatter.historyDateFormatter.string(from: historyDate)
|
||||
|
||||
let newHistoryEntry = HistoryEntry(context: backgroundContext)
|
||||
|
||||
newHistoryEntry.source = entryJson.source
|
||||
newHistoryEntry.name = entryJson.name
|
||||
newHistoryEntry.url = entryJson.url
|
||||
newHistoryEntry.subName = entryJson.subName
|
||||
newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970
|
||||
|
||||
let historyRequest = History.fetchRequest()
|
||||
historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString)
|
||||
|
||||
// Safely add entries to a parent history if it exists
|
||||
if var histories = try? backgroundContext.fetch(historyRequest) {
|
||||
for (i, history) in histories.enumerated() {
|
||||
let existingEntries = history.entryArray.filter { $0.url == newHistoryEntry.url && $0.name == newHistoryEntry.name }
|
||||
|
||||
if !existingEntries.isEmpty {
|
||||
for entry in existingEntries {
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
|
||||
if history.entryArray.isEmpty {
|
||||
PersistenceController.shared.delete(history, context: backgroundContext)
|
||||
histories.remove(at: i)
|
||||
}
|
||||
}
|
||||
|
||||
newHistoryEntry.parentHistory = histories.first ?? History(context: backgroundContext)
|
||||
} else {
|
||||
newHistoryEntry.parentHistory = History(context: backgroundContext)
|
||||
}
|
||||
|
||||
newHistoryEntry.parentHistory?.dateString = historyDateString
|
||||
newHistoryEntry.parentHistory?.date = historyDate
|
||||
}
|
||||
|
||||
func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? {
|
||||
if range == .allTime {
|
||||
return nil
|
||||
}
|
||||
|
||||
var components = Calendar.current.dateComponents([.day, .month, .year], from: Date())
|
||||
components.hour = 0
|
||||
components.minute = 0
|
||||
components.second = 0
|
||||
|
||||
guard let today = Calendar.current.date(from: components) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
var offsetComponents = DateComponents(day: 1)
|
||||
guard let tomorrow = Calendar.current.date(byAdding: offsetComponents, to: today) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch range {
|
||||
case .week:
|
||||
offsetComponents.day = -7
|
||||
case .month:
|
||||
offsetComponents.day = -28
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
guard var offsetDate = Calendar.current.date(byAdding: offsetComponents, to: today) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
if TimeZone.current.isDaylightSavingTime(for: offsetDate) {
|
||||
offsetDate = offsetDate.addingTimeInterval(3600)
|
||||
}
|
||||
|
||||
let predicate = NSPredicate(format: "date >= %@ && date < %@", range == .day ? today as NSDate : offsetDate as NSDate, tomorrow as NSDate)
|
||||
|
||||
return predicate
|
||||
}
|
||||
|
||||
// Always use the background context to batch delete
|
||||
// Merge changes into both contexts to update views
|
||||
func batchDeleteHistory(range: HistoryDeleteRange) throws {
|
||||
let predicate = getHistoryPredicate(range: range)
|
||||
|
||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "History")
|
||||
|
||||
if let predicate {
|
||||
fetchRequest.predicate = predicate
|
||||
} else if range != .allTime {
|
||||
throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?")
|
||||
}
|
||||
|
||||
let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
||||
|
||||
batchDeleteRequest.resultType = .resultTypeObjectIDs
|
||||
let result = try backgroundContext.execute(batchDeleteRequest) as? NSBatchDeleteResult
|
||||
let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []]
|
||||
NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [container.viewContext, backgroundContext])
|
||||
}
|
||||
}
|
||||
|
|
|
|||
17
Ferrite/Extensions/DateFormatter.swift
Normal file
17
Ferrite/Extensions/DateFormatter.swift
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
//
|
||||
// DateFormatter.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/4/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension DateFormatter {
|
||||
static let historyDateFormatter: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateFormat = "ddMMyyyy"
|
||||
|
||||
return df
|
||||
}()
|
||||
}
|
||||
14
Ferrite/Extensions/FileManager.swift
Normal file
14
Ferrite/Extensions/FileManager.swift
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// FileManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/17/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension FileManager {
|
||||
var appDirectory: URL {
|
||||
urls(for: .documentDirectory, in: .userDomainMask)[0]
|
||||
}
|
||||
}
|
||||
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 {
|
||||
Notification.Name("Deleted bookmark")
|
||||
}
|
||||
}
|
||||
29
Ferrite/Extensions/URL.swift
Normal file
29
Ferrite/Extensions/URL.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// URL.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/20/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension URL {
|
||||
// From https://github.com/Aidoku/Aidoku/blob/main/Shared/Extensions/FileManager.swift
|
||||
// Used for FileManager
|
||||
var contentsByDateAdded: [URL] {
|
||||
if let urls = try? FileManager.default.contentsOfDirectory(
|
||||
at: self,
|
||||
includingPropertiesForKeys: [.contentModificationDateKey]
|
||||
) {
|
||||
return urls.sorted {
|
||||
((try? $0.resourceValues(forKeys: [.addedToDirectoryDateKey]))?.addedToDirectoryDate ?? Date.distantPast)
|
||||
>
|
||||
((try? $1.resourceValues(forKeys: [.addedToDirectoryDateKey]))?.addedToDirectoryDate ?? Date.distantPast)
|
||||
}
|
||||
}
|
||||
|
||||
let contents = try? FileManager.default.contentsOfDirectory(at: self, includingPropertiesForKeys: nil)
|
||||
|
||||
return contents ?? []
|
||||
}
|
||||
}
|
||||
|
|
@ -5,12 +5,45 @@
|
|||
// Created by Brian Dashore on 8/15/22.
|
||||
//
|
||||
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
|
||||
extension View {
|
||||
// MARK: Custom introspect functions
|
||||
|
||||
func introspectCollectionView(customize: @escaping (UICollectionView) -> Void) -> some View {
|
||||
inject(UIKitIntrospectionView(
|
||||
selector: { introspectionView in
|
||||
guard let viewHost = Introspect.findViewHost(from: introspectionView) else {
|
||||
return nil
|
||||
}
|
||||
return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost)
|
||||
},
|
||||
customize: customize
|
||||
))
|
||||
}
|
||||
|
||||
// MARK: Modifiers
|
||||
|
||||
func dynamicAccentColor(_ color: Color) -> some View {
|
||||
modifier(DynamicAccentColor(color: color))
|
||||
func conditionalContextMenu(id: some Hashable,
|
||||
@ViewBuilder _ internalContent: @escaping () -> some View) -> some View
|
||||
{
|
||||
modifier(ConditionalContextMenu(internalContent, id: id))
|
||||
}
|
||||
|
||||
func conditionalId(_ id: some Hashable) -> some View {
|
||||
modifier(ConditionalId(id: id))
|
||||
}
|
||||
|
||||
func disabledAppearance(_ disabled: Bool, dimmedOpacity: Double? = nil, animation: Animation? = nil) -> some View {
|
||||
modifier(DisabledAppearance(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation))
|
||||
}
|
||||
|
||||
func disableInteraction(_ disabled: Bool) -> some View {
|
||||
modifier(DisableInteraction(disabled: disabled))
|
||||
}
|
||||
|
||||
func inlinedList() -> some View {
|
||||
modifier(InlinedList())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ struct FerriteApp: App {
|
|||
@StateObject var debridManager: DebridManager = .init()
|
||||
@StateObject var navModel: NavigationViewModel = .init()
|
||||
@StateObject var sourceManager: SourceManager = .init()
|
||||
@StateObject var backupManager: BackupManager = .init()
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
|
|
@ -24,6 +25,7 @@ struct FerriteApp: App {
|
|||
scrapingModel.toastModel = toastModel
|
||||
debridManager.toastModel = toastModel
|
||||
sourceManager.toastModel = toastModel
|
||||
backupManager.toastModel = toastModel
|
||||
navModel.toastModel = toastModel
|
||||
}
|
||||
.environmentObject(debridManager)
|
||||
|
|
@ -31,6 +33,7 @@ struct FerriteApp: App {
|
|||
.environmentObject(toastModel)
|
||||
.environmentObject(navModel)
|
||||
.environmentObject(sourceManager)
|
||||
.environmentObject(backupManager)
|
||||
.environment(\.managedObjectContext, persistenceController.container.viewContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,19 @@
|
|||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>Ferrite Backup</string>
|
||||
<key>LSHandlerRank</key>
|
||||
<string>Owner</string>
|
||||
<key>LSItemContentTypes</key>
|
||||
<array>
|
||||
<string>me.kingbri.Ferrite.feb</string>
|
||||
</array>
|
||||
</dict>
|
||||
</array>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
|
|
@ -9,5 +22,27 @@
|
|||
</dict>
|
||||
<key>UILaunchScreen</key>
|
||||
<false/>
|
||||
<key>UTExportedTypeDeclarations</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UTTypeConformsTo</key>
|
||||
<array>
|
||||
<string>public.json</string>
|
||||
</array>
|
||||
<key>UTTypeDescription</key>
|
||||
<string>Ferrite Backup</string>
|
||||
<key>UTTypeIconFiles</key>
|
||||
<array/>
|
||||
<key>UTTypeIdentifier</key>
|
||||
<string>me.kingbri.Ferrite.feb</string>
|
||||
<key>UTTypeTagSpecification</key>
|
||||
<dict>
|
||||
<key>public.filename-extension</key>
|
||||
<array>
|
||||
<string>feb</string>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
|
|
|||
42
Ferrite/Models/BackupModels.swift
Normal file
42
Ferrite/Models/BackupModels.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// BackupModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/17/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public struct Backup: Codable {
|
||||
var bookmarks: [BookmarkJson]?
|
||||
var history: [HistoryJson]?
|
||||
var sourceNames: [String]?
|
||||
var sourceLists: [SourceListBackupJson]?
|
||||
}
|
||||
|
||||
// MARK: - CoreData translation
|
||||
|
||||
typealias BookmarkJson = SearchResult
|
||||
|
||||
// Date is an epoch timestamp
|
||||
struct HistoryJson: Codable {
|
||||
let dateString: String?
|
||||
let date: Double
|
||||
let entries: [HistoryEntryJson]
|
||||
}
|
||||
|
||||
struct HistoryEntryJson: Codable {
|
||||
let name: String
|
||||
let subName: String?
|
||||
let url: String
|
||||
let timeStamp: Double?
|
||||
let source: String?
|
||||
}
|
||||
|
||||
// Differs from SourceListJson
|
||||
struct SourceListBackupJson: Codable {
|
||||
let name: String
|
||||
let author: String
|
||||
let id: String
|
||||
let urlString: String
|
||||
}
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public struct GithubRelease: Codable, Hashable {
|
||||
public struct GithubRelease: Codable, Hashable, Sendable {
|
||||
let htmlUrl: String
|
||||
let tagName: String
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
// RealDebridModels.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/5/22.
|
||||
// Created by Brian Dashore on 11/19/22.
|
||||
//
|
||||
// Structures generated from Quicktype
|
||||
|
||||
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
|
||||
// MARK: - device code endpoint
|
||||
|
||||
public struct DeviceCodeResponse: Codable {
|
||||
public struct DeviceCodeResponse: Codable, Sendable {
|
||||
let deviceCode, userCode: String
|
||||
let interval, expiresIn: Int
|
||||
let verificationURL, directVerificationURL: String
|
||||
|
|
@ -27,7 +27,7 @@ public struct DeviceCodeResponse: Codable {
|
|||
|
||||
// MARK: - device credentials endpoint
|
||||
|
||||
public struct DeviceCredentialsResponse: Codable {
|
||||
public struct DeviceCredentialsResponse: Codable, Sendable {
|
||||
let clientID, clientSecret: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
|
|
@ -38,7 +38,7 @@ public struct DeviceCredentialsResponse: Codable {
|
|||
|
||||
// MARK: - token endpoint
|
||||
|
||||
public struct TokenResponse: Codable {
|
||||
public struct TokenResponse: Codable, Sendable {
|
||||
let accessToken: String
|
||||
let expiresIn: Int
|
||||
let refreshToken, tokenType: String
|
||||
|
|
@ -54,7 +54,7 @@ public struct TokenResponse: Codable {
|
|||
// MARK: - instantAvailability endpoint
|
||||
|
||||
// Thanks Skitty!
|
||||
public struct InstantAvailabilityResponse: Codable {
|
||||
public struct InstantAvailabilityResponse: Codable, Sendable {
|
||||
var data: InstantAvailabilityData?
|
||||
|
||||
public init(from decoder: Decoder) throws {
|
||||
|
|
@ -66,39 +66,40 @@ public struct InstantAvailabilityResponse: Codable {
|
|||
}
|
||||
}
|
||||
|
||||
struct InstantAvailabilityData: Codable {
|
||||
// MARK: - Instant Availability client side structures
|
||||
|
||||
struct InstantAvailabilityData: Codable, Sendable {
|
||||
var rd: [[String: InstantAvailabilityInfo]]
|
||||
}
|
||||
|
||||
struct InstantAvailabilityInfo: Codable {
|
||||
struct InstantAvailabilityInfo: Codable, Sendable {
|
||||
var filename: String
|
||||
var filesize: Int
|
||||
}
|
||||
|
||||
// MARK: - Instant Availability client side structures
|
||||
|
||||
public struct RealDebridIA: Codable, Hashable {
|
||||
public struct RealDebridIA: Codable, Hashable, Sendable {
|
||||
let hash: String
|
||||
let expiryTimeStamp: Double
|
||||
var files: [RealDebridIAFile] = []
|
||||
var batches: [RealDebridIABatch] = []
|
||||
}
|
||||
|
||||
public struct RealDebridIABatch: Codable, Hashable {
|
||||
public struct RealDebridIABatch: Codable, Hashable, Sendable {
|
||||
let files: [RealDebridIABatchFile]
|
||||
}
|
||||
|
||||
public struct RealDebridIABatchFile: Codable, Hashable {
|
||||
public struct RealDebridIABatchFile: Codable, Hashable, Sendable {
|
||||
let id: Int
|
||||
let fileName: String
|
||||
}
|
||||
|
||||
public struct RealDebridIAFile: Codable, Hashable {
|
||||
public struct RealDebridIAFile: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let batchIndex: Int
|
||||
let batchFileIndex: Int
|
||||
}
|
||||
|
||||
public enum RealDebridIAStatus: Codable, Hashable {
|
||||
public enum RealDebridIAStatus: Codable, Hashable, Sendable {
|
||||
case full
|
||||
case partial
|
||||
case none
|
||||
|
|
@ -106,14 +107,14 @@ public enum RealDebridIAStatus: Codable, Hashable {
|
|||
|
||||
// MARK: - addMagnet endpoint
|
||||
|
||||
public struct AddMagnetResponse: Codable {
|
||||
public struct AddMagnetResponse: Codable, Sendable {
|
||||
let id: String
|
||||
let uri: String
|
||||
}
|
||||
|
||||
// MARK: - torrentInfo endpoint
|
||||
|
||||
struct TorrentInfoResponse: Codable {
|
||||
struct TorrentInfoResponse: Codable, Sendable {
|
||||
let id, filename, originalFilename, hash: String
|
||||
let bytes, originalBytes: Int
|
||||
let host: String
|
||||
|
|
@ -121,27 +122,41 @@ struct TorrentInfoResponse: Codable {
|
|||
let status, added: String
|
||||
let files: [TorrentInfoFile]
|
||||
let links: [String]
|
||||
let ended: String
|
||||
let ended: String?
|
||||
let speed: Int?
|
||||
let seeders: Int?
|
||||
|
||||
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
|
||||
case host, split, progress, status, added, files, links, ended, speed, seeders
|
||||
}
|
||||
}
|
||||
|
||||
struct TorrentInfoFile: Codable {
|
||||
struct TorrentInfoFile: Codable, Sendable {
|
||||
let id: Int
|
||||
let path: String
|
||||
let bytes, selected: Int
|
||||
}
|
||||
|
||||
public struct UserTorrentsResponse: Codable, Sendable {
|
||||
let id, filename, hash: String
|
||||
let bytes: Int
|
||||
let host: String
|
||||
let split, progress: Int
|
||||
let status, added: String
|
||||
let links: [String]
|
||||
let speed, seeders: Int?
|
||||
let ended: String?
|
||||
}
|
||||
|
||||
// MARK: - unrestrictLink endpoint
|
||||
|
||||
struct UnrestrictLinkResponse: Codable {
|
||||
let id, filename, mimeType: String
|
||||
struct UnrestrictLinkResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
|
|
@ -156,3 +171,24 @@ struct UnrestrictLinkResponse: Codable {
|
|||
case chunks, crc, download, streamable
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - User downloads list
|
||||
|
||||
public struct UserDownloadsResponse: Codable, Sendable {
|
||||
let id, filename: String
|
||||
let mimeType: String?
|
||||
let filesize: Int
|
||||
let link: String
|
||||
let host: String
|
||||
let hostIcon: String
|
||||
let chunks: Int
|
||||
let download: String
|
||||
let streamable: Int
|
||||
let generated: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case id, filename, mimeType, filesize, link, host
|
||||
case hostIcon = "host_icon"
|
||||
case chunks, download, streamable, generated
|
||||
}
|
||||
}
|
||||
|
|
|
|||
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, Sendable {
|
||||
let title: String?
|
||||
let source: String
|
||||
let size: String?
|
||||
let magnetLink: String?
|
||||
let magnetHash: String?
|
||||
let seeders: String?
|
||||
let leechers: String?
|
||||
}
|
||||
|
|
@ -7,18 +7,18 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
public enum ApiCredentialResponseType: String, Codable, Hashable {
|
||||
public enum ApiCredentialResponseType: String, Codable, Hashable, Sendable {
|
||||
case json
|
||||
case text
|
||||
}
|
||||
|
||||
public struct SourceListJson: Codable {
|
||||
public struct SourceListJson: Codable, Sendable {
|
||||
let name: String
|
||||
let author: String
|
||||
var sources: [SourceJson]
|
||||
}
|
||||
|
||||
public struct SourceJson: Codable, Hashable {
|
||||
public struct SourceJson: Codable, Hashable, Sendable {
|
||||
let name: String
|
||||
let version: Int16
|
||||
let minVersion: String?
|
||||
|
|
@ -34,20 +34,20 @@ public struct SourceJson: Codable, Hashable {
|
|||
let htmlParser: SourceHtmlParserJson?
|
||||
}
|
||||
|
||||
public enum SourcePreferredParser: Int16, CaseIterable {
|
||||
public enum SourcePreferredParser: Int16, CaseIterable, Sendable {
|
||||
// case none = 0
|
||||
case scraping = 1
|
||||
case rss = 2
|
||||
case siteApi = 3
|
||||
}
|
||||
|
||||
public struct SourceApiJson: Codable, Hashable {
|
||||
public struct SourceApiJson: Codable, Hashable, Sendable {
|
||||
let apiUrl: String?
|
||||
let clientId: SourceApiCredentialJson?
|
||||
let clientSecret: SourceApiCredentialJson?
|
||||
}
|
||||
|
||||
public struct SourceApiCredentialJson: Codable, Hashable {
|
||||
public struct SourceApiCredentialJson: Codable, Hashable, Sendable {
|
||||
let query: String?
|
||||
let value: String?
|
||||
let dynamic: Bool?
|
||||
|
|
@ -56,7 +56,7 @@ public struct SourceApiCredentialJson: Codable, Hashable {
|
|||
let expiryLength: Double?
|
||||
}
|
||||
|
||||
public struct SourceJsonParserJson: Codable, Hashable {
|
||||
public struct SourceJsonParserJson: Codable, Hashable, Sendable {
|
||||
let searchUrl: String
|
||||
let results: String?
|
||||
let subResults: String?
|
||||
|
|
@ -67,7 +67,7 @@ public struct SourceJsonParserJson: Codable, Hashable {
|
|||
let sl: SourceSLJson?
|
||||
}
|
||||
|
||||
public struct SourceRssParserJson: Codable, Hashable {
|
||||
public struct SourceRssParserJson: Codable, Hashable, Sendable {
|
||||
let rssUrl: String?
|
||||
let searchUrl: String
|
||||
let items: String
|
||||
|
|
@ -78,7 +78,7 @@ public struct SourceRssParserJson: Codable, Hashable {
|
|||
let sl: SourceSLJson?
|
||||
}
|
||||
|
||||
public struct SourceHtmlParserJson: Codable, Hashable {
|
||||
public struct SourceHtmlParserJson: Codable, Hashable, Sendable {
|
||||
let searchUrl: String
|
||||
let rows: String
|
||||
let magnet: SourceMagnetJson
|
||||
|
|
@ -87,21 +87,21 @@ public struct SourceHtmlParserJson: Codable, Hashable {
|
|||
let sl: SourceSLJson?
|
||||
}
|
||||
|
||||
public struct SouceComplexQueryJson: Codable, Hashable {
|
||||
public struct SouceComplexQueryJson: Codable, Hashable, Sendable {
|
||||
let query: String
|
||||
let discriminator: String?
|
||||
let attribute: String?
|
||||
let regex: String?
|
||||
}
|
||||
|
||||
public struct SourceMagnetJson: Codable, Hashable {
|
||||
public struct SourceMagnetJson: Codable, Hashable, Sendable {
|
||||
let query: String
|
||||
let attribute: String
|
||||
let regex: String?
|
||||
let externalLinkQuery: String?
|
||||
}
|
||||
|
||||
public struct SourceSLJson: Codable, Hashable {
|
||||
public struct SourceSLJson: Codable, Hashable, Sendable {
|
||||
let seeders: String?
|
||||
let leechers: String?
|
||||
let combined: String?
|
||||
|
|
|
|||
220
Ferrite/ViewModels/BackupManager.swift
Normal file
220
Ferrite/ViewModels/BackupManager.swift
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
//
|
||||
// BackupManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/16/22.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public class BackupManager: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var showRestoreAlert = false
|
||||
@Published var showRestoreCompletedAlert = false
|
||||
|
||||
@Published var backupUrls: [URL] = []
|
||||
@Published var backupSourceNames: [String] = []
|
||||
@Published var selectedBackupUrl: URL?
|
||||
|
||||
func createBackup() {
|
||||
var backup = Backup()
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let bookmarkRequest = Bookmark.fetchRequest()
|
||||
if let fetchedBookmarks = try? backgroundContext.fetch(bookmarkRequest) {
|
||||
backup.bookmarks = fetchedBookmarks.compactMap {
|
||||
BookmarkJson(
|
||||
title: $0.title,
|
||||
source: $0.source,
|
||||
size: $0.size,
|
||||
magnetLink: $0.magnetLink,
|
||||
magnetHash: $0.magnetHash,
|
||||
seeders: $0.seeders,
|
||||
leechers: $0.leechers
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let historyRequest = History.fetchRequest()
|
||||
if let fetchedHistory = try? backgroundContext.fetch(historyRequest) {
|
||||
backup.history = fetchedHistory.compactMap { history in
|
||||
if history.entries == nil {
|
||||
return nil
|
||||
} else {
|
||||
return HistoryJson(
|
||||
dateString: history.dateString,
|
||||
date: history.date?.timeIntervalSince1970 ?? Date().timeIntervalSince1970,
|
||||
entries: history.entryArray.compactMap { entry in
|
||||
if let name = entry.name, let url = entry.url {
|
||||
return HistoryEntryJson(
|
||||
name: name,
|
||||
subName: entry.subName,
|
||||
url: url,
|
||||
timeStamp: entry.timeStamp,
|
||||
source: entry.source
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let sourceRequest = Source.fetchRequest()
|
||||
if let sources = try? backgroundContext.fetch(sourceRequest) {
|
||||
backup.sourceNames = sources.map(\.name)
|
||||
}
|
||||
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
if let sourceLists = try? backgroundContext.fetch(sourceListRequest) {
|
||||
backup.sourceLists = sourceLists.map {
|
||||
SourceListBackupJson(
|
||||
name: $0.name,
|
||||
author: $0.author,
|
||||
id: $0.id.uuidString,
|
||||
urlString: $0.urlString
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
let encodedJson = try JSONEncoder().encode(backup)
|
||||
let backupsPath = FileManager.default.appDirectory.appendingPathComponent("Backups")
|
||||
if !FileManager.default.fileExists(atPath: backupsPath.path) {
|
||||
try FileManager.default.createDirectory(atPath: backupsPath.path, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
let snapshot = Int(Date().timeIntervalSince1970.rounded())
|
||||
let writeUrl = backupsPath.appendingPathComponent("Ferrite-backup-\(snapshot).feb")
|
||||
|
||||
try encodedJson.write(to: writeUrl)
|
||||
backupUrls.append(writeUrl)
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
|
||||
// Backup is in local documents directory, so no need to restore it from the shared URL
|
||||
func restoreBackup() {
|
||||
guard let backupUrl = selectedBackupUrl else {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Could not find the selected backup in the local directory.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
do {
|
||||
let file = try Data(contentsOf: backupUrl)
|
||||
|
||||
let backup = try JSONDecoder().decode(Backup.self, from: file)
|
||||
|
||||
if let bookmarks = backup.bookmarks {
|
||||
for bookmark in bookmarks {
|
||||
PersistenceController.shared.createBookmark(bookmark)
|
||||
}
|
||||
}
|
||||
|
||||
if let storedHistories = backup.history {
|
||||
for storedHistory in storedHistories {
|
||||
for storedEntry in storedHistory.entries {
|
||||
PersistenceController.shared.createHistory(entryJson: storedEntry, date: storedHistory.date)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let storedLists = backup.sourceLists {
|
||||
for list in storedLists {
|
||||
let sourceListRequest = SourceList.fetchRequest()
|
||||
let urlPredicate = NSPredicate(format: "urlString == %@", list.urlString)
|
||||
let infoPredicate = NSPredicate(format: "author == %@ AND name == %@", list.author, list.name)
|
||||
sourceListRequest.predicate = NSCompoundPredicate(type: .or, subpredicates: [urlPredicate, infoPredicate])
|
||||
sourceListRequest.fetchLimit = 1
|
||||
|
||||
if (try? backgroundContext.fetch(sourceListRequest).first) != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
let newSourceList = SourceList(context: backgroundContext)
|
||||
newSourceList.name = list.name
|
||||
newSourceList.urlString = list.urlString
|
||||
newSourceList.id = UUID(uuidString: list.id) ?? UUID()
|
||||
newSourceList.author = list.author
|
||||
}
|
||||
}
|
||||
|
||||
backupSourceNames = backup.sourceNames ?? []
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
|
||||
// if iOS 14 is available, sleep to prevent any issues with alerts
|
||||
if #available(iOS 15, *) {
|
||||
showRestoreCompletedAlert.toggle()
|
||||
} else {
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 0.1)
|
||||
|
||||
Task { @MainActor in
|
||||
showRestoreCompletedAlert.toggle()
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup restore: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the backup from files and then the list
|
||||
// Removes an index if it's provided
|
||||
func removeBackup(backupUrl: URL, index: Int?) {
|
||||
do {
|
||||
try FileManager.default.removeItem(at: backupUrl)
|
||||
|
||||
if let index {
|
||||
backupUrls.remove(at: index)
|
||||
} else {
|
||||
backupUrls.removeAll(where: { $0 == backupUrl })
|
||||
}
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup removal: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyBackup(backupUrl: URL) {
|
||||
let backupSecured = backupUrl.startAccessingSecurityScopedResource()
|
||||
|
||||
defer {
|
||||
if backupSecured {
|
||||
backupUrl.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
}
|
||||
|
||||
let backupsPath = FileManager.default.appDirectory.appendingPathComponent("Backups")
|
||||
let localBackupPath = backupsPath.appendingPathComponent(backupUrl.lastPathComponent)
|
||||
|
||||
do {
|
||||
if FileManager.default.fileExists(atPath: localBackupPath.path) {
|
||||
try FileManager.default.removeItem(at: localBackupPath)
|
||||
} else if !FileManager.default.fileExists(atPath: backupsPath.path) {
|
||||
try FileManager.default.createDirectory(atPath: backupsPath.path, withIntermediateDirectories: true, attributes: nil)
|
||||
}
|
||||
|
||||
try FileManager.default.copyItem(at: backupUrl, to: localBackupPath)
|
||||
|
||||
selectedBackupUrl = localBackupPath
|
||||
} catch {
|
||||
Task {
|
||||
await toastModel?.updateToastDescription("Backup copy: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -10,17 +10,18 @@ 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
|
||||
|
||||
// Service agnostic variables
|
||||
@Published var currentDebridTask: Task<Void, Never>?
|
||||
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")
|
||||
|
|
@ -28,31 +29,47 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
|
||||
@Published var realDebridAuthProcessing: Bool = false
|
||||
@Published var realDebridAuthUrl: String = ""
|
||||
var realDebridAuthUrl: String = ""
|
||||
|
||||
// RealDebrid fetch variables
|
||||
@Published var realDebridHashes: [RealDebridIA] = []
|
||||
@Published var realDebridDownloadUrl: String = ""
|
||||
@Published var selectedRealDebridItem: RealDebridIA?
|
||||
@Published var selectedRealDebridFile: RealDebridIAFile?
|
||||
@Published var realDebridIAValues: [RealDebridIA] = []
|
||||
var realDebridDownloadUrl: String = ""
|
||||
|
||||
@Published var showDeleteAlert: Bool = false
|
||||
|
||||
// TODO: Switch to an individual item based sheet system to remove these variables
|
||||
var selectedRealDebridItem: RealDebridIA?
|
||||
var selectedRealDebridFile: RealDebridIAFile?
|
||||
var selectedRealDebridID: String?
|
||||
|
||||
init() {
|
||||
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
|
||||
|
||||
|
|
@ -65,11 +82,11 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
|
||||
public func matchSearchResult(result: SearchResult?) -> RealDebridIAStatus {
|
||||
guard let result = result else {
|
||||
guard let result else {
|
||||
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 +103,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 {
|
||||
|
|
@ -126,7 +143,7 @@ public class DebridManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func fetchRdDownload(searchResult: SearchResult, iaFile: RealDebridIAFile? = nil) async {
|
||||
public func fetchRdDownload(searchResult: SearchResult) async {
|
||||
defer {
|
||||
currentDebridTask = nil
|
||||
showLoadingProgress = false
|
||||
|
|
@ -141,14 +158,10 @@ public class DebridManager: ObservableObject {
|
|||
return
|
||||
}
|
||||
|
||||
var realDebridId: String?
|
||||
|
||||
do {
|
||||
realDebridId = try await realDebrid.addMagnet(magnetLink: magnetLink)
|
||||
|
||||
var fileIds: [Int] = []
|
||||
|
||||
if let iaFile = iaFile {
|
||||
if let iaFile = selectedRealDebridFile {
|
||||
guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else {
|
||||
return
|
||||
}
|
||||
|
|
@ -156,29 +169,52 @@ public class DebridManager: ObservableObject {
|
|||
fileIds = iaBatchFromFile.files.map(\.id)
|
||||
}
|
||||
|
||||
if let realDebridId = realDebridId {
|
||||
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
|
||||
// If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link
|
||||
let existingTorrents = try await realDebrid.userTorrents().filter { $0.hash == selectedRealDebridItem?.hash }
|
||||
|
||||
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile?.batchFileIndex ?? 0)
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
// If the links match from a user's downloads, no need to re-run a download
|
||||
if let existingTorrent = existingTorrents[safe: 0],
|
||||
let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0]
|
||||
{
|
||||
let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink }
|
||||
if let existingLink = existingLinks[safe: 0]?.download {
|
||||
realDebridDownloadUrl = existingLink
|
||||
} else {
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
}
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
|
||||
// Add a magnet after all the cache checks fail
|
||||
selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink)
|
||||
|
||||
if let realDebridId = selectedRealDebridID {
|
||||
try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds)
|
||||
|
||||
let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0)
|
||||
let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink)
|
||||
|
||||
realDebridDownloadUrl = downloadLink
|
||||
} else {
|
||||
toastModel?.updateToastDescription("Could not cache this torrent. Aborting.")
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
let error = error as NSError
|
||||
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
|
||||
switch error {
|
||||
case RealDebridError.EmptyTorrents:
|
||||
showDeleteAlert.toggle()
|
||||
default:
|
||||
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
|
||||
}
|
||||
let error = error as NSError
|
||||
|
||||
// Delete the torrent download if it exists
|
||||
if let realDebridId = realDebridId {
|
||||
try? await realDebrid.deleteTorrent(debridID: realDebridId)
|
||||
switch error.code {
|
||||
case -999:
|
||||
toastModel?.updateToastDescription("Download cancelled", newToastType: .info)
|
||||
default:
|
||||
toastModel?.updateToastDescription("RealDebrid download error: \(error)")
|
||||
}
|
||||
|
||||
await deleteRdTorrent()
|
||||
}
|
||||
|
||||
showLoadingProgress = false
|
||||
|
|
@ -186,4 +222,12 @@ public class DebridManager: ObservableObject {
|
|||
print("RealDebrid download error: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
public func deleteRdTorrent() async {
|
||||
if let realDebridId = selectedRealDebridID {
|
||||
try? await realDebrid.deleteTorrent(debridID: realDebridId)
|
||||
}
|
||||
|
||||
selectedRealDebridID = nil
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ enum ViewTab {
|
|||
case search
|
||||
case sources
|
||||
case settings
|
||||
case library
|
||||
}
|
||||
|
||||
@MainActor
|
||||
|
|
@ -31,10 +32,16 @@ class NavigationViewModel: ObservableObject {
|
|||
@Published var isEditingSearch: Bool = false
|
||||
@Published var isSearching: Bool = false
|
||||
|
||||
@Published var selectedSearchResult: SearchResult?
|
||||
|
||||
// For giving information in magnet choice sheet
|
||||
@Published var selectedTitle: String?
|
||||
@Published var selectedBatchTitle: String?
|
||||
|
||||
@Published var hideNavigationBar = false
|
||||
|
||||
@Published var currentChoiceSheet: ChoiceSheetType?
|
||||
@Published var activityItems: [Any] = []
|
||||
var activityItems: [Any] = []
|
||||
|
||||
// Used to show the activity sheet in the share menu
|
||||
@Published var showLocalActivitySheet = false
|
||||
|
|
@ -44,15 +51,15 @@ class NavigationViewModel: ObservableObject {
|
|||
|
||||
// Used between SourceListView and SourceSettingsView
|
||||
@Published var showSourceSettings: Bool = false
|
||||
@Published var selectedSource: Source?
|
||||
var selectedSource: Source?
|
||||
|
||||
@Published var showSourceListEditor: Bool = false
|
||||
@Published var selectedSourceList: SourceList?
|
||||
var selectedSourceList: SourceList?
|
||||
|
||||
@AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none
|
||||
@AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none
|
||||
|
||||
public func runDebridAction(action: DefaultDebridActionType?, urlString: String) {
|
||||
public func runDebridAction(urlString: String, _ action: DefaultDebridActionType? = nil) {
|
||||
let selectedAction = action ?? defaultDebridAction
|
||||
|
||||
switch selectedAction {
|
||||
|
|
@ -86,11 +93,11 @@ class NavigationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func runMagnetAction(action: DefaultMagnetActionType?, searchResult: SearchResult) {
|
||||
public func runMagnetAction(magnetString: String?, _ action: DefaultMagnetActionType? = nil) {
|
||||
let selectedAction = action ?? defaultMagnetAction
|
||||
|
||||
guard let magnetLink = searchResult.magnetLink else {
|
||||
toastModel?.toastDescription = "Could not run your action because the magnet link is invalid."
|
||||
guard let magnetLink = magnetString else {
|
||||
toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.")
|
||||
print("Magnet action error: The magnet link is invalid.")
|
||||
|
||||
return
|
||||
|
|
@ -116,4 +123,22 @@ class NavigationViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
// The timeStamp and date are nil because the create function will make them automatically
|
||||
PersistenceController.shared.createHistory(
|
||||
entryJson: HistoryEntryJson(
|
||||
name: name ?? "",
|
||||
subName: subName,
|
||||
url: url ?? "",
|
||||
timeStamp: nil,
|
||||
source: source
|
||||
),
|
||||
date: nil
|
||||
)
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -28,10 +18,9 @@ class ScrapingViewModel: ObservableObject {
|
|||
var toastModel: ToastViewModel?
|
||||
let byteCountFormatter: ByteCountFormatter = .init()
|
||||
|
||||
@Published var runningSearchTask: Task<Void, Error>?
|
||||
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?
|
||||
|
||||
|
|
@ -40,6 +29,17 @@ class ScrapingViewModel: ObservableObject {
|
|||
searchResults = newResults
|
||||
}
|
||||
|
||||
// Utility function to print source specific errors
|
||||
func sendSourceError(_ description: String, newToastType: ToastViewModel.ToastType? = nil) async {
|
||||
let newDescription = "\(currentSourceName ?? "No source given"): \(description)"
|
||||
await toastModel?.updateToastDescription(
|
||||
newDescription,
|
||||
newToastType: newToastType
|
||||
)
|
||||
|
||||
print(newDescription)
|
||||
}
|
||||
|
||||
public func scanSources(sources: [Source]) async {
|
||||
if sources.isEmpty {
|
||||
await toastModel?.updateToastDescription("There are no sources to search!", newToastType: .info)
|
||||
|
|
@ -67,8 +67,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let preferredParser = SourcePreferredParser(rawValue: source.preferredParser) ?? .none
|
||||
|
||||
guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
|
||||
await toastModel?.updateToastDescription("Could not process search query, invalid characters present.")
|
||||
print("Could not process search query, invalid characters present")
|
||||
await sendSourceError("Could not process search query, invalid characters present.")
|
||||
|
||||
continue
|
||||
}
|
||||
|
|
@ -85,7 +84,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
fallbackUrls: source.fallbackUrls
|
||||
)
|
||||
|
||||
if let data = data,
|
||||
if let data,
|
||||
let html = String(data: data, encoding: .utf8)
|
||||
{
|
||||
let sourceResults = await scrapeHtml(source: source, baseUrl: baseUrl, html: html)
|
||||
|
|
@ -110,7 +109,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
)
|
||||
}
|
||||
|
||||
if let data = data,
|
||||
if let data,
|
||||
let rss = String(data: data, encoding: .utf8)
|
||||
{
|
||||
let sourceResults = await scrapeRss(source: source, rss: rss)
|
||||
|
|
@ -156,7 +155,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
fallbackUrls: source.fallbackUrls
|
||||
)
|
||||
|
||||
if let data = data {
|
||||
if let data {
|
||||
let sourceResults = await scrapeJson(source: source, jsonData: data)
|
||||
tempResults += sourceResults
|
||||
}
|
||||
|
|
@ -181,7 +180,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
return data
|
||||
}
|
||||
|
||||
if let fallbackUrls = fallbackUrls {
|
||||
if let fallbackUrls {
|
||||
for fallbackUrl in fallbackUrls {
|
||||
if let data = await fetchWebsiteData(urlString: fallbackUrl + replacedSearchUrl) {
|
||||
return data
|
||||
|
|
@ -266,13 +265,11 @@ class ScrapingViewModel: ObservableObject {
|
|||
case -999:
|
||||
await toastModel?.updateToastDescription("Search cancelled", newToastType: .info)
|
||||
case -1001:
|
||||
await toastModel?.updateToastDescription("Credentials request timed out")
|
||||
await sendSourceError("Credentials request timed out")
|
||||
default:
|
||||
await toastModel?.updateToastDescription("Error in fetching an API credential \(error)")
|
||||
await sendSourceError("Error in fetching an API credential \(error)")
|
||||
}
|
||||
|
||||
print("Error in fetching an API credential \(error)")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -280,9 +277,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
// Fetches the data for a URL
|
||||
public func fetchWebsiteData(urlString: String) async -> Data? {
|
||||
guard let url = URL(string: urlString) else {
|
||||
await toastModel?.updateToastDescription("Source doesn't contain a valid URL, contact the source dev!")
|
||||
|
||||
print("Source doesn't contain a valid URL, contact the source dev!")
|
||||
await sendSourceError("Source doesn't contain a valid URL, contact the source dev!")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -299,13 +294,11 @@ class ScrapingViewModel: ObservableObject {
|
|||
case -999:
|
||||
await toastModel?.updateToastDescription("Search cancelled", newToastType: .info)
|
||||
case -1001:
|
||||
await toastModel?.updateToastDescription("Data request timed out. Trying fallback URLs if present.")
|
||||
await sendSourceError("Data request timed out. Trying fallback URLs if present.")
|
||||
default:
|
||||
await toastModel?.updateToastDescription("Error in fetching website data \(error)")
|
||||
await sendSourceError("Error in fetching website data \(error)")
|
||||
}
|
||||
|
||||
print("Error in fetching data \(error)")
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
@ -370,7 +363,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
} else if
|
||||
let searchResult = searchResult,
|
||||
let searchResult,
|
||||
let magnetLink = searchResult.magnetLink,
|
||||
magnetLink.starts(with: "magnet:"),
|
||||
!tempResults.contains(searchResult)
|
||||
|
|
@ -415,7 +408,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
if let magnetLinkParser = jsonParser.magnetLink, existingSearchResult?.magnetLink == nil {
|
||||
let rawLink = result[magnetLinkParser.query.components(separatedBy: ".")].rawValue
|
||||
link = rawLink is NSNull ? nil : String(describing: rawLink)
|
||||
} else if let magnetHash = magnetHash {
|
||||
} else if let magnetHash {
|
||||
link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: source.trackers)
|
||||
}
|
||||
|
||||
|
|
@ -476,8 +469,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let document = try SwiftSoup.parse(rss, "", Parser.xmlParser())
|
||||
items = try document.getElementsByTag(rssParser.items)
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription("RSS scraping error, couldn't fetch items: \(error)")
|
||||
print("RSS scraping error, couldn't fetch items: \(error)")
|
||||
await sendSourceError("RSS scraping error, couldn't fetch items: \(error)")
|
||||
|
||||
return tempResults
|
||||
}
|
||||
|
|
@ -517,7 +509,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
discriminator: magnetLinkParser.discriminator,
|
||||
regexString: magnetLinkParser.regex
|
||||
)
|
||||
} else if let magnetHash = magnetHash {
|
||||
} else if let magnetHash {
|
||||
link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: source.trackers)
|
||||
} else {
|
||||
continue
|
||||
|
|
@ -597,7 +589,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
parsedValue = try item.getElementsByTag(query).first()?.text()
|
||||
default:
|
||||
// If there's a key/value to lookup the attribute with, query it. Othewise assume the value is in the same attribute
|
||||
if let discriminator = discriminator {
|
||||
if let discriminator {
|
||||
let containerElement = try item.getElementsByAttributeValue(discriminator, query).first()
|
||||
parsedValue = try containerElement?.attr(attribute)
|
||||
} else {
|
||||
|
|
@ -607,8 +599,8 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
// A capture group must be used in the provided regex
|
||||
if let regexString = regexString,
|
||||
let parsedValue = parsedValue,
|
||||
if let regexString,
|
||||
let parsedValue,
|
||||
let regexValue = try? Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value
|
||||
{
|
||||
return regexValue
|
||||
|
|
@ -631,8 +623,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
let document = try SwiftSoup.parse(html)
|
||||
rows = try document.select(htmlParser.rows)
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription("Scraping error, couldn't fetch rows: \(error)")
|
||||
print("Scraping error, couldn't fetch rows: \(error)")
|
||||
await sendSourceError("Scraping error, couldn't fetch rows: \(error)")
|
||||
|
||||
return tempResults
|
||||
}
|
||||
|
|
@ -764,8 +755,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
tempResults.append(result)
|
||||
}
|
||||
} catch {
|
||||
await toastModel?.updateToastDescription("Scraping error: \(error)")
|
||||
print("Scraping error: \(error)")
|
||||
await sendSourceError("Scraping error: \(error)")
|
||||
|
||||
continue
|
||||
}
|
||||
|
|
@ -788,8 +778,8 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
|
||||
// A capture group must be used in the provided regex
|
||||
if let regexString = regexString,
|
||||
let parsedValue = parsedValue,
|
||||
if let regexString,
|
||||
let parsedValue,
|
||||
let regexValue = try? Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value
|
||||
{
|
||||
return regexValue
|
||||
|
|
@ -802,10 +792,10 @@ class ScrapingViewModel: ObservableObject {
|
|||
public func fetchMagnetHash(magnetLink: String? = nil, existingHash: String? = nil) -> String? {
|
||||
var magnetHash: String
|
||||
|
||||
if let existingHash = existingHash {
|
||||
if let existingHash {
|
||||
magnetHash = existingHash
|
||||
} else if
|
||||
let magnetLink = magnetLink,
|
||||
let magnetLink,
|
||||
let firstSplit = magnetLink.split(separator: ":")[safe: 3],
|
||||
let tempHash = firstSplit.split(separator: "&")[safe: 0]
|
||||
{
|
||||
|
|
@ -850,11 +840,11 @@ class ScrapingViewModel: ObservableObject {
|
|||
|
||||
magnetLinkArray.append(magnetHash)
|
||||
|
||||
if let title = title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
|
||||
if let title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) {
|
||||
magnetLinkArray.append("&dn=\(encodedTitle)")
|
||||
}
|
||||
|
||||
if let trackers = trackers {
|
||||
if let trackers {
|
||||
for trackerUrl in trackers {
|
||||
if URL(string: trackerUrl) != nil,
|
||||
let encodedUrlString = trackerUrl.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)
|
||||
|
|
@ -912,7 +902,7 @@ class ScrapingViewModel: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
await toastModel?.updateToastDescription(responseArray.joined())
|
||||
await sendSourceError(responseArray.joined(separator: " "))
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SourceViewModel.swift
|
||||
// SourceManager.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/25/22.
|
||||
|
|
@ -7,14 +7,14 @@
|
|||
|
||||
import CoreData
|
||||
import Foundation
|
||||
import UIKit
|
||||
import SwiftUI
|
||||
|
||||
public class SourceManager: ObservableObject {
|
||||
var toastModel: ToastViewModel?
|
||||
|
||||
@Published var availableSources: [SourceJson] = []
|
||||
|
||||
@Published var urlErrorAlertText = ""
|
||||
var urlErrorAlertText = ""
|
||||
@Published var showUrlErrorAlert = false
|
||||
|
||||
@MainActor
|
||||
|
|
@ -52,14 +52,30 @@ public class SourceManager: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
func fetchUpdatedSources(installedSources: FetchedResults<Source>) -> [SourceJson] {
|
||||
var updatedSources: [SourceJson] = []
|
||||
|
||||
for source in installedSources {
|
||||
if let availableSource = availableSources.first(where: {
|
||||
source.listId == $0.listId && source.name == $0.name && source.author == $0.author
|
||||
}),
|
||||
availableSource.version > source.version
|
||||
{
|
||||
updatedSources.append(availableSource)
|
||||
}
|
||||
}
|
||||
|
||||
return updatedSources
|
||||
}
|
||||
|
||||
// Checks if the current app version is supported by the source
|
||||
func checkAppVersion(minVersion: String?) -> Bool {
|
||||
// If there's no min version, assume that every version is supported
|
||||
guard let minVersion = minVersion else {
|
||||
guard let minVersion else {
|
||||
return true
|
||||
}
|
||||
|
||||
return UIApplication.shared.appVersion >= minVersion
|
||||
return Application.shared.appVersion >= minVersion
|
||||
}
|
||||
|
||||
// Fetches sources using the background context
|
||||
|
|
@ -369,7 +385,7 @@ public class SourceManager: ObservableObject {
|
|||
let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!))
|
||||
let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data)
|
||||
|
||||
if let existingSourceList = existingSourceList {
|
||||
if let existingSourceList {
|
||||
existingSourceList.urlString = sourceUrl
|
||||
existingSourceList.name = rawResponse.name
|
||||
existingSourceList.author = rawResponse.author
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ class ToastViewModel: ObservableObject {
|
|||
@Published var showToast: Bool = false
|
||||
|
||||
public func updateToastDescription(_ description: String, newToastType: ToastType? = nil) {
|
||||
if let newToastType = newToastType {
|
||||
if let newToastType {
|
||||
toastType = newToastType
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,24 +9,32 @@ import SwiftUI
|
|||
|
||||
struct AboutView: View {
|
||||
var body: some View {
|
||||
VStack {
|
||||
Image("AppImage")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.cornerRadius(25)
|
||||
|
||||
Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.")
|
||||
.padding()
|
||||
|
||||
List {
|
||||
ListRowTextView(leftText: "Version", rightText: UIApplication.shared.appVersion)
|
||||
ListRowTextView(leftText: "Build number", rightText: UIApplication.shared.appBuild)
|
||||
ListRowTextView(leftText: "Build type", rightText: UIApplication.shared.buildType)
|
||||
List {
|
||||
Section {
|
||||
ListRowTextView(leftText: "Version", rightText: Application.shared.appVersion)
|
||||
ListRowTextView(leftText: "Build number", rightText: Application.shared.appBuild)
|
||||
ListRowTextView(leftText: "Build type", rightText: Application.shared.buildType)
|
||||
ListRowLinkView(text: "Discord server", link: "https://discord.gg/sYQxnuD7Fj")
|
||||
ListRowLinkView(text: "GitHub repository", link: "https://github.com/bdashore3/Ferrite")
|
||||
} header: {
|
||||
VStack(alignment: .center) {
|
||||
Image("AppImage")
|
||||
.resizable()
|
||||
.frame(width: 100, height: 100)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 100 * 0.225, style: .continuous))
|
||||
.padding(.top, 24)
|
||||
|
||||
Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.")
|
||||
.textCase(.none)
|
||||
.foregroundColor(.label)
|
||||
.font(.body)
|
||||
.padding(.top, 8)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 7, bottom: 0, trailing: 0))
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("About")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
import SwiftUI
|
||||
|
||||
struct BatchChoiceView: View {
|
||||
@Environment(\.presentationMode) var presentationMode
|
||||
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
|
|
@ -21,14 +21,16 @@ 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)
|
||||
await debridManager.fetchRdDownload(searchResult: searchResult)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
// The download may complete before this sheet dismisses
|
||||
try? await Task.sleep(seconds: 1)
|
||||
navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl)
|
||||
navModel.selectedBatchTitle = file.name
|
||||
navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
|
||||
}
|
||||
|
||||
debridManager.selectedRealDebridFile = nil
|
||||
|
|
@ -36,8 +38,9 @@ struct BatchChoiceView: View {
|
|||
}
|
||||
}
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
navModel.currentChoiceSheet = nil
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
|
@ -46,9 +49,12 @@ struct BatchChoiceView: View {
|
|||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
navModel.currentChoiceSheet = nil
|
||||
|
||||
presentationMode.wrappedValue.dismiss()
|
||||
Task {
|
||||
try? await Task.sleep(seconds: 1)
|
||||
debridManager.selectedRealDebridItem = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
Ferrite/Views/CommonViews/AlertButton.swift
Normal file
71
Ferrite/Views/CommonViews/AlertButton.swift
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
//
|
||||
// AlertButton.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/8/22.
|
||||
//
|
||||
// Universal alert button for dynamic alert views
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct AlertButton: Identifiable {
|
||||
enum Role {
|
||||
case destructive
|
||||
case cancel
|
||||
}
|
||||
|
||||
let id: UUID
|
||||
let label: String
|
||||
let action: () -> Void
|
||||
let role: Role?
|
||||
|
||||
// Used for all buttons
|
||||
init(_ label: String, role: Role? = nil, action: @escaping () -> Void) {
|
||||
id = UUID()
|
||||
self.label = label
|
||||
self.action = action
|
||||
self.role = role
|
||||
}
|
||||
|
||||
// Used for buttons with no action
|
||||
init(_ label: String = "Cancel", role: Role? = nil) {
|
||||
id = UUID()
|
||||
self.label = label
|
||||
action = {}
|
||||
self.role = role
|
||||
}
|
||||
|
||||
func toActionButton() -> Alert.Button {
|
||||
if let role {
|
||||
switch role {
|
||||
case .cancel:
|
||||
return .cancel(Text(label))
|
||||
case .destructive:
|
||||
return .destructive(Text(label), action: action)
|
||||
}
|
||||
} else {
|
||||
return .default(Text(label), action: action)
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
@ViewBuilder
|
||||
func toButtonView() -> some View {
|
||||
Button(label, role: toButtonRole(role), action: action)
|
||||
}
|
||||
|
||||
@available(iOS 15.0, *)
|
||||
func toButtonRole(_ role: Role?) -> ButtonRole? {
|
||||
if let role {
|
||||
switch role {
|
||||
case .destructive:
|
||||
return .destructive
|
||||
case .cancel:
|
||||
return .cancel
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Ferrite/Views/CommonViews/Backport.swift
Normal file
103
Ferrite/Views/CommonViews/Backport.swift
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
//
|
||||
// Backport.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/29/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct Backport<Content> {
|
||||
public let content: Content
|
||||
|
||||
public init(_ content: Content) {
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
var backport: Backport<Self> { Backport(self) }
|
||||
}
|
||||
|
||||
extension Backport where Content: View {
|
||||
@ViewBuilder func alert(isPresented: Binding<Bool>, title: String, message: String?, buttons: [AlertButton]) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.alert(
|
||||
title,
|
||||
isPresented: isPresented,
|
||||
actions: {
|
||||
ForEach(buttons) { button in
|
||||
button.toButtonView()
|
||||
}
|
||||
},
|
||||
message: {
|
||||
if let message {
|
||||
Text(message)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
content
|
||||
.background {
|
||||
Color.clear
|
||||
.alert(isPresented: isPresented) {
|
||||
if let primaryButton = buttons[safe: 0],
|
||||
let secondaryButton = buttons[safe: 1]
|
||||
{
|
||||
return Alert(
|
||||
title: Text(title),
|
||||
message: message.map { Text($0) } ?? nil,
|
||||
primaryButton: primaryButton.toActionButton(),
|
||||
secondaryButton: secondaryButton.toActionButton()
|
||||
)
|
||||
} else {
|
||||
return Alert(
|
||||
title: Text(title),
|
||||
message: message.map { Text($0) } ?? nil,
|
||||
dismissButton: buttons[0].toActionButton()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func confirmationDialog(isPresented: Binding<Bool>, title: String, message: String?, buttons: [AlertButton]) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.confirmationDialog(
|
||||
title,
|
||||
isPresented: isPresented,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
ForEach(buttons) { button in
|
||||
button.toButtonView()
|
||||
}
|
||||
} message: {
|
||||
if let message {
|
||||
Text(message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content
|
||||
.actionSheet(isPresented: isPresented) {
|
||||
ActionSheet(
|
||||
title: Text(title),
|
||||
message: message.map { Text($0) } ?? nil,
|
||||
buttons: [buttons.map { $0.toActionButton() }, [.cancel()]].flatMap { $0 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func tint(_ color: Color) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.tint(color)
|
||||
} else {
|
||||
content
|
||||
.accentColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
//
|
||||
// dynamicAccentColor.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/15/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DynamicAccentColor: ViewModifier {
|
||||
let color: Color
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.tint(color)
|
||||
} else {
|
||||
content
|
||||
.accentColor(color)
|
||||
}
|
||||
}
|
||||
}
|
||||
29
Ferrite/Views/CommonViews/DynamicFetchRequest.swift
Normal file
29
Ferrite/Views/CommonViews/DynamicFetchRequest.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// DynamicFetchRequest.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/6/22.
|
||||
//
|
||||
// Used for FetchRequests with a dynamic predicate
|
||||
// iOS 14 compatible view
|
||||
//
|
||||
|
||||
import CoreData
|
||||
import SwiftUI
|
||||
|
||||
struct DynamicFetchRequest<T: NSManagedObject, Content: View>: View {
|
||||
@FetchRequest var fetchRequest: FetchedResults<T>
|
||||
|
||||
let content: (FetchedResults<T>) -> Content
|
||||
|
||||
var body: some View {
|
||||
content(fetchRequest)
|
||||
}
|
||||
|
||||
init(predicate: NSPredicate?,
|
||||
@ViewBuilder content: @escaping (FetchedResults<T>) -> Content)
|
||||
{
|
||||
_fetchRequest = FetchRequest<T>(sortDescriptors: [], predicate: predicate)
|
||||
self.content = content
|
||||
}
|
||||
}
|
||||
27
Ferrite/Views/CommonViews/EmptyInstructionView.swift
Normal file
27
Ferrite/Views/CommonViews/EmptyInstructionView.swift
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
//
|
||||
// EmptyInstructionView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/5/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct EmptyInstructionView: View {
|
||||
let title: String
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 5) {
|
||||
Text(title)
|
||||
.font(.system(size: 25, weight: .semibold))
|
||||
|
||||
Text(message)
|
||||
.padding(.horizontal, 50)
|
||||
}
|
||||
.multilineTextAlignment(.center)
|
||||
.foregroundColor(.secondaryLabel)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.ignoresSafeArea()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
//
|
||||
// GroupBoxStyle.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/21/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ErrorGroupBoxStyle: GroupBoxStyle {
|
||||
func makeBody(configuration: Configuration) -> some View {
|
||||
VStack {
|
||||
configuration.label
|
||||
configuration.content
|
||||
}
|
||||
.padding(10)
|
||||
.background(Color(UIColor.secondarySystemGroupedBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous))
|
||||
}
|
||||
}
|
||||
29
Ferrite/Views/CommonViews/InlineHeader.swift
Normal file
29
Ferrite/Views/CommonViews/InlineHeader.swift
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// InlineHeader.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/5/22.
|
||||
//
|
||||
// For iOS 15's weird defaults regarding sectioned list padding
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InlineHeader: View {
|
||||
let title: String
|
||||
|
||||
init(_ title: String) {
|
||||
self.title = title
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 16, *) {
|
||||
Text(title)
|
||||
} else if #available(iOS 15, *) {
|
||||
Text(title)
|
||||
.listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 0))
|
||||
} else {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@
|
|||
//
|
||||
// Created by Brian Dashore on 7/26/22.
|
||||
//
|
||||
// List row button, text, and link boilerplate
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// These views were imported from Asobi
|
||||
// View alias for a list row with an external link
|
||||
struct ListRowLinkView: View {
|
||||
let text: String
|
||||
let link: String
|
||||
|
|
@ -64,7 +64,7 @@ struct ListRowTextView: View {
|
|||
|
||||
Spacer()
|
||||
|
||||
if let rightText = rightText {
|
||||
if let rightText {
|
||||
Text(rightText)
|
||||
} else {
|
||||
Image(systemName: rightSymbol!)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift
Normal file
24
Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// ConditionalId.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/4/22.
|
||||
//
|
||||
// Applies an ID below iOS 16
|
||||
// This is due to ID workarounds making iOS 16 apps crash
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ConditionalId<ID: Hashable>: ViewModifier {
|
||||
let id: ID
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
} else {
|
||||
content
|
||||
.id(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
25
Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift
Normal file
25
Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
//
|
||||
// DisableInteraction.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/13/22.
|
||||
//
|
||||
// Disables interaction on any view without applying the appearance
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DisableInteraction: ViewModifier {
|
||||
let disabled: Bool
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.overlay {
|
||||
if disabled {
|
||||
Color.clear
|
||||
.contentShape(Rectangle())
|
||||
.gesture(TapGesture())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
23
Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift
Normal file
23
Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// DisabledAppearance.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/10/22.
|
||||
//
|
||||
// Adds opacity transitions to the disabled modifier
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DisabledAppearance: ViewModifier {
|
||||
let disabled: Bool
|
||||
let dimmedOpacity: Double?
|
||||
let animation: Animation?
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
content
|
||||
.disabled(disabled)
|
||||
.opacity(disabled ? dimmedOpacity.map { $0 } ?? 0.5 : 1)
|
||||
.animation(animation.map { $0 } ?? .none, value: disabled)
|
||||
}
|
||||
}
|
||||
28
Ferrite/Views/CommonViews/Modifiers/InlinedList.swift
Normal file
28
Ferrite/Views/CommonViews/Modifiers/InlinedList.swift
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// InlinedList.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/4/22.
|
||||
//
|
||||
// Removes the top padding on unsectioned lists
|
||||
// If a list is sectioned, see InlineHeader
|
||||
//
|
||||
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
|
||||
struct InlinedList: ViewModifier {
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 16, *) {
|
||||
content
|
||||
.introspectCollectionView { collectionView in
|
||||
collectionView.contentInset.top = -20
|
||||
}
|
||||
} else {
|
||||
content
|
||||
.introspectTableView { tableView in
|
||||
tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,9 @@
|
|||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 7/4/22.
|
||||
// Contributed by Mantton
|
||||
//
|
||||
// A wrapper that switches between NavigationStack and the legacy NavigationView
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ struct ContentView: View {
|
|||
var body: some View {
|
||||
NavView {
|
||||
VStack(spacing: 10) {
|
||||
HStack {
|
||||
HStack(spacing: 6) {
|
||||
Text("Filter")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
|
|
@ -50,10 +50,10 @@ struct ContentView: View {
|
|||
Button {
|
||||
selectedSource = source
|
||||
} label: {
|
||||
Text(name)
|
||||
|
||||
if selectedSource == source {
|
||||
Image(systemName: "checkmark")
|
||||
Label(name, systemImage: "checkmark")
|
||||
} else {
|
||||
Text(name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,30 +72,6 @@ struct ContentView: View {
|
|||
|
||||
SearchResultsView()
|
||||
}
|
||||
.sheet(item: $navModel.currentChoiceSheet) { item in
|
||||
Group {
|
||||
switch item {
|
||||
case .magnet:
|
||||
MagnetChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
case .batch:
|
||||
BatchChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
case .activity:
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
.presentationDetents([.medium, .large])
|
||||
} else {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search",
|
||||
|
|
@ -111,7 +87,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
|
||||
|
|
|
|||
95
Ferrite/Views/LibraryView.swift
Normal file
95
Ferrite/Views/LibraryView.swift
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
//
|
||||
// LibraryView.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>
|
||||
|
||||
@FetchRequest(
|
||||
entity: History.entity(),
|
||||
sortDescriptors: [
|
||||
NSSortDescriptor(keyPath: \History.date, ascending: false)
|
||||
]
|
||||
) var history: FetchedResults<History>
|
||||
|
||||
@State private var historyEmpty = true
|
||||
|
||||
@State private var selectedSegment: LibraryPickerSegment = .bookmarks
|
||||
@State private var editMode: EditMode = .inactive
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
VStack {
|
||||
Picker("Segments", selection: $selectedSegment) {
|
||||
Text("Bookmarks").tag(LibraryPickerSegment.bookmarks)
|
||||
Text("History").tag(LibraryPickerSegment.history)
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding()
|
||||
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
BookmarksView(bookmarks: bookmarks)
|
||||
case .history:
|
||||
HistoryView(history: history)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.overlay {
|
||||
switch selectedSegment {
|
||||
case .bookmarks:
|
||||
if bookmarks.isEmpty {
|
||||
EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results")
|
||||
}
|
||||
case .history:
|
||||
if history.isEmpty {
|
||||
EmptyInstructionView(title: "No History", message: "Start watching to build history")
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Library")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
HStack {
|
||||
EditButton()
|
||||
|
||||
if selectedSegment == .history {
|
||||
HistoryActionsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.editMode, $editMode)
|
||||
}
|
||||
.onChange(of: selectedSegment) { _ in
|
||||
editMode = .inactive
|
||||
}
|
||||
.onDisappear {
|
||||
editMode = .inactive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LibraryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LibraryView()
|
||||
}
|
||||
}
|
||||
68
Ferrite/Views/LibraryViews/BookmarksView.swift
Normal file
68
Ferrite/Views/LibraryViews/BookmarksView.swift
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
.inlinedList()
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if realDebridEnabled {
|
||||
viewTask = Task {
|
||||
let hashes = bookmarks.compactMap(\.magnetHash)
|
||||
await debridManager.populateDebridHashes(hashes)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
54
Ferrite/Views/LibraryViews/HistoryActionsView.swift
Normal file
54
Ferrite/Views/LibraryViews/HistoryActionsView.swift
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
//
|
||||
// HistoryActionsView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/7/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryActionsView: View {
|
||||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
|
||||
@State private var showActionSheet = false
|
||||
|
||||
var body: some View {
|
||||
Button("Clear") {
|
||||
showActionSheet.toggle()
|
||||
}
|
||||
.backport.tint(.red)
|
||||
.backport.confirmationDialog(
|
||||
isPresented: $showActionSheet,
|
||||
title: "Clear watch history",
|
||||
message: "This is an irreversible action!",
|
||||
buttons: [
|
||||
AlertButton("Past day", role: .destructive) {
|
||||
deleteHistory(.day)
|
||||
},
|
||||
AlertButton("Past week", role: .destructive) {
|
||||
deleteHistory(.week)
|
||||
},
|
||||
AlertButton("Past month", role: .destructive) {
|
||||
deleteHistory(.month)
|
||||
},
|
||||
AlertButton("All time", role: .destructive) {
|
||||
deleteHistory(.allTime)
|
||||
}
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func deleteHistory(_ deleteRange: HistoryDeleteRange) {
|
||||
do {
|
||||
try PersistenceController.shared.batchDeleteHistory(range: deleteRange)
|
||||
} catch {
|
||||
toastModel.updateToastDescription("History delete error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryActionsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryActionsView()
|
||||
}
|
||||
}
|
||||
80
Ferrite/Views/LibraryViews/HistoryButtonView.swift
Normal file
80
Ferrite/Views/LibraryViews/HistoryButtonView.swift
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// HistoryButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/9/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryButtonView: View {
|
||||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
let entry: HistoryEntry
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
navModel.selectedTitle = entry.name
|
||||
navModel.selectedBatchTitle = entry.subName
|
||||
|
||||
if let url = entry.url {
|
||||
if url.starts(with: "https://") {
|
||||
Task {
|
||||
debridManager.realDebridDownloadUrl = url
|
||||
navModel.runDebridAction(urlString: url)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
}
|
||||
}
|
||||
} else {
|
||||
navModel.runMagnetAction(magnetString: url)
|
||||
}
|
||||
} else {
|
||||
toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.")
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading) {
|
||||
VStack(alignment: .leading) {
|
||||
Text(entry.name ?? "Unknown title")
|
||||
.font(entry.subName == nil ? .body : .subheadline)
|
||||
.lineLimit(entry.subName == nil ? 2 : 1)
|
||||
|
||||
if let subName = entry.subName {
|
||||
Text(subName)
|
||||
.foregroundColor(.gray)
|
||||
.font(.subheadline)
|
||||
.lineLimit(2)
|
||||
}
|
||||
}
|
||||
|
||||
HStack {
|
||||
Text(entry.source ?? "Unknown source")
|
||||
|
||||
Spacer()
|
||||
|
||||
Text("DEBRID")
|
||||
.fontWeight(.bold)
|
||||
.padding(3)
|
||||
.background {
|
||||
Group {
|
||||
if let url = entry.url, url.starts(with: "https://") {
|
||||
Color.green
|
||||
} else {
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
}
|
||||
}
|
||||
65
Ferrite/Views/LibraryViews/HistoryView.swift
Normal file
65
Ferrite/Views/LibraryViews/HistoryView.swift
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
//
|
||||
// HistoryView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
var history: FetchedResults<History>
|
||||
var formatter: DateFormatter = .init()
|
||||
|
||||
@State private var historyIndex = 0
|
||||
|
||||
init(history: FetchedResults<History>) {
|
||||
self.history = history
|
||||
|
||||
formatter.dateStyle = .medium
|
||||
formatter.timeStyle = .none
|
||||
}
|
||||
|
||||
func groupedEntries(_ result: FetchedResults<History>) -> [[History]] {
|
||||
Dictionary(grouping: result) { (element: History) in
|
||||
element.dateString ?? ""
|
||||
}.values.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() }
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !history.isEmpty {
|
||||
List {
|
||||
ForEach(groupedEntries(history), id: \.self) { (section: [History]) in
|
||||
Section(header: Text(formatter.string(from: section[0].date ?? Date()))) {
|
||||
ForEach(section, id: \.self) { history in
|
||||
ForEach(history.entryArray) { entry in
|
||||
HistoryButtonView(entry: entry)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
removeEntry(at: offsets, from: history)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
|
||||
func removeEntry(at offsets: IndexSet, from history: History) {
|
||||
for index in offsets {
|
||||
if let entry = history.entryArray[safe: index] {
|
||||
history.removeFromEntries(entry)
|
||||
PersistenceController.shared.delete(entry, context: backgroundContext)
|
||||
}
|
||||
|
||||
if history.entryArray.isEmpty {
|
||||
PersistenceController.shared.delete(history, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -23,31 +23,44 @@ struct MagnetChoiceView: View {
|
|||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
if realDebridEnabled, debridManager.matchSearchResult(result: scrapingModel.selectedSearchResult) != .none {
|
||||
Section(header: "Now Playing") {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(navModel.selectedTitle ?? "No title")
|
||||
.font(.callout)
|
||||
.lineLimit(navModel.selectedBatchTitle == nil ? .max : 1)
|
||||
|
||||
if let batchTitle = navModel.selectedBatchTitle {
|
||||
Text(batchTitle)
|
||||
.foregroundColor(.gray)
|
||||
.font(.subheadline)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
Section(header: "Real Debrid options") {
|
||||
ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .outplayer)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(action: .vlc, urlString: debridManager.realDebridDownloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .vlc)
|
||||
}
|
||||
|
||||
ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") {
|
||||
navModel.runDebridAction(action: .infuse, urlString: debridManager.realDebridDownloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .infuse)
|
||||
}
|
||||
|
||||
ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") {
|
||||
UIPasteboard.general.string = debridManager.realDebridDownloadUrl
|
||||
showLinkCopyAlert.toggle()
|
||||
}
|
||||
.alert(isPresented: $showLinkCopyAlert) {
|
||||
Alert(
|
||||
title: Text("Copied"),
|
||||
message: Text("Download link copied successfully"),
|
||||
dismissButton: .cancel(Text("OK"))
|
||||
)
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $showLinkCopyAlert,
|
||||
title: "Copied",
|
||||
message: "Download link copied successfully",
|
||||
buttons: [AlertButton("OK")]
|
||||
)
|
||||
|
||||
ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") {
|
||||
if let url = URL(string: debridManager.realDebridDownloadUrl) {
|
||||
|
|
@ -60,19 +73,18 @@ 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) {
|
||||
Alert(
|
||||
title: Text("Copied"),
|
||||
message: Text("Magnet link copied successfully"),
|
||||
dismissButton: .cancel(Text("OK"))
|
||||
)
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $showMagnetCopyAlert,
|
||||
title: "Copied",
|
||||
message: "Magnet link copied successfully",
|
||||
buttons: [AlertButton("OK")]
|
||||
)
|
||||
|
||||
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,12 +94,11 @@ 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(magnetString: navModel.selectedSearchResult?.magnetLink, .webtor)
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
.sheet(isPresented: $navModel.showLocalActivitySheet) {
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
|
|
@ -96,6 +107,9 @@ struct MagnetChoiceView: View {
|
|||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
}
|
||||
.navigationTitle("Link actions")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,8 @@ struct MainView: View {
|
|||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var toastModel: ToastViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var backupManager: BackupManager
|
||||
|
||||
@AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true
|
||||
|
||||
|
|
@ -28,6 +30,12 @@ struct MainView: View {
|
|||
}
|
||||
.tag(ViewTab.search)
|
||||
|
||||
LibraryView()
|
||||
.tabItem {
|
||||
Label("Library", systemImage: "book.closed")
|
||||
}
|
||||
.tag(ViewTab.library)
|
||||
|
||||
SourcesView()
|
||||
.tabItem {
|
||||
Label("Sources", systemImage: "doc.text")
|
||||
|
|
@ -40,19 +48,26 @@ struct MainView: View {
|
|||
}
|
||||
.tag(ViewTab.settings)
|
||||
}
|
||||
.alert(isPresented: $showUpdateAlert) {
|
||||
Alert(
|
||||
title: Text("Update available"),
|
||||
message: Text("Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings."),
|
||||
primaryButton: .default(Text("Download")) {
|
||||
guard let releaseUrl = URL(string: releaseUrlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(releaseUrl)
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
.sheet(item: $navModel.currentChoiceSheet) { item in
|
||||
switch item {
|
||||
case .magnet:
|
||||
MagnetChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
case .batch:
|
||||
BatchChoiceView()
|
||||
.environmentObject(debridManager)
|
||||
.environmentObject(scrapingModel)
|
||||
.environmentObject(navModel)
|
||||
case .activity:
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
.presentationDetents([.medium, .large])
|
||||
} else {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if autoUpdateNotifs {
|
||||
|
|
@ -64,8 +79,7 @@ struct MainView: View {
|
|||
}
|
||||
|
||||
let releaseVersion = String(latestRelease.tagName.dropFirst())
|
||||
if releaseVersion > UIApplication.shared.appVersion {
|
||||
print("Greater")
|
||||
if releaseVersion > Application.shared.appVersion {
|
||||
releaseVersionString = latestRelease.tagName
|
||||
releaseUrlString = latestRelease.htmlUrl
|
||||
showUpdateAlert.toggle()
|
||||
|
|
@ -79,6 +93,52 @@ struct MainView: View {
|
|||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
.onOpenURL { url in
|
||||
if url.scheme == "file" {
|
||||
// Attempt to copy to backups directory if backup doesn't exist
|
||||
backupManager.copyBackup(backupUrl: url)
|
||||
|
||||
backupManager.showRestoreAlert.toggle()
|
||||
}
|
||||
}
|
||||
// Global alerts for backups
|
||||
.backport.alert(
|
||||
isPresented: $backupManager.showRestoreAlert,
|
||||
title: "Restore backup?",
|
||||
message: "Restoring this backup will merge all your data!",
|
||||
buttons: [
|
||||
.init("Restore", role: .destructive) {
|
||||
backupManager.restoreBackup()
|
||||
},
|
||||
.init(role: .cancel)
|
||||
]
|
||||
)
|
||||
.backport.alert(
|
||||
isPresented: $backupManager.showRestoreCompletedAlert,
|
||||
title: "Backup restored",
|
||||
message: backupManager.backupSourceNames.isEmpty ?
|
||||
"No sources need to be reinstalled" :
|
||||
"Reinstall sources: \(backupManager.backupSourceNames.joined(separator: ", "))",
|
||||
buttons: [
|
||||
.init("OK") {}
|
||||
]
|
||||
)
|
||||
// Updater alert
|
||||
.backport.alert(
|
||||
isPresented: $showUpdateAlert,
|
||||
title: "Update available",
|
||||
message: "Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings.",
|
||||
buttons: [
|
||||
AlertButton("Download") {
|
||||
guard let releaseUrl = URL(string: releaseUrlString) else {
|
||||
return
|
||||
}
|
||||
|
||||
UIApplication.shared.open(releaseUrl)
|
||||
},
|
||||
AlertButton(role: .cancel)
|
||||
]
|
||||
)
|
||||
.overlay {
|
||||
VStack {
|
||||
Spacer()
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ struct WebView: UIViewRepresentable {
|
|||
|
||||
func makeUIView(context: Context) -> WKWebView {
|
||||
let webView = WKWebView()
|
||||
webView.load(URLRequest(url: url))
|
||||
let _ = webView.load(URLRequest(url: url))
|
||||
return webView
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
//
|
||||
// SearchProgressView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/8/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchProgressView: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
}
|
||||
}
|
||||
|
||||
struct SearchProgressView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SearchProgressView()
|
||||
}
|
||||
}
|
||||
134
Ferrite/Views/SearchResultViews/SearchResultButtonView.swift
Normal file
134
Ferrite/Views/SearchResultViews/SearchResultButtonView.swift
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
//
|
||||
// SearchResultButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/2/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SearchResultButtonView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
var result: SearchResult
|
||||
|
||||
@State private var runOnce = false
|
||||
@State var existingBookmark: Bookmark? = nil
|
||||
|
||||
var body: some View {
|
||||
Button {
|
||||
if debridManager.currentDebridTask == nil {
|
||||
navModel.selectedSearchResult = result
|
||||
navModel.selectedTitle = result.title
|
||||
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
debridManager.currentDebridTask = Task {
|
||||
await debridManager.fetchRdDownload(searchResult: result)
|
||||
|
||||
if !debridManager.realDebridDownloadUrl.isEmpty {
|
||||
navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl)
|
||||
navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl)
|
||||
|
||||
if navModel.currentChoiceSheet != .magnet {
|
||||
debridManager.realDebridDownloadUrl = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case .partial:
|
||||
if debridManager.setSelectedRdResult(result: result) {
|
||||
navModel.currentChoiceSheet = .batch
|
||||
}
|
||||
case .none:
|
||||
navModel.addToHistory(name: result.title, source: result.source, url: result.magnetLink)
|
||||
navModel.runMagnetAction(magnetString: result.magnetLink)
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
Text(result.title ?? "No title")
|
||||
.font(.callout)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(4)
|
||||
|
||||
SearchResultRDView(result: result)
|
||||
}
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
.backport.tint(.primary)
|
||||
.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $debridManager.showDeleteAlert,
|
||||
title: "Caching file",
|
||||
message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.",
|
||||
buttons: [
|
||||
AlertButton("Yes", role: .destructive) {
|
||||
Task {
|
||||
await debridManager.deleteRdTorrent()
|
||||
}
|
||||
},
|
||||
AlertButton(role: .cancel)
|
||||
]
|
||||
)
|
||||
.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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -37,20 +37,18 @@ struct SearchResultRDView: View {
|
|||
.fontWeight(.bold)
|
||||
.padding(2)
|
||||
.background {
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
Color.green
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
case .partial:
|
||||
Color.orange
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
case .none:
|
||||
Color.red
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
Group {
|
||||
switch debridManager.matchSearchResult(result: result) {
|
||||
case .full:
|
||||
Color.green
|
||||
case .partial:
|
||||
Color.orange
|
||||
case .none:
|
||||
Color.red
|
||||
}
|
||||
}
|
||||
.cornerRadius(4)
|
||||
.opacity(0.5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -9,51 +9,18 @@ import SwiftUI
|
|||
|
||||
struct SearchResultsView: View {
|
||||
@EnvironmentObject var scrapingModel: ScrapingViewModel
|
||||
@EnvironmentObject var debridManager: DebridManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@AppStorage("RealDebrid.Enabled") var realDebridEnabled = false
|
||||
|
||||
var body: some 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.overlay {
|
||||
if scrapingModel.searchResults.isEmpty {
|
||||
if navModel.showSearchProgress {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
// Created by Brian Dashore on 7/11/22.
|
||||
//
|
||||
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
|
||||
struct SettingsView: View {
|
||||
|
|
@ -21,7 +22,7 @@ struct SettingsView: View {
|
|||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
Section(header: "Debrid services") {
|
||||
Section(header: InlineHeader("Debrid Services")) {
|
||||
HStack {
|
||||
Text("Real Debrid")
|
||||
Spacer()
|
||||
|
|
@ -36,15 +37,18 @@ struct SettingsView: View {
|
|||
} label: {
|
||||
Text(debridManager.realDebridEnabled ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login"))
|
||||
.foregroundColor(debridManager.realDebridEnabled ? .red : .blue)
|
||||
.onChange(of: debridManager.realDebridEnabled) { changed in
|
||||
print("Debrid enabled changed to \(changed)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: "Source management") {
|
||||
Section(header: Text("Source management")) {
|
||||
NavigationLink("Source lists", destination: SettingsSourceListView())
|
||||
}
|
||||
|
||||
Section(header: "Default actions") {
|
||||
Section(header: Text("Default actions")) {
|
||||
if debridManager.realDebridEnabled {
|
||||
NavigationLink(
|
||||
destination: DebridActionPickerView(),
|
||||
|
|
@ -94,6 +98,12 @@ struct SettingsView: View {
|
|||
)
|
||||
}
|
||||
|
||||
Section(header: Text("Backups")) {
|
||||
NavigationLink(destination: BackupsView()) {
|
||||
Text("Backups")
|
||||
}
|
||||
}
|
||||
|
||||
Section(header: Text("Updates")) {
|
||||
Toggle(isOn: $autoUpdateNotifs) {
|
||||
Text("Show update alerts")
|
||||
|
|
|
|||
73
Ferrite/Views/SettingsViews/BackupsView.swift
Normal file
73
Ferrite/Views/SettingsViews/BackupsView.swift
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
//
|
||||
// BackupsView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/16/22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BackupsView: View {
|
||||
@EnvironmentObject var backupManager: BackupManager
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
||||
@State private var selectedBackupUrl: URL?
|
||||
@State private var showRestoreAlert = false
|
||||
@State private var showRestoreCompletedAlert = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
if backupManager.backupUrls.isEmpty {
|
||||
EmptyInstructionView(title: "No Backups", message: "Create one using the + button in the top-right")
|
||||
} else {
|
||||
List {
|
||||
ForEach(backupManager.backupUrls, id: \.self) { url in
|
||||
Button(url.lastPathComponent) {
|
||||
backupManager.selectedBackupUrl = url
|
||||
backupManager.showRestoreAlert.toggle()
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.activityItems = [url]
|
||||
navModel.currentChoiceSheet = .activity
|
||||
} label: {
|
||||
Label("Export", systemImage: "square.and.arrow.up")
|
||||
}
|
||||
}
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let url = backupManager.backupUrls[safe: index] {
|
||||
backupManager.removeBackup(backupUrl: url, index: index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.inlinedList()
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
backupManager.backupUrls = FileManager.default.appDirectory
|
||||
.appendingPathComponent("Backups", isDirectory: true).contentsByDateAdded
|
||||
}
|
||||
.navigationTitle("Backups")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button {
|
||||
backupManager.createBackup()
|
||||
} label: {
|
||||
Image(systemName: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BackupsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BackupsView()
|
||||
}
|
||||
}
|
||||
|
|
@ -25,10 +25,11 @@ struct MagnetActionPickerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.navigationTitle("Default magnet action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
|
@ -63,10 +64,11 @@ struct DebridActionPickerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
.navigationTitle("Default debrid action")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,21 @@ struct SettingsAppVersionView: View {
|
|||
@State private var viewTask: Task<Void, Never>?
|
||||
@State private var releases: [GithubRelease] = []
|
||||
|
||||
@State private var loadedReleases = false
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("GitHub links")) {
|
||||
ForEach(releases, id: \.self) { release in
|
||||
ListRowLinkView(text: release.tagName, link: release.htmlUrl)
|
||||
ZStack {
|
||||
if !loadedReleases {
|
||||
ProgressView()
|
||||
} else if !releases.isEmpty {
|
||||
List {
|
||||
Section(header: InlineHeader("GitHub links")) {
|
||||
ForEach(releases, id: \.self) { release in
|
||||
ListRowLinkView(text: release.tagName, link: release.htmlUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
|
|
@ -32,13 +41,16 @@ struct SettingsAppVersionView: View {
|
|||
} catch {
|
||||
toastModel.updateToastDescription("Github error: \(error)")
|
||||
}
|
||||
|
||||
withAnimation {
|
||||
loadedReleases = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.navigationTitle("Version history")
|
||||
.navigationTitle("Version History")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,39 +21,61 @@ struct SettingsSourceListView: View {
|
|||
@State private var selectedSourceList: SourceList?
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section(header: Text("List information")) {
|
||||
ForEach(sourceLists, id: \.self) { sourceList in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(sourceList.name)
|
||||
ZStack {
|
||||
if sourceLists.isEmpty {
|
||||
EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right")
|
||||
} else {
|
||||
List {
|
||||
ForEach(sourceLists, id: \.self) { sourceList in
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(sourceList.name)
|
||||
|
||||
Text(sourceList.author)
|
||||
.foregroundColor(.gray)
|
||||
Text(sourceList.author)
|
||||
.foregroundColor(.gray)
|
||||
|
||||
Text("ID: \(sourceList.id)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedSourceList = sourceList
|
||||
presentSourceSheet.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
Image(systemName: "pencil")
|
||||
Text("ID: \(sourceList.id)")
|
||||
.font(.caption)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
.contextMenu {
|
||||
Button {
|
||||
navModel.selectedSourceList = sourceList
|
||||
presentSourceSheet.toggle()
|
||||
} label: {
|
||||
Text("Edit")
|
||||
Image(systemName: "pencil")
|
||||
}
|
||||
|
||||
Button {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(sourceList, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.onDelete { offsets in
|
||||
for index in offsets {
|
||||
if let list = sourceLists[safe: index] {
|
||||
PersistenceController.shared.delete(list, context: backgroundContext)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.inlinedList()
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.sheet(isPresented: $presentSourceSheet) {
|
||||
if #available(iOS 16, *) {
|
||||
SourceListEditorView()
|
||||
|
|
@ -62,7 +84,7 @@ struct SettingsSourceListView: View {
|
|||
SourceListEditorView()
|
||||
}
|
||||
}
|
||||
.navigationTitle("Source lists")
|
||||
.navigationTitle("Source Lists")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
|
|
|
|||
|
|
@ -15,28 +15,29 @@ struct SourceListEditorView: View {
|
|||
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@State private var sourceUrl = ""
|
||||
@State private var sourceUrlSet = false
|
||||
|
||||
@State private var sourceUrl: String = ""
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
Form {
|
||||
Section {
|
||||
TextField("Enter URL", text: $sourceUrl)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
}
|
||||
TextField("Enter URL", text: $sourceUrl)
|
||||
.disableAutocorrection(true)
|
||||
.keyboardType(.URL)
|
||||
.autocapitalization(.none)
|
||||
.conditionalId(sourceUrlSet)
|
||||
}
|
||||
.onAppear {
|
||||
sourceUrl = navModel.selectedSourceList?.urlString ?? ""
|
||||
sourceUrlSet = true
|
||||
}
|
||||
.alert(isPresented: $sourceManager.showUrlErrorAlert) {
|
||||
Alert(
|
||||
title: Text("Error"),
|
||||
message: Text(sourceManager.urlErrorAlertText),
|
||||
dismissButton: .default(Text("OK"))
|
||||
)
|
||||
}
|
||||
.backport.alert(
|
||||
isPresented: $sourceManager.showUrlErrorAlert,
|
||||
title: "Error",
|
||||
message: sourceManager.urlErrorAlertText,
|
||||
buttons: [AlertButton("OK")]
|
||||
)
|
||||
.navigationTitle("Editing source list")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// InstalledSourceView.swift
|
||||
// InstalledSourceButtonView.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/5/22.
|
||||
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import SwiftUI
|
||||
|
||||
struct InstalledSourceView: View {
|
||||
struct InstalledSourceButtonView: View {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
@EnvironmentObject var navModel: NavigationViewModel
|
||||
|
|
@ -32,6 +32,7 @@ struct InstalledSourceView: View {
|
|||
Text("by \(installedSource.author)")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
.contextMenu {
|
||||
Button {
|
||||
|
|
@ -42,11 +43,20 @@ struct InstalledSourceView: View {
|
|||
Image(systemName: "gear")
|
||||
}
|
||||
|
||||
Button {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
if #available(iOS 15.0, *) {
|
||||
Button(role: .destructive) {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
} else {
|
||||
Button {
|
||||
PersistenceController.shared.delete(installedSource, context: backgroundContext)
|
||||
} label: {
|
||||
Text("Remove")
|
||||
Image(systemName: "trash")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -33,5 +33,6 @@ struct SourceCatalogButtonView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
}
|
||||
|
|
@ -24,6 +24,7 @@ struct SourceUpdateButtonView: View {
|
|||
Text("by \(updatedSource.author ?? "Unknown")")
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
|
||||
Spacer()
|
||||
|
||||
|
|
@ -16,7 +16,7 @@ struct SourceSettingsView: View {
|
|||
NavView {
|
||||
List {
|
||||
if let selectedSource = navModel.selectedSource {
|
||||
Section(header: "Info") {
|
||||
Section(header: InlineHeader("Info")) {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
HStack {
|
||||
Text(selectedSource.name)
|
||||
|
|
@ -40,6 +40,7 @@ struct SourceSettingsView: View {
|
|||
.foregroundColor(.secondary)
|
||||
.font(.caption)
|
||||
}
|
||||
.padding(.vertical, 2)
|
||||
}
|
||||
|
||||
if selectedSource.dynamicBaseUrl {
|
||||
|
|
@ -77,7 +78,7 @@ struct SourceSettingsBaseUrlView: View {
|
|||
@State private var tempBaseUrl: String = ""
|
||||
var body: some View {
|
||||
Section(
|
||||
header: Text("Base URL"),
|
||||
header: InlineHeader("Base URL"),
|
||||
footer: Text("Enter the base URL of your server.")
|
||||
) {
|
||||
TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in
|
||||
|
|
@ -109,7 +110,7 @@ struct SourceSettingsApiView: View {
|
|||
|
||||
var body: some View {
|
||||
Section(
|
||||
header: Text("API credentials"),
|
||||
header: InlineHeader("API credentials"),
|
||||
footer: Text("Grab the required API credentials from the website. A client secret can be an API token.")
|
||||
) {
|
||||
if let clientId = selectedSourceApi.clientId, clientId.dynamic {
|
||||
|
|
@ -145,8 +146,8 @@ struct SourceSettingsMethodView: View {
|
|||
@ObservedObject var selectedSource: Source
|
||||
|
||||
var body: some View {
|
||||
Section(header: Text("Fetch method")) {
|
||||
if selectedSource.api != nil, selectedSource.jsonParser != nil {
|
||||
Section(header: InlineHeader("Fetch method")) {
|
||||
if selectedSource.jsonParser != nil {
|
||||
Button {
|
||||
selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue
|
||||
} label: {
|
||||
|
|
@ -191,6 +192,6 @@ struct SourceSettingsMethodView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,9 @@
|
|||
// Created by Brian Dashore on 7/24/22.
|
||||
//
|
||||
|
||||
import Introspect
|
||||
import SwiftUI
|
||||
import SwiftUIX
|
||||
|
||||
struct SourcesView: View {
|
||||
@EnvironmentObject var sourceManager: SourceManager
|
||||
|
|
@ -18,83 +20,112 @@ struct SourcesView: View {
|
|||
sortDescriptors: []
|
||||
) var sources: FetchedResults<Source>
|
||||
|
||||
private var updatedSources: [SourceJson] {
|
||||
var tempSources: [SourceJson] = []
|
||||
|
||||
for source in sources {
|
||||
guard let availableSource = sourceManager.availableSources.first(where: {
|
||||
source.listId == $0.listId && source.name == $0.name && source.author == $0.author
|
||||
}) else {
|
||||
continue
|
||||
}
|
||||
|
||||
if availableSource.version > source.version {
|
||||
tempSources.append(availableSource)
|
||||
}
|
||||
}
|
||||
|
||||
return tempSources
|
||||
}
|
||||
@State private var checkedForSources = false
|
||||
@State private var isEditing = false
|
||||
|
||||
@State private var viewTask: Task<Void, Never>? = nil
|
||||
@State private var searchText: String = ""
|
||||
@State private var filteredUpdatedSources: [SourceJson] = []
|
||||
@State private var filteredAvailableSources: [SourceJson] = []
|
||||
@State private var sourcePredicate: NSPredicate?
|
||||
|
||||
var body: some View {
|
||||
NavView {
|
||||
List {
|
||||
if !updatedSources.isEmpty {
|
||||
Section(header: "Updates") {
|
||||
ForEach(updatedSources, id: \.self) { source in
|
||||
SourceUpdateButtonView(updatedSource: source)
|
||||
DynamicFetchRequest(predicate: sourcePredicate) { (installedSources: FetchedResults<Source>) in
|
||||
ZStack {
|
||||
if !checkedForSources {
|
||||
ProgressView()
|
||||
} else if sources.isEmpty, sourceManager.availableSources.isEmpty {
|
||||
EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings")
|
||||
} else {
|
||||
List {
|
||||
if !filteredUpdatedSources.isEmpty {
|
||||
Section(header: InlineHeader("Updates")) {
|
||||
ForEach(filteredUpdatedSources, id: \.self) { source in
|
||||
SourceUpdateButtonView(updatedSource: source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !installedSources.isEmpty {
|
||||
Section(header: InlineHeader("Installed")) {
|
||||
ForEach(installedSources, id: \.self) { source in
|
||||
InstalledSourceButtonView(installedSource: source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !filteredAvailableSources.isEmpty {
|
||||
Section(header: InlineHeader("Catalog")) {
|
||||
ForEach(filteredAvailableSources, id: \.self) { availableSource in
|
||||
if !installedSources.contains(where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
}) {
|
||||
SourceCatalogButtonView(availableSource: availableSource)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.conditionalId(UUID())
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
|
||||
if !sources.isEmpty {
|
||||
Section(header: "Installed") {
|
||||
ForEach(sources, id: \.self) { source in
|
||||
InstalledSourceView(installedSource: source)
|
||||
.sheet(isPresented: $navModel.showSourceSettings) {
|
||||
SourceSettingsView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
.onAppear {
|
||||
viewTask = Task {
|
||||
await sourceManager.fetchSourcesFromUrl()
|
||||
filteredAvailableSources = sourceManager.availableSources.filter { availableSource in
|
||||
!installedSources.contains(where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
})
|
||||
}
|
||||
|
||||
filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources)
|
||||
checkedForSources = true
|
||||
}
|
||||
}
|
||||
|
||||
if sourceManager.availableSources.contains(where: { availableSource in
|
||||
!sources.contains(
|
||||
where: {
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
.onChange(of: searchText) { _ in
|
||||
sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText)
|
||||
}
|
||||
.onReceive(installedSources.publisher.count()) { _ in
|
||||
filteredAvailableSources = sourceManager.availableSources.filter { availableSource in
|
||||
let sourceExists = installedSources.contains(where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
}
|
||||
)
|
||||
}) {
|
||||
Section(header: "Catalog") {
|
||||
ForEach(sourceManager.availableSources, id: \.self) { availableSource in
|
||||
if !sources.contains(
|
||||
where: {
|
||||
availableSource.name == $0.name &&
|
||||
availableSource.listId == $0.listId &&
|
||||
availableSource.author == $0.author
|
||||
}
|
||||
) {
|
||||
SourceCatalogButtonView(availableSource: availableSource)
|
||||
}
|
||||
})
|
||||
|
||||
if searchText.isEmpty {
|
||||
return !sourceExists
|
||||
} else {
|
||||
return !sourceExists && availableSource.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
|
||||
filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources).filter {
|
||||
searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased())
|
||||
}
|
||||
}
|
||||
.navigationTitle("Sources")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search", text: $searchText, isEditing: $isEditing)
|
||||
.showsCancelButton(isEditing)
|
||||
.onCancel {
|
||||
searchText = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
.sheet(isPresented: $navModel.showSourceSettings) {
|
||||
SourceSettingsView()
|
||||
.environmentObject(navModel)
|
||||
}
|
||||
.onAppear {
|
||||
viewTask = Task {
|
||||
await sourceManager.fetchSourcesFromUrl()
|
||||
}
|
||||
}
|
||||
.onDisappear {
|
||||
viewTask?.cancel()
|
||||
}
|
||||
.navigationTitle("Sources")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue