diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 1027ccc..36097b0 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -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 = ""; }; 0C32FB562890D1F2002BD219 /* ListRowViews.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListRowViews.swift; sourceTree = ""; }; 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicFetchRequest.swift; sourceTree = ""; }; - 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicActionSheet.swift; sourceTree = ""; }; 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertButton.swift; sourceTree = ""; }; 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = ""; }; 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.swift; sourceTree = ""; }; 0C44E2A728D4DDDC007711AE /* Application.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Application.swift; sourceTree = ""; }; + 0C44E2AC28D51C63007711AE /* BackupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupManager.swift; sourceTree = ""; }; + 0C44E2AE28D52E8A007711AE /* BackupsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupsView.swift; sourceTree = ""; }; 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataClass.swift"; sourceTree = ""; }; 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceComplexQuery+CoreDataProperties.swift"; sourceTree = ""; }; 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataClass.swift"; sourceTree = ""; }; 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "History+CoreDataProperties.swift"; sourceTree = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; - 0C626A9428CADB25003C7129 /* DynamicAlert.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAlert.swift; sourceTree = ""; }; 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubWrapper.swift; sourceTree = ""; }; 0C68135128BC1A7C00FAD890 /* GithubModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GithubModels.swift; sourceTree = ""; }; 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalContextMenu.swift; sourceTree = ""; }; @@ -135,8 +138,10 @@ 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = LaunchScreen.storyboard; sourceTree = ""; }; 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataClass.swift"; sourceTree = ""; }; 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceSeedLeech+CoreDataProperties.swift"; sourceTree = ""; }; - 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicAccentColor.swift; sourceTree = ""; }; + 0C7C128528DAA3CD00381CD1 /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 0C7D11FD28AA03FE00ED92DB /* View.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = View.swift; sourceTree = ""; }; + 0C7ED14028D61BBA009E29AD /* BackupModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackupModels.swift; sourceTree = ""; }; + 0C7ED14228D65518009E29AD /* FileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManager.swift; sourceTree = ""; }; 0C84F4762895BE680074B7C9 /* FerriteDB.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = FerriteDB.xcdatamodel; sourceTree = ""; }; 0C84F47A2895BFED0074B7C9 /* Source+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataClass.swift"; sourceTree = ""; }; 0C84F47B2895BFED0074B7C9 /* Source+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Source+CoreDataProperties.swift"; sourceTree = ""; }; @@ -187,6 +192,7 @@ 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryActionsView.swift; sourceTree = ""; }; 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisabledAppearance.swift; sourceTree = ""; }; 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; + 0CE66B3928E640D200F69346 /* Backport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backport.swift; sourceTree = ""; }; /* 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 = ""; @@ -393,6 +401,7 @@ 0CA148CF288903F000DE2211 /* ToastViewModel.swift */, 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */, 0CA05458288EE9E600850554 /* SourceManager.swift */, + 0C44E2AC28D51C63007711AE /* BackupManager.swift */, ); path = ViewModels; sourceTree = ""; @@ -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 */, diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index 92c34a2..85934ec 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -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 diff --git a/Ferrite/Extensions/FileManager.swift b/Ferrite/Extensions/FileManager.swift new file mode 100644 index 0000000..33656b6 --- /dev/null +++ b/Ferrite/Extensions/FileManager.swift @@ -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] + } +} diff --git a/Ferrite/Extensions/URL.swift b/Ferrite/Extensions/URL.swift new file mode 100644 index 0000000..8fd8a41 --- /dev/null +++ b/Ferrite/Extensions/URL.swift @@ -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 ?? [] + } +} diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 5f07409..16092d5 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -43,26 +43,6 @@ extension View { modifier(DisableInteraction(disabled: disabled)) } - func dynamicAccentColor(_ color: Color) -> some View { - modifier(DynamicAccentColor(color: color)) - } - - func dynamicActionSheet(isPresented: Binding, - title: String, - message: String? = nil, - buttons: [AlertButton]) -> some View - { - modifier(DynamicActionSheet(isPresented: isPresented, title: title, message: message, buttons: buttons)) - } - - func dynamicAlert(isPresented: Binding, - 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()) } diff --git a/Ferrite/FerriteApp.swift b/Ferrite/FerriteApp.swift index 4d486fd..3f26413 100644 --- a/Ferrite/FerriteApp.swift +++ b/Ferrite/FerriteApp.swift @@ -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) } } diff --git a/Ferrite/Info.plist b/Ferrite/Info.plist index 3eba462..a289566 100644 --- a/Ferrite/Info.plist +++ b/Ferrite/Info.plist @@ -2,6 +2,19 @@ + CFBundleDocumentTypes + + + CFBundleTypeName + Ferrite Backup + LSHandlerRank + Owner + LSItemContentTypes + + me.kingbri.Ferrite.feb + + + NSAppTransportSecurity NSAllowsArbitraryLoads @@ -9,5 +22,27 @@ UILaunchScreen + UTExportedTypeDeclarations + + + UTTypeConformsTo + + public.json + + UTTypeDescription + Ferrite Backup + UTTypeIconFiles + + UTTypeIdentifier + me.kingbri.Ferrite.feb + UTTypeTagSpecification + + public.filename-extension + + feb + + + + diff --git a/Ferrite/Models/BackupModels.swift b/Ferrite/Models/BackupModels.swift new file mode 100644 index 0000000..94b3b27 --- /dev/null +++ b/Ferrite/Models/BackupModels.swift @@ -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 +} diff --git a/Ferrite/ViewModels/BackupManager.swift b/Ferrite/ViewModels/BackupManager.swift new file mode 100644 index 0000000..dbc68c9 --- /dev/null +++ b/Ferrite/ViewModels/BackupManager.swift @@ -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)") + } + } + } +} diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 1f5320d..186dd55 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -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) } diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index afae3c8..72fa4d0 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -39,7 +39,7 @@ struct BatchChoiceView: View { navModel.currentChoiceSheet = nil } - .dynamicAccentColor(.primary) + .backport.tint(.primary) } } .listStyle(.insetGrouped) diff --git a/Ferrite/Views/CommonViews/Backport.swift b/Ferrite/Views/CommonViews/Backport.swift new file mode 100644 index 0000000..e5077e1 --- /dev/null +++ b/Ferrite/Views/CommonViews/Backport.swift @@ -0,0 +1,103 @@ +// +// Backport.swift +// Ferrite +// +// Created by Brian Dashore on 9/29/22. +// + +import SwiftUI + +public struct Backport { + public let content: Content + + public init(_ content: Content) { + self.content = content + } +} + +extension View { + var backport: Backport { Backport(self) } +} + +extension Backport where Content: View { + @ViewBuilder func alert(isPresented: Binding, 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, 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) + } + } +} diff --git a/Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift b/Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift deleted file mode 100644 index ab1fd3c..0000000 --- a/Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift +++ /dev/null @@ -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) - } - } -} diff --git a/Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift b/Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift deleted file mode 100644 index 9d9d4dd..0000000 --- a/Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift +++ /dev/null @@ -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 } - ) - } - } - } -} diff --git a/Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift b/Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift deleted file mode 100644 index 7f39cfe..0000000 --- a/Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift +++ /dev/null @@ -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() - ) - } - } - } - } -} diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index a821842..9a117fa 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -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", diff --git a/Ferrite/Views/LibraryViews/HistoryActionsView.swift b/Ferrite/Views/LibraryViews/HistoryActionsView.swift index 5b3e6a1..083b094 100644 --- a/Ferrite/Views/LibraryViews/HistoryActionsView.swift +++ b/Ferrite/Views/LibraryViews/HistoryActionsView.swift @@ -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!", diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/LibraryViews/HistoryButtonView.swift index 681f970..a6576e1 100644 --- a/Ferrite/Views/LibraryViews/HistoryButtonView.swift +++ b/Ferrite/Views/LibraryViews/HistoryButtonView.swift @@ -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) } } diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index bc78d43..df9e253 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -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) diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 3cc2399..1a4da16 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -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() diff --git a/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift b/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift index cb177e8..e1504de 100644 --- a/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift +++ b/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift @@ -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.", diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 1531a9a..292080a 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -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") diff --git a/Ferrite/Views/SettingsViews/BackupsView.swift b/Ferrite/Views/SettingsViews/BackupsView.swift new file mode 100644 index 0000000..6a91153 --- /dev/null +++ b/Ferrite/Views/SettingsViews/BackupsView.swift @@ -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() + } +} diff --git a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift b/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift index a03be82..c9c069c 100644 --- a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift +++ b/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift @@ -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) diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index 5c20198..a7fd295 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -32,7 +32,7 @@ struct SourceListEditorView: View { sourceUrl = navModel.selectedSourceList?.urlString ?? "" sourceUrlSet = true } - .dynamicAlert( + .backport.alert( isPresented: $sourceManager.showUrlErrorAlert, title: "Error", message: sourceManager.urlErrorAlertText, diff --git a/Ferrite/Views/SourceViews/SourceSettingsView.swift b/Ferrite/Views/SourceViews/SourceSettingsView.swift index 18d8e53..0936658 100644 --- a/Ferrite/Views/SourceViews/SourceSettingsView.swift +++ b/Ferrite/Views/SourceViews/SourceSettingsView.swift @@ -192,6 +192,6 @@ struct SourceSettingsMethodView: View { } } } - .dynamicAccentColor(.primary) + .backport.tint(.primary) } }