mirror of
https://github.com/Ferrite-iOS/Ferrite.git
synced 2026-01-11 20:10:27 +00:00
Ferrite: Add backups and massive cleanup
Backups in Ferrite archive a user's bookmarks, history, source lists, and source names. Sources are not archived due to the size of the backup increasing exponentially. These files use the .feb format to avoid JSON conflicts when opening the file in Ferrite. The backup file can be renamed to JSON for editing at any time. Add the Backport namespace to be used for ported features rather than making a file for every iOS 14 adaptation. Move history and bookmark creation to the PersistenceController rather than individual functions. Signed-off-by: kingbri <bdashore3@proton.me>
This commit is contained in:
parent
a89e832d1c
commit
e3e8924547
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