v0.5 #11
26 changed files with 705 additions and 246 deletions
|
|
@ -16,18 +16,18 @@
|
|||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; };
|
||||
0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; };
|
||||
0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; };
|
||||
0C391EC928CA63F0009F1CA1 /* DynamicActionSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.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 */; };
|
||||
0C626A9528CADB25003C7129 /* DynamicAlert.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C626A9428CADB25003C7129 /* DynamicAlert.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 */; };
|
||||
|
|
@ -46,8 +46,10 @@
|
|||
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 */; };
|
||||
|
|
@ -98,6 +100,7 @@
|
|||
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 */
|
||||
|
|
@ -110,17 +113,17 @@
|
|||
0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.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>"; };
|
||||
0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicActionSheet.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>"; };
|
||||
0C626A9428CADB25003C7129 /* DynamicAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAlert.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>"; };
|
||||
|
|
@ -135,8 +138,10 @@
|
|||
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>"; };
|
||||
|
|
@ -187,6 +192,7 @@
|
|||
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 */
|
||||
|
|
@ -235,6 +241,7 @@
|
|||
0C0D50E3288DFE6E0035ECC8 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C7ED14028D61BBA009E29AD /* BackupModels.swift */,
|
||||
0C68135128BC1A7C00FAD890 /* GithubModels.swift */,
|
||||
0CA148C4288903F000DE2211 /* RealDebridModels.swift */,
|
||||
0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */,
|
||||
|
|
@ -259,9 +266,6 @@
|
|||
0CB6516228C5A57300DCA721 /* ConditionalId.swift */,
|
||||
0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */,
|
||||
0CBAB83528D12ED500AC903E /* DisableInteraction.swift */,
|
||||
0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */,
|
||||
0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */,
|
||||
0C626A9428CADB25003C7129 /* DynamicAlert.swift */,
|
||||
0CB6516428C5A5D700DCA721 /* InlinedList.swift */,
|
||||
);
|
||||
path = Modifiers;
|
||||
|
|
@ -298,6 +302,7 @@
|
|||
0CA0545C288F7CB200850554 /* SettingsViews */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2AE28D52E8A007711AE /* BackupsView.swift */,
|
||||
0CA05456288EE58200850554 /* SettingsSourceListView.swift */,
|
||||
0CA0545A288EEA4E00850554 /* SourceListEditorView.swift */,
|
||||
0C95D8D728A55B03005E22B3 /* DefaultActionsPickerViews.swift */,
|
||||
|
|
@ -329,6 +334,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44E2A928D4DFC4007711AE /* Modifiers */,
|
||||
0CE66B3928E640D200F69346 /* Backport.swift */,
|
||||
0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */,
|
||||
0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */,
|
||||
0CDCB91728C662640098B513 /* EmptyInstructionView.swift */,
|
||||
|
|
@ -358,6 +364,8 @@
|
|||
0C78041C28BFB3EA001E8CA3 /* String.swift */,
|
||||
0CA148CB288903F000DE2211 /* Task.swift */,
|
||||
0C7D11FD28AA03FE00ED92DB /* View.swift */,
|
||||
0C7ED14228D65518009E29AD /* FileManager.swift */,
|
||||
0C7C128528DAA3CD00381CD1 /* URL.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -393,6 +401,7 @@
|
|||
0CA148CF288903F000DE2211 /* ToastViewModel.swift */,
|
||||
0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */,
|
||||
0CA05458288EE9E600850554 /* SourceManager.swift */,
|
||||
0C44E2AC28D51C63007711AE /* BackupManager.swift */,
|
||||
);
|
||||
path = ViewModels;
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -540,6 +549,7 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
0C7ED14328D65518009E29AD /* FileManager.swift in Sources */,
|
||||
0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */,
|
||||
0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */,
|
||||
0C32FB532890D19D002BD219 /* AboutView.swift in Sources */,
|
||||
|
|
@ -556,6 +566,7 @@
|
|||
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 /* SourceCatalogButtonView.swift in Sources */,
|
||||
0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */,
|
||||
|
|
@ -567,8 +578,10 @@
|
|||
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 /* InstalledSourceButtonView.swift in Sources */,
|
||||
|
|
@ -582,10 +595,8 @@
|
|||
0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */,
|
||||
0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */,
|
||||
0CA148E3288903F000DE2211 /* Task.swift in Sources */,
|
||||
0C626A9528CADB25003C7129 /* DynamicAlert.swift in Sources */,
|
||||
0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */,
|
||||
0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */,
|
||||
0C391EC928CA63F0009F1CA1 /* DynamicActionSheet.swift in Sources */,
|
||||
0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */,
|
||||
0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */,
|
||||
0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */,
|
||||
|
|
@ -595,7 +606,6 @@
|
|||
0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */,
|
||||
0CA148E2288903F000DE2211 /* Data.swift in Sources */,
|
||||
0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */,
|
||||
0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */,
|
||||
0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */,
|
||||
0CA05459288EE9E600850554 /* SourceManager.swift in Sources */,
|
||||
0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */,
|
||||
|
|
@ -607,6 +617,7 @@
|
|||
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 */,
|
||||
|
|
@ -617,6 +628,7 @@
|
|||
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 */,
|
||||
|
|
|
|||
|
|
@ -91,6 +91,70 @@ struct PersistenceController {
|
|||
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
|
||||
}
|
||||
|
||||
// TODO: Change timestamp to use a date instead of a double
|
||||
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.source
|
||||
|
||||
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
|
||||
|
|
|
|||
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]
|
||||
}
|
||||
}
|
||||
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 ?? []
|
||||
}
|
||||
}
|
||||
|
|
@ -43,26 +43,6 @@ extension View {
|
|||
modifier(DisableInteraction(disabled: disabled))
|
||||
}
|
||||
|
||||
func dynamicAccentColor(_ color: Color) -> some View {
|
||||
modifier(DynamicAccentColor(color: color))
|
||||
}
|
||||
|
||||
func dynamicActionSheet(isPresented: Binding<Bool>,
|
||||
title: String,
|
||||
message: String? = nil,
|
||||
buttons: [AlertButton]) -> some View
|
||||
{
|
||||
modifier(DynamicActionSheet(isPresented: isPresented, title: title, message: message, buttons: buttons))
|
||||
}
|
||||
|
||||
func dynamicAlert(isPresented: Binding<Bool>,
|
||||
title: String,
|
||||
message: String? = nil,
|
||||
buttons: [AlertButton]) -> some View
|
||||
{
|
||||
modifier(DynamicAlert(isPresented: isPresented, title: title, message: message, buttons: buttons))
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
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 = 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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -123,43 +123,16 @@ class NavigationViewModel: ObservableObject {
|
|||
public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) {
|
||||
let backgroundContext = PersistenceController.shared.backgroundContext
|
||||
|
||||
let newHistoryEntry = HistoryEntry(context: backgroundContext)
|
||||
newHistoryEntry.name = name
|
||||
newHistoryEntry.source = source
|
||||
newHistoryEntry.url = url
|
||||
newHistoryEntry.subName = subName
|
||||
|
||||
let now = Date()
|
||||
newHistoryEntry.timeStamp = now.timeIntervalSince1970
|
||||
|
||||
let dateString = DateFormatter.historyDateFormatter.string(from: now)
|
||||
|
||||
let historyRequest = History.fetchRequest()
|
||||
historyRequest.predicate = NSPredicate(format: "dateString = %@", dateString)
|
||||
|
||||
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 = dateString
|
||||
newHistoryEntry.parentHistory?.date = now
|
||||
PersistenceController.shared.createHistory(
|
||||
entryJson: HistoryEntryJson(
|
||||
name: name ?? "",
|
||||
subName: subName,
|
||||
url: url ?? "",
|
||||
timeStamp: nil,
|
||||
source: source
|
||||
),
|
||||
date: nil
|
||||
)
|
||||
|
||||
PersistenceController.shared.save(backgroundContext)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ struct BatchChoiceView: View {
|
|||
|
||||
navModel.currentChoiceSheet = nil
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
|
|
|||
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 = 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 = 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,24 +0,0 @@
|
|||
//
|
||||
// DynamicAccentColor.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 8/15/22.
|
||||
//
|
||||
// Wrapper that switches between tint and accentColor
|
||||
//
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
//
|
||||
// DynamicActionSheet.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/8/22.
|
||||
//
|
||||
// Switches between confirmationDialog and actionSheet
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DynamicActionSheet: ViewModifier {
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
let title: String
|
||||
let message: String?
|
||||
let buttons: [AlertButton]
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.confirmationDialog(
|
||||
title,
|
||||
isPresented: $isPresented,
|
||||
titleVisibility: .visible
|
||||
) {
|
||||
ForEach(buttons) { button in
|
||||
button.toButtonView()
|
||||
}
|
||||
} message: {
|
||||
if let message = 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
//
|
||||
// DynamicAlert.swift
|
||||
// Ferrite
|
||||
//
|
||||
// Created by Brian Dashore on 9/8/22.
|
||||
//
|
||||
// Switches between iOS 15 and 14 alert initalizers
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct DynamicAlert: ViewModifier {
|
||||
@Binding var isPresented: Bool
|
||||
|
||||
let title: String
|
||||
let message: String?
|
||||
let buttons: [AlertButton]
|
||||
|
||||
func body(content: Content) -> some View {
|
||||
if #available(iOS 15, *) {
|
||||
content
|
||||
.alert(
|
||||
title,
|
||||
isPresented: $isPresented,
|
||||
actions: {
|
||||
ForEach(buttons) { button in
|
||||
button.toButtonView()
|
||||
}
|
||||
},
|
||||
message: {
|
||||
if let message = message {
|
||||
Text(message)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
content
|
||||
.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()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -72,27 +72,6 @@ struct ContentView: View {
|
|||
|
||||
SearchResultsView()
|
||||
}
|
||||
.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Search")
|
||||
.navigationSearchBar {
|
||||
SearchBar("Search",
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ struct HistoryActionsView: View {
|
|||
Button("Clear") {
|
||||
showActionSheet.toggle()
|
||||
}
|
||||
.dynamicAccentColor(.red)
|
||||
.dynamicActionSheet(
|
||||
.backport.tint(.red)
|
||||
.backport.confirmationDialog(
|
||||
isPresented: $showActionSheet,
|
||||
title: "Clear watch history",
|
||||
message: "This is an irreversible action!",
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ struct HistoryButtonView: View {
|
|||
.lineLimit(1)
|
||||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
.dynamicAccentColor(.white)
|
||||
.backport.tint(.white)
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ struct MagnetChoiceView: View {
|
|||
UIPasteboard.general.string = debridManager.realDebridDownloadUrl
|
||||
showLinkCopyAlert.toggle()
|
||||
}
|
||||
.dynamicAlert(
|
||||
.backport.alert(
|
||||
isPresented: $showLinkCopyAlert,
|
||||
title: "Copied",
|
||||
message: "Download link copied successfully",
|
||||
|
|
@ -62,7 +62,7 @@ struct MagnetChoiceView: View {
|
|||
UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink
|
||||
showMagnetCopyAlert.toggle()
|
||||
}
|
||||
.dynamicAlert(
|
||||
.backport.alert(
|
||||
isPresented: $showMagnetCopyAlert,
|
||||
title: "Copied",
|
||||
message: "Magnet link copied successfully",
|
||||
|
|
@ -84,7 +84,7 @@ struct MagnetChoiceView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
.sheet(isPresented: $navModel.showLocalActivitySheet) {
|
||||
if #available(iOS 16, *) {
|
||||
AppActivityView(activityItems: navModel.activityItems)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -46,21 +48,27 @@ struct MainView: View {
|
|||
}
|
||||
.tag(ViewTab.settings)
|
||||
}
|
||||
.dynamicAlert(
|
||||
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)
|
||||
]
|
||||
)
|
||||
.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 {
|
||||
viewTask = Task {
|
||||
|
|
@ -85,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()
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ struct SearchResultButtonView: View {
|
|||
.disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2))
|
||||
}
|
||||
.disableInteraction(navModel.currentChoiceSheet != nil)
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
.conditionalContextMenu(id: existingBookmark) {
|
||||
if let bookmark = existingBookmark {
|
||||
Button {
|
||||
|
|
@ -93,7 +93,7 @@ struct SearchResultButtonView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAlert(
|
||||
.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.",
|
||||
|
|
|
|||
|
|
@ -95,6 +95,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,7 +25,7 @@ struct MagnetActionPickerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
|
@ -64,7 +64,7 @@ struct DebridActionPickerView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
.listStyle(.insetGrouped)
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ struct SourceListEditorView: View {
|
|||
sourceUrl = navModel.selectedSourceList?.urlString ?? ""
|
||||
sourceUrlSet = true
|
||||
}
|
||||
.dynamicAlert(
|
||||
.backport.alert(
|
||||
isPresented: $sourceManager.showUrlErrorAlert,
|
||||
title: "Error",
|
||||
message: sourceManager.urlErrorAlertText,
|
||||
|
|
|
|||
|
|
@ -192,6 +192,6 @@ struct SourceSettingsMethodView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.dynamicAccentColor(.primary)
|
||||
.backport.tint(.primary)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue