v0.5 #11

Merged
kingbri1 merged 22 commits from next into default 2022-11-19 17:51:06 +00:00
26 changed files with 705 additions and 246 deletions
Showing only changes of commit e3e8924547 - Show all commits

View file

@ -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 */,

View file

@ -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

View 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]
}
}

View 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 ?? []
}
}

View file

@ -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())
}

View file

@ -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)
}
}

View file

@ -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>

View 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
}

View 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)")
}
}
}
}

View file

@ -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)
}

View file

@ -39,7 +39,7 @@ struct BatchChoiceView: View {
navModel.currentChoiceSheet = nil
}
.dynamicAccentColor(.primary)
.backport.tint(.primary)
}
}
.listStyle(.insetGrouped)

View 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)
}
}
}

View file

@ -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)
}
}
}

View file

@ -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 }
)
}
}
}
}

View file

@ -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()
)
}
}
}
}
}

View file

@ -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",

View file

@ -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!",

View file

@ -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)
}
}

View file

@ -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)

View file

@ -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()

View file

@ -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.",

View file

@ -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")

View 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()
}
}

View file

@ -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)

View file

@ -32,7 +32,7 @@ struct SourceListEditorView: View {
sourceUrl = navModel.selectedSourceList?.urlString ?? ""
sourceUrlSet = true
}
.dynamicAlert(
.backport.alert(
isPresented: $sourceManager.showUrlErrorAlert,
title: "Error",
message: sourceManager.urlErrorAlertText,

View file

@ -192,6 +192,6 @@ struct SourceSettingsMethodView: View {
}
}
}
.dynamicAccentColor(.primary)
.backport.tint(.primary)
}
}