From f7e1b87c73009b142ca35c5839a2dfdc4d7bd9f4 Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 14:14:34 -0500 Subject: [PATCH 01/22] Various UI tweaks - Make about header scrollable - Add no sources text - Add no source lists text - Make edit source list start with current url - Add loading indicator for versions --- Ferrite/Views/AboutView.swift | 29 ++++--- Ferrite/Views/ContentView.swift | 8 +- .../SettingsAppVersionView.swift | 21 +++-- .../SettingsSourceListView.swift | 27 +++++-- .../SettingsViews/SourceListEditorView.swift | 6 +- Ferrite/Views/SourcesView.swift | 80 ++++++++++++------- 6 files changed, 111 insertions(+), 60 deletions(-) diff --git a/Ferrite/Views/AboutView.swift b/Ferrite/Views/AboutView.swift index b2285aa..70e8934 100644 --- a/Ferrite/Views/AboutView.swift +++ b/Ferrite/Views/AboutView.swift @@ -9,24 +9,31 @@ import SwiftUI struct AboutView: View { var body: some View { - VStack { - Image("AppImage") - .resizable() - .frame(width: 100, height: 100) - .cornerRadius(25) - - Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.") - .padding() - - List { + List { + Section { ListRowTextView(leftText: "Version", rightText: UIApplication.shared.appVersion) ListRowTextView(leftText: "Build number", rightText: UIApplication.shared.appBuild) ListRowTextView(leftText: "Build type", rightText: UIApplication.shared.buildType) ListRowLinkView(text: "Discord server", link: "https://discord.gg/sYQxnuD7Fj") ListRowLinkView(text: "GitHub repository", link: "https://github.com/bdashore3/Ferrite") + } header: { + VStack(alignment: .center) { + Image("AppImage") + .resizable() + .frame(width: 100, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 100*0.225, style: .continuous)) + + Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.") + .textCase(.none) + .foregroundColor(.label) + .font(.body) + .padding(.top, 8) + .padding(.bottom, 20) + } + .listRowInsets(EdgeInsets(top: 0, leading: 7, bottom: 0, trailing: 0)) } - .listStyle(.insetGrouped) } + .listStyle(.insetGrouped) .navigationTitle("About") } } diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 6f92f4f..1d9a60f 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -30,7 +30,7 @@ struct ContentView: View { var body: some View { NavView { VStack(spacing: 10) { - HStack { + HStack(spacing: 6) { Text("Filter") .foregroundColor(.secondary) @@ -50,10 +50,10 @@ struct ContentView: View { Button { selectedSource = source } label: { - Text(name) - if selectedSource == source { - Image(systemName: "checkmark") + Label(name, systemImage: "checkmark") + } else { + Text(name) } } } diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index eaf87eb..1a31c91 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftUIX struct SettingsAppVersionView: View { @EnvironmentObject var toastModel: ToastViewModel @@ -14,18 +15,27 @@ struct SettingsAppVersionView: View { @State private var releases: [GithubRelease] = [] var body: some View { - List { - Section(header: Text("GitHub links")) { - ForEach(releases, id: \.self) { release in - ListRowLinkView(text: release.tagName, link: release.htmlUrl) + ZStack { + if releases.isEmpty { + ActivityIndicator() + } else { + List { + Section(header: Text("GitHub links")) { + ForEach(releases, id: \.self) { release in + ListRowLinkView(text: release.tagName, link: release.htmlUrl) + } + } } + .listStyle(.insetGrouped) } } .onAppear { viewTask = Task { do { if let fetchedReleases = try await Github().fetchReleases() { - releases = fetchedReleases + withAnimation { + releases = fetchedReleases + } } else { toastModel.updateToastDescription("Github error: No releases found") } @@ -37,7 +47,6 @@ struct SettingsAppVersionView: View { .onDisappear { viewTask?.cancel() } - .listStyle(.insetGrouped) .navigationTitle("Version history") .navigationBarTitleDisplayMode(.inline) } diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index 38f3618..42f7995 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -22,7 +22,9 @@ struct SettingsSourceListView: View { var body: some View { List { - Section(header: Text("List information")) { + if sourceLists.isEmpty { + Text("No source lists") + } else { ForEach(sourceLists, id: \.self) { sourceList in VStack(alignment: .leading, spacing: 5) { Text(sourceList.name) @@ -43,11 +45,20 @@ struct SettingsSourceListView: View { Image(systemName: "pencil") } - Button { - PersistenceController.shared.delete(sourceList, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") + if #available(iOS 15.0, *) { + Button(role: .destructive) { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } else { + Button { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } } } } @@ -56,10 +67,10 @@ struct SettingsSourceListView: View { .listStyle(.insetGrouped) .sheet(isPresented: $presentSourceSheet) { if #available(iOS 16, *) { - SourceListEditorView() + SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") .presentationDetents([.medium]) } else { - SourceListEditorView() + SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") } } .navigationTitle("Source lists") diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index 9db11c7..d82d367 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -15,7 +15,11 @@ struct SourceListEditorView: View { let backgroundContext = PersistenceController.shared.backgroundContext - @State private var sourceUrl = "" + @State private var sourceUrl: String + + init(sourceUrl: String = "") { + _sourceUrl = State(initialValue: sourceUrl) + } var body: some View { NavView { diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 570c92b..283bbf7 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -6,8 +6,11 @@ // import SwiftUI +import SwiftUIX struct SourcesView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? + @EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var navModel: NavigationViewModel @@ -37,51 +40,67 @@ struct SourcesView: View { } @State private var viewTask: Task? = nil + @State private var checkedForSources = false var body: some View { NavView { - List { - if !updatedSources.isEmpty { - Section(header: "Updates") { - ForEach(updatedSources, id: \.self) { source in - SourceUpdateButtonView(updatedSource: source) - } + ZStack { + if !checkedForSources { + ActivityIndicator() + } else if sources.isEmpty && sourceManager.availableSources.isEmpty { + VStack { + Text("No Sources") + .font(.system(size: 25, weight: .semibold)) + .foregroundColor(.secondaryLabel) + Text("Add a source list in Settings") + .foregroundColor(.secondaryLabel) } - } - - if !sources.isEmpty { - Section(header: "Installed") { - ForEach(sources, id: \.self) { source in - InstalledSourceView(installedSource: source) + .padding(.top, verticalSizeClass == .regular ? -50 : 0) + } else { + List { + if !updatedSources.isEmpty { + Section(header: "Updates") { + ForEach(updatedSources, id: \.self) { source in + SourceUpdateButtonView(updatedSource: source) + } + } } - } - } - if sourceManager.availableSources.contains(where: { availableSource in - !sources.contains( - where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author + if !sources.isEmpty { + Section(header: "Installed") { + ForEach(sources, id: \.self) { source in + InstalledSourceView(installedSource: source) + } + } } - ) - }) { - Section(header: "Catalog") { - ForEach(sourceManager.availableSources, id: \.self) { availableSource in - if !sources.contains( + + if sourceManager.availableSources.contains(where: { availableSource in + !sources.contains( where: { availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author + availableSource.listId == $0.listId && + availableSource.author == $0.author + } + ) + }) { + Section(header: "Catalog") { + ForEach(sourceManager.availableSources, id: \.self) { availableSource in + if !sources.contains( + where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + } + ) { + SourceCatalogButtonView(availableSource: availableSource) + } } - ) { - SourceCatalogButtonView(availableSource: availableSource) } } } + .listStyle(.insetGrouped) } } - .listStyle(.insetGrouped) .sheet(isPresented: $navModel.showSourceSettings) { SourceSettingsView() .environmentObject(navModel) @@ -89,6 +108,7 @@ struct SourcesView: View { .onAppear { viewTask = Task { await sourceManager.fetchSourcesFromUrl() + checkedForSources = true } } .onDisappear { -- 2.45.2 From 4e149814a32e402131040a8a75e6162cb2ffbe31 Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 14:47:04 -0500 Subject: [PATCH 02/22] Add sources page searching - Fix table top inset --- Ferrite/Views/SourcesView.swift | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 283bbf7..3f3f175 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -42,6 +42,12 @@ struct SourcesView: View { @State private var viewTask: Task? = nil @State private var checkedForSources = false + @State private var searchText: String = "" + @State private var isEditing = false + + @State var filteredUpdatedSources: [SourceJson] = [] + @State var filteredAvailableSources: [SourceJson] = [] + var body: some View { NavView { ZStack { @@ -58,9 +64,9 @@ struct SourcesView: View { .padding(.top, verticalSizeClass == .regular ? -50 : 0) } else { List { - if !updatedSources.isEmpty { + if !filteredUpdatedSources.isEmpty { Section(header: "Updates") { - ForEach(updatedSources, id: \.self) { source in + ForEach(filteredUpdatedSources, id: \.self) { source in SourceUpdateButtonView(updatedSource: source) } } @@ -74,7 +80,7 @@ struct SourcesView: View { } } - if sourceManager.availableSources.contains(where: { availableSource in + if !filteredAvailableSources.isEmpty && sourceManager.availableSources.contains(where: { availableSource in !sources.contains( where: { availableSource.name == $0.name && @@ -84,7 +90,7 @@ struct SourcesView: View { ) }) { Section(header: "Catalog") { - ForEach(sourceManager.availableSources, id: \.self) { availableSource in + ForEach(filteredAvailableSources, id: \.self) { availableSource in if !sources.contains( where: { availableSource.name == $0.name && @@ -106,8 +112,10 @@ struct SourcesView: View { .environmentObject(navModel) } .onAppear { + filteredUpdatedSources = updatedSources viewTask = Task { await sourceManager.fetchSourcesFromUrl() + filteredAvailableSources = sourceManager.availableSources checkedForSources = true } } @@ -115,6 +123,21 @@ struct SourcesView: View { viewTask?.cancel() } .navigationTitle("Sources") + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditing) + .showsCancelButton(isEditing) + } + .onChange(of: searchText) { newValue in + filteredAvailableSources = sourceManager.availableSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } + filteredUpdatedSources = updatedSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } + if #available(iOS 15.0, *) { + if searchText.isEmpty { + sources.nsPredicate = nil + } else { + sources.nsPredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + } + } } } } -- 2.45.2 From ff1fef85b0f522affd13925ab3629bb7dc45351e Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 14:56:49 -0500 Subject: [PATCH 03/22] Fix link actions view done button color --- Ferrite/Views/ContentView.swift | 1 - Ferrite/Views/MagnetChoiceView.swift | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 1d9a60f..0c1332f 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -94,7 +94,6 @@ struct ContentView: View { } } } - .dynamicAccentColor(.primary) } .navigationTitle("Search") .navigationSearchBar { diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index a9701df..ae142b2 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -88,6 +88,7 @@ struct MagnetChoiceView: View { } } } + .dynamicAccentColor(.primary) .sheet(isPresented: $navModel.showLocalActivitySheet) { if #available(iOS 16, *) { AppActivityView(activityItems: navModel.activityItems) -- 2.45.2 From aad24d4b1d3af4617d88e7920e1f4f5010bdb39a Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 15:01:10 -0500 Subject: [PATCH 04/22] Title Case for settings pages --- Ferrite/Views/SettingsViews/SettingsAppVersionView.swift | 2 +- Ferrite/Views/SettingsViews/SettingsSourceListView.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index 1a31c91..78aaba9 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -47,7 +47,7 @@ struct SettingsAppVersionView: View { .onDisappear { viewTask?.cancel() } - .navigationTitle("Version history") + .navigationTitle("Version History") .navigationBarTitleDisplayMode(.inline) } } diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index 42f7995..f691f49 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -73,7 +73,7 @@ struct SettingsSourceListView: View { SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") } } - .navigationTitle("Source lists") + .navigationTitle("Source Lists") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .navigationBarTrailing) { -- 2.45.2 From aa344b8ea8d6c7e4c972966cefaddd6ffdef10c8 Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 15:03:57 -0500 Subject: [PATCH 05/22] Fix infinite loading indicator with versions --- .../SettingsViews/SettingsAppVersionView.swift | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index 78aaba9..a9a180d 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -14,11 +14,13 @@ struct SettingsAppVersionView: View { @State private var viewTask: Task? @State private var releases: [GithubRelease] = [] + @State private var loadedReleases = false + var body: some View { ZStack { - if releases.isEmpty { + if !loadedReleases { ActivityIndicator() - } else { + } else if !releases.isEmpty { List { Section(header: Text("GitHub links")) { ForEach(releases, id: \.self) { release in @@ -33,15 +35,16 @@ struct SettingsAppVersionView: View { viewTask = Task { do { if let fetchedReleases = try await Github().fetchReleases() { - withAnimation { - releases = fetchedReleases - } + releases = fetchedReleases } else { toastModel.updateToastDescription("Github error: No releases found") } } catch { toastModel.updateToastDescription("Github error: \(error)") } + withAnimation { + loadedReleases = true + } } } .onDisappear { -- 2.45.2 From d1eb67cc16a07f50372bf4351c73f5ab951f4ef0 Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 15:05:38 -0500 Subject: [PATCH 06/22] Fix about header top inset --- Ferrite/Views/AboutView.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Ferrite/Views/AboutView.swift b/Ferrite/Views/AboutView.swift index 70e8934..6ee081a 100644 --- a/Ferrite/Views/AboutView.swift +++ b/Ferrite/Views/AboutView.swift @@ -22,6 +22,7 @@ struct AboutView: View { .resizable() .frame(width: 100, height: 100) .clipShape(RoundedRectangle(cornerRadius: 100*0.225, style: .continuous)) + .padding(.top, 24) Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.") .textCase(.none) -- 2.45.2 From d4f2dba27996a0e2ad3621e5097b58c7cda68f61 Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 15:10:47 -0500 Subject: [PATCH 07/22] Slightly increase cell height --- .../SettingsSourceListView.swift | 1 + .../SourceViews/InstalledSourceView.swift | 20 ++++++++++++++----- .../Views/SourceViews/SourceCatalogView.swift | 1 + .../SourceViews/SourceSettingsView.swift | 1 + .../SourceViews/SourceUpdateButtonView.swift | 1 + 5 files changed, 19 insertions(+), 5 deletions(-) diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index f691f49..c3649df 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -36,6 +36,7 @@ struct SettingsSourceListView: View { .font(.caption) .foregroundColor(.gray) } + .padding(.vertical, 2) .contextMenu { Button { navModel.selectedSourceList = sourceList diff --git a/Ferrite/Views/SourceViews/InstalledSourceView.swift b/Ferrite/Views/SourceViews/InstalledSourceView.swift index 429ab9e..88065f3 100644 --- a/Ferrite/Views/SourceViews/InstalledSourceView.swift +++ b/Ferrite/Views/SourceViews/InstalledSourceView.swift @@ -32,6 +32,7 @@ struct InstalledSourceView: View { Text("by \(installedSource.author)") .foregroundColor(.secondary) } + .padding(.vertical, 2) } .contextMenu { Button { @@ -42,11 +43,20 @@ struct InstalledSourceView: View { Image(systemName: "gear") } - Button { - PersistenceController.shared.delete(installedSource, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") + if #available(iOS 15.0, *) { + Button(role: .destructive) { + PersistenceController.shared.delete(installedSource, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } else { + Button { + PersistenceController.shared.delete(installedSource, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } } } } diff --git a/Ferrite/Views/SourceViews/SourceCatalogView.swift b/Ferrite/Views/SourceViews/SourceCatalogView.swift index 9566df8..cdfa129 100644 --- a/Ferrite/Views/SourceViews/SourceCatalogView.swift +++ b/Ferrite/Views/SourceViews/SourceCatalogView.swift @@ -33,5 +33,6 @@ struct SourceCatalogButtonView: View { } } } + .padding(.vertical, 2) } } diff --git a/Ferrite/Views/SourceViews/SourceSettingsView.swift b/Ferrite/Views/SourceViews/SourceSettingsView.swift index 7f8f926..f195a16 100644 --- a/Ferrite/Views/SourceViews/SourceSettingsView.swift +++ b/Ferrite/Views/SourceViews/SourceSettingsView.swift @@ -40,6 +40,7 @@ struct SourceSettingsView: View { .foregroundColor(.secondary) .font(.caption) } + .padding(.vertical, 2) } if selectedSource.dynamicBaseUrl { diff --git a/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift b/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift index 029788b..2b0f1f2 100644 --- a/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift +++ b/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift @@ -24,6 +24,7 @@ struct SourceUpdateButtonView: View { Text("by \(updatedSource.author ?? "Unknown")") .foregroundColor(.secondary) } + .padding(.vertical, 2) Spacer() -- 2.45.2 From 9a3573a2225127cf65a9850e1702ef6325a56fa8 Mon Sep 17 00:00:00 2001 From: Skitty Date: Sun, 4 Sep 2022 18:07:31 -0500 Subject: [PATCH 08/22] Use ProgressView instead of ActivityIndicator --- Ferrite/Views/SettingsViews/SettingsAppVersionView.swift | 3 +-- Ferrite/Views/SourcesView.swift | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index a9a180d..3553c64 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -6,7 +6,6 @@ // import SwiftUI -import SwiftUIX struct SettingsAppVersionView: View { @EnvironmentObject var toastModel: ToastViewModel @@ -19,7 +18,7 @@ struct SettingsAppVersionView: View { var body: some View { ZStack { if !loadedReleases { - ActivityIndicator() + ProgressView() } else if !releases.isEmpty { List { Section(header: Text("GitHub links")) { diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 3f3f175..7e11757 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -52,7 +52,7 @@ struct SourcesView: View { NavView { ZStack { if !checkedForSources { - ActivityIndicator() + ProgressView() } else if sources.isEmpty && sourceManager.availableSources.isEmpty { VStack { Text("No Sources") -- 2.45.2 From 52409099d7413a7ae3613a8e7e00bede228fd951 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 5 Sep 2022 12:23:05 -0400 Subject: [PATCH 09/22] Ferrite: Properly inline lists The inset grouped list style has a top inset that adds extra space between the navigation bar title and the list rows. Use introspect to remove this space on UITableView and UICollectionView (for iOS 16). Sections completely ignore the introspect changes, so add a section header which removes the list row insets. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 29 ++++++++++++++++++ Ferrite/Extensions/View.swift | 23 ++++++++++++++ Ferrite/Views/CommonViews/ConditionalId.swift | 24 +++++++++++++++ Ferrite/Views/CommonViews/InlineHeader.swift | 30 +++++++++++++++++++ Ferrite/Views/CommonViews/InlinedList.swift | 28 +++++++++++++++++ Ferrite/Views/SearchResultsView.swift | 1 + Ferrite/Views/SettingsView.swift | 11 +++---- .../DefaultActionsPickerViews.swift | 2 ++ .../SettingsAppVersionView.swift | 2 +- .../SettingsSourceListView.swift | 1 + .../SettingsViews/SourceListEditorView.swift | 10 +++---- .../SourceViews/SourceSettingsView.swift | 8 ++--- Ferrite/Views/SourcesView.swift | 8 +++-- 13 files changed, 158 insertions(+), 19 deletions(-) create mode 100644 Ferrite/Views/CommonViews/ConditionalId.swift create mode 100644 Ferrite/Views/CommonViews/InlineHeader.swift create mode 100644 Ferrite/Views/CommonViews/InlinedList.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 47c3300..cb07a28 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -71,6 +71,10 @@ 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; }; 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; + 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; + 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; + 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; }; + 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; @@ -137,6 +141,9 @@ 0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; + 0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = ""; }; + 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = ""; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; @@ -153,6 +160,7 @@ 0C64A4B4288903680079976D /* Base32 in Frameworks */, 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */, 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */, + 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */, 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */, 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */, ); @@ -241,6 +249,9 @@ 0C32FB562890D1F2002BD219 /* ListRowViews.swift */, 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */, 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, + 0CB6516228C5A57300DCA721 /* ConditionalId.swift */, + 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, + 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, ); path = CommonViews; sourceTree = ""; @@ -366,6 +377,7 @@ 0C4CFC452897030D00AD9FAD /* Regex */, 0C7376EF28A97D1400D60918 /* SwiftUIX */, 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */, + 0CB6516728C5A5EC00DCA721 /* Introspect */, ); productName = Torrenter; productReference = 0CAF1C68286F5C0E00296F86 /* Ferrite.app */; @@ -402,6 +414,7 @@ 0C4CFC442897030D00AD9FAD /* XCRemoteSwiftPackageReference "Regex" */, 0C7376EE28A97D1400D60918 /* XCRemoteSwiftPackageReference "SwiftUIX" */, 0C7506D528B1AC9A008BEE38 /* XCRemoteSwiftPackageReference "SwiftyJSON" */, + 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */, ); productRefGroup = 0CAF1C69286F5C0E00296F86 /* Products */; projectDirPath = ""; @@ -431,8 +444,10 @@ buildActionMask = 2147483647; files = ( 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, + 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, + 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, @@ -454,6 +469,7 @@ 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, + 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, @@ -747,6 +763,14 @@ minimumVersion = 2.0.0; }; }; + 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/siteline/SwiftUI-Introspect"; + requirement = { + branch = master; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -780,6 +804,11 @@ package = 0CAF1C79286F5C8600296F86 /* XCRemoteSwiftPackageReference "SwiftSoup" */; productName = SwiftSoup; }; + 0CB6516728C5A5EC00DCA721 /* Introspect */ = { + isa = XCSwiftPackageProductDependency; + package = 0CB6516628C5A5EC00DCA721 /* XCRemoteSwiftPackageReference "SwiftUI-Introspect" */; + productName = Introspect; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 7b7e4cc..c0290fd 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -6,11 +6,34 @@ // import SwiftUI +import Introspect extension View { + // MARK: Custom introspect functions + + func introspectCollectionView(customize: @escaping (UICollectionView) -> ()) -> some View { + return inject(UIKitIntrospectionView( + selector: { introspectionView in + guard let viewHost = Introspect.findViewHost(from: introspectionView) else { + return nil + } + return Introspect.previousSibling(containing: UICollectionView.self, from: viewHost) + }, + customize: customize + )) + } + // MARK: Modifiers func dynamicAccentColor(_ color: Color) -> some View { modifier(DynamicAccentColor(color: color)) } + + func conditionalId(_ id: ID) -> some View { + modifier(ConditionalId(id: id)) + } + + func inlinedList() -> some View { + modifier(InlinedList()) + } } diff --git a/Ferrite/Views/CommonViews/ConditionalId.swift b/Ferrite/Views/CommonViews/ConditionalId.swift new file mode 100644 index 0000000..53a033d --- /dev/null +++ b/Ferrite/Views/CommonViews/ConditionalId.swift @@ -0,0 +1,24 @@ +// +// ConditionalId.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// Only applies an ID for below iOS 16 +// This is due to ID workarounds making iOS 16 apps crash +// + +import SwiftUI + +struct ConditionalId: ViewModifier { + let id: ID + + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + } else { + content + .id(id) + } + } +} diff --git a/Ferrite/Views/CommonViews/InlineHeader.swift b/Ferrite/Views/CommonViews/InlineHeader.swift new file mode 100644 index 0000000..dbb70c4 --- /dev/null +++ b/Ferrite/Views/CommonViews/InlineHeader.swift @@ -0,0 +1,30 @@ +// +// InlineHeader.swift +// Ferrite +// +// Created by Brian Dashore on 9/5/22. +// + +import SwiftUI + +struct InlineHeader: View { + let title: String + + init(_ title: String) { + self.title = title + } + + var body: some View { + Group { + if #available(iOS 16, *) { + Text(title) + .padding(.vertical, 5) + } else { + Text(title) + .padding(.vertical, 10) + } + } + .padding(.horizontal, 20) + .listRowInsets(EdgeInsets()) + } +} diff --git a/Ferrite/Views/CommonViews/InlinedList.swift b/Ferrite/Views/CommonViews/InlinedList.swift new file mode 100644 index 0000000..572693e --- /dev/null +++ b/Ferrite/Views/CommonViews/InlinedList.swift @@ -0,0 +1,28 @@ +// +// InlinedList.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// Removes the top padding on lists for iOS 16 +// Use UITableView.appearance().contentInset.top = -20 for iOS 15 and below in the App file +// + +import SwiftUI +import Introspect + +struct InlinedList: ViewModifier { + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + .introspectCollectionView { collectionView in + collectionView.contentInset.top = -20 + } + } else { + content + .introspectTableView { tableView in + tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: 0, height: 20)) + } + } + } +} diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift index 4ca3a17..9cf050d 100644 --- a/Ferrite/Views/SearchResultsView.swift +++ b/Ferrite/Views/SearchResultsView.swift @@ -54,6 +54,7 @@ struct SearchResultsView: View { } } .listStyle(.insetGrouped) + .inlinedList() .overlay { if scrapingModel.searchResults.isEmpty { if navModel.showSearchProgress { diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 72bfa53..9d9c1d9 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Introspect struct SettingsView: View { @EnvironmentObject var debridManager: DebridManager @@ -21,7 +22,7 @@ struct SettingsView: View { var body: some View { NavView { Form { - Section(header: "Debrid services") { + Section(header: InlineHeader("Debrid Services")) { HStack { Text("Real Debrid") Spacer() @@ -40,11 +41,11 @@ struct SettingsView: View { } } - Section(header: "Source management") { + Section(header: InlineHeader("Source management")) { NavigationLink("Source lists", destination: SettingsSourceListView()) } - Section(header: "Default actions") { + Section(header: InlineHeader("Default actions")) { if debridManager.realDebridEnabled { NavigationLink( destination: DebridActionPickerView(), @@ -94,14 +95,14 @@ struct SettingsView: View { ) } - Section(header: Text("Updates")) { + Section(header: InlineHeader("Updates")) { Toggle(isOn: $autoUpdateNotifs) { Text("Show update alerts") } NavigationLink("Version history", destination: SettingsAppVersionView()) } - Section(header: Text("Information")) { + Section(header: InlineHeader("Information")) { ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri") ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues") NavigationLink("About", destination: AboutView()) diff --git a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift b/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift index 8eb1c8d..a03be82 100644 --- a/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift +++ b/Ferrite/Views/SettingsViews/DefaultActionsPickerViews.swift @@ -29,6 +29,7 @@ struct MagnetActionPickerView: View { } } .listStyle(.insetGrouped) + .inlinedList() .navigationTitle("Default magnet action") .navigationBarTitleDisplayMode(.inline) } @@ -67,6 +68,7 @@ struct DebridActionPickerView: View { } } .listStyle(.insetGrouped) + .inlinedList() .navigationTitle("Default debrid action") .navigationBarTitleDisplayMode(.inline) } diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index 3553c64..be1d19f 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -21,7 +21,7 @@ struct SettingsAppVersionView: View { ProgressView() } else if !releases.isEmpty { List { - Section(header: Text("GitHub links")) { + Section(header: InlineHeader("GitHub links")) { ForEach(releases, id: \.self) { release in ListRowLinkView(text: release.tagName, link: release.htmlUrl) } diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index c3649df..320ab00 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -66,6 +66,7 @@ struct SettingsSourceListView: View { } } .listStyle(.insetGrouped) + .inlinedList() .sheet(isPresented: $presentSourceSheet) { if #available(iOS 16, *) { SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index d82d367..4b9a728 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -24,12 +24,10 @@ struct SourceListEditorView: View { var body: some View { NavView { Form { - Section { - TextField("Enter URL", text: $sourceUrl) - .disableAutocorrection(true) - .keyboardType(.URL) - .autocapitalization(.none) - } + TextField("Enter URL", text: $sourceUrl) + .disableAutocorrection(true) + .keyboardType(.URL) + .autocapitalization(.none) } .onAppear { sourceUrl = navModel.selectedSourceList?.urlString ?? "" diff --git a/Ferrite/Views/SourceViews/SourceSettingsView.swift b/Ferrite/Views/SourceViews/SourceSettingsView.swift index f195a16..6a0daa8 100644 --- a/Ferrite/Views/SourceViews/SourceSettingsView.swift +++ b/Ferrite/Views/SourceViews/SourceSettingsView.swift @@ -16,7 +16,7 @@ struct SourceSettingsView: View { NavView { List { if let selectedSource = navModel.selectedSource { - Section(header: "Info") { + Section(header: InlineHeader("Info")) { VStack(alignment: .leading, spacing: 5) { HStack { Text(selectedSource.name) @@ -78,7 +78,7 @@ struct SourceSettingsBaseUrlView: View { @State private var tempBaseUrl: String = "" var body: some View { Section( - header: Text("Base URL"), + header: InlineHeader("Base URL"), footer: Text("Enter the base URL of your server.") ) { TextField("https://...", text: $tempBaseUrl, onEditingChanged: { isFocused in @@ -110,7 +110,7 @@ struct SourceSettingsApiView: View { var body: some View { Section( - header: Text("API credentials"), + header: InlineHeader("API credentials"), footer: Text("Grab the required API credentials from the website. A client secret can be an API token.") ) { if let clientId = selectedSourceApi.clientId, clientId.dynamic { @@ -146,7 +146,7 @@ struct SourceSettingsMethodView: View { @ObservedObject var selectedSource: Source var body: some View { - Section(header: Text("Fetch method")) { + Section(header: InlineHeader("Fetch method")) { if selectedSource.api != nil, selectedSource.jsonParser != nil { Button { selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 7e11757..faf7388 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -7,6 +7,7 @@ import SwiftUI import SwiftUIX +import Introspect struct SourcesView: View { @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? @@ -65,7 +66,7 @@ struct SourcesView: View { } else { List { if !filteredUpdatedSources.isEmpty { - Section(header: "Updates") { + Section(header: InlineHeader("Updates")) { ForEach(filteredUpdatedSources, id: \.self) { source in SourceUpdateButtonView(updatedSource: source) } @@ -73,7 +74,7 @@ struct SourcesView: View { } if !sources.isEmpty { - Section(header: "Installed") { + Section(header: InlineHeader("Installed")) { ForEach(sources, id: \.self) { source in InstalledSourceView(installedSource: source) } @@ -89,7 +90,7 @@ struct SourcesView: View { } ) }) { - Section(header: "Catalog") { + Section(header: InlineHeader("Catalog")) { ForEach(filteredAvailableSources, id: \.self) { availableSource in if !sources.contains( where: { @@ -104,6 +105,7 @@ struct SourcesView: View { } } } + .conditionalId(UUID()) .listStyle(.insetGrouped) } } -- 2.45.2 From a9d2604fb3b84aa9c6477319fdbf02f1bff0a4a5 Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 5 Sep 2022 12:42:01 -0400 Subject: [PATCH 10/22] Sources: Fix searchbar behavior Cancelling the search now actually cancels the search. Signed-off-by: kingbri --- Ferrite/Views/SourcesView.swift | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index faf7388..6a99021 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -40,14 +40,13 @@ struct SourcesView: View { return tempSources } - @State private var viewTask: Task? = nil @State private var checkedForSources = false - - @State private var searchText: String = "" @State private var isEditing = false - @State var filteredUpdatedSources: [SourceJson] = [] - @State var filteredAvailableSources: [SourceJson] = [] + @State private var viewTask: Task? = nil + @State private var searchText: String = "" + @State private var filteredUpdatedSources: [SourceJson] = [] + @State private var filteredAvailableSources: [SourceJson] = [] var body: some View { NavView { @@ -128,6 +127,9 @@ struct SourcesView: View { .navigationSearchBar { SearchBar("Search", text: $searchText, isEditing: $isEditing) .showsCancelButton(isEditing) + .onCancel { + searchText = "" + } } .onChange(of: searchText) { newValue in filteredAvailableSources = sourceManager.availableSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } -- 2.45.2 From 8306ca1f9bbfced2b4ef4ff64154d5bc5742c3bd Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 5 Sep 2022 14:58:02 -0400 Subject: [PATCH 11/22] Ferrite: Clean up UI changes - Migrate the empty view to a common view which vertically centers itself to the screen's bounds - Don't initialize underlying state variables in init as this is discouraged behavior. Instead, hook the source list editor to an ID that refreshes when an existing source list URL has been set Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 ++ Ferrite/Extensions/View.swift | 6 +- Ferrite/Views/AboutView.swift | 2 +- Ferrite/Views/BatchChoiceView.swift | 1 + .../CommonViews/EmptyInstructionView.swift | 27 +++++++ Ferrite/Views/CommonViews/InlineHeader.swift | 19 +++-- Ferrite/Views/CommonViews/InlinedList.swift | 2 +- Ferrite/Views/SettingsView.swift | 10 +-- .../SettingsAppVersionView.swift | 1 + .../SettingsSourceListView.swift | 72 ++++++++++--------- .../SettingsViews/SourceListEditorView.swift | 8 +-- Ferrite/Views/SourcesView.swift | 27 +++---- 12 files changed, 102 insertions(+), 77 deletions(-) create mode 100644 Ferrite/Views/CommonViews/EmptyInstructionView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index cb07a28..414aeda 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -78,6 +78,7 @@ 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; + 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */; }; 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; }; /* End PBXBuildFile section */ @@ -148,6 +149,7 @@ 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyInstructionView.swift; sourceTree = ""; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -252,6 +254,7 @@ 0CB6516228C5A57300DCA721 /* ConditionalId.swift */, 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, + 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, ); path = CommonViews; sourceTree = ""; @@ -482,6 +485,7 @@ 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */, 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, + 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */, 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */, 0C7D11FC28AA01E900ED92DB /* DynamicAccentColor.swift in Sources */, diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index c0290fd..d1dc471 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -5,14 +5,14 @@ // Created by Brian Dashore on 8/15/22. // -import SwiftUI import Introspect +import SwiftUI extension View { // MARK: Custom introspect functions - func introspectCollectionView(customize: @escaping (UICollectionView) -> ()) -> some View { - return inject(UIKitIntrospectionView( + func introspectCollectionView(customize: @escaping (UICollectionView) -> Void) -> some View { + inject(UIKitIntrospectionView( selector: { introspectionView in guard let viewHost = Introspect.findViewHost(from: introspectionView) else { return nil diff --git a/Ferrite/Views/AboutView.swift b/Ferrite/Views/AboutView.swift index 6ee081a..2933955 100644 --- a/Ferrite/Views/AboutView.swift +++ b/Ferrite/Views/AboutView.swift @@ -21,7 +21,7 @@ struct AboutView: View { Image("AppImage") .resizable() .frame(width: 100, height: 100) - .clipShape(RoundedRectangle(cornerRadius: 100*0.225, style: .continuous)) + .clipShape(RoundedRectangle(cornerRadius: 100 * 0.225, style: .continuous)) .padding(.top, 24) Text("Ferrite is a free and open source application developed by kingbri under the GNU-GPLv3 license.") diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index 54b6237..692855c 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -38,6 +38,7 @@ struct BatchChoiceView: View { presentationMode.wrappedValue.dismiss() } + .dynamicAccentColor(.primary) } } .listStyle(.insetGrouped) diff --git a/Ferrite/Views/CommonViews/EmptyInstructionView.swift b/Ferrite/Views/CommonViews/EmptyInstructionView.swift new file mode 100644 index 0000000..8cd0055 --- /dev/null +++ b/Ferrite/Views/CommonViews/EmptyInstructionView.swift @@ -0,0 +1,27 @@ +// +// EmptyInstructionView.swift +// Ferrite +// +// Created by Brian Dashore on 9/5/22. +// + +import SwiftUI + +struct EmptyInstructionView: View { + let title: String + let message: String + + var body: some View { + VStack(spacing: 5) { + Text(title) + .font(.system(size: 25, weight: .semibold)) + + Text(message) + .padding(.horizontal, 50) + } + .multilineTextAlignment(.center) + .foregroundColor(.secondaryLabel) + .frame(maxWidth: .infinity, maxHeight: .infinity) + .ignoresSafeArea() + } +} diff --git a/Ferrite/Views/CommonViews/InlineHeader.swift b/Ferrite/Views/CommonViews/InlineHeader.swift index dbb70c4..faff7b7 100644 --- a/Ferrite/Views/CommonViews/InlineHeader.swift +++ b/Ferrite/Views/CommonViews/InlineHeader.swift @@ -4,6 +4,8 @@ // // Created by Brian Dashore on 9/5/22. // +// For iOS 15's weird defaults regarding sectioned list padding +// import SwiftUI @@ -15,16 +17,13 @@ struct InlineHeader: View { } var body: some View { - Group { - if #available(iOS 16, *) { - Text(title) - .padding(.vertical, 5) - } else { - Text(title) - .padding(.vertical, 10) - } + if #available(iOS 16, *) { + Text(title) + } else if #available(iOS 15, *) { + Text(title) + .listRowInsets(EdgeInsets(top: 10, leading: 15, bottom: 0, trailing: 0)) + } else { + Text(title) } - .padding(.horizontal, 20) - .listRowInsets(EdgeInsets()) } } diff --git a/Ferrite/Views/CommonViews/InlinedList.swift b/Ferrite/Views/CommonViews/InlinedList.swift index 572693e..a0eb034 100644 --- a/Ferrite/Views/CommonViews/InlinedList.swift +++ b/Ferrite/Views/CommonViews/InlinedList.swift @@ -8,8 +8,8 @@ // Use UITableView.appearance().contentInset.top = -20 for iOS 15 and below in the App file // -import SwiftUI import Introspect +import SwiftUI struct InlinedList: ViewModifier { func body(content: Content) -> some View { diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 9d9c1d9..1531a9a 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -5,8 +5,8 @@ // Created by Brian Dashore on 7/11/22. // -import SwiftUI import Introspect +import SwiftUI struct SettingsView: View { @EnvironmentObject var debridManager: DebridManager @@ -41,11 +41,11 @@ struct SettingsView: View { } } - Section(header: InlineHeader("Source management")) { + Section(header: Text("Source management")) { NavigationLink("Source lists", destination: SettingsSourceListView()) } - Section(header: InlineHeader("Default actions")) { + Section(header: Text("Default actions")) { if debridManager.realDebridEnabled { NavigationLink( destination: DebridActionPickerView(), @@ -95,14 +95,14 @@ struct SettingsView: View { ) } - Section(header: InlineHeader("Updates")) { + Section(header: Text("Updates")) { Toggle(isOn: $autoUpdateNotifs) { Text("Show update alerts") } NavigationLink("Version history", destination: SettingsAppVersionView()) } - Section(header: InlineHeader("Information")) { + Section(header: Text("Information")) { ListRowLinkView(text: "Donate", link: "https://ko-fi.com/kingbri") ListRowLinkView(text: "Report issues", link: "https://github.com/bdashore3/Ferrite/issues") NavigationLink("About", destination: AboutView()) diff --git a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift index be1d19f..f15f15d 100644 --- a/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift +++ b/Ferrite/Views/SettingsViews/SettingsAppVersionView.swift @@ -41,6 +41,7 @@ struct SettingsAppVersionView: View { } catch { toastModel.updateToastDescription("Github error: \(error)") } + withAnimation { loadedReleases = true } diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index 320ab00..cc1032e 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -21,58 +21,60 @@ struct SettingsSourceListView: View { @State private var selectedSourceList: SourceList? var body: some View { - List { + ZStack { if sourceLists.isEmpty { - Text("No source lists") + EmptyInstructionView(title: "No Lists", message: "Add a source list using the + button in the top-right") } else { - ForEach(sourceLists, id: \.self) { sourceList in - VStack(alignment: .leading, spacing: 5) { - Text(sourceList.name) + List { + ForEach(sourceLists, id: \.self) { sourceList in + VStack(alignment: .leading, spacing: 5) { + Text(sourceList.name) - Text(sourceList.author) - .foregroundColor(.gray) + Text(sourceList.author) + .foregroundColor(.gray) - Text("ID: \(sourceList.id)") - .font(.caption) - .foregroundColor(.gray) - } - .padding(.vertical, 2) - .contextMenu { - Button { - navModel.selectedSourceList = sourceList - presentSourceSheet.toggle() - } label: { - Text("Edit") - Image(systemName: "pencil") + Text("ID: \(sourceList.id)") + .font(.caption) + .foregroundColor(.gray) } - - if #available(iOS 15.0, *) { - Button(role: .destructive) { - PersistenceController.shared.delete(sourceList, context: backgroundContext) - } label: { - Text("Remove") - Image(systemName: "trash") - } - } else { + .padding(.vertical, 2) + .contextMenu { Button { - PersistenceController.shared.delete(sourceList, context: backgroundContext) + navModel.selectedSourceList = sourceList + presentSourceSheet.toggle() } label: { - Text("Remove") - Image(systemName: "trash") + Text("Edit") + Image(systemName: "pencil") + } + + if #available(iOS 15.0, *) { + Button(role: .destructive) { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } + } else { + Button { + PersistenceController.shared.delete(sourceList, context: backgroundContext) + } label: { + Text("Remove") + Image(systemName: "trash") + } } } } } + .listStyle(.insetGrouped) + .inlinedList() } } - .listStyle(.insetGrouped) - .inlinedList() .sheet(isPresented: $presentSourceSheet) { if #available(iOS 16, *) { - SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") + SourceListEditorView() .presentationDetents([.medium]) } else { - SourceListEditorView(sourceUrl: navModel.selectedSourceList?.urlString ?? "") + SourceListEditorView() } } .navigationTitle("Source Lists") diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index 4b9a728..5ddfae3 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -15,11 +15,9 @@ struct SourceListEditorView: View { let backgroundContext = PersistenceController.shared.backgroundContext - @State private var sourceUrl: String + @State private var sourceUrlSet = false - init(sourceUrl: String = "") { - _sourceUrl = State(initialValue: sourceUrl) - } + @State private var sourceUrl: String = "" var body: some View { NavView { @@ -28,9 +26,11 @@ struct SourceListEditorView: View { .disableAutocorrection(true) .keyboardType(.URL) .autocapitalization(.none) + .conditionalId(sourceUrlSet) } .onAppear { sourceUrl = navModel.selectedSourceList?.urlString ?? "" + sourceUrlSet = true } .alert(isPresented: $sourceManager.showUrlErrorAlert) { Alert( diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 6a99021..2d1ab4b 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -5,13 +5,11 @@ // Created by Brian Dashore on 7/24/22. // +import Introspect import SwiftUI import SwiftUIX -import Introspect struct SourcesView: View { - @Environment(\.verticalSizeClass) var verticalSizeClass: UserInterfaceSizeClass? - @EnvironmentObject var sourceManager: SourceManager @EnvironmentObject var navModel: NavigationViewModel @@ -53,15 +51,8 @@ struct SourcesView: View { ZStack { if !checkedForSources { ProgressView() - } else if sources.isEmpty && sourceManager.availableSources.isEmpty { - VStack { - Text("No Sources") - .font(.system(size: 25, weight: .semibold)) - .foregroundColor(.secondaryLabel) - Text("Add a source list in Settings") - .foregroundColor(.secondaryLabel) - } - .padding(.top, verticalSizeClass == .regular ? -50 : 0) + } else if sources.isEmpty, sourceManager.availableSources.isEmpty { + EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") } else { List { if !filteredUpdatedSources.isEmpty { @@ -80,12 +71,12 @@ struct SourcesView: View { } } - if !filteredAvailableSources.isEmpty && sourceManager.availableSources.contains(where: { availableSource in + if !filteredAvailableSources.isEmpty, sourceManager.availableSources.contains(where: { availableSource in !sources.contains( where: { availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author + availableSource.listId == $0.listId && + availableSource.author == $0.author } ) }) { @@ -94,8 +85,8 @@ struct SourcesView: View { if !sources.contains( where: { availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author + availableSource.listId == $0.listId && + availableSource.author == $0.author } ) { SourceCatalogButtonView(availableSource: availableSource) @@ -131,7 +122,7 @@ struct SourcesView: View { searchText = "" } } - .onChange(of: searchText) { newValue in + .onChange(of: searchText) { _ in filteredAvailableSources = sourceManager.availableSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } filteredUpdatedSources = updatedSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } if #available(iOS 15.0, *) { -- 2.45.2 From 5d97c7511ff3b79ded8196675a8bee85ec24c091 Mon Sep 17 00:00:00 2001 From: Skitty Date: Tue, 6 Sep 2022 20:23:50 -0500 Subject: [PATCH 12/22] Sources: Fix source searching (#8) - Make searching case insensitive - Fix catalog title not hiding when searching an installed source name - Cancelling a search doesn't add an installed source to the catalog - Add dynamic predicate changing for iOS 14 and up instead of restricting to iOS 15 - Migrate updated source fetching to the source model - Change how filtering works to adapt with the dynamic predicate changes Signed-off-by: kingbri Co-authored-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 + Ferrite/ViewModels/SourceManager.swift | 18 +- .../CommonViews/DynamicFetchRequest.swift | 26 +++ Ferrite/Views/SourcesView.swift | 157 +++++++++--------- 4 files changed, 122 insertions(+), 83 deletions(-) create mode 100644 Ferrite/Views/CommonViews/DynamicFetchRequest.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 414aeda..0bd3d04 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; + 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; 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 */; }; @@ -91,6 +92,7 @@ 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.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 = ""; }; 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 = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; @@ -255,6 +257,7 @@ 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, + 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, ); path = CommonViews; sourceTree = ""; @@ -458,6 +461,7 @@ 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, + 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index 072a829..d4f83e0 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -7,7 +7,7 @@ import CoreData import Foundation -import UIKit +import SwiftUI public class SourceManager: ObservableObject { var toastModel: ToastViewModel? @@ -52,6 +52,22 @@ public class SourceManager: ObservableObject { } } + func fetchUpdatedSources(installedSources: FetchedResults) -> [SourceJson] { + var updatedSources: [SourceJson] = [] + + for source in installedSources { + if let availableSource = availableSources.first(where: { + source.listId == $0.listId && source.name == $0.name && source.author == $0.author + }), + availableSource.version > source.version + { + updatedSources.append(availableSource) + } + } + + return updatedSources + } + // Checks if the current app version is supported by the source func checkAppVersion(minVersion: String?) -> Bool { // If there's no min version, assume that every version is supported diff --git a/Ferrite/Views/CommonViews/DynamicFetchRequest.swift b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift new file mode 100644 index 0000000..02ccac8 --- /dev/null +++ b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift @@ -0,0 +1,26 @@ +// +// DynamicFetchRequest.swift +// Ferrite +// +// Created by Brian Dashore on 9/6/22. +// + +import CoreData +import SwiftUI + +struct DynamicFetchRequest: View { + @FetchRequest var fetchRequest: FetchedResults + + let content: (FetchedResults) -> Content + + var body: some View { + content(fetchRequest) + } + + init(predicate: NSPredicate?, + @ViewBuilder content: @escaping (FetchedResults) -> Content) + { + _fetchRequest = FetchRequest(sortDescriptors: [], predicate: predicate) + self.content = content + } +} diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index 2d1ab4b..c57e8f5 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -20,24 +20,6 @@ struct SourcesView: View { sortDescriptors: [] ) var sources: FetchedResults - private var updatedSources: [SourceJson] { - var tempSources: [SourceJson] = [] - - for source in sources { - guard let availableSource = sourceManager.availableSources.first(where: { - source.listId == $0.listId && source.name == $0.name && source.author == $0.author - }) else { - continue - } - - if availableSource.version > source.version { - tempSources.append(availableSource) - } - } - - return tempSources - } - @State private var checkedForSources = false @State private var isEditing = false @@ -45,92 +27,103 @@ struct SourcesView: View { @State private var searchText: String = "" @State private var filteredUpdatedSources: [SourceJson] = [] @State private var filteredAvailableSources: [SourceJson] = [] + @State private var sourcePredicate: NSPredicate? var body: some View { NavView { - ZStack { - if !checkedForSources { - ProgressView() - } else if sources.isEmpty, sourceManager.availableSources.isEmpty { - EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") - } else { - List { - if !filteredUpdatedSources.isEmpty { - Section(header: InlineHeader("Updates")) { - ForEach(filteredUpdatedSources, id: \.self) { source in - SourceUpdateButtonView(updatedSource: source) + DynamicFetchRequest(predicate: sourcePredicate) { (installedSources: FetchedResults) in + ZStack { + if !checkedForSources { + ProgressView() + } else if sources.isEmpty, sourceManager.availableSources.isEmpty { + EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") + } else { + List { + if !filteredUpdatedSources.isEmpty { + Section(header: InlineHeader("Updates")) { + ForEach(filteredUpdatedSources, id: \.self) { source in + SourceUpdateButtonView(updatedSource: source) + } } } - } - if !sources.isEmpty { - Section(header: InlineHeader("Installed")) { - ForEach(sources, id: \.self) { source in - InstalledSourceView(installedSource: source) + if !installedSources.isEmpty { + Section(header: InlineHeader("Installed")) { + ForEach(installedSources, id: \.self) { source in + InstalledSourceView(installedSource: source) + } } } - } - if !filteredAvailableSources.isEmpty, sourceManager.availableSources.contains(where: { availableSource in - !sources.contains( - where: { - availableSource.name == $0.name && - availableSource.listId == $0.listId && - availableSource.author == $0.author - } - ) - }) { - Section(header: InlineHeader("Catalog")) { - ForEach(filteredAvailableSources, id: \.self) { availableSource in - if !sources.contains( - where: { + if !filteredAvailableSources.isEmpty { + Section(header: InlineHeader("Catalog")) { + ForEach(filteredAvailableSources, id: \.self) { availableSource in + if !installedSources.contains(where: { availableSource.name == $0.name && availableSource.listId == $0.listId && availableSource.author == $0.author + }) { + SourceCatalogButtonView(availableSource: availableSource) } - ) { - SourceCatalogButtonView(availableSource: availableSource) } } } } + .conditionalId(UUID()) + .listStyle(.insetGrouped) } - .conditionalId(UUID()) - .listStyle(.insetGrouped) } - } - .sheet(isPresented: $navModel.showSourceSettings) { - SourceSettingsView() - .environmentObject(navModel) - } - .onAppear { - filteredUpdatedSources = updatedSources - viewTask = Task { - await sourceManager.fetchSourcesFromUrl() - filteredAvailableSources = sourceManager.availableSources - checkedForSources = true + .sheet(isPresented: $navModel.showSourceSettings) { + SourceSettingsView() + .environmentObject(navModel) } - } - .onDisappear { - viewTask?.cancel() - } - .navigationTitle("Sources") - .navigationSearchBar { - SearchBar("Search", text: $searchText, isEditing: $isEditing) - .showsCancelButton(isEditing) - .onCancel { - searchText = "" + .onAppear { + viewTask = Task { + await sourceManager.fetchSourcesFromUrl() + filteredAvailableSources = sourceManager.availableSources.filter { availableSource in + !installedSources.contains(where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + }) + } + + filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources) + checkedForSources = true } - } - .onChange(of: searchText) { _ in - filteredAvailableSources = sourceManager.availableSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } - filteredUpdatedSources = updatedSources.filter { searchText.isEmpty ? true : $0.name.contains(searchText) } - if #available(iOS 15.0, *) { - if searchText.isEmpty { - sources.nsPredicate = nil - } else { - sources.nsPredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + .onDisappear { + viewTask?.cancel() + } + .onChange(of: searchText) { _ in + sourcePredicate = searchText.isEmpty ? nil : NSPredicate(format: "name CONTAINS[cd] %@", searchText) + } + .onReceive(installedSources.publisher.count()) { _ in + filteredAvailableSources = sourceManager.availableSources.filter { availableSource in + let sourceExists = installedSources.contains(where: { + availableSource.name == $0.name && + availableSource.listId == $0.listId && + availableSource.author == $0.author + }) + + if searchText.isEmpty { + return !sourceExists + } else { + return !sourceExists && availableSource.name.lowercased().contains(searchText.lowercased()) + } } + + filteredUpdatedSources = sourceManager.fetchUpdatedSources(installedSources: installedSources).filter { + searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) + } + } + .navigationTitle("Sources") + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditing) + .showsCancelButton(isEditing) + .onCancel { + searchText = "" + } } } } -- 2.45.2 From 2f870b9410decb1e1e14418b59f69a47847b05fc Mon Sep 17 00:00:00 2001 From: kingbri Date: Mon, 5 Sep 2022 18:32:41 -0400 Subject: [PATCH 13/22] Ferrite: Add bookmarks Bookmarks are added through search results and can be accessed through the library. These can be moved and deleted within the list. Add a RealDebrid instant availability cache for bookmark IA status to not overwhelm the API. Instant availability results are fresh on every search results since the cache is cleared. Also don't require a source API object to be present for the API parser button in source settings. If a JSON parser exists for a source, allow the option to be presented. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 44 +++++ Ferrite/API/RealDebridWrapper.swift | 16 +- .../Classes/Bookmark+CoreDataClass.swift | 25 +++ .../Classes/Bookmark+CoreDataProperties.swift | 32 ++++ .../FerriteDB.xcdatamodel/contents | 21 +++ Ferrite/Extensions/NotificationCenter.swift | 14 ++ Ferrite/Extensions/View.swift | 7 + Ferrite/Models/RealDebridModels.swift | 161 +++++++++--------- Ferrite/Models/SearchModels.swift | 18 ++ Ferrite/ViewModels/DebridManager.swift | 46 +++-- Ferrite/ViewModels/NavigationViewModel.swift | 14 +- Ferrite/ViewModels/ScrapingViewModel.swift | 11 -- Ferrite/Views/BatchChoiceView.swift | 2 +- .../CommonViews/ConditionalContextMenu.swift | 39 +++++ Ferrite/Views/ContentView.swift | 6 +- Ferrite/Views/LibraryView.swift | 83 +++++++++ .../Views/LibraryViews/BookmarksView.swift | 67 ++++++++ Ferrite/Views/LibraryViews/HistoryView.swift | 22 +++ Ferrite/Views/MagnetChoiceView.swift | 10 +- Ferrite/Views/MainView.swift | 6 + Ferrite/Views/SearchResultButtonView.swift | 109 ++++++++++++ Ferrite/Views/SearchResultsView.swift | 34 +--- .../SourceViews/SourceSettingsView.swift | 2 +- 23 files changed, 635 insertions(+), 154 deletions(-) create mode 100644 Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift create mode 100644 Ferrite/Extensions/NotificationCenter.swift create mode 100644 Ferrite/Models/SearchModels.swift create mode 100644 Ferrite/Views/CommonViews/ConditionalContextMenu.swift create mode 100644 Ferrite/Views/LibraryView.swift create mode 100644 Ferrite/Views/LibraryViews/BookmarksView.swift create mode 100644 Ferrite/Views/LibraryViews/HistoryView.swift create mode 100644 Ferrite/Views/SearchResultButtonView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 0bd3d04..497ae48 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -16,6 +16,8 @@ 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; + 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */; }; + 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C41BC6428C2AEB900B47DD6 /* SearchModels.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 */; }; @@ -25,6 +27,8 @@ 0C64A4B7288903880079976D /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 0C64A4B6288903880079976D /* KeychainSwift */; }; 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */; }; 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C68135128BC1A7C00FAD890 /* GithubModels.swift */; }; + 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */; }; + 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */; }; 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C733286289C4C820058D1FE /* SourceSettingsView.swift */; }; 0C7376F028A97D1400D60918 /* SwiftUIX in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7376EF28A97D1400D60918 /* SwiftUIX */; }; 0C7506D728B1AC9A008BEE38 /* SwiftyJSON in Frameworks */ = {isa = PBXBuildFile; productRef = 0C7506D628B1AC9A008BEE38 /* SwiftyJSON */; }; @@ -70,6 +74,11 @@ 0CA148E9288903F000DE2211 /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D1288903F000DE2211 /* MainView.swift */; }; 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D3288903F000DE2211 /* SearchResultsView.swift */; }; 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148D4288903F000DE2211 /* ContentView.swift */; }; + 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23328C2658700616D3A /* LibraryView.swift */; }; + 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23628C2660700616D3A /* HistoryView.swift */; }; + 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23828C2660D00616D3A /* BookmarksView.swift */; }; + 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */; }; + 0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */; }; 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; @@ -93,12 +102,16 @@ 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.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 = ""; }; + 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultButtonView.swift; sourceTree = ""; }; + 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchModels.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 = ""; }; 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultRDView.swift; sourceTree = ""; }; 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.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 = ""; }; + 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationCenter.swift; sourceTree = ""; }; 0C733286289C4C820058D1FE /* SourceSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceSettingsView.swift; sourceTree = ""; }; 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataClass.swift"; sourceTree = ""; }; 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = ""; }; @@ -142,6 +155,11 @@ 0CA148D1288903F000DE2211 /* MainView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; 0CA148D3288903F000DE2211 /* SearchResultsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SearchResultsView.swift; sourceTree = ""; }; 0CA148D4288903F000DE2211 /* ContentView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 0CA3B23328C2658700616D3A /* LibraryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibraryView.swift; sourceTree = ""; }; + 0CA3B23628C2660700616D3A /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; + 0CA3B23828C2660D00616D3A /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = ""; }; + 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataClass.swift"; sourceTree = ""; }; + 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataProperties.swift"; sourceTree = ""; }; 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; @@ -176,6 +194,8 @@ 0C0D50DE288DF72D0035ECC8 /* Classes */ = { isa = PBXGroup; children = ( + 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */, + 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */, 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */, 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */, 0C750742289B003E004B3906 /* SourceRssParser+CoreDataClass.swift */, @@ -201,6 +221,7 @@ 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, 0C68135128BC1A7C00FAD890 /* GithubModels.swift */, + 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, ); path = Models; sourceTree = ""; @@ -258,6 +279,7 @@ 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, + 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */, ); path = CommonViews; sourceTree = ""; @@ -279,6 +301,7 @@ 0C32FB542890D1BF002BD219 /* UIApplication.swift */, 0C7D11FD28AA03FE00ED92DB /* View.swift */, 0C78041C28BFB3EA001E8CA3 /* String.swift */, + 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */, ); path = Extensions; sourceTree = ""; @@ -286,6 +309,7 @@ 0CA148EE2889061200DE2211 /* Views */ = { isa = PBXGroup; children = ( + 0CA3B23528C265FD00616D3A /* LibraryViews */, 0C794B65289DAC9F00DD1CC8 /* SourceViews */, 0CA148F02889062700DE2211 /* RepresentableViews */, 0CA148C0288903F000DE2211 /* CommonViews */, @@ -301,6 +325,8 @@ 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */, + 0CA3B23328C2658700616D3A /* LibraryView.swift */, + 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, ); path = Views; sourceTree = ""; @@ -334,6 +360,15 @@ path = API; sourceTree = ""; }; + 0CA3B23528C265FD00616D3A /* LibraryViews */ = { + isa = PBXGroup; + children = ( + 0CA3B23828C2660D00616D3A /* BookmarksView.swift */, + 0CA3B23628C2660700616D3A /* HistoryView.swift */, + ); + path = LibraryViews; + sourceTree = ""; + }; 0CAF1C5F286F5C0D00296F86 = { isa = PBXGroup; children = ( @@ -454,8 +489,10 @@ 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, + 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */, + 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */, 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */, @@ -468,7 +505,10 @@ 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, + 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */, + 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */, 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */, + 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, @@ -479,20 +519,24 @@ 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, 0C84F4862895BFED0074B7C9 /* SourceList+CoreDataClass.swift in Sources */, + 0C41BC6528C2AEB900B47DD6 /* SearchModels.swift in Sources */, 0C68135028BC1A2D00FAD890 /* GithubWrapper.swift in Sources */, 0C95D8DA28A55BB6005E22B3 /* SettingsModels.swift in Sources */, 0CA148E3288903F000DE2211 /* Task.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */, + 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, 0CA148E6288903F000DE2211 /* WebView.swift in Sources */, + 0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */, 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */, 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 */, 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */, diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index fb4e8e3..ffd06f3 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -248,9 +248,21 @@ public class RealDebrid { } } - availableHashes.append(RealDebridIA(hash: hash, files: files, batches: batches)) + // TTL: 5 minutes + availableHashes.append( + RealDebridIA( + hash: hash, + expiryTimeStamp: Date().timeIntervalSince1970 + 300, + files: files, + batches: batches) + ) } else { - availableHashes.append(RealDebridIA(hash: hash)) + availableHashes.append( + RealDebridIA( + hash: hash, + expiryTimeStamp: Date().timeIntervalSince1970 + 300 + ) + ) } } diff --git a/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift b/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift new file mode 100644 index 0000000..dcd0f86 --- /dev/null +++ b/Ferrite/DataManagement/Classes/Bookmark+CoreDataClass.swift @@ -0,0 +1,25 @@ +// +// Bookmark+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 9/2/22. +// +// + +import CoreData +import Foundation + +@objc(Bookmark) +public class Bookmark: NSManagedObject { + func toSearchResult() -> SearchResult { + SearchResult( + title: title, + source: source, + size: size, + magnetLink: magnetLink, + magnetHash: magnetHash, + seeders: seeders, + leechers: leechers + ) + } +} diff --git a/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift new file mode 100644 index 0000000..6528695 --- /dev/null +++ b/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift @@ -0,0 +1,32 @@ +// +// Bookmark+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 9/3/22. +// +// + +import Foundation +import CoreData + + +extension Bookmark { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "Bookmark") + } + + @NSManaged public var leechers: String? + @NSManaged public var magnetHash: String? + @NSManaged public var magnetLink: String? + @NSManaged public var seeders: String? + @NSManaged public var size: String? + @NSManaged public var source: String + @NSManaged public var title: String? + @NSManaged public var orderNum: Int16 + +} + +extension Bookmark : Identifiable { + +} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents index 99cfeeb..f2d6e3f 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents @@ -1,5 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/Ferrite/Extensions/NotificationCenter.swift b/Ferrite/Extensions/NotificationCenter.swift new file mode 100644 index 0000000..be21d68 --- /dev/null +++ b/Ferrite/Extensions/NotificationCenter.swift @@ -0,0 +1,14 @@ +// +// NotificationCenter.swift +// Ferrite +// +// Created by Brian Dashore on 9/3/22. +// + +import Foundation + +extension Notification.Name { + static var didDeleteBookmark: Notification.Name { + return Notification.Name("Deleted bookmark") + } +} diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index d1dc471..e7debe2 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -36,4 +36,11 @@ extension View { func inlinedList() -> some View { modifier(InlinedList()) } + + func conditionalContextMenu( + id: ID, + @ViewBuilder _ internalContent: @escaping () -> InternalContent + ) -> some View { + modifier(ConditionalContextMenu(internalContent, id: id)) + } } diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index a9860ec..b070fd9 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -11,148 +11,149 @@ import Foundation // MARK: - device code endpoint public struct DeviceCodeResponse: Codable { - let deviceCode, userCode: String - let interval, expiresIn: Int - let verificationURL, directVerificationURL: String + let deviceCode, userCode: String + let interval, expiresIn: Int + let verificationURL, directVerificationURL: String - enum CodingKeys: String, CodingKey { - case deviceCode = "device_code" - case userCode = "user_code" - case interval - case expiresIn = "expires_in" - case verificationURL = "verification_url" - case directVerificationURL = "direct_verification_url" - } + enum CodingKeys: String, CodingKey { + case deviceCode = "device_code" + case userCode = "user_code" + case interval + case expiresIn = "expires_in" + case verificationURL = "verification_url" + case directVerificationURL = "direct_verification_url" + } } // MARK: - device credentials endpoint public struct DeviceCredentialsResponse: Codable { - let clientID, clientSecret: String? + let clientID, clientSecret: String? - enum CodingKeys: String, CodingKey { - case clientID = "client_id" - case clientSecret = "client_secret" - } + enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + } } // MARK: - token endpoint public struct TokenResponse: Codable { - let accessToken: String - let expiresIn: Int - let refreshToken, tokenType: String + let accessToken: String + let expiresIn: Int + let refreshToken, tokenType: String - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" - } + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case tokenType = "token_type" + } } // MARK: - instantAvailability endpoint // Thanks Skitty! public struct InstantAvailabilityResponse: Codable { - var data: InstantAvailabilityData? + var data: InstantAvailabilityData? - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() - if let data = try? container.decode(InstantAvailabilityData.self) { - self.data = data - } - } + if let data = try? container.decode(InstantAvailabilityData.self) { + self.data = data + } + } } struct InstantAvailabilityData: Codable { - var rd: [[String: InstantAvailabilityInfo]] + var rd: [[String: InstantAvailabilityInfo]] } struct InstantAvailabilityInfo: Codable { - var filename: String - var filesize: Int + var filename: String + var filesize: Int } // MARK: - Instant Availability client side structures public struct RealDebridIA: Codable, Hashable { - let hash: String - var files: [RealDebridIAFile] = [] - var batches: [RealDebridIABatch] = [] + let hash: String + let expiryTimeStamp: Double + var files: [RealDebridIAFile] = [] + var batches: [RealDebridIABatch] = [] } public struct RealDebridIABatch: Codable, Hashable { - let files: [RealDebridIABatchFile] + let files: [RealDebridIABatchFile] } public struct RealDebridIABatchFile: Codable, Hashable { - let id: Int - let fileName: String + let id: Int + let fileName: String } public struct RealDebridIAFile: Codable, Hashable { - let name: String - let batchIndex: Int - let batchFileIndex: Int + let name: String + let batchIndex: Int + let batchFileIndex: Int } public enum RealDebridIAStatus: Codable, Hashable { - case full - case partial - case none + case full + case partial + case none } // MARK: - addMagnet endpoint public struct AddMagnetResponse: Codable { - let id: String - let uri: String + let id: String + let uri: String } // MARK: - torrentInfo endpoint struct TorrentInfoResponse: Codable { - let id, filename, originalFilename, hash: String - let bytes, originalBytes: Int - let host: String - let split, progress: Int - let status, added: String - let files: [TorrentInfoFile] - let links: [String] - let ended: String + let id, filename, originalFilename, hash: String + let bytes, originalBytes: Int + let host: String + let split, progress: Int + let status, added: String + let files: [TorrentInfoFile] + let links: [String] + let ended: String - enum CodingKeys: String, CodingKey { - case id, filename - case originalFilename = "original_filename" - case hash, bytes - case originalBytes = "original_bytes" - case host, split, progress, status, added, files, links, ended - } + enum CodingKeys: String, CodingKey { + case id, filename + case originalFilename = "original_filename" + case hash, bytes + case originalBytes = "original_bytes" + case host, split, progress, status, added, files, links, ended + } } struct TorrentInfoFile: Codable { - let id: Int - let path: String - let bytes, selected: Int + let id: Int + let path: String + let bytes, selected: Int } // MARK: - unrestrictLink endpoint struct UnrestrictLinkResponse: Codable { - let id, filename, mimeType: String - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks, crc: Int - let download: String - let streamable: Int + let id, filename, mimeType: String + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks, crc: Int + let download: String + let streamable: Int - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, crc, download, streamable - } + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, crc, download, streamable + } } diff --git a/Ferrite/Models/SearchModels.swift b/Ferrite/Models/SearchModels.swift new file mode 100644 index 0000000..2751a82 --- /dev/null +++ b/Ferrite/Models/SearchModels.swift @@ -0,0 +1,18 @@ +// +// SearchModels.swift +// Ferrite +// +// Created by Brian Dashore on 9/2/22. +// + +import Foundation + +public struct SearchResult: Hashable, Codable { + let title: String? + let source: String + let size: String? + let magnetLink: String? + let magnetHash: String? + let seeders: String? + let leechers: String? +} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index b1a05a9..a579d21 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -10,8 +10,11 @@ import SwiftUI @MainActor public class DebridManager: ObservableObject { - // UI Variables + // Linked classes var toastModel: ToastViewModel? + let realDebrid: RealDebrid = .init() + + // UI Variables @Published var showWebView: Bool = false @Published var showLoadingProgress: Bool = false @@ -19,8 +22,6 @@ public class DebridManager: ObservableObject { @Published var currentDebridTask: Task? // RealDebrid auth variables - let realDebrid: RealDebrid = .init() - @Published var realDebridEnabled: Bool = false { didSet { UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled") @@ -31,7 +32,7 @@ public class DebridManager: ObservableObject { @Published var realDebridAuthUrl: String = "" // RealDebrid fetch variables - @Published var realDebridHashes: [RealDebridIA] = [] + @Published var realDebridIAValues: [RealDebridIA] = [] @Published var realDebridDownloadUrl: String = "" @Published var selectedRealDebridItem: RealDebridIA? @Published var selectedRealDebridFile: RealDebridIAFile? @@ -40,19 +41,30 @@ public class DebridManager: ObservableObject { realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled") } - public func populateDebridHashes(_ searchResults: [SearchResult]) async { - var hashes: [String] = [] - - for result in searchResults { - if let hash = result.magnetHash { - hashes.append(hash) - } - } - + public func populateDebridHashes(_ resultHashes: [String]) async { do { - let debridHashes = try await realDebrid.instantAvailability(magnetHashes: hashes) + let now = Date() - realDebridHashes = debridHashes + // If a hash isn't found in the IA, update it + // If the hash is expired, remove it and update it + let sendHashes = resultHashes.filter { hash in + if let IAIndex = realDebridIAValues.firstIndex(where: { $0.hash == hash }) { + if now.timeIntervalSince1970 > realDebridIAValues[IAIndex].expiryTimeStamp { + realDebridIAValues.remove(at: IAIndex) + return true + } else { + return false + } + } else { + return true + } + } + + if !sendHashes.isEmpty { + let fetchedIAValues = try await realDebrid.instantAvailability(magnetHashes: sendHashes) + + realDebridIAValues += fetchedIAValues + } } catch { let error = error as NSError @@ -69,7 +81,7 @@ public class DebridManager: ObservableObject { return .none } - guard let debridMatch = realDebridHashes.first(where: { result.magnetHash == $0.hash }) else { + guard let debridMatch = realDebridIAValues.first(where: { result.magnetHash == $0.hash }) else { return .none } @@ -86,7 +98,7 @@ public class DebridManager: ObservableObject { return false } - if let realDebridItem = realDebridHashes.first(where: { magnetHash == $0.hash }) { + if let realDebridItem = realDebridIAValues.first(where: { magnetHash == $0.hash }) { selectedRealDebridItem = realDebridItem return true } else { diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index a55ad27..6dba198 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -11,6 +11,7 @@ enum ViewTab { case search case sources case settings + case library } @MainActor @@ -31,6 +32,8 @@ class NavigationViewModel: ObservableObject { @Published var isEditingSearch: Bool = false @Published var isSearching: Bool = false + @Published var selectedSearchResult: SearchResult? + @Published var hideNavigationBar = false @Published var currentChoiceSheet: ChoiceSheetType? @@ -86,11 +89,18 @@ class NavigationViewModel: ObservableObject { } } - public func runMagnetAction(action: DefaultMagnetActionType?, searchResult: SearchResult) { + public func runMagnetAction(_ action: DefaultMagnetActionType? = nil) { + guard let searchResult = selectedSearchResult else { + toastModel?.updateToastDescription("Magnet action error: A search result was not selected.") + print("Magnet action error: A search result was not selected.") + + return + } + let selectedAction = action ?? defaultMagnetAction guard let magnetLink = searchResult.magnetLink else { - toastModel?.toastDescription = "Could not run your action because the magnet link is invalid." + toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") print("Magnet action error: The magnet link is invalid.") return diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index eb7adc4..d7a4ef3 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -11,16 +11,6 @@ import SwiftSoup import SwiftUI import SwiftyJSON -public struct SearchResult: Hashable, Codable { - let title: String? - let source: String - let size: String? - let magnetLink: String? - let magnetHash: String? - let seeders: String? - let leechers: String? -} - class ScrapingViewModel: ObservableObject { @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false @@ -31,7 +21,6 @@ class ScrapingViewModel: ObservableObject { @Published var runningSearchTask: Task? @Published var searchResults: [SearchResult] = [] @Published var searchText: String = "" - @Published var selectedSearchResult: SearchResult? @Published var filteredSource: Source? @Published var currentSourceName: String? diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index 692855c..60e1c64 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -21,7 +21,7 @@ struct BatchChoiceView: View { Button(file.name) { debridManager.selectedRealDebridFile = file - if let searchResult = scrapingModel.selectedSearchResult { + if let searchResult = navModel.selectedSearchResult { debridManager.currentDebridTask = Task { await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file) diff --git a/Ferrite/Views/CommonViews/ConditionalContextMenu.swift b/Ferrite/Views/CommonViews/ConditionalContextMenu.swift new file mode 100644 index 0000000..d981673 --- /dev/null +++ b/Ferrite/Views/CommonViews/ConditionalContextMenu.swift @@ -0,0 +1,39 @@ +// +// ConditionalContextMenu.swift +// Ferrite +// +// Created by Brian Dashore on 9/3/22. +// +// Used as a workaround for iOS 15 not updating context views with conditional variables +// A stateful ID is required for the contextMenu to update itself. +// + +import SwiftUI + +struct ConditionalContextMenu: ViewModifier { + let internalContent: () -> InternalContent + let id: ID + + init(@ViewBuilder _ internalContent: @escaping () -> InternalContent, id: ID) { + self.internalContent = internalContent + self.id = id + } + + func body(content: Content) -> some View { + if #available(iOS 16, *) { + content + .contextMenu { + internalContent() + } + } else { + content + .background { + Color.clear + .contextMenu { + internalContent() + } + .id(id) + } + } + } +} diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index 0c1332f..fc3bb21 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -110,7 +110,11 @@ struct ContentView: View { await scrapingModel.scanSources(sources: sources) if realDebridEnabled, !scrapingModel.searchResults.isEmpty { - await debridManager.populateDebridHashes(scrapingModel.searchResults) + debridManager.realDebridIAValues = [] + + await debridManager.populateDebridHashes( + scrapingModel.searchResults.compactMap(\.magnetHash) + ) } navModel.showSearchProgress = false diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift new file mode 100644 index 0000000..61caa19 --- /dev/null +++ b/Ferrite/Views/LibraryView.swift @@ -0,0 +1,83 @@ +// +// Library.swift +// Ferrite +// +// Created by Brian Dashore on 9/2/22. +// + +import SwiftUI + +struct LibraryView: View { + enum LibraryPickerSegment { + case bookmarks + case history + } + + @EnvironmentObject var navModel: NavigationViewModel + + @FetchRequest( + entity: Bookmark.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true) + ] + ) var bookmarks: FetchedResults + + @State private var historyEmpty = true + + @State private var selectedSegment: LibraryPickerSegment = .bookmarks + @State private var editMode: EditMode = .inactive + + var body: some View { + NavView { + VStack(spacing: 0) { + Picker("Segments", selection: $selectedSegment) { + Text("Bookmarks").tag(LibraryPickerSegment.bookmarks) + Text("History").tag(LibraryPickerSegment.history) + } + .pickerStyle(.segmented) + .padding(.horizontal) + .padding(.top) + + switch selectedSegment { + case .bookmarks: + BookmarksView(bookmarks: bookmarks) + case .history: + HistoryView() + } + + Spacer() + } + .overlay { + switch selectedSegment { + case .bookmarks: + if bookmarks.isEmpty { + EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") + } + case .history: + if historyEmpty { + EmptyInstructionView(title: "No History", message: "Start watching to build history") + } + } + } + .navigationTitle("Library") + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + EditButton() + } + } + .environment(\.editMode, $editMode) + } + .onChange(of: selectedSegment) { _ in + editMode = .inactive + } + .onDisappear { + editMode = .inactive + } + } +} + +struct LibraryView_Previews: PreviewProvider { + static var previews: some View { + LibraryView() + } +} diff --git a/Ferrite/Views/LibraryViews/BookmarksView.swift b/Ferrite/Views/LibraryViews/BookmarksView.swift new file mode 100644 index 0000000..2ad3fb2 --- /dev/null +++ b/Ferrite/Views/LibraryViews/BookmarksView.swift @@ -0,0 +1,67 @@ +// +// BookmarksView.swift +// Ferrite +// +// Created by Brian Dashore on 9/2/22. +// + +import SwiftUI + +struct BookmarksView: View { + @Environment(\.verticalSizeClass) var verticalSizeClass + + @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager + + @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false + + let backgroundContext = PersistenceController.shared.backgroundContext + + var bookmarks: FetchedResults + + @State private var viewTask: Task? + + var body: some View { + ZStack { + if !bookmarks.isEmpty { + List { + ForEach(bookmarks, id: \.self) { bookmark in + SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark) + } + .onDelete { offsets in + for index in offsets { + if let bookmark = bookmarks[safe: index] { + PersistenceController.shared.delete(bookmark, context: backgroundContext) + + NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) + } + } + } + .onMove { (source, destination) in + var changedBookmarks = bookmarks.map { $0 } + + changedBookmarks.move(fromOffsets: source, toOffset: destination) + + for reverseIndex in stride(from: changedBookmarks.count - 1, through: 0, by: -1) { + changedBookmarks[reverseIndex].orderNum = Int16(reverseIndex) + } + + PersistenceController.shared.save() + } + } + .listStyle(.insetGrouped) + .onAppear { + if realDebridEnabled { + viewTask = Task { + let hashes = bookmarks.compactMap { $0.magnetHash } + await debridManager.populateDebridHashes(hashes) + } + } + } + .onDisappear { + viewTask?.cancel() + } + } + } + } +} diff --git a/Ferrite/Views/LibraryViews/HistoryView.swift b/Ferrite/Views/LibraryViews/HistoryView.swift new file mode 100644 index 0000000..078ac30 --- /dev/null +++ b/Ferrite/Views/LibraryViews/HistoryView.swift @@ -0,0 +1,22 @@ +// +// HistoryView.swift +// Ferrite +// +// Created by Brian Dashore on 9/2/22. +// + +import SwiftUI + +struct HistoryView: View { + var body: some View { + ZStack { + EmptyView() + } + } +} + +struct HistoryView_Previews: PreviewProvider { + static var previews: some View { + HistoryView() + } +} diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index ae142b2..e3eafc5 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -23,7 +23,7 @@ struct MagnetChoiceView: View { var body: some View { NavView { Form { - if realDebridEnabled, debridManager.matchSearchResult(result: scrapingModel.selectedSearchResult) != .none { + if realDebridEnabled, debridManager.matchSearchResult(result: navModel.selectedSearchResult) != .none { Section(header: "Real Debrid options") { ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl) @@ -60,7 +60,7 @@ struct MagnetChoiceView: View { Section(header: "Magnet options") { ListRowButtonView("Copy magnet", systemImage: "doc.on.doc.fill") { - UIPasteboard.general.string = scrapingModel.selectedSearchResult?.magnetLink + UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink showMagnetCopyAlert.toggle() } .alert(isPresented: $showMagnetCopyAlert) { @@ -72,7 +72,7 @@ struct MagnetChoiceView: View { } ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") { - if let result = scrapingModel.selectedSearchResult, + if let result = navModel.selectedSearchResult, let magnetLink = result.magnetLink, let url = URL(string: magnetLink) { @@ -82,9 +82,7 @@ struct MagnetChoiceView: View { } ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { - if let result = scrapingModel.selectedSearchResult { - navModel.runMagnetAction(action: .webtor, searchResult: result) - } + navModel.runMagnetAction(.webtor) } } } diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 693795c..0580c28 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -28,6 +28,12 @@ struct MainView: View { } .tag(ViewTab.search) + LibraryView() + .tabItem { + Label("Library", systemImage: "book.closed") + } + .tag(ViewTab.library) + SourcesView() .tabItem { Label("Sources", systemImage: "doc.text") diff --git a/Ferrite/Views/SearchResultButtonView.swift b/Ferrite/Views/SearchResultButtonView.swift new file mode 100644 index 0000000..06a90b1 --- /dev/null +++ b/Ferrite/Views/SearchResultButtonView.swift @@ -0,0 +1,109 @@ +// +// SearchResultButtonView.swift +// Ferrite +// +// Created by Brian Dashore on 9/2/22. +// + +import SwiftUI + +// BUG: iOS 15 cannot refresh the context menu. Debating using swipe actions or adopting a workaround. +struct SearchResultButtonView: View { + let backgroundContext = PersistenceController.shared.backgroundContext + + @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager + + var result: SearchResult + + @State private var runOnce = false + @State var existingBookmark: Bookmark? = nil + + var body: some View { + VStack(alignment: .leading) { + Button { + if debridManager.currentDebridTask == nil { + navModel.selectedSearchResult = result + + switch debridManager.matchSearchResult(result: result) { + case .full: + debridManager.currentDebridTask = Task { + await debridManager.fetchRdDownload(searchResult: result) + + if !debridManager.realDebridDownloadUrl.isEmpty { + navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl) + } + } + case .partial: + if debridManager.setSelectedRdResult(result: result) { + navModel.currentChoiceSheet = .batch + } + case .none: + navModel.runMagnetAction() + } + } + } label: { + Text(result.title ?? "No title") + .font(.callout) + .fixedSize(horizontal: false, vertical: true) + } + .dynamicAccentColor(.primary) + .padding(.bottom, 5) + .conditionalContextMenu(id: existingBookmark) { + if let bookmark = existingBookmark { + Button { + PersistenceController.shared.delete(bookmark, context: backgroundContext) + + // When the entity is deleted, let other instances know to remove that reference + NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) + } label: { + Text("Remove bookmark") + Image(systemName: "bookmark.slash.fill") + } + } else { + Button { + let newBookmark = Bookmark(context: backgroundContext) + newBookmark.title = result.title + newBookmark.source = result.source + newBookmark.magnetHash = result.magnetHash + newBookmark.magnetLink = result.magnetLink + newBookmark.seeders = result.seeders + newBookmark.leechers = result.leechers + + existingBookmark = newBookmark + + PersistenceController.shared.save(backgroundContext) + } label: { + Text("Bookmark") + Image(systemName: "bookmark") + } + } + } + + SearchResultRDView(result: result) + } + .onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in + existingBookmark = nil + } + .onAppear { + // Only run a exists request if a bookmark isn't passed to the view + if existingBookmark == nil && !runOnce { + let bookmarkRequest = Bookmark.fetchRequest() + bookmarkRequest.predicate = NSPredicate( + format: "title == %@ AND source == %@ AND magnetLink == %@ AND magnetHash = %@", + result.title ?? "", + result.source, + result.magnetLink ?? "", + result.magnetHash ?? "" + ) + bookmarkRequest.fetchLimit = 1 + + if let fetchedBookmark = try? backgroundContext.fetch(bookmarkRequest).first { + existingBookmark = fetchedBookmark + } + + runOnce = true + } + } + } +} diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift index 9cf050d..2c04731 100644 --- a/Ferrite/Views/SearchResultsView.swift +++ b/Ferrite/Views/SearchResultsView.swift @@ -9,7 +9,6 @@ import SwiftUI struct SearchResultsView: View { @EnvironmentObject var scrapingModel: ScrapingViewModel - @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false @@ -18,38 +17,7 @@ struct SearchResultsView: View { List { ForEach(scrapingModel.searchResults, id: \.self) { result in if result.source == scrapingModel.filteredSource?.name || scrapingModel.filteredSource == nil { - VStack(alignment: .leading) { - Button { - if debridManager.currentDebridTask == nil { - scrapingModel.selectedSearchResult = result - - switch debridManager.matchSearchResult(result: result) { - case .full: - debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: result) - - if !debridManager.realDebridDownloadUrl.isEmpty { - navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl) - } - } - case .partial: - if debridManager.setSelectedRdResult(result: result) { - navModel.currentChoiceSheet = .batch - } - case .none: - navModel.runMagnetAction(action: nil, searchResult: result) - } - } - } label: { - Text(result.title ?? "No title") - .font(.callout) - .fixedSize(horizontal: false, vertical: true) - } - .dynamicAccentColor(.primary) - .padding(.bottom, 5) - - SearchResultRDView(result: result) - } + SearchResultButtonView(result: result) } } } diff --git a/Ferrite/Views/SourceViews/SourceSettingsView.swift b/Ferrite/Views/SourceViews/SourceSettingsView.swift index 6a0daa8..18d8e53 100644 --- a/Ferrite/Views/SourceViews/SourceSettingsView.swift +++ b/Ferrite/Views/SourceViews/SourceSettingsView.swift @@ -147,7 +147,7 @@ struct SourceSettingsMethodView: View { var body: some View { Section(header: InlineHeader("Fetch method")) { - if selectedSource.api != nil, selectedSource.jsonParser != nil { + if selectedSource.jsonParser != nil { Button { selectedSource.preferredParser = SourcePreferredParser.siteApi.rawValue } label: { -- 2.45.2 From 4d3a16f77eca1fe53de145abfc820f59cf3a63a8 Mon Sep 17 00:00:00 2001 From: kingbri Date: Tue, 6 Sep 2022 00:05:34 -0400 Subject: [PATCH 14/22] Library: Add history functionality Action history is logged and displayed to the user's library. These are triggered whenever the magnet choice sheet is displayed. Also redo alerts and action sheets to avoid deprecation notices for >iOS 14. These will be removed when iOS 14 support is dropped. There was also a problem with sheet presentation not working after a sheet was dismissed. Disable the appropriate view when a sheet is being presented. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 48 +++++++- .../Classes/History+CoreDataClass.swift | 15 +++ .../Classes/History+CoreDataProperties.swift | 51 ++++++++ .../FerriteDB.xcdatamodel/contents | 6 +- .../PersistenceController.swift | 75 ++++++++++++ Ferrite/Extensions/DateFormatter.swift | 17 +++ Ferrite/Extensions/View.swift | 26 ++++ Ferrite/ViewModels/NavigationViewModel.swift | 57 +++++++-- Ferrite/Views/BatchChoiceView.swift | 16 ++- Ferrite/Views/CommonViews/AlertButton.swift | 69 +++++++++++ .../CommonViews/DisableInteraction.swift | 25 ++++ .../CommonViews/DisabledAppearance.swift | 21 ++++ .../CommonViews/DynamicActionSheet.swift | 44 +++++++ Ferrite/Views/CommonViews/DynamicAlert.swift | 56 +++++++++ Ferrite/Views/ContentView.swift | 36 +++--- Ferrite/Views/LibraryView.swift | 19 ++- .../LibraryViews/HistoryActionsView.swift | 54 +++++++++ .../LibraryViews/HistoryButtonView.swift | 76 ++++++++++++ Ferrite/Views/LibraryViews/HistoryView.swift | 59 ++++++++-- Ferrite/Views/MagnetChoiceView.swift | 39 +++--- Ferrite/Views/MainView.swift | 18 +-- Ferrite/Views/SearchProgressView.swift | 20 ---- Ferrite/Views/SearchResultButtonView.swift | 111 ++++++++++-------- Ferrite/Views/SearchResultRDView.swift | 24 ++-- .../SettingsViews/SourceListEditorView.swift | 13 +- 25 files changed, 823 insertions(+), 172 deletions(-) create mode 100644 Ferrite/DataManagement/Classes/History+CoreDataClass.swift create mode 100644 Ferrite/DataManagement/Classes/History+CoreDataProperties.swift create mode 100644 Ferrite/Extensions/DateFormatter.swift create mode 100644 Ferrite/Views/CommonViews/AlertButton.swift create mode 100644 Ferrite/Views/CommonViews/DisableInteraction.swift create mode 100644 Ferrite/Views/CommonViews/DisabledAppearance.swift create mode 100644 Ferrite/Views/CommonViews/DynamicActionSheet.swift create mode 100644 Ferrite/Views/CommonViews/DynamicAlert.swift create mode 100644 Ferrite/Views/LibraryViews/HistoryActionsView.swift create mode 100644 Ferrite/Views/LibraryViews/HistoryButtonView.swift delete mode 100644 Ferrite/Views/SearchProgressView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 497ae48..051a2e8 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -10,19 +10,24 @@ 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; + 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */; }; 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; + 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 */; }; 0C4CFC462897030D00AD9FAD /* Regex in Frameworks */ = {isa = PBXBuildFile; productRef = 0C4CFC452897030D00AD9FAD /* Regex */; }; 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4728970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift */; }; 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C4CFC4828970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift */; }; + 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */; }; + 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */; }; 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */; }; - 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */; }; + 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 */; }; @@ -80,14 +85,18 @@ 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */; }; 0CA3B23D28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */; }; 0CA3FB2028B91D9500FA10A8 /* IndeterminateProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */; }; + 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA429F728C5098D000D0610 /* DateFormatter.swift */; }; 0CAF1C7B286F5C8600296F86 /* SwiftSoup in Frameworks */ = {isa = PBXBuildFile; productRef = 0CAF1C7A286F5C8600296F86 /* SwiftSoup */; }; 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516228C5A57300DCA721 /* ConditionalId.swift */; }; 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516428C5A5D700DCA721 /* InlinedList.swift */; }; 0CB6516828C5A5EC00DCA721 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 0CB6516728C5A5EC00DCA721 /* Introspect */; }; 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */; }; + 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */; }; 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */; }; 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */; }; 0CBC7705288DE7F40054BE44 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */; }; + 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 */; }; 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; }; /* End PBXBuildFile section */ @@ -96,18 +105,23 @@ 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; + 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryButtonView.swift; sourceTree = ""; }; 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = ""; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.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 = ""; }; 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 = ""; }; - 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchProgressView.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 = ""; }; @@ -161,14 +175,18 @@ 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataClass.swift"; sourceTree = ""; }; 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bookmark+CoreDataProperties.swift"; sourceTree = ""; }; 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IndeterminateProgressView.swift; sourceTree = ""; }; + 0CA429F728C5098D000D0610 /* DateFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DateFormatter.swift; sourceTree = ""; }; 0CAF1C68286F5C0E00296F86 /* Ferrite.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Ferrite.app; sourceTree = BUILT_PRODUCTS_DIR; }; 0CB6516228C5A57300DCA721 /* ConditionalId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalId.swift; sourceTree = ""; }; 0CB6516428C5A5D700DCA721 /* InlinedList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlinedList.swift; sourceTree = ""; }; 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineHeader.swift; sourceTree = ""; }; + 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisableInteraction.swift; sourceTree = ""; }; 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchChoiceView.swift; sourceTree = ""; }; 0CBC76FE288DAAD00054BE44 /* NavigationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationViewModel.swift; sourceTree = ""; }; 0CBC7704288DE7F40054BE44 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = ""; }; 0CC6E4D428A45BA000AF2BCC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + 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 = ""; }; 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -194,6 +212,8 @@ 0C0D50DE288DF72D0035ECC8 /* Classes */ = { isa = PBXGroup; children = ( + 0C54D36128C5086E00BFEEE2 /* History+CoreDataClass.swift */, + 0C54D36228C5086E00BFEEE2 /* History+CoreDataProperties.swift */, 0CA3B23A28C2AA5600616D3A /* Bookmark+CoreDataClass.swift */, 0CA3B23B28C2AA5600616D3A /* Bookmark+CoreDataProperties.swift */, 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */, @@ -280,6 +300,11 @@ 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */, + 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */, + 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */, + 0C626A9428CADB25003C7129 /* DynamicAlert.swift */, + 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, + 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, ); path = CommonViews; sourceTree = ""; @@ -302,6 +327,7 @@ 0C7D11FD28AA03FE00ED92DB /* View.swift */, 0C78041C28BFB3EA001E8CA3 /* String.swift */, 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */, + 0CA429F728C5098D000D0610 /* DateFormatter.swift */, ); path = Extensions; sourceTree = ""; @@ -315,6 +341,7 @@ 0CA148C0288903F000DE2211 /* CommonViews */, 0CA0545C288F7CB200850554 /* SettingsViews */, 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, + 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */, 0CA148D4288903F000DE2211 /* ContentView.swift */, 0CA148D1288903F000DE2211 /* MainView.swift */, @@ -324,9 +351,7 @@ 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, 0C32FB522890D19D002BD219 /* AboutView.swift */, - 0C60B1EE28A1A00000E3FD7E /* SearchProgressView.swift */, 0CA3B23328C2658700616D3A /* LibraryView.swift */, - 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, ); path = Views; sourceTree = ""; @@ -365,6 +390,8 @@ children = ( 0CA3B23828C2660D00616D3A /* BookmarksView.swift */, 0CA3B23628C2660700616D3A /* HistoryView.swift */, + 0CD4CAC528C980EB0046E1DC /* HistoryActionsView.swift */, + 0C12D43C28CC332A000195BF /* HistoryButtonView.swift */, ); path = LibraryViews; sourceTree = ""; @@ -486,11 +513,11 @@ files = ( 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */, 0CB6516528C5A5D700DCA721 /* InlinedList.swift in Sources */, - 0C60B1EF28A1A00000E3FD7E /* SearchProgressView.swift in Sources */, 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */, 0CB6516328C5A57300DCA721 /* ConditionalId.swift in Sources */, 0C70E40628C40C4E00A5C72D /* NotificationCenter.swift in Sources */, 0C84F4832895BFED0074B7C9 /* Source+CoreDataProperties.swift in Sources */, + 0CD5E78928CD932B001BF684 /* DisabledAppearance.swift in Sources */, 0CA148DB288903F000DE2211 /* NavView.swift in Sources */, 0CA3B23C28C2AA5600616D3A /* Bookmark+CoreDataClass.swift in Sources */, 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */, @@ -499,8 +526,10 @@ 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */, 0CA0545B288EEA4E00850554 /* SourceListEditorView.swift in Sources */, 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, + 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */, + 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */, @@ -515,6 +544,7 @@ 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, + 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, 0CB6516A28C5B4A600DCA721 /* InlineHeader.swift in Sources */, 0CA148D8288903F000DE2211 /* MagnetChoiceView.swift in Sources */, @@ -523,9 +553,11 @@ 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 */, 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */, + 0C391EC928CA63F0009F1CA1 /* DynamicActionSheet.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */, @@ -539,6 +571,7 @@ 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, + 0C12D43D28CC332A000195BF /* HistoryButtonView.swift in Sources */, 0C733287289C4C820058D1FE /* SourceSettingsView.swift in Sources */, 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */, 0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */, @@ -550,9 +583,12 @@ 0CA148E8288903F000DE2211 /* RealDebridWrapper.swift in Sources */, 0CA148D6288903F000DE2211 /* SettingsView.swift in Sources */, 0CA148E5288903F000DE2211 /* DebridManager.swift in Sources */, + 0C54D36328C5086E00BFEEE2 /* History+CoreDataClass.swift in Sources */, + 0CBAB83628D12ED500AC903E /* DisableInteraction.swift in Sources */, 0C84F4842895BFED0074B7C9 /* SourceHtmlParser+CoreDataClass.swift in Sources */, 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */, 0C84F4822895BFED0074B7C9 /* Source+CoreDataClass.swift in Sources */, + 0C391ECB28CAA44B009F1CA1 /* AlertButton.swift in Sources */, 0C84F4852895BFED0074B7C9 /* SourceHtmlParser+CoreDataProperties.swift in Sources */, 0CA148EB288903F000DE2211 /* SearchResultsView.swift in Sources */, 0CA148D7288903F000DE2211 /* LoginWebView.swift in Sources */, @@ -705,6 +741,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -738,6 +775,7 @@ PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_STRICT_CONCURRENCY = minimal; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/Ferrite/DataManagement/Classes/History+CoreDataClass.swift b/Ferrite/DataManagement/Classes/History+CoreDataClass.swift new file mode 100644 index 0000000..b7108b3 --- /dev/null +++ b/Ferrite/DataManagement/Classes/History+CoreDataClass.swift @@ -0,0 +1,15 @@ +// +// History+CoreDataClass.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// + +import Foundation +import CoreData + +@objc(History) +public class History: NSManagedObject { + +} diff --git a/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift new file mode 100644 index 0000000..7e384ab --- /dev/null +++ b/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift @@ -0,0 +1,51 @@ +// +// History+CoreDataProperties.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// +// + +import Foundation +import CoreData + + +extension History { + + @nonobjc public class func fetchRequest() -> NSFetchRequest { + return NSFetchRequest(entityName: "History") + } + + @NSManaged public var date: Date? + @NSManaged public var dateString: String? + @NSManaged public var entries: NSSet? + + var entryArray: [HistoryEntry] { + let entrySet = entries as? Set ?? [] + + return entrySet.sorted { + $0.timeStamp > $1.timeStamp + } + } +} + +// MARK: Generated accessors for entries +extension History { + + @objc(addEntriesObject:) + @NSManaged public func addToEntries(_ value: HistoryEntry) + + @objc(removeEntriesObject:) + @NSManaged public func removeFromEntries(_ value: HistoryEntry) + + @objc(addEntries:) + @NSManaged public func addToEntries(_ values: NSSet) + + @objc(removeEntries:) + @NSManaged public func removeFromEntries(_ values: NSSet) + +} + +extension History : Identifiable { + +} diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents index f2d6e3f..5407971 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents @@ -1,5 +1,5 @@ - + @@ -10,13 +10,15 @@ - + + + diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index de19701..1fecdc2 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -7,6 +7,18 @@ import CoreData +enum HistoryDeleteRange { + case day + case week + case month + case allTime +} + +enum HistoryDeleteError: Error { + case noDate(String) + case unknown(String) +} + // No iCloud until finalized sources struct PersistenceController { static var shared = PersistenceController() @@ -78,4 +90,67 @@ struct PersistenceController { container.viewContext.delete(object) save() } + + + func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { + if range == .allTime { + return nil + } + + var components = Calendar.current.dateComponents([.day, .month, .year], from: Date()) + components.hour = 0 + components.minute = 0 + components.second = 0 + + guard let today = Calendar.current.date(from: components) else { + return nil + } + + var offsetComponents = DateComponents(day: 1) + guard let tomorrow = Calendar.current.date(byAdding: offsetComponents, to: today) else { + return nil + } + + switch range { + case .week: + offsetComponents.day = -7 + case .month: + offsetComponents.day = -28 + default: + break + } + + guard var offsetDate = Calendar.current.date(byAdding: offsetComponents, to: today) else { + return nil + } + + if TimeZone.current.isDaylightSavingTime(for: offsetDate) { + offsetDate = offsetDate.addingTimeInterval(3600) + } + + let predicate = NSPredicate(format: "date >= %@ && date < %@", range == .day ? today as NSDate : offsetDate as NSDate, tomorrow as NSDate) + + return predicate + } + + // Always use the background context to batch delete + // Merge changes into both contexts to update views + func batchDeleteHistory(range: HistoryDeleteRange) throws { + let predicate = getHistoryPredicate(range: range) + + let fetchRequest = NSFetchRequest(entityName: "History") + + if let predicate = predicate { + fetchRequest.predicate = predicate + } else if range != .allTime { + throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?") + } + + let batchDeleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest) + + batchDeleteRequest.resultType = .resultTypeObjectIDs + let result = try backgroundContext.execute(batchDeleteRequest) as? NSBatchDeleteResult + let changes: [AnyHashable: Any] = [NSDeletedObjectsKey: result?.result as? [NSManagedObjectID] ?? []] + NSManagedObjectContext.mergeChanges(fromRemoteContextSave: changes, into: [container.viewContext, backgroundContext]) + } } diff --git a/Ferrite/Extensions/DateFormatter.swift b/Ferrite/Extensions/DateFormatter.swift new file mode 100644 index 0000000..4177912 --- /dev/null +++ b/Ferrite/Extensions/DateFormatter.swift @@ -0,0 +1,17 @@ +// +// DateFormatter.swift +// Ferrite +// +// Created by Brian Dashore on 9/4/22. +// + +import Foundation + +extension DateFormatter { + static let historyDateFormatter: DateFormatter = { + let df = DateFormatter() + df.dateFormat = "ddMMyyyy" + + return df + }() +} diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index e7debe2..2226805 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -33,6 +33,10 @@ extension View { modifier(ConditionalId(id: id)) } + func disabledAppearance(_ disabled: Bool, dimmedOpacity: Double? = nil, animation: Animation? = nil) -> some View { + modifier(DisabledAppearance(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation)) + } + func inlinedList() -> some View { modifier(InlinedList()) } @@ -43,4 +47,26 @@ extension View { ) -> some View { modifier(ConditionalContextMenu(internalContent, id: id)) } + + 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 disableInteraction(_ disabled: Bool) -> some View { + modifier(DisableInteraction(disabled: disabled)) + } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 6dba198..4fa24bf 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -55,7 +55,7 @@ class NavigationViewModel: ObservableObject { @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none - public func runDebridAction(action: DefaultDebridActionType?, urlString: String) { + public func runDebridAction(urlString: String, _ action: DefaultDebridActionType? = nil) { let selectedAction = action ?? defaultDebridAction switch selectedAction { @@ -89,17 +89,10 @@ class NavigationViewModel: ObservableObject { } } - public func runMagnetAction(_ action: DefaultMagnetActionType? = nil) { - guard let searchResult = selectedSearchResult else { - toastModel?.updateToastDescription("Magnet action error: A search result was not selected.") - print("Magnet action error: A search result was not selected.") - - return - } - + public func runMagnetAction(magnetString: String?, _ action: DefaultMagnetActionType? = nil) { let selectedAction = action ?? defaultMagnetAction - guard let magnetLink = searchResult.magnetLink else { + guard let magnetLink = magnetString else { toastModel?.updateToastDescription("Could not run your action because the magnet link is invalid.") print("Magnet action error: The magnet link is invalid.") @@ -126,4 +119,48 @@ 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.save(backgroundContext) + } } diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index 60e1c64..eb7369c 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -8,12 +8,12 @@ import SwiftUI struct BatchChoiceView: View { - @Environment(\.presentationMode) var presentationMode - @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel + let backgroundContext = PersistenceController.shared.backgroundContext + var body: some View { NavView { List { @@ -28,7 +28,8 @@ struct BatchChoiceView: View { if !debridManager.realDebridDownloadUrl.isEmpty { // The download may complete before this sheet dismisses try? await Task.sleep(seconds: 1) - navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl) + navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) } debridManager.selectedRealDebridFile = nil @@ -36,7 +37,7 @@ struct BatchChoiceView: View { } } - presentationMode.wrappedValue.dismiss() + navModel.currentChoiceSheet = nil } .dynamicAccentColor(.primary) } @@ -47,9 +48,12 @@ struct BatchChoiceView: View { .toolbar { ToolbarItem(placement: .navigationBarTrailing) { Button("Done") { - debridManager.selectedRealDebridItem = nil + navModel.currentChoiceSheet = nil - presentationMode.wrappedValue.dismiss() + Task { + try? await Task.sleep(seconds: 1) + debridManager.selectedRealDebridItem = nil + } } } } diff --git a/Ferrite/Views/CommonViews/AlertButton.swift b/Ferrite/Views/CommonViews/AlertButton.swift new file mode 100644 index 0000000..8d6922e --- /dev/null +++ b/Ferrite/Views/CommonViews/AlertButton.swift @@ -0,0 +1,69 @@ +// +// AlertButton.swift +// Ferrite +// +// Created by Brian Dashore on 9/8/22. +// + +import SwiftUI + +struct AlertButton: Identifiable { + enum Role { + case destructive + case cancel + } + + let id: UUID + let label: String + let action: () -> Void + let role: Role? + + // Used for all buttons + init(_ label: String, role: Role? = nil, action: @escaping () -> Void) { + self.id = UUID() + self.label = label + self.action = action + self.role = role + } + + // Used for buttons with no action + init(_ label: String = "Cancel", role: Role? = nil) { + self.id = UUID() + self.label = label + self.action = { } + self.role = role + } + + func toActionButton() -> Alert.Button { + if let role = role { + switch role { + case .cancel: + return .cancel(Text(label)) + case .destructive: + return .destructive(Text(label), action: action) + } + } else { + return .default(Text(label), action: action) + } + } + + @available(iOS 15.0, *) + @ViewBuilder + func toButtonView() -> some View { + Button(label, role: toButtonRole(role), action: action) + } + + @available(iOS 15.0, *) + func toButtonRole(_ role: Role?) -> ButtonRole? { + if let role = role { + switch role { + case .destructive: + return .destructive + case .cancel: + return .cancel + } + } else { + return nil + } + } +} diff --git a/Ferrite/Views/CommonViews/DisableInteraction.swift b/Ferrite/Views/CommonViews/DisableInteraction.swift new file mode 100644 index 0000000..a8073d8 --- /dev/null +++ b/Ferrite/Views/CommonViews/DisableInteraction.swift @@ -0,0 +1,25 @@ +// +// DisableInteraction.swift +// Ferrite +// +// Created by Brian Dashore on 9/13/22. +// +// Disables interaction without applying the appearance +// + +import SwiftUI + +struct DisableInteraction: ViewModifier { + let disabled: Bool + + func body(content: Content) -> some View { + content + .overlay { + if disabled { + Color.clear + .contentShape(Rectangle()) + .gesture(TapGesture()) + } + } + } +} diff --git a/Ferrite/Views/CommonViews/DisabledAppearance.swift b/Ferrite/Views/CommonViews/DisabledAppearance.swift new file mode 100644 index 0000000..132d23e --- /dev/null +++ b/Ferrite/Views/CommonViews/DisabledAppearance.swift @@ -0,0 +1,21 @@ +// +// DisabledAppearance.swift +// Ferrite +// +// Created by Brian Dashore on 9/10/22. +// + +import SwiftUI + +struct DisabledAppearance: ViewModifier { + let disabled: Bool + let dimmedOpacity: Double? + let animation: Animation? + + func body(content: Content) -> some View { + content + .disabled(disabled) + .opacity(disabled ? dimmedOpacity.map { $0 } ?? 0.5 : 1) + .animation(animation.map { $0 } ?? .none, value: disabled) + } +} diff --git a/Ferrite/Views/CommonViews/DynamicActionSheet.swift b/Ferrite/Views/CommonViews/DynamicActionSheet.swift new file mode 100644 index 0000000..9e2a3c5 --- /dev/null +++ b/Ferrite/Views/CommonViews/DynamicActionSheet.swift @@ -0,0 +1,44 @@ +// +// DynamicActionSheet.swift +// Ferrite +// +// Created by Brian Dashore on 9/8/22. +// + +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/DynamicAlert.swift b/Ferrite/Views/CommonViews/DynamicAlert.swift new file mode 100644 index 0000000..2a15546 --- /dev/null +++ b/Ferrite/Views/CommonViews/DynamicAlert.swift @@ -0,0 +1,56 @@ +// +// DynamicAlert.swift +// Ferrite +// +// Created by Brian Dashore on 9/8/22. +// + +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 fc3bb21..a821842 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -73,25 +73,23 @@ struct ContentView: View { SearchResultsView() } .sheet(item: $navModel.currentChoiceSheet) { item in - Group { - switch item { - case .magnet: - MagnetChoiceView() - .environmentObject(debridManager) - .environmentObject(scrapingModel) - .environmentObject(navModel) - case .batch: - BatchChoiceView() - .environmentObject(debridManager) - .environmentObject(scrapingModel) - .environmentObject(navModel) - case .activity: - if #available(iOS 16, *) { - AppActivityView(activityItems: navModel.activityItems) - .presentationDetents([.medium, .large]) - } else { - AppActivityView(activityItems: navModel.activityItems) - } + 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) } } } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 61caa19..90d560e 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -22,6 +22,13 @@ struct LibraryView: View { ] ) var bookmarks: FetchedResults + @FetchRequest( + entity: History.entity(), + sortDescriptors: [ + NSSortDescriptor(keyPath: \History.date, ascending: false) + ] + ) var history: FetchedResults + @State private var historyEmpty = true @State private var selectedSegment: LibraryPickerSegment = .bookmarks @@ -42,7 +49,7 @@ struct LibraryView: View { case .bookmarks: BookmarksView(bookmarks: bookmarks) case .history: - HistoryView() + HistoryView(history: history) } Spacer() @@ -54,7 +61,7 @@ struct LibraryView: View { EmptyInstructionView(title: "No Bookmarks", message: "Add a bookmark from search results") } case .history: - if historyEmpty { + if history.isEmpty { EmptyInstructionView(title: "No History", message: "Start watching to build history") } } @@ -62,7 +69,13 @@ struct LibraryView: View { .navigationTitle("Library") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - EditButton() + HStack { + EditButton() + + if selectedSegment == .history { + HistoryActionsView() + } + } } } .environment(\.editMode, $editMode) diff --git a/Ferrite/Views/LibraryViews/HistoryActionsView.swift b/Ferrite/Views/LibraryViews/HistoryActionsView.swift new file mode 100644 index 0000000..5b3e6a1 --- /dev/null +++ b/Ferrite/Views/LibraryViews/HistoryActionsView.swift @@ -0,0 +1,54 @@ +// +// HistoryActionsView.swift +// Ferrite +// +// Created by Brian Dashore on 9/7/22. +// + +import SwiftUI + +struct HistoryActionsView: View { + @EnvironmentObject var toastModel: ToastViewModel + + @State private var showActionSheet = false + + var body: some View { + Button("Clear") { + showActionSheet.toggle() + } + .dynamicAccentColor(.red) + .dynamicActionSheet( + isPresented: $showActionSheet, + title: "Clear watch history", + message: "This is an irreversible action!", + buttons: [ + AlertButton("Past day", role: .destructive) { + deleteHistory(.day) + }, + AlertButton("Past week", role: .destructive) { + deleteHistory(.week) + }, + AlertButton("Past month", role: .destructive) { + deleteHistory(.month) + }, + AlertButton("All time", role: .destructive) { + deleteHistory(.allTime) + } + ] + ) + } + + func deleteHistory(_ deleteRange: HistoryDeleteRange) { + do { + try PersistenceController.shared.batchDeleteHistory(range: deleteRange) + } catch { + toastModel.updateToastDescription("History delete error: \(error)") + } + } +} + +struct HistoryActionsView_Previews: PreviewProvider { + static var previews: some View { + HistoryActionsView() + } +} diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/LibraryViews/HistoryButtonView.swift new file mode 100644 index 0000000..cf607c3 --- /dev/null +++ b/Ferrite/Views/LibraryViews/HistoryButtonView.swift @@ -0,0 +1,76 @@ +// +// HistoryButtonView.swift +// Ferrite +// +// Created by Brian Dashore on 9/9/22. +// + +import SwiftUI + +struct HistoryButtonView: View { + @EnvironmentObject var toastModel: ToastViewModel + @EnvironmentObject var navModel: NavigationViewModel + @EnvironmentObject var debridManager: DebridManager + + let entry: HistoryEntry + + var body: some View { + Button { + if let url = entry.url { + if url.starts(with: "https://") { + Task { + debridManager.realDebridDownloadUrl = url + navModel.runDebridAction(urlString: url) + + if navModel.currentChoiceSheet != .magnet { + debridManager.realDebridDownloadUrl = "" + } + } + } else { + navModel.runMagnetAction(magnetString: url) + } + } else { + toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.") + } + } label: { + VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 3) { + Text(entry.name ?? "Unknown title") + .font(entry.subName == nil ? .body : .subheadline) + + if let subName = entry.subName { + Text(subName) + .foregroundColor(.gray) + .font(.subheadline) + } + } + + HStack { + Text(entry.source ?? "Unknown source") + + Spacer() + + Text("DEBRID") + .fontWeight(.bold) + .padding(3) + .background { + Group { + if let url = entry.url, url.starts(with: "https://") { + Color.green + } else { + Color.red + } + } + .cornerRadius(4) + .opacity(0.5) + } + } + .font(.caption) + } + .lineLimit(1) + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + } + .dynamicAccentColor(.white) + .disableInteraction(navModel.currentChoiceSheet != nil) + } +} diff --git a/Ferrite/Views/LibraryViews/HistoryView.swift b/Ferrite/Views/LibraryViews/HistoryView.swift index 078ac30..e53ffb5 100644 --- a/Ferrite/Views/LibraryViews/HistoryView.swift +++ b/Ferrite/Views/LibraryViews/HistoryView.swift @@ -8,15 +8,58 @@ import SwiftUI struct HistoryView: View { + @EnvironmentObject var navModel: NavigationViewModel + + let backgroundContext = PersistenceController.shared.backgroundContext + + var history: FetchedResults + var formatter: DateFormatter = .init() + + @State private var historyIndex = 0 + + init(history: FetchedResults) { + self.history = history + + formatter.dateStyle = .medium + formatter.timeStyle = .none + } + + func groupedEntries(_ result: FetchedResults) -> [[History]] { + Dictionary(grouping: result) { (element: History) in + element.dateString ?? "" + }.values.sorted { $0[0].date ?? Date() > $1[0].date ?? Date() } + } + var body: some View { - ZStack { - EmptyView() + if !history.isEmpty { + List { + ForEach(groupedEntries(history), id: \.self) { (section: [History]) in + Section(header: Text(formatter.string(from: section[0].date ?? Date()))) { + ForEach(section, id: \.self) { history in + ForEach(history.entryArray) { entry in + HistoryButtonView(entry: entry) + } + .onDelete { offsets in + removeEntry(at: offsets, from: history) + } + } + } + } + } + .listStyle(.insetGrouped) + } + } + + func removeEntry(at offsets: IndexSet, from history: History) { + for index in offsets { + if let entry = history.entryArray[safe: index] { + history.removeFromEntries(entry) + PersistenceController.shared.delete(entry, context: backgroundContext) + } + + if history.entryArray.isEmpty { + PersistenceController.shared.delete(history, context: backgroundContext) + } } } } - -struct HistoryView_Previews: PreviewProvider { - static var previews: some View { - HistoryView() - } -} diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index e3eafc5..4ab9ab2 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -23,31 +23,30 @@ struct MagnetChoiceView: View { var body: some View { NavView { Form { - if realDebridEnabled, debridManager.matchSearchResult(result: navModel.selectedSearchResult) != .none { + if !debridManager.realDebridDownloadUrl.isEmpty { Section(header: "Real Debrid options") { ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(action: .outplayer, urlString: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .outplayer) } ListRowButtonView("Play on VLC", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(action: .vlc, urlString: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .vlc) } ListRowButtonView("Play on Infuse", systemImage: "arrow.up.forward.app.fill") { - navModel.runDebridAction(action: .infuse, urlString: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl, .infuse) } ListRowButtonView("Copy download URL", systemImage: "doc.on.doc.fill") { UIPasteboard.general.string = debridManager.realDebridDownloadUrl showLinkCopyAlert.toggle() } - .alert(isPresented: $showLinkCopyAlert) { - Alert( - title: Text("Copied"), - message: Text("Download link copied successfully"), - dismissButton: .cancel(Text("OK")) - ) - } + .dynamicAlert( + isPresented: $showLinkCopyAlert , + title: "Copied", + message: "Download link copied successfully", + buttons: [AlertButton("OK")] + ) ListRowButtonView("Share download URL", systemImage: "square.and.arrow.up.fill") { if let url = URL(string: debridManager.realDebridDownloadUrl) { @@ -63,13 +62,12 @@ struct MagnetChoiceView: View { UIPasteboard.general.string = navModel.selectedSearchResult?.magnetLink showMagnetCopyAlert.toggle() } - .alert(isPresented: $showMagnetCopyAlert) { - Alert( - title: Text("Copied"), - message: Text("Magnet link copied successfully"), - dismissButton: .cancel(Text("OK")) - ) - } + .dynamicAlert( + isPresented: $showMagnetCopyAlert, + title: "Copied", + message: "Magnet link copied successfully", + buttons: [AlertButton("OK")] + ) ListRowButtonView("Share magnet", systemImage: "square.and.arrow.up.fill") { if let result = navModel.selectedSearchResult, @@ -82,7 +80,7 @@ struct MagnetChoiceView: View { } ListRowButtonView("Open in WebTor", systemImage: "arrow.up.forward.app.fill") { - navModel.runMagnetAction(.webtor) + navModel.runMagnetAction(magnetString: navModel.selectedSearchResult?.magnetLink, .webtor) } } } @@ -95,6 +93,9 @@ struct MagnetChoiceView: View { AppActivityView(activityItems: navModel.activityItems) } } + .onDisappear { + debridManager.realDebridDownloadUrl = "" + } .navigationTitle("Link actions") .navigationBarTitleDisplayMode(.inline) .toolbar { diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 0580c28..3e20efc 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -46,20 +46,21 @@ struct MainView: View { } .tag(ViewTab.settings) } - .alert(isPresented: $showUpdateAlert) { - Alert( - title: Text("Update available"), - message: Text("Ferrite \(releaseVersionString) can be downloaded. \n\n This alert can be disabled in Settings."), - primaryButton: .default(Text("Download")) { + .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) }, - secondaryButton: .cancel() - ) - } + AlertButton(role: .cancel) + ] + ) .onAppear { if autoUpdateNotifs { viewTask = Task { @@ -71,7 +72,6 @@ struct MainView: View { let releaseVersion = String(latestRelease.tagName.dropFirst()) if releaseVersion > UIApplication.shared.appVersion { - print("Greater") releaseVersionString = latestRelease.tagName releaseUrlString = latestRelease.htmlUrl showUpdateAlert.toggle() diff --git a/Ferrite/Views/SearchProgressView.swift b/Ferrite/Views/SearchProgressView.swift deleted file mode 100644 index 0923ccb..0000000 --- a/Ferrite/Views/SearchProgressView.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// SearchProgressView.swift -// Ferrite -// -// Created by Brian Dashore on 8/8/22. -// - -import SwiftUI - -struct SearchProgressView: View { - var body: some View { - Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) - } -} - -struct SearchProgressView_Previews: PreviewProvider { - static var previews: some View { - SearchProgressView() - } -} diff --git a/Ferrite/Views/SearchResultButtonView.swift b/Ferrite/Views/SearchResultButtonView.swift index 06a90b1..b952c2a 100644 --- a/Ferrite/Views/SearchResultButtonView.swift +++ b/Ferrite/Views/SearchResultButtonView.swift @@ -14,73 +14,82 @@ struct SearchResultButtonView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false + var result: SearchResult @State private var runOnce = false @State var existingBookmark: Bookmark? = nil var body: some View { - VStack(alignment: .leading) { - Button { - if debridManager.currentDebridTask == nil { - navModel.selectedSearchResult = result - - switch debridManager.matchSearchResult(result: result) { - case .full: - debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: result) - - if !debridManager.realDebridDownloadUrl.isEmpty { - navModel.runDebridAction(action: nil, urlString: debridManager.realDebridDownloadUrl) + Button { + if debridManager.currentDebridTask == nil { + navModel.selectedSearchResult = result + + switch debridManager.matchSearchResult(result: result) { + case .full: + debridManager.currentDebridTask = Task { + await debridManager.fetchRdDownload(searchResult: result) + + if !debridManager.realDebridDownloadUrl.isEmpty { + navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) + + if navModel.currentChoiceSheet != .magnet { + debridManager.realDebridDownloadUrl = "" } } - case .partial: - if debridManager.setSelectedRdResult(result: result) { - navModel.currentChoiceSheet = .batch - } - case .none: - navModel.runMagnetAction() } + case .partial: + if debridManager.setSelectedRdResult(result: result) { + navModel.currentChoiceSheet = .batch + } + case .none: + navModel.addToHistory(name: result.title, source: result.source, url: result.magnetLink) + navModel.runMagnetAction(magnetString: result.magnetLink) } - } label: { + } + } label: { + VStack(alignment: .leading, spacing: 10) { Text(result.title ?? "No title") .font(.callout) .fixedSize(horizontal: false, vertical: true) - } - .dynamicAccentColor(.primary) - .padding(.bottom, 5) - .conditionalContextMenu(id: existingBookmark) { - if let bookmark = existingBookmark { - Button { - PersistenceController.shared.delete(bookmark, context: backgroundContext) - // When the entity is deleted, let other instances know to remove that reference - NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) - } label: { - Text("Remove bookmark") - Image(systemName: "bookmark.slash.fill") - } - } else { - Button { - let newBookmark = Bookmark(context: backgroundContext) - newBookmark.title = result.title - newBookmark.source = result.source - newBookmark.magnetHash = result.magnetHash - newBookmark.magnetLink = result.magnetLink - newBookmark.seeders = result.seeders - newBookmark.leechers = result.leechers - - existingBookmark = newBookmark - - PersistenceController.shared.save(backgroundContext) - } label: { - Text("Bookmark") - Image(systemName: "bookmark") - } + SearchResultRDView(result: result) + } + .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) + } + .disableInteraction(navModel.currentChoiceSheet != nil) + .dynamicAccentColor(.primary) + .conditionalContextMenu(id: existingBookmark) { + if let bookmark = existingBookmark { + Button { + PersistenceController.shared.delete(bookmark, context: backgroundContext) + + // When the entity is deleted, let other instances know to remove that reference + NotificationCenter.default.post(name: .didDeleteBookmark, object: nil) + } label: { + Text("Remove bookmark") + Image(systemName: "bookmark.slash.fill") + } + } else { + Button { + let newBookmark = Bookmark(context: backgroundContext) + newBookmark.title = result.title + newBookmark.source = result.source + newBookmark.magnetHash = result.magnetHash + newBookmark.magnetLink = result.magnetLink + newBookmark.seeders = result.seeders + newBookmark.leechers = result.leechers + + existingBookmark = newBookmark + + PersistenceController.shared.save(backgroundContext) + } label: { + Text("Bookmark") + Image(systemName: "bookmark") } } - - SearchResultRDView(result: result) } .onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in existingBookmark = nil diff --git a/Ferrite/Views/SearchResultRDView.swift b/Ferrite/Views/SearchResultRDView.swift index dbcc913..8c51280 100644 --- a/Ferrite/Views/SearchResultRDView.swift +++ b/Ferrite/Views/SearchResultRDView.swift @@ -37,20 +37,18 @@ struct SearchResultRDView: View { .fontWeight(.bold) .padding(2) .background { - switch debridManager.matchSearchResult(result: result) { - case .full: - Color.green - .cornerRadius(4) - .opacity(0.5) - case .partial: - Color.orange - .cornerRadius(4) - .opacity(0.5) - case .none: - Color.red - .cornerRadius(4) - .opacity(0.5) + Group { + switch debridManager.matchSearchResult(result: result) { + case .full: + Color.green + case .partial: + Color.orange + case .none: + Color.red + } } + .cornerRadius(4) + .opacity(0.5) } } } diff --git a/Ferrite/Views/SettingsViews/SourceListEditorView.swift b/Ferrite/Views/SettingsViews/SourceListEditorView.swift index 5ddfae3..5c20198 100644 --- a/Ferrite/Views/SettingsViews/SourceListEditorView.swift +++ b/Ferrite/Views/SettingsViews/SourceListEditorView.swift @@ -32,13 +32,12 @@ struct SourceListEditorView: View { sourceUrl = navModel.selectedSourceList?.urlString ?? "" sourceUrlSet = true } - .alert(isPresented: $sourceManager.showUrlErrorAlert) { - Alert( - title: Text("Error"), - message: Text(sourceManager.urlErrorAlertText), - dismissButton: .default(Text("OK")) - ) - } + .dynamicAlert( + isPresented: $sourceManager.showUrlErrorAlert, + title: "Error", + message: sourceManager.urlErrorAlertText, + buttons: [AlertButton("OK")] + ) .navigationTitle("Editing source list") .navigationBarTitleDisplayMode(.inline) .toolbar { -- 2.45.2 From b85752c92c3eb1d06529e22146cfb17ed3c059d4 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 16 Sep 2022 12:29:08 -0400 Subject: [PATCH 15/22] RealDebrid: Improve fetch and recache times To declutter a RealDebrid user's library, check if the file and unrestricted link exist and serve those existing links. Otherwise perform a download like normal. Sometimes RealDebrid deletes cached items, but still keeps them on instant availability. Add a way to tell the user that the item is downloading along with an option to cancel it. Also remove unnecessary published variables from viewmodels Signed-off-by: kingbri --- Ferrite/API/RealDebridWrapper.swift | 29 +++++- Ferrite/Models/RealDebridModels.swift | 37 +++++++- Ferrite/ViewModels/DebridManager.swift | 88 +++++++++++++------ Ferrite/ViewModels/NavigationViewModel.swift | 6 +- Ferrite/ViewModels/ScrapingViewModel.swift | 2 +- Ferrite/ViewModels/SourceManager.swift | 2 +- Ferrite/Views/BatchChoiceView.swift | 2 +- .../Views/LibraryViews/BookmarksView.swift | 21 ++--- Ferrite/Views/SearchResultButtonView.swift | 31 +++++-- Ferrite/Views/SearchResultsView.swift | 2 - 10 files changed, 161 insertions(+), 59 deletions(-) diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index ffd06f3..fb60a5f 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -14,6 +14,7 @@ public enum RealDebridError: Error { case InvalidResponse case InvalidToken case EmptyData + case EmptyTorrents case FailedRequest(description: String) case AuthQuery(description: String) } @@ -306,21 +307,33 @@ public class RealDebrid { try await performRequest(request: &request, requestName: #function) } - // Fetches the info of a torrent + // Gets the info of a torrent from a given ID public func torrentInfo(debridID: String, selectedIndex: Int?) async throws -> String { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/info/\(debridID)")!) let data = try await performRequest(request: &request, requestName: #function) let rawResponse = try jsonDecoder.decode(TorrentInfoResponse.self, from: data) - // Error out if no index is provided - if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1] { + // Let the user know if a torrent is downloading + if let torrentLink = rawResponse.links[safe: selectedIndex ?? -1], rawResponse.status == "downloaded" { return torrentLink + } else if rawResponse.status == "downloading" || rawResponse.status == "queued" { + throw RealDebridError.EmptyTorrents } else { throw RealDebridError.EmptyData } } + // Gets the user's torrent library + public func userTorrents() async throws -> [UserTorrentsResponse] { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents")!) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode([UserTorrentsResponse].self, from: data) + + return rawResponse + } + // Deletes a torrent download from RD public func deleteTorrent(debridID: String) async throws { var request = URLRequest(url: URL(string: "\(baseApiUrl)/torrents/delete/\(debridID)")!) @@ -345,4 +358,14 @@ public class RealDebrid { return rawResponse.download } + + // Gets the user's downloads + public func userDownloads() async throws -> [UserDownloadsResponse] { + var request = URLRequest(url: URL(string: "\(baseApiUrl)/downloads")!) + + let data = try await performRequest(request: &request, requestName: #function) + let rawResponse = try jsonDecoder.decode([UserDownloadsResponse].self, from: data) + + return rawResponse + } } diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index b070fd9..39506f0 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -122,14 +122,16 @@ struct TorrentInfoResponse: Codable { let status, added: String let files: [TorrentInfoFile] let links: [String] - let ended: String + let ended: String? + let speed: Int? + let seeders: Int? enum CodingKeys: String, CodingKey { case id, filename case originalFilename = "original_filename" case hash, bytes case originalBytes = "original_bytes" - case host, split, progress, status, added, files, links, ended + case host, split, progress, status, added, files, links, ended, speed, seeders } } @@ -139,6 +141,17 @@ struct TorrentInfoFile: Codable { let bytes, selected: Int } +public struct UserTorrentsResponse: Codable { + let id, filename, hash: String + let bytes: Int + let host: String + let split, progress: Int + let status, added: String + let links: [String] + let speed, seeders: Int? + let ended: String? +} + // MARK: - unrestrictLink endpoint struct UnrestrictLinkResponse: Codable { @@ -157,3 +170,23 @@ struct UnrestrictLinkResponse: Codable { case chunks, crc, download, streamable } } + +// MARK: - User downloads list + +public struct UserDownloadsResponse: Codable { + let id, filename, mimeType: String + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks: Int + let download: String + let streamable: Int + let generated: String + + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, download, streamable, generated + } +} diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index a579d21..fc6457e 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -19,23 +19,28 @@ public class DebridManager: ObservableObject { @Published var showLoadingProgress: Bool = false // Service agnostic variables - @Published var currentDebridTask: Task? + var currentDebridTask: Task? // RealDebrid auth variables - @Published var realDebridEnabled: Bool = false { + var realDebridEnabled: Bool = false { didSet { UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled") } } @Published var realDebridAuthProcessing: Bool = false - @Published var realDebridAuthUrl: String = "" + var realDebridAuthUrl: String = "" // RealDebrid fetch variables @Published var realDebridIAValues: [RealDebridIA] = [] - @Published var realDebridDownloadUrl: String = "" - @Published var selectedRealDebridItem: RealDebridIA? - @Published var selectedRealDebridFile: RealDebridIAFile? + var realDebridDownloadUrl: String = "" + + @Published var showDeleteAlert: Bool = false + + // TODO: Switch to an individual item based sheet system to remove these variables + var selectedRealDebridItem: RealDebridIA? + var selectedRealDebridFile: RealDebridIAFile? + var selectedRealDebridID: String? init() { realDebridEnabled = UserDefaults.standard.bool(forKey: "RealDebrid.Enabled") @@ -138,7 +143,7 @@ public class DebridManager: ObservableObject { } } - public func fetchRdDownload(searchResult: SearchResult, iaFile: RealDebridIAFile? = nil) async { + public func fetchRdDownload(searchResult: SearchResult) async { defer { currentDebridTask = nil showLoadingProgress = false @@ -153,14 +158,10 @@ public class DebridManager: ObservableObject { return } - var realDebridId: String? - do { - realDebridId = try await realDebrid.addMagnet(magnetLink: magnetLink) - var fileIds: [Int] = [] - if let iaFile = iaFile { + if let iaFile = selectedRealDebridFile { guard let iaBatchFromFile = selectedRealDebridItem?.batches[safe: iaFile.batchIndex] else { return } @@ -168,29 +169,52 @@ public class DebridManager: ObservableObject { fileIds = iaBatchFromFile.files.map(\.id) } - if let realDebridId = realDebridId { - try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds) + // If there's an existing torrent, check for a download link. Otherwise check for an unrestrict link + let existingTorrents = try await realDebrid.userTorrents().filter { $0.hash == selectedRealDebridItem?.hash } - let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: iaFile?.batchFileIndex ?? 0) - let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) + // If the links match from a user's downloads, no need to re-run a download + if let existingTorrent = existingTorrents[safe: 0], + let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] + { + let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink } + if let existingLink = existingLinks[safe: 0]?.download { + realDebridDownloadUrl = existingLink + } else { + let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) + + realDebridDownloadUrl = downloadLink + } - realDebridDownloadUrl = downloadLink } else { - toastModel?.updateToastDescription("Could not cache this torrent. Aborting.") + // Add a magnet after all the cache checks fail + selectedRealDebridID = try await realDebrid.addMagnet(magnetLink: magnetLink) + + if let realDebridId = selectedRealDebridID { + try await realDebrid.selectFiles(debridID: realDebridId, fileIds: fileIds) + + let torrentLink = try await realDebrid.torrentInfo(debridID: realDebridId, selectedIndex: selectedRealDebridFile?.batchFileIndex ?? 0) + let downloadLink = try await realDebrid.unrestrictLink(debridDownloadLink: torrentLink) + + realDebridDownloadUrl = downloadLink + } else { + toastModel?.updateToastDescription("Could not cache this torrent. Aborting.") + } } } catch { - let error = error as NSError - - switch error.code { - case -999: - toastModel?.updateToastDescription("Download cancelled", newToastType: .info) + switch error { + case RealDebridError.EmptyTorrents: + showDeleteAlert.toggle() default: - toastModel?.updateToastDescription("RealDebrid download error: \(error)") - } + let error = error as NSError - // Delete the torrent download if it exists - if let realDebridId = realDebridId { - try? await realDebrid.deleteTorrent(debridID: realDebridId) + switch error.code { + case -999: + toastModel?.updateToastDescription("Download cancelled", newToastType: .info) + default: + toastModel?.updateToastDescription("RealDebrid download error: \(error)") + } + + await deleteRdTorrent() } showLoadingProgress = false @@ -198,4 +222,12 @@ public class DebridManager: ObservableObject { print("RealDebrid download error: \(error)") } } + + public func deleteRdTorrent() async { + if let realDebridId = selectedRealDebridID { + try? await realDebrid.deleteTorrent(debridID: realDebridId) + } + + selectedRealDebridID = nil + } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 4fa24bf..1f5320d 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -37,7 +37,7 @@ class NavigationViewModel: ObservableObject { @Published var hideNavigationBar = false @Published var currentChoiceSheet: ChoiceSheetType? - @Published var activityItems: [Any] = [] + var activityItems: [Any] = [] // Used to show the activity sheet in the share menu @Published var showLocalActivitySheet = false @@ -47,10 +47,10 @@ class NavigationViewModel: ObservableObject { // Used between SourceListView and SourceSettingsView @Published var showSourceSettings: Bool = false - @Published var selectedSource: Source? + var selectedSource: Source? @Published var showSourceListEditor: Bool = false - @Published var selectedSourceList: SourceList? + var selectedSourceList: SourceList? @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @AppStorage("Actions.DefaultMagnet") var defaultMagnetAction: DefaultMagnetActionType = .none diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index d7a4ef3..b64ac54 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -18,7 +18,7 @@ class ScrapingViewModel: ObservableObject { var toastModel: ToastViewModel? let byteCountFormatter: ByteCountFormatter = .init() - @Published var runningSearchTask: Task? + var runningSearchTask: Task? @Published var searchResults: [SearchResult] = [] @Published var searchText: String = "" @Published var filteredSource: Source? diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index d4f83e0..2484bfb 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -14,7 +14,7 @@ public class SourceManager: ObservableObject { @Published var availableSources: [SourceJson] = [] - @Published var urlErrorAlertText = "" + var urlErrorAlertText = "" @Published var showUrlErrorAlert = false @MainActor diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index eb7369c..afae3c8 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -23,7 +23,7 @@ struct BatchChoiceView: View { if let searchResult = navModel.selectedSearchResult { debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: searchResult, iaFile: file) + await debridManager.fetchRdDownload(searchResult: searchResult) if !debridManager.realDebridDownloadUrl.isEmpty { // The download may complete before this sheet dismisses diff --git a/Ferrite/Views/LibraryViews/BookmarksView.swift b/Ferrite/Views/LibraryViews/BookmarksView.swift index 2ad3fb2..3ed4b51 100644 --- a/Ferrite/Views/LibraryViews/BookmarksView.swift +++ b/Ferrite/Views/LibraryViews/BookmarksView.swift @@ -49,19 +49,20 @@ struct BookmarksView: View { PersistenceController.shared.save() } } + .id(UUID()) .listStyle(.insetGrouped) - .onAppear { - if realDebridEnabled { - viewTask = Task { - let hashes = bookmarks.compactMap { $0.magnetHash } - await debridManager.populateDebridHashes(hashes) - } - } - } - .onDisappear { - viewTask?.cancel() + } + } + .onAppear { + if realDebridEnabled { + viewTask = Task { + let hashes = bookmarks.compactMap { $0.magnetHash } + await debridManager.populateDebridHashes(hashes) } } } + .onDisappear { + viewTask?.cancel() + } } } diff --git a/Ferrite/Views/SearchResultButtonView.swift b/Ferrite/Views/SearchResultButtonView.swift index b952c2a..e4b6e82 100644 --- a/Ferrite/Views/SearchResultButtonView.swift +++ b/Ferrite/Views/SearchResultButtonView.swift @@ -28,15 +28,17 @@ struct SearchResultButtonView: View { switch debridManager.matchSearchResult(result: result) { case .full: - debridManager.currentDebridTask = Task { - await debridManager.fetchRdDownload(searchResult: result) - - if !debridManager.realDebridDownloadUrl.isEmpty { - navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) - navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) + if debridManager.setSelectedRdResult(result: result) { + debridManager.currentDebridTask = Task { + await debridManager.fetchRdDownload(searchResult: result) + + if !debridManager.realDebridDownloadUrl.isEmpty { + navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) + navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) - if navModel.currentChoiceSheet != .magnet { - debridManager.realDebridDownloadUrl = "" + if navModel.currentChoiceSheet != .magnet { + debridManager.realDebridDownloadUrl = "" + } } } } @@ -91,6 +93,19 @@ struct SearchResultButtonView: View { } } } + .dynamicAlert( + isPresented: $debridManager.showDeleteAlert, + title: "Caching file", + message: "RealDebrid is currently caching this file. Would you like to delete it? \n\nProgress can be checked on the RealDebrid website.", + buttons: [ + AlertButton("Yes", role: .destructive) { + Task { + await debridManager.deleteRdTorrent() + } + }, + AlertButton(role: .cancel) + ] + ) .onReceive(NotificationCenter.default.publisher(for: .didDeleteBookmark)) { _ in existingBookmark = nil } diff --git a/Ferrite/Views/SearchResultsView.swift b/Ferrite/Views/SearchResultsView.swift index 2c04731..110813e 100644 --- a/Ferrite/Views/SearchResultsView.swift +++ b/Ferrite/Views/SearchResultsView.swift @@ -11,8 +11,6 @@ struct SearchResultsView: View { @EnvironmentObject var scrapingModel: ScrapingViewModel @EnvironmentObject var navModel: NavigationViewModel - @AppStorage("RealDebrid.Enabled") var realDebridEnabled = false - var body: some View { List { ForEach(scrapingModel.searchResults, id: \.self) { result in -- 2.45.2 From a89e832d1c2a49055653eb3333f3e44de0a17d53 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 16 Sep 2022 13:07:28 -0400 Subject: [PATCH 16/22] Ferrite: Concurrency, cleanup, and format Use strict concurrency checking in Xcode 14 to find misuses with Swift concurrency. Cleanup files and rearrange them along with fixing comment headers. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 120 +++++++---- Ferrite/API/RealDebridWrapper.swift | 3 +- .../Application.swift} | 13 +- .../Classes/Bookmark+CoreDataProperties.swift | 31 ++- .../Classes/History+CoreDataClass.swift | 6 +- .../Classes/History+CoreDataProperties.swift | 35 ++- .../PersistenceController.swift | 3 +- Ferrite/Extensions/NotificationCenter.swift | 2 +- Ferrite/Extensions/View.swift | 39 ++-- Ferrite/Models/GithubModels.swift | 2 +- Ferrite/Models/RealDebridModels.swift | 200 +++++++++--------- Ferrite/Models/SearchModels.swift | 2 +- Ferrite/Models/SourceModels.swift | 24 +-- Ferrite/ViewModels/DebridManager.swift | 2 +- Ferrite/ViewModels/SourceManager.swift | 4 +- Ferrite/Views/AboutView.swift | 6 +- Ferrite/Views/CommonViews/AlertButton.swift | 16 +- .../CommonViews/DynamicFetchRequest.swift | 3 + Ferrite/Views/CommonViews/GroupBoxStyle.swift | 20 -- Ferrite/Views/CommonViews/ListRowViews.swift | 4 +- .../ConditionalContextMenu.swift | 0 .../{ => Modifiers}/ConditionalId.swift | 2 +- .../{ => Modifiers}/DisableInteraction.swift | 2 +- .../{ => Modifiers}/DisabledAppearance.swift | 2 + .../{ => Modifiers}/DynamicAccentColor.swift | 4 +- .../{ => Modifiers}/DynamicActionSheet.swift | 4 +- .../{ => Modifiers}/DynamicAlert.swift | 2 + .../{ => Modifiers}/InlinedList.swift | 4 +- Ferrite/Views/CommonViews/NavView.swift | 3 + Ferrite/Views/LibraryView.swift | 2 +- .../Views/LibraryViews/BookmarksView.swift | 4 +- .../LibraryViews/HistoryButtonView.swift | 10 +- Ferrite/Views/MagnetChoiceView.swift | 2 +- Ferrite/Views/MainView.swift | 2 +- .../Views/RepresentableViews/WebView.swift | 2 +- .../SearchResultButtonView.swift | 10 +- .../SearchResultRDView.swift | 0 .../InstalledSourceButtonView.swift} | 4 +- .../SourceCatalogButtonView.swift} | 0 .../SourceUpdateButtonView.swift | 0 Ferrite/Views/SourcesView.swift | 2 +- 41 files changed, 304 insertions(+), 292 deletions(-) rename Ferrite/{Extensions/UIApplication.swift => Classes/Application.swift} (64%) delete mode 100644 Ferrite/Views/CommonViews/GroupBoxStyle.swift rename Ferrite/Views/CommonViews/{ => Modifiers}/ConditionalContextMenu.swift (100%) rename Ferrite/Views/CommonViews/{ => Modifiers}/ConditionalId.swift (91%) rename Ferrite/Views/CommonViews/{ => Modifiers}/DisableInteraction.swift (87%) rename Ferrite/Views/CommonViews/{ => Modifiers}/DisabledAppearance.swift (89%) rename Ferrite/Views/CommonViews/{ => Modifiers}/DynamicAccentColor.swift (80%) rename Ferrite/Views/CommonViews/{ => Modifiers}/DynamicActionSheet.swift (92%) rename Ferrite/Views/CommonViews/{ => Modifiers}/DynamicAlert.swift (96%) rename Ferrite/Views/CommonViews/{ => Modifiers}/InlinedList.swift (81%) rename Ferrite/Views/{ => SearchResultViews}/SearchResultButtonView.swift (97%) rename Ferrite/Views/{ => SearchResultViews}/SearchResultRDView.swift (100%) rename Ferrite/Views/SourceViews/{InstalledSourceView.swift => Buttons/InstalledSourceButtonView.swift} (95%) rename Ferrite/Views/SourceViews/{SourceCatalogView.swift => Buttons/SourceCatalogButtonView.swift} (100%) rename Ferrite/Views/SourceViews/{ => Buttons}/SourceUpdateButtonView.swift (100%) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 051a2e8..1027ccc 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -14,13 +14,13 @@ 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */; }; 0C31133D28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */; }; 0C32FB532890D19D002BD219 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB522890D19D002BD219 /* AboutView.swift */; }; - 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB542890D1BF002BD219 /* UIApplication.swift */; }; 0C32FB572890D1F2002BD219 /* ListRowViews.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C32FB562890D1F2002BD219 /* ListRowViews.swift */; }; 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */; }; 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 */; }; 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 */; }; @@ -41,8 +41,8 @@ 0C750745289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */; }; 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C78041C28BFB3EA001E8CA3 /* String.swift */; }; 0C794B67289DACB600DD1CC8 /* SourceUpdateButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */; }; - 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */; }; - 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */; }; + 0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */; }; + 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */; }; 0C794B6D289EFA2E00DD1CC8 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 0C794B6C289EFA2E00DD1CC8 /* LaunchScreen.storyboard */; }; 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC052899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift */; }; 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C79DC062899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift */; }; @@ -98,7 +98,6 @@ 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 */; }; - 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -109,13 +108,13 @@ 0C31133A28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataClass.swift"; sourceTree = ""; }; 0C31133B28B1ABFA004DCB0D /* SourceJsonParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceJsonParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C32FB522890D19D002BD219 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = ""; }; - 0C32FB542890D1BF002BD219 /* UIApplication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIApplication.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 = ""; }; 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 = ""; }; @@ -131,8 +130,8 @@ 0C750743289B003E004B3906 /* SourceRssParser+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SourceRssParser+CoreDataProperties.swift"; sourceTree = ""; }; 0C78041C28BFB3EA001E8CA3 /* String.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = String.swift; sourceTree = ""; }; 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceUpdateButtonView.swift; sourceTree = ""; }; - 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceView.swift; sourceTree = ""; }; - 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogView.swift; sourceTree = ""; }; + 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledSourceButtonView.swift; sourceTree = ""; }; + 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceCatalogButtonView.swift; sourceTree = ""; }; 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 = ""; }; @@ -188,7 +187,6 @@ 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 = ""; }; - 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupBoxStyle.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -237,22 +235,62 @@ 0C0D50E3288DFE6E0035ECC8 /* Models */ = { isa = PBXGroup; children = ( - 0CA148C4288903F000DE2211 /* RealDebridModels.swift */, - 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, - 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, 0C68135128BC1A7C00FAD890 /* GithubModels.swift */, + 0CA148C4288903F000DE2211 /* RealDebridModels.swift */, 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, + 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, + 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, ); path = Models; sourceTree = ""; }; + 0C44E2A628D4DDC6007711AE /* Classes */ = { + isa = PBXGroup; + children = ( + 0C44E2A728D4DDDC007711AE /* Application.swift */, + ); + path = Classes; + sourceTree = ""; + }; + 0C44E2A928D4DFC4007711AE /* Modifiers */ = { + isa = PBXGroup; + children = ( + 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */, + 0CB6516228C5A57300DCA721 /* ConditionalId.swift */, + 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, + 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, + 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */, + 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */, + 0C626A9428CADB25003C7129 /* DynamicAlert.swift */, + 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, + ); + path = Modifiers; + sourceTree = ""; + }; + 0C44E2AA28D4E09B007711AE /* Buttons */ = { + isa = PBXGroup; + children = ( + 0C794B68289DACC800DD1CC8 /* InstalledSourceButtonView.swift */, + 0C794B6A289DACF100DD1CC8 /* SourceCatalogButtonView.swift */, + 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */, + ); + path = Buttons; + sourceTree = ""; + }; + 0C44E2AB28D4E126007711AE /* SearchResultViews */ = { + isa = PBXGroup; + children = ( + 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, + 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */, + ); + path = SearchResultViews; + sourceTree = ""; + }; 0C794B65289DAC9F00DD1CC8 /* SourceViews */ = { isa = PBXGroup; children = ( + 0C44E2AA28D4E09B007711AE /* Buttons */, 0C733286289C4C820058D1FE /* SourceSettingsView.swift */, - 0C794B66289DACB600DD1CC8 /* SourceUpdateButtonView.swift */, - 0C794B68289DACC800DD1CC8 /* InstalledSourceView.swift */, - 0C794B6A289DACF100DD1CC8 /* SourceCatalogView.swift */, ); path = SourceViews; sourceTree = ""; @@ -277,6 +315,7 @@ 0C0D50E3288DFE6E0035ECC8 /* Models */, 0CA148EF2889061600DE2211 /* ViewModels */, 0CA148EE2889061200DE2211 /* Views */, + 0C44E2A628D4DDC6007711AE /* Classes */, 0CA148C8288903F000DE2211 /* Extensions */, 0CA148C5288903F000DE2211 /* Preview Content */, 0CA148C7288903F000DE2211 /* FerriteApp.swift */, @@ -289,22 +328,14 @@ 0CA148C0288903F000DE2211 /* CommonViews */ = { isa = PBXGroup; children = ( - 0CA148C1288903F000DE2211 /* NavView.swift */, - 0CFEFCFC288A006200B3F490 /* GroupBoxStyle.swift */, - 0C32FB562890D1F2002BD219 /* ListRowViews.swift */, - 0C7D11FB28AA01E900ED92DB /* DynamicAccentColor.swift */, - 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, - 0CB6516228C5A57300DCA721 /* ConditionalId.swift */, - 0CB6516428C5A5D700DCA721 /* InlinedList.swift */, - 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, - 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, - 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, - 0C70E40128C3CE9C00A5C72D /* ConditionalContextMenu.swift */, - 0C391EC828CA63F0009F1CA1 /* DynamicActionSheet.swift */, + 0C44E2A928D4DFC4007711AE /* Modifiers */, 0C391ECA28CAA44B009F1CA1 /* AlertButton.swift */, - 0C626A9428CADB25003C7129 /* DynamicAlert.swift */, - 0CD5E78828CD932B001BF684 /* DisabledAppearance.swift */, - 0CBAB83528D12ED500AC903E /* DisableInteraction.swift */, + 0C360C5B28C7DF1400884ED3 /* DynamicFetchRequest.swift */, + 0CDCB91728C662640098B513 /* EmptyInstructionView.swift */, + 0CA148C1288903F000DE2211 /* NavView.swift */, + 0CA3FB1F28B91D9500FA10A8 /* IndeterminateProgressView.swift */, + 0CB6516928C5B4A600DCA721 /* InlineHeader.swift */, + 0C32FB562890D1F2002BD219 /* ListRowViews.swift */, ); path = CommonViews; sourceTree = ""; @@ -322,12 +353,11 @@ children = ( 0CA148C9288903F000DE2211 /* Collection.swift */, 0CA148CA288903F000DE2211 /* Data.swift */, - 0CA148CB288903F000DE2211 /* Task.swift */, - 0C32FB542890D1BF002BD219 /* UIApplication.swift */, - 0C7D11FD28AA03FE00ED92DB /* View.swift */, - 0C78041C28BFB3EA001E8CA3 /* String.swift */, - 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */, 0CA429F728C5098D000D0610 /* DateFormatter.swift */, + 0C70E40528C40C4E00A5C72D /* NotificationCenter.swift */, + 0C78041C28BFB3EA001E8CA3 /* String.swift */, + 0CA148CB288903F000DE2211 /* Task.swift */, + 0C7D11FD28AA03FE00ED92DB /* View.swift */, ); path = Extensions; sourceTree = ""; @@ -339,19 +369,18 @@ 0C794B65289DAC9F00DD1CC8 /* SourceViews */, 0CA148F02889062700DE2211 /* RepresentableViews */, 0CA148C0288903F000DE2211 /* CommonViews */, + 0C44E2AB28D4E126007711AE /* SearchResultViews */, 0CA0545C288F7CB200850554 /* SettingsViews */, - 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, - 0C41BC6228C2AD0F00B47DD6 /* SearchResultButtonView.swift */, - 0C57D4CB289032ED008534E8 /* SearchResultRDView.swift */, - 0CA148D4288903F000DE2211 /* ContentView.swift */, 0CA148D1288903F000DE2211 /* MainView.swift */, + 0CA148D4288903F000DE2211 /* ContentView.swift */, + 0CA148D3288903F000DE2211 /* SearchResultsView.swift */, + 0CA3B23328C2658700616D3A /* LibraryView.swift */, + 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, 0CA148BB288903F000DE2211 /* SettingsView.swift */, + 0C32FB522890D19D002BD219 /* AboutView.swift */, 0CA148BC288903F000DE2211 /* LoginWebView.swift */, 0CBC76FC288D914F0054BE44 /* BatchChoiceView.swift */, 0CA148BD288903F000DE2211 /* MagnetChoiceView.swift */, - 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */, - 0C32FB522890D19D002BD219 /* AboutView.swift */, - 0CA3B23328C2658700616D3A /* LibraryView.swift */, ); path = Views; sourceTree = ""; @@ -379,8 +408,8 @@ 0CA148F12889066000DE2211 /* API */ = { isa = PBXGroup; children = ( - 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */, 0C68134F28BC1A2D00FAD890 /* GithubWrapper.swift */, + 0CA148D0288903F000DE2211 /* RealDebridWrapper.swift */, ); path = API; sourceTree = ""; @@ -528,21 +557,21 @@ 0C360C5C28C7DF1400884ED3 /* DynamicFetchRequest.swift in Sources */, 0CA429F828C5098D000D0610 /* DateFormatter.swift in Sources */, 0C84F4872895BFED0074B7C9 /* SourceList+CoreDataProperties.swift in Sources */, - 0C794B6B289DACF100DD1CC8 /* SourceCatalogView.swift in Sources */, + 0C794B6B289DACF100DD1CC8 /* SourceCatalogButtonView.swift in Sources */, 0C54D36428C5086E00BFEEE2 /* History+CoreDataProperties.swift in Sources */, 0CA148E9288903F000DE2211 /* MainView.swift in Sources */, 0CBC76FD288D914F0054BE44 /* BatchChoiceView.swift in Sources */, - 0C32FB552890D1BF002BD219 /* UIApplication.swift in Sources */, 0C7D11FE28AA03FE00ED92DB /* View.swift in Sources */, 0CA3B23728C2660700616D3A /* HistoryView.swift in Sources */, 0C70E40228C3CE9C00A5C72D /* ConditionalContextMenu.swift in Sources */, 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */, 0CA3B23428C2658700616D3A /* LibraryView.swift in Sources */, + 0C44E2A828D4DDDC007711AE /* Application.swift in Sources */, 0CA148EC288903F000DE2211 /* ContentView.swift in Sources */, 0C95D8D828A55B03005E22B3 /* DefaultActionsPickerViews.swift in Sources */, 0CA148E1288903F000DE2211 /* Collection.swift in Sources */, 0C750744289B003E004B3906 /* SourceRssParser+CoreDataClass.swift in Sources */, - 0C794B69289DACC800DD1CC8 /* InstalledSourceView.swift in Sources */, + 0C794B69289DACC800DD1CC8 /* InstalledSourceButtonView.swift in Sources */, 0C79DC082899AF3C003F1C5A /* SourceSeedLeech+CoreDataProperties.swift in Sources */, 0CD4CAC628C980EB0046E1DC /* HistoryActionsView.swift in Sources */, 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */, @@ -556,7 +585,6 @@ 0C626A9528CADB25003C7129 /* DynamicAlert.swift in Sources */, 0CA148E7288903F000DE2211 /* ToastViewModel.swift in Sources */, 0C68135228BC1A7C00FAD890 /* GithubModels.swift in Sources */, - 0CFEFCFD288A006200B3F490 /* GroupBoxStyle.swift in Sources */, 0C391EC928CA63F0009F1CA1 /* DynamicActionSheet.swift in Sources */, 0CA3B23928C2660D00616D3A /* BookmarksView.swift in Sources */, 0C79DC072899AF3C003F1C5A /* SourceSeedLeech+CoreDataClass.swift in Sources */, diff --git a/Ferrite/API/RealDebridWrapper.swift b/Ferrite/API/RealDebridWrapper.swift index fb60a5f..ecefd2a 100644 --- a/Ferrite/API/RealDebridWrapper.swift +++ b/Ferrite/API/RealDebridWrapper.swift @@ -255,7 +255,8 @@ public class RealDebrid { hash: hash, expiryTimeStamp: Date().timeIntervalSince1970 + 300, files: files, - batches: batches) + batches: batches + ) ) } else { availableHashes.append( diff --git a/Ferrite/Extensions/UIApplication.swift b/Ferrite/Classes/Application.swift similarity index 64% rename from Ferrite/Extensions/UIApplication.swift rename to Ferrite/Classes/Application.swift index 3490ab4..6ebdbab 100644 --- a/Ferrite/Extensions/UIApplication.swift +++ b/Ferrite/Classes/Application.swift @@ -1,14 +1,17 @@ // -// UIApplication.swift +// Application.swift // Ferrite // -// Created by Brian Dashore on 7/26/22. +// Created by Brian Dashore on 9/16/22. +// +// A thread-safe UIApplication alternative for specifying app properties // -import SwiftUI +import Foundation + +public class Application { + static let shared = Application() -// Extensions to get the version/build number for AboutView -extension UIApplication { var appVersion: String { Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "0.0.0" } diff --git a/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift index 6528695..36f2e9e 100644 --- a/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/Bookmark+CoreDataProperties.swift @@ -6,27 +6,22 @@ // // -import Foundation import CoreData +import Foundation - -extension Bookmark { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "Bookmark") +public extension Bookmark { + @nonobjc class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "Bookmark") } - @NSManaged public var leechers: String? - @NSManaged public var magnetHash: String? - @NSManaged public var magnetLink: String? - @NSManaged public var seeders: String? - @NSManaged public var size: String? - @NSManaged public var source: String - @NSManaged public var title: String? - @NSManaged public var orderNum: Int16 - + @NSManaged var leechers: String? + @NSManaged var magnetHash: String? + @NSManaged var magnetLink: String? + @NSManaged var seeders: String? + @NSManaged var size: String? + @NSManaged var source: String + @NSManaged var title: String? + @NSManaged var orderNum: Int16 } -extension Bookmark : Identifiable { - -} +extension Bookmark: Identifiable {} diff --git a/Ferrite/DataManagement/Classes/History+CoreDataClass.swift b/Ferrite/DataManagement/Classes/History+CoreDataClass.swift index b7108b3..18677e1 100644 --- a/Ferrite/DataManagement/Classes/History+CoreDataClass.swift +++ b/Ferrite/DataManagement/Classes/History+CoreDataClass.swift @@ -6,10 +6,8 @@ // // -import Foundation import CoreData +import Foundation @objc(History) -public class History: NSManagedObject { - -} +public class History: NSManagedObject {} diff --git a/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift b/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift index 7e384ab..cc8ad58 100644 --- a/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift +++ b/Ferrite/DataManagement/Classes/History+CoreDataProperties.swift @@ -6,21 +6,19 @@ // // -import Foundation import CoreData +import Foundation - -extension History { - - @nonobjc public class func fetchRequest() -> NSFetchRequest { - return NSFetchRequest(entityName: "History") +public extension History { + @nonobjc class func fetchRequest() -> NSFetchRequest { + NSFetchRequest(entityName: "History") } - @NSManaged public var date: Date? - @NSManaged public var dateString: String? - @NSManaged public var entries: NSSet? - - var entryArray: [HistoryEntry] { + @NSManaged var date: Date? + @NSManaged var dateString: String? + @NSManaged var entries: NSSet? + + internal var entryArray: [HistoryEntry] { let entrySet = entries as? Set ?? [] return entrySet.sorted { @@ -30,22 +28,19 @@ extension History { } // MARK: Generated accessors for entries -extension History { +public extension History { @objc(addEntriesObject:) - @NSManaged public func addToEntries(_ value: HistoryEntry) + @NSManaged func addToEntries(_ value: HistoryEntry) @objc(removeEntriesObject:) - @NSManaged public func removeFromEntries(_ value: HistoryEntry) + @NSManaged func removeFromEntries(_ value: HistoryEntry) @objc(addEntries:) - @NSManaged public func addToEntries(_ values: NSSet) + @NSManaged func addToEntries(_ values: NSSet) @objc(removeEntries:) - @NSManaged public func removeFromEntries(_ values: NSSet) - + @NSManaged func removeFromEntries(_ values: NSSet) } -extension History : Identifiable { - -} +extension History: Identifiable {} diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index 1fecdc2..92c34a2 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -21,7 +21,7 @@ enum HistoryDeleteError: Error { // No iCloud until finalized sources struct PersistenceController { - static var shared = PersistenceController() + static let shared = PersistenceController() // Coredata storage let container: NSPersistentContainer @@ -91,7 +91,6 @@ struct PersistenceController { save() } - func getHistoryPredicate(range: HistoryDeleteRange) -> NSPredicate? { if range == .allTime { return nil diff --git a/Ferrite/Extensions/NotificationCenter.swift b/Ferrite/Extensions/NotificationCenter.swift index be21d68..7bc0db5 100644 --- a/Ferrite/Extensions/NotificationCenter.swift +++ b/Ferrite/Extensions/NotificationCenter.swift @@ -9,6 +9,6 @@ import Foundation extension Notification.Name { static var didDeleteBookmark: Notification.Name { - return Notification.Name("Deleted bookmark") + Notification.Name("Deleted bookmark") } } diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 2226805..5f07409 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -25,8 +25,10 @@ extension View { // MARK: Modifiers - func dynamicAccentColor(_ color: Color) -> some View { - modifier(DynamicAccentColor(color: color)) + func conditionalContextMenu(id: ID, + @ViewBuilder _ internalContent: @escaping () -> InternalContent) -> some View + { + modifier(ConditionalContextMenu(internalContent, id: id)) } func conditionalId(_ id: ID) -> some View { @@ -37,36 +39,31 @@ extension View { modifier(DisabledAppearance(disabled: disabled, dimmedOpacity: dimmedOpacity, animation: animation)) } - func inlinedList() -> some View { - modifier(InlinedList()) + func disableInteraction(_ disabled: Bool) -> some View { + modifier(DisableInteraction(disabled: disabled)) } - func conditionalContextMenu( - id: ID, - @ViewBuilder _ internalContent: @escaping () -> InternalContent - ) -> some View { - modifier(ConditionalContextMenu(internalContent, id: id)) + func dynamicAccentColor(_ color: Color) -> some View { + modifier(DynamicAccentColor(color: color)) } - func dynamicActionSheet( - isPresented: Binding, - title: String, - message: String? = nil, - buttons: [AlertButton]) -> some View + 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 + func dynamicAlert(isPresented: Binding, + title: String, + message: String? = nil, + buttons: [AlertButton]) -> some View { modifier(DynamicAlert(isPresented: isPresented, title: title, message: message, buttons: buttons)) } - func disableInteraction(_ disabled: Bool) -> some View { - modifier(DisableInteraction(disabled: disabled)) + func inlinedList() -> some View { + modifier(InlinedList()) } } diff --git a/Ferrite/Models/GithubModels.swift b/Ferrite/Models/GithubModels.swift index 575dba5..fb4a066 100644 --- a/Ferrite/Models/GithubModels.swift +++ b/Ferrite/Models/GithubModels.swift @@ -7,7 +7,7 @@ import Foundation -public struct GithubRelease: Codable, Hashable { +public struct GithubRelease: Codable, Hashable, Sendable { let htmlUrl: String let tagName: String diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 39506f0..208f253 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -10,138 +10,138 @@ import Foundation // MARK: - device code endpoint -public struct DeviceCodeResponse: Codable { - let deviceCode, userCode: String - let interval, expiresIn: Int - let verificationURL, directVerificationURL: String +public struct DeviceCodeResponse: Codable, Sendable { + let deviceCode, userCode: String + let interval, expiresIn: Int + let verificationURL, directVerificationURL: String - enum CodingKeys: String, CodingKey { - case deviceCode = "device_code" - case userCode = "user_code" - case interval - case expiresIn = "expires_in" - case verificationURL = "verification_url" - case directVerificationURL = "direct_verification_url" - } + enum CodingKeys: String, CodingKey { + case deviceCode = "device_code" + case userCode = "user_code" + case interval + case expiresIn = "expires_in" + case verificationURL = "verification_url" + case directVerificationURL = "direct_verification_url" + } } // MARK: - device credentials endpoint -public struct DeviceCredentialsResponse: Codable { - let clientID, clientSecret: String? +public struct DeviceCredentialsResponse: Codable, Sendable { + let clientID, clientSecret: String? - enum CodingKeys: String, CodingKey { - case clientID = "client_id" - case clientSecret = "client_secret" - } + enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + } } // MARK: - token endpoint -public struct TokenResponse: Codable { - let accessToken: String - let expiresIn: Int - let refreshToken, tokenType: String +public struct TokenResponse: Codable, Sendable { + let accessToken: String + let expiresIn: Int + let refreshToken, tokenType: String - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" - } + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case tokenType = "token_type" + } } // MARK: - instantAvailability endpoint // Thanks Skitty! -public struct InstantAvailabilityResponse: Codable { - var data: InstantAvailabilityData? +public struct InstantAvailabilityResponse: Codable, Sendable { + var data: InstantAvailabilityData? - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() - if let data = try? container.decode(InstantAvailabilityData.self) { - self.data = data - } - } + if let data = try? container.decode(InstantAvailabilityData.self) { + self.data = data + } + } } -struct InstantAvailabilityData: Codable { - var rd: [[String: InstantAvailabilityInfo]] +struct InstantAvailabilityData: Codable, Sendable { + var rd: [[String: InstantAvailabilityInfo]] } -struct InstantAvailabilityInfo: Codable { - var filename: String - var filesize: Int +struct InstantAvailabilityInfo: Codable, Sendable { + var filename: String + var filesize: Int } // MARK: - Instant Availability client side structures -public struct RealDebridIA: Codable, Hashable { - let hash: String - let expiryTimeStamp: Double - var files: [RealDebridIAFile] = [] - var batches: [RealDebridIABatch] = [] +public struct RealDebridIA: Codable, Hashable, Sendable { + let hash: String + let expiryTimeStamp: Double + var files: [RealDebridIAFile] = [] + var batches: [RealDebridIABatch] = [] } -public struct RealDebridIABatch: Codable, Hashable { - let files: [RealDebridIABatchFile] +public struct RealDebridIABatch: Codable, Hashable, Sendable { + let files: [RealDebridIABatchFile] } -public struct RealDebridIABatchFile: Codable, Hashable { - let id: Int - let fileName: String +public struct RealDebridIABatchFile: Codable, Hashable, Sendable { + let id: Int + let fileName: String } -public struct RealDebridIAFile: Codable, Hashable { - let name: String - let batchIndex: Int - let batchFileIndex: Int +public struct RealDebridIAFile: Codable, Hashable, Sendable { + let name: String + let batchIndex: Int + let batchFileIndex: Int } -public enum RealDebridIAStatus: Codable, Hashable { - case full - case partial - case none +public enum RealDebridIAStatus: Codable, Hashable, Sendable { + case full + case partial + case none } // MARK: - addMagnet endpoint -public struct AddMagnetResponse: Codable { - let id: String - let uri: String +public struct AddMagnetResponse: Codable, Sendable { + let id: String + let uri: String } // MARK: - torrentInfo endpoint -struct TorrentInfoResponse: Codable { - let id, filename, originalFilename, hash: String - let bytes, originalBytes: Int - let host: String - let split, progress: Int - let status, added: String - let files: [TorrentInfoFile] - let links: [String] - let ended: String? - let speed: Int? - let seeders: Int? +struct TorrentInfoResponse: Codable, Sendable { + let id, filename, originalFilename, hash: String + let bytes, originalBytes: Int + let host: String + let split, progress: Int + let status, added: String + let files: [TorrentInfoFile] + let links: [String] + let ended: String? + let speed: Int? + let seeders: Int? - enum CodingKeys: String, CodingKey { - case id, filename - case originalFilename = "original_filename" - case hash, bytes - case originalBytes = "original_bytes" - case host, split, progress, status, added, files, links, ended, speed, seeders - } + enum CodingKeys: String, CodingKey { + case id, filename + case originalFilename = "original_filename" + case hash, bytes + case originalBytes = "original_bytes" + case host, split, progress, status, added, files, links, ended, speed, seeders + } } -struct TorrentInfoFile: Codable { - let id: Int - let path: String - let bytes, selected: Int +struct TorrentInfoFile: Codable, Sendable { + let id: Int + let path: String + let bytes, selected: Int } -public struct UserTorrentsResponse: Codable { +public struct UserTorrentsResponse: Codable, Sendable { let id, filename, hash: String let bytes: Int let host: String @@ -154,26 +154,26 @@ public struct UserTorrentsResponse: Codable { // MARK: - unrestrictLink endpoint -struct UnrestrictLinkResponse: Codable { - let id, filename, mimeType: String - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks, crc: Int - let download: String - let streamable: Int +struct UnrestrictLinkResponse: Codable, Sendable { + let id, filename, mimeType: String + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks, crc: Int + let download: String + let streamable: Int - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, crc, download, streamable - } + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, crc, download, streamable + } } // MARK: - User downloads list -public struct UserDownloadsResponse: Codable { +public struct UserDownloadsResponse: Codable, Sendable { let id, filename, mimeType: String let filesize: Int let link: String diff --git a/Ferrite/Models/SearchModels.swift b/Ferrite/Models/SearchModels.swift index 2751a82..4f7fb59 100644 --- a/Ferrite/Models/SearchModels.swift +++ b/Ferrite/Models/SearchModels.swift @@ -7,7 +7,7 @@ import Foundation -public struct SearchResult: Hashable, Codable { +public struct SearchResult: Hashable, Codable, Sendable { let title: String? let source: String let size: String? diff --git a/Ferrite/Models/SourceModels.swift b/Ferrite/Models/SourceModels.swift index 558af84..8cf3221 100644 --- a/Ferrite/Models/SourceModels.swift +++ b/Ferrite/Models/SourceModels.swift @@ -7,18 +7,18 @@ import Foundation -public enum ApiCredentialResponseType: String, Codable, Hashable { +public enum ApiCredentialResponseType: String, Codable, Hashable, Sendable { case json case text } -public struct SourceListJson: Codable { +public struct SourceListJson: Codable, Sendable { let name: String let author: String var sources: [SourceJson] } -public struct SourceJson: Codable, Hashable { +public struct SourceJson: Codable, Hashable, Sendable { let name: String let version: Int16 let minVersion: String? @@ -34,20 +34,20 @@ public struct SourceJson: Codable, Hashable { let htmlParser: SourceHtmlParserJson? } -public enum SourcePreferredParser: Int16, CaseIterable { +public enum SourcePreferredParser: Int16, CaseIterable, Sendable { // case none = 0 case scraping = 1 case rss = 2 case siteApi = 3 } -public struct SourceApiJson: Codable, Hashable { +public struct SourceApiJson: Codable, Hashable, Sendable { let apiUrl: String? let clientId: SourceApiCredentialJson? let clientSecret: SourceApiCredentialJson? } -public struct SourceApiCredentialJson: Codable, Hashable { +public struct SourceApiCredentialJson: Codable, Hashable, Sendable { let query: String? let value: String? let dynamic: Bool? @@ -56,7 +56,7 @@ public struct SourceApiCredentialJson: Codable, Hashable { let expiryLength: Double? } -public struct SourceJsonParserJson: Codable, Hashable { +public struct SourceJsonParserJson: Codable, Hashable, Sendable { let searchUrl: String let results: String? let subResults: String? @@ -67,7 +67,7 @@ public struct SourceJsonParserJson: Codable, Hashable { let sl: SourceSLJson? } -public struct SourceRssParserJson: Codable, Hashable { +public struct SourceRssParserJson: Codable, Hashable, Sendable { let rssUrl: String? let searchUrl: String let items: String @@ -78,7 +78,7 @@ public struct SourceRssParserJson: Codable, Hashable { let sl: SourceSLJson? } -public struct SourceHtmlParserJson: Codable, Hashable { +public struct SourceHtmlParserJson: Codable, Hashable, Sendable { let searchUrl: String let rows: String let magnet: SourceMagnetJson @@ -87,21 +87,21 @@ public struct SourceHtmlParserJson: Codable, Hashable { let sl: SourceSLJson? } -public struct SouceComplexQueryJson: Codable, Hashable { +public struct SouceComplexQueryJson: Codable, Hashable, Sendable { let query: String let discriminator: String? let attribute: String? let regex: String? } -public struct SourceMagnetJson: Codable, Hashable { +public struct SourceMagnetJson: Codable, Hashable, Sendable { let query: String let attribute: String let regex: String? let externalLinkQuery: String? } -public struct SourceSLJson: Codable, Hashable { +public struct SourceSLJson: Codable, Hashable, Sendable { let seeders: String? let leechers: String? let combined: String? diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index fc6457e..42ccccd 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -174,7 +174,7 @@ public class DebridManager: ObservableObject { // If the links match from a user's downloads, no need to re-run a download if let existingTorrent = existingTorrents[safe: 0], - let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] + let torrentLink = existingTorrent.links[safe: selectedRealDebridFile?.batchFileIndex ?? 0] { let existingLinks = try await realDebrid.userDownloads().filter { $0.link == torrentLink } if let existingLink = existingLinks[safe: 0]?.download { diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index 2484bfb..8c8d0e2 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -1,5 +1,5 @@ // -// SourceViewModel.swift +// SourceManager.swift // Ferrite // // Created by Brian Dashore on 7/25/22. @@ -75,7 +75,7 @@ public class SourceManager: ObservableObject { return true } - return UIApplication.shared.appVersion >= minVersion + return Application.shared.appVersion >= minVersion } // Fetches sources using the background context diff --git a/Ferrite/Views/AboutView.swift b/Ferrite/Views/AboutView.swift index 2933955..1b1bed5 100644 --- a/Ferrite/Views/AboutView.swift +++ b/Ferrite/Views/AboutView.swift @@ -11,9 +11,9 @@ struct AboutView: View { var body: some View { List { Section { - ListRowTextView(leftText: "Version", rightText: UIApplication.shared.appVersion) - ListRowTextView(leftText: "Build number", rightText: UIApplication.shared.appBuild) - ListRowTextView(leftText: "Build type", rightText: UIApplication.shared.buildType) + ListRowTextView(leftText: "Version", rightText: Application.shared.appVersion) + ListRowTextView(leftText: "Build number", rightText: Application.shared.appBuild) + ListRowTextView(leftText: "Build type", rightText: Application.shared.buildType) ListRowLinkView(text: "Discord server", link: "https://discord.gg/sYQxnuD7Fj") ListRowLinkView(text: "GitHub repository", link: "https://github.com/bdashore3/Ferrite") } header: { diff --git a/Ferrite/Views/CommonViews/AlertButton.swift b/Ferrite/Views/CommonViews/AlertButton.swift index 8d6922e..19cf66d 100644 --- a/Ferrite/Views/CommonViews/AlertButton.swift +++ b/Ferrite/Views/CommonViews/AlertButton.swift @@ -4,6 +4,8 @@ // // Created by Brian Dashore on 9/8/22. // +// Universal alert button for dynamic alert views +// import SwiftUI @@ -20,7 +22,7 @@ struct AlertButton: Identifiable { // Used for all buttons init(_ label: String, role: Role? = nil, action: @escaping () -> Void) { - self.id = UUID() + id = UUID() self.label = label self.action = action self.role = role @@ -28,19 +30,19 @@ struct AlertButton: Identifiable { // Used for buttons with no action init(_ label: String = "Cancel", role: Role? = nil) { - self.id = UUID() + id = UUID() self.label = label - self.action = { } + action = {} self.role = role } func toActionButton() -> Alert.Button { if let role = role { switch role { - case .cancel: - return .cancel(Text(label)) - case .destructive: - return .destructive(Text(label), action: action) + case .cancel: + return .cancel(Text(label)) + case .destructive: + return .destructive(Text(label), action: action) } } else { return .default(Text(label), action: action) diff --git a/Ferrite/Views/CommonViews/DynamicFetchRequest.swift b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift index 02ccac8..145addb 100644 --- a/Ferrite/Views/CommonViews/DynamicFetchRequest.swift +++ b/Ferrite/Views/CommonViews/DynamicFetchRequest.swift @@ -4,6 +4,9 @@ // // Created by Brian Dashore on 9/6/22. // +// Used for FetchRequests with a dynamic predicate +// iOS 14 compatible view +// import CoreData import SwiftUI diff --git a/Ferrite/Views/CommonViews/GroupBoxStyle.swift b/Ferrite/Views/CommonViews/GroupBoxStyle.swift deleted file mode 100644 index 6398a3b..0000000 --- a/Ferrite/Views/CommonViews/GroupBoxStyle.swift +++ /dev/null @@ -1,20 +0,0 @@ -// -// GroupBoxStyle.swift -// Ferrite -// -// Created by Brian Dashore on 7/21/22. -// - -import SwiftUI - -struct ErrorGroupBoxStyle: GroupBoxStyle { - func makeBody(configuration: Configuration) -> some View { - VStack { - configuration.label - configuration.content - } - .padding(10) - .background(Color(UIColor.secondarySystemGroupedBackground)) - .clipShape(RoundedRectangle(cornerRadius: 8, style: .continuous)) - } -} diff --git a/Ferrite/Views/CommonViews/ListRowViews.swift b/Ferrite/Views/CommonViews/ListRowViews.swift index 770c4be..80e736f 100644 --- a/Ferrite/Views/CommonViews/ListRowViews.swift +++ b/Ferrite/Views/CommonViews/ListRowViews.swift @@ -4,11 +4,11 @@ // // Created by Brian Dashore on 7/26/22. // +// List row button, text, and link boilerplate +// import SwiftUI -// These views were imported from Asobi -// View alias for a list row with an external link struct ListRowLinkView: View { let text: String let link: String diff --git a/Ferrite/Views/CommonViews/ConditionalContextMenu.swift b/Ferrite/Views/CommonViews/Modifiers/ConditionalContextMenu.swift similarity index 100% rename from Ferrite/Views/CommonViews/ConditionalContextMenu.swift rename to Ferrite/Views/CommonViews/Modifiers/ConditionalContextMenu.swift diff --git a/Ferrite/Views/CommonViews/ConditionalId.swift b/Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift similarity index 91% rename from Ferrite/Views/CommonViews/ConditionalId.swift rename to Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift index 53a033d..146f87f 100644 --- a/Ferrite/Views/CommonViews/ConditionalId.swift +++ b/Ferrite/Views/CommonViews/Modifiers/ConditionalId.swift @@ -4,7 +4,7 @@ // // Created by Brian Dashore on 9/4/22. // -// Only applies an ID for below iOS 16 +// Applies an ID below iOS 16 // This is due to ID workarounds making iOS 16 apps crash // diff --git a/Ferrite/Views/CommonViews/DisableInteraction.swift b/Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift similarity index 87% rename from Ferrite/Views/CommonViews/DisableInteraction.swift rename to Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift index a8073d8..97d428c 100644 --- a/Ferrite/Views/CommonViews/DisableInteraction.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DisableInteraction.swift @@ -4,7 +4,7 @@ // // Created by Brian Dashore on 9/13/22. // -// Disables interaction without applying the appearance +// Disables interaction on any view without applying the appearance // import SwiftUI diff --git a/Ferrite/Views/CommonViews/DisabledAppearance.swift b/Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift similarity index 89% rename from Ferrite/Views/CommonViews/DisabledAppearance.swift rename to Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift index 132d23e..1caeaf6 100644 --- a/Ferrite/Views/CommonViews/DisabledAppearance.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DisabledAppearance.swift @@ -4,6 +4,8 @@ // // Created by Brian Dashore on 9/10/22. // +// Adds opacity transitions to the disabled modifier +// import SwiftUI diff --git a/Ferrite/Views/CommonViews/DynamicAccentColor.swift b/Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift similarity index 80% rename from Ferrite/Views/CommonViews/DynamicAccentColor.swift rename to Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift index 02a2e79..ab1fd3c 100644 --- a/Ferrite/Views/CommonViews/DynamicAccentColor.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift @@ -1,9 +1,11 @@ // -// dynamicAccentColor.swift +// DynamicAccentColor.swift // Ferrite // // Created by Brian Dashore on 8/15/22. // +// Wrapper that switches between tint and accentColor +// import SwiftUI diff --git a/Ferrite/Views/CommonViews/DynamicActionSheet.swift b/Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift similarity index 92% rename from Ferrite/Views/CommonViews/DynamicActionSheet.swift rename to Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift index 9e2a3c5..9d9d4dd 100644 --- a/Ferrite/Views/CommonViews/DynamicActionSheet.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift @@ -4,6 +4,8 @@ // // Created by Brian Dashore on 9/8/22. // +// Switches between confirmationDialog and actionSheet +// import SwiftUI @@ -36,7 +38,7 @@ struct DynamicActionSheet: ViewModifier { ActionSheet( title: Text(title), message: message.map { Text($0) } ?? nil, - buttons: [buttons.map { $0.toActionButton() }, [.cancel()]].flatMap{ $0 } + buttons: [buttons.map { $0.toActionButton() }, [.cancel()]].flatMap { $0 } ) } } diff --git a/Ferrite/Views/CommonViews/DynamicAlert.swift b/Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift similarity index 96% rename from Ferrite/Views/CommonViews/DynamicAlert.swift rename to Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift index 2a15546..7f39cfe 100644 --- a/Ferrite/Views/CommonViews/DynamicAlert.swift +++ b/Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift @@ -4,6 +4,8 @@ // // Created by Brian Dashore on 9/8/22. // +// Switches between iOS 15 and 14 alert initalizers +// import SwiftUI diff --git a/Ferrite/Views/CommonViews/InlinedList.swift b/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift similarity index 81% rename from Ferrite/Views/CommonViews/InlinedList.swift rename to Ferrite/Views/CommonViews/Modifiers/InlinedList.swift index a0eb034..3e2cac2 100644 --- a/Ferrite/Views/CommonViews/InlinedList.swift +++ b/Ferrite/Views/CommonViews/Modifiers/InlinedList.swift @@ -4,8 +4,8 @@ // // Created by Brian Dashore on 9/4/22. // -// Removes the top padding on lists for iOS 16 -// Use UITableView.appearance().contentInset.top = -20 for iOS 15 and below in the App file +// Removes the top padding on unsectioned lists +// If a list is sectioned, see InlineHeader // import Introspect diff --git a/Ferrite/Views/CommonViews/NavView.swift b/Ferrite/Views/CommonViews/NavView.swift index cd8ffd4..b89b95f 100644 --- a/Ferrite/Views/CommonViews/NavView.swift +++ b/Ferrite/Views/CommonViews/NavView.swift @@ -3,6 +3,9 @@ // Ferrite // // Created by Brian Dashore on 7/4/22. +// Contributed by Mantton +// +// A wrapper that switches between NavigationStack and the legacy NavigationView // import SwiftUI diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 90d560e..3d1b88e 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -1,5 +1,5 @@ // -// Library.swift +// LibraryView.swift // Ferrite // // Created by Brian Dashore on 9/2/22. diff --git a/Ferrite/Views/LibraryViews/BookmarksView.swift b/Ferrite/Views/LibraryViews/BookmarksView.swift index 3ed4b51..aea6c9e 100644 --- a/Ferrite/Views/LibraryViews/BookmarksView.swift +++ b/Ferrite/Views/LibraryViews/BookmarksView.swift @@ -37,7 +37,7 @@ struct BookmarksView: View { } } } - .onMove { (source, destination) in + .onMove { source, destination in var changedBookmarks = bookmarks.map { $0 } changedBookmarks.move(fromOffsets: source, toOffset: destination) @@ -56,7 +56,7 @@ struct BookmarksView: View { .onAppear { if realDebridEnabled { viewTask = Task { - let hashes = bookmarks.compactMap { $0.magnetHash } + let hashes = bookmarks.compactMap(\.magnetHash) await debridManager.populateDebridHashes(hashes) } } diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/LibraryViews/HistoryButtonView.swift index cf607c3..681f970 100644 --- a/Ferrite/Views/LibraryViews/HistoryButtonView.swift +++ b/Ferrite/Views/LibraryViews/HistoryButtonView.swift @@ -21,7 +21,7 @@ struct HistoryButtonView: View { Task { debridManager.realDebridDownloadUrl = url navModel.runDebridAction(urlString: url) - + if navModel.currentChoiceSheet != .magnet { debridManager.realDebridDownloadUrl = "" } @@ -37,19 +37,19 @@ struct HistoryButtonView: View { VStack(alignment: .leading, spacing: 3) { Text(entry.name ?? "Unknown title") .font(entry.subName == nil ? .body : .subheadline) - + if let subName = entry.subName { Text(subName) .foregroundColor(.gray) .font(.subheadline) } } - + HStack { Text(entry.source ?? "Unknown source") - + Spacer() - + Text("DEBRID") .fontWeight(.bold) .padding(3) diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index 4ab9ab2..bc78d43 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -42,7 +42,7 @@ struct MagnetChoiceView: View { showLinkCopyAlert.toggle() } .dynamicAlert( - isPresented: $showLinkCopyAlert , + isPresented: $showLinkCopyAlert, title: "Copied", message: "Download link copied successfully", buttons: [AlertButton("OK")] diff --git a/Ferrite/Views/MainView.swift b/Ferrite/Views/MainView.swift index 3e20efc..3cc2399 100644 --- a/Ferrite/Views/MainView.swift +++ b/Ferrite/Views/MainView.swift @@ -71,7 +71,7 @@ struct MainView: View { } let releaseVersion = String(latestRelease.tagName.dropFirst()) - if releaseVersion > UIApplication.shared.appVersion { + if releaseVersion > Application.shared.appVersion { releaseVersionString = latestRelease.tagName releaseUrlString = latestRelease.htmlUrl showUpdateAlert.toggle() diff --git a/Ferrite/Views/RepresentableViews/WebView.swift b/Ferrite/Views/RepresentableViews/WebView.swift index 89301bb..9ef07fd 100644 --- a/Ferrite/Views/RepresentableViews/WebView.swift +++ b/Ferrite/Views/RepresentableViews/WebView.swift @@ -13,7 +13,7 @@ struct WebView: UIViewRepresentable { func makeUIView(context: Context) -> WKWebView { let webView = WKWebView() - webView.load(URLRequest(url: url)) + let _ = webView.load(URLRequest(url: url)) return webView } diff --git a/Ferrite/Views/SearchResultButtonView.swift b/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift similarity index 97% rename from Ferrite/Views/SearchResultButtonView.swift rename to Ferrite/Views/SearchResultViews/SearchResultButtonView.swift index e4b6e82..cb177e8 100644 --- a/Ferrite/Views/SearchResultButtonView.swift +++ b/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift @@ -25,13 +25,13 @@ struct SearchResultButtonView: View { Button { if debridManager.currentDebridTask == nil { navModel.selectedSearchResult = result - + switch debridManager.matchSearchResult(result: result) { case .full: if debridManager.setSelectedRdResult(result: result) { debridManager.currentDebridTask = Task { await debridManager.fetchRdDownload(searchResult: result) - + if !debridManager.realDebridDownloadUrl.isEmpty { navModel.addToHistory(name: result.title, source: result.source, url: debridManager.realDebridDownloadUrl) navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) @@ -83,9 +83,9 @@ struct SearchResultButtonView: View { newBookmark.magnetLink = result.magnetLink newBookmark.seeders = result.seeders newBookmark.leechers = result.leechers - + existingBookmark = newBookmark - + PersistenceController.shared.save(backgroundContext) } label: { Text("Bookmark") @@ -111,7 +111,7 @@ struct SearchResultButtonView: View { } .onAppear { // Only run a exists request if a bookmark isn't passed to the view - if existingBookmark == nil && !runOnce { + if existingBookmark == nil, !runOnce { let bookmarkRequest = Bookmark.fetchRequest() bookmarkRequest.predicate = NSPredicate( format: "title == %@ AND source == %@ AND magnetLink == %@ AND magnetHash = %@", diff --git a/Ferrite/Views/SearchResultRDView.swift b/Ferrite/Views/SearchResultViews/SearchResultRDView.swift similarity index 100% rename from Ferrite/Views/SearchResultRDView.swift rename to Ferrite/Views/SearchResultViews/SearchResultRDView.swift diff --git a/Ferrite/Views/SourceViews/InstalledSourceView.swift b/Ferrite/Views/SourceViews/Buttons/InstalledSourceButtonView.swift similarity index 95% rename from Ferrite/Views/SourceViews/InstalledSourceView.swift rename to Ferrite/Views/SourceViews/Buttons/InstalledSourceButtonView.swift index 88065f3..3564629 100644 --- a/Ferrite/Views/SourceViews/InstalledSourceView.swift +++ b/Ferrite/Views/SourceViews/Buttons/InstalledSourceButtonView.swift @@ -1,5 +1,5 @@ // -// InstalledSourceView.swift +// InstalledSourceButtonView.swift // Ferrite // // Created by Brian Dashore on 8/5/22. @@ -7,7 +7,7 @@ import SwiftUI -struct InstalledSourceView: View { +struct InstalledSourceButtonView: View { let backgroundContext = PersistenceController.shared.backgroundContext @EnvironmentObject var navModel: NavigationViewModel diff --git a/Ferrite/Views/SourceViews/SourceCatalogView.swift b/Ferrite/Views/SourceViews/Buttons/SourceCatalogButtonView.swift similarity index 100% rename from Ferrite/Views/SourceViews/SourceCatalogView.swift rename to Ferrite/Views/SourceViews/Buttons/SourceCatalogButtonView.swift diff --git a/Ferrite/Views/SourceViews/SourceUpdateButtonView.swift b/Ferrite/Views/SourceViews/Buttons/SourceUpdateButtonView.swift similarity index 100% rename from Ferrite/Views/SourceViews/SourceUpdateButtonView.swift rename to Ferrite/Views/SourceViews/Buttons/SourceUpdateButtonView.swift diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index c57e8f5..aa60a27 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -50,7 +50,7 @@ struct SourcesView: View { if !installedSources.isEmpty { Section(header: InlineHeader("Installed")) { ForEach(installedSources, id: \.self) { source in - InstalledSourceView(installedSource: source) + InstalledSourceButtonView(installedSource: source) } } } -- 2.45.2 From e3e8924547154303d0784a2d70edfd2b32180854 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sun, 30 Oct 2022 14:45:52 -0400 Subject: [PATCH 17/22] Ferrite: Add backups and massive cleanup Backups in Ferrite archive a user's bookmarks, history, source lists, and source names. Sources are not archived due to the size of the backup increasing exponentially. These files use the .feb format to avoid JSON conflicts when opening the file in Ferrite. The backup file can be renamed to JSON for editing at any time. Add the Backport namespace to be used for ported features rather than making a file for every iOS 14 adaptation. Move history and bookmark creation to the PersistenceController rather than individual functions. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 36 ++- .../PersistenceController.swift | 64 +++++ Ferrite/Extensions/FileManager.swift | 14 ++ Ferrite/Extensions/URL.swift | 29 +++ Ferrite/Extensions/View.swift | 20 -- Ferrite/FerriteApp.swift | 3 + Ferrite/Info.plist | 35 +++ Ferrite/Models/BackupModels.swift | 42 ++++ Ferrite/ViewModels/BackupManager.swift | 220 ++++++++++++++++++ Ferrite/ViewModels/NavigationViewModel.swift | 47 +--- Ferrite/Views/BatchChoiceView.swift | 2 +- Ferrite/Views/CommonViews/Backport.swift | 103 ++++++++ .../Modifiers/DynamicAccentColor.swift | 24 -- .../Modifiers/DynamicActionSheet.swift | 46 ---- .../CommonViews/Modifiers/DynamicAlert.swift | 58 ----- Ferrite/Views/ContentView.swift | 21 -- .../LibraryViews/HistoryActionsView.swift | 4 +- .../LibraryViews/HistoryButtonView.swift | 2 +- Ferrite/Views/MagnetChoiceView.swift | 6 +- Ferrite/Views/MainView.swift | 84 +++++-- .../SearchResultButtonView.swift | 4 +- Ferrite/Views/SettingsView.swift | 6 + Ferrite/Views/SettingsViews/BackupsView.swift | 73 ++++++ .../DefaultActionsPickerViews.swift | 4 +- .../SettingsViews/SourceListEditorView.swift | 2 +- .../SourceViews/SourceSettingsView.swift | 2 +- 26 files changed, 705 insertions(+), 246 deletions(-) create mode 100644 Ferrite/Extensions/FileManager.swift create mode 100644 Ferrite/Extensions/URL.swift create mode 100644 Ferrite/Models/BackupModels.swift create mode 100644 Ferrite/ViewModels/BackupManager.swift create mode 100644 Ferrite/Views/CommonViews/Backport.swift delete mode 100644 Ferrite/Views/CommonViews/Modifiers/DynamicAccentColor.swift delete mode 100644 Ferrite/Views/CommonViews/Modifiers/DynamicActionSheet.swift delete mode 100644 Ferrite/Views/CommonViews/Modifiers/DynamicAlert.swift create mode 100644 Ferrite/Views/SettingsViews/BackupsView.swift 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) } } -- 2.45.2 From a774564212aba5f3785ed69880ddd903eb5763a0 Mon Sep 17 00:00:00 2001 From: kingbri Date: Fri, 18 Nov 2022 17:10:42 -0500 Subject: [PATCH 18/22] Ferrite: Improve overall UI - Make history buttons have 2 lines of text to give more context. If a batch is given, the full episode name is shown - Add a "now playing" section to player choices to show what the user will play in the event of a misclick - Make the maximum line limit in search results 4 lines to prevent long title results from taking up the entire cell - Fix light theme appearance with library since the picker and list weren't aligned right Signed-off-by: kingbri --- .../FerriteDB.xcdatamodel/contents | 2 +- .../PersistenceController.swift | 4 +- Ferrite/Models/RealDebridModels.swift | 212 +++++++++--------- Ferrite/ViewModels/NavigationViewModel.swift | 5 + Ferrite/ViewModels/ScrapingViewModel.swift | 2 +- Ferrite/Views/BatchChoiceView.swift | 1 + Ferrite/Views/LibraryView.swift | 5 +- .../Views/LibraryViews/BookmarksView.swift | 2 +- .../LibraryViews/HistoryButtonView.swift | 8 +- Ferrite/Views/MagnetChoiceView.swift | 14 ++ .../SearchResultButtonView.swift | 3 +- 11 files changed, 142 insertions(+), 116 deletions(-) diff --git a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents index 5407971..b89a6fc 100644 --- a/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents +++ b/Ferrite/DataManagement/FerriteDB.xcdatamodeld/FerriteDB.xcdatamodel/contents @@ -1,5 +1,5 @@ - + diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index 85934ec..a3ac7f0 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -114,7 +114,6 @@ struct PersistenceController { 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) @@ -124,7 +123,8 @@ struct PersistenceController { newHistoryEntry.source = entryJson.source newHistoryEntry.name = entryJson.name newHistoryEntry.url = entryJson.url - newHistoryEntry.subName = entryJson.source + newHistoryEntry.subName = entryJson.subName + newHistoryEntry.timeStamp = entryJson.timeStamp ?? Date().timeIntervalSince1970 let historyRequest = History.fetchRequest() historyRequest.predicate = NSPredicate(format: "dateString = %@", historyDateString) diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 208f253..3d6487e 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -11,182 +11,184 @@ import Foundation // MARK: - device code endpoint public struct DeviceCodeResponse: Codable, Sendable { - let deviceCode, userCode: String - let interval, expiresIn: Int - let verificationURL, directVerificationURL: String + let deviceCode, userCode: String + let interval, expiresIn: Int + let verificationURL, directVerificationURL: String - enum CodingKeys: String, CodingKey { - case deviceCode = "device_code" - case userCode = "user_code" - case interval - case expiresIn = "expires_in" - case verificationURL = "verification_url" - case directVerificationURL = "direct_verification_url" - } + enum CodingKeys: String, CodingKey { + case deviceCode = "device_code" + case userCode = "user_code" + case interval + case expiresIn = "expires_in" + case verificationURL = "verification_url" + case directVerificationURL = "direct_verification_url" + } } // MARK: - device credentials endpoint public struct DeviceCredentialsResponse: Codable, Sendable { - let clientID, clientSecret: String? + let clientID, clientSecret: String? - enum CodingKeys: String, CodingKey { - case clientID = "client_id" - case clientSecret = "client_secret" - } + enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + } } // MARK: - token endpoint public struct TokenResponse: Codable, Sendable { - let accessToken: String - let expiresIn: Int - let refreshToken, tokenType: String + let accessToken: String + let expiresIn: Int + let refreshToken, tokenType: String - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" - } + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case tokenType = "token_type" + } } // MARK: - instantAvailability endpoint // Thanks Skitty! public struct InstantAvailabilityResponse: Codable, Sendable { - var data: InstantAvailabilityData? + var data: InstantAvailabilityData? - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() - if let data = try? container.decode(InstantAvailabilityData.self) { - self.data = data - } - } + if let data = try? container.decode(InstantAvailabilityData.self) { + self.data = data + } + } } struct InstantAvailabilityData: Codable, Sendable { - var rd: [[String: InstantAvailabilityInfo]] + var rd: [[String: InstantAvailabilityInfo]] } struct InstantAvailabilityInfo: Codable, Sendable { - var filename: String - var filesize: Int + var filename: String + var filesize: Int } // MARK: - Instant Availability client side structures public struct RealDebridIA: Codable, Hashable, Sendable { - let hash: String - let expiryTimeStamp: Double - var files: [RealDebridIAFile] = [] - var batches: [RealDebridIABatch] = [] + let hash: String + let expiryTimeStamp: Double + var files: [RealDebridIAFile] = [] + var batches: [RealDebridIABatch] = [] } public struct RealDebridIABatch: Codable, Hashable, Sendable { - let files: [RealDebridIABatchFile] + let files: [RealDebridIABatchFile] } public struct RealDebridIABatchFile: Codable, Hashable, Sendable { - let id: Int - let fileName: String + let id: Int + let fileName: String } public struct RealDebridIAFile: Codable, Hashable, Sendable { - let name: String - let batchIndex: Int - let batchFileIndex: Int + let name: String + let batchIndex: Int + let batchFileIndex: Int } public enum RealDebridIAStatus: Codable, Hashable, Sendable { - case full - case partial - case none + case full + case partial + case none } // MARK: - addMagnet endpoint public struct AddMagnetResponse: Codable, Sendable { - let id: String - let uri: String + let id: String + let uri: String } // MARK: - torrentInfo endpoint struct TorrentInfoResponse: Codable, Sendable { - let id, filename, originalFilename, hash: String - let bytes, originalBytes: Int - let host: String - let split, progress: Int - let status, added: String - let files: [TorrentInfoFile] - let links: [String] - let ended: String? - let speed: Int? - let seeders: Int? + let id, filename, originalFilename, hash: String + let bytes, originalBytes: Int + let host: String + let split, progress: Int + let status, added: String + let files: [TorrentInfoFile] + let links: [String] + let ended: String? + let speed: Int? + let seeders: Int? - enum CodingKeys: String, CodingKey { - case id, filename - case originalFilename = "original_filename" - case hash, bytes - case originalBytes = "original_bytes" - case host, split, progress, status, added, files, links, ended, speed, seeders - } + enum CodingKeys: String, CodingKey { + case id, filename + case originalFilename = "original_filename" + case hash, bytes + case originalBytes = "original_bytes" + case host, split, progress, status, added, files, links, ended, speed, seeders + } } struct TorrentInfoFile: Codable, Sendable { - let id: Int - let path: String - let bytes, selected: Int + let id: Int + let path: String + let bytes, selected: Int } public struct UserTorrentsResponse: Codable, Sendable { - let id, filename, hash: String - let bytes: Int - let host: String - let split, progress: Int - let status, added: String - let links: [String] - let speed, seeders: Int? - let ended: String? + let id, filename, hash: String + let bytes: Int + let host: String + let split, progress: Int + let status, added: String + let links: [String] + let speed, seeders: Int? + let ended: String? } // MARK: - unrestrictLink endpoint struct UnrestrictLinkResponse: Codable, Sendable { - let id, filename, mimeType: String - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks, crc: Int - let download: String - let streamable: Int + let id, filename: String + let mimeType: String? + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks, crc: Int + let download: String + let streamable: Int - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, crc, download, streamable - } + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, crc, download, streamable + } } // MARK: - User downloads list public struct UserDownloadsResponse: Codable, Sendable { - let id, filename, mimeType: String - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks: Int - let download: String - let streamable: Int - let generated: String + let id, filename: String + let mimeType: String? + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks: Int + let download: String + let streamable: Int + let generated: String - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, download, streamable, generated - } + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, download, streamable, generated + } } diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index 186dd55..1767d93 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -34,6 +34,10 @@ class NavigationViewModel: ObservableObject { @Published var selectedSearchResult: SearchResult? + // For giving information in magnet choice sheet + @Published var selectedTitle: String? + @Published var selectedBatchTitle: String? + @Published var hideNavigationBar = false @Published var currentChoiceSheet: ChoiceSheetType? @@ -123,6 +127,7 @@ class NavigationViewModel: ObservableObject { public func addToHistory(name: String?, source: String?, url: String?, subName: String? = nil) { let backgroundContext = PersistenceController.shared.backgroundContext + // The timeStamp and date are nil because the create function will make them automatically PersistenceController.shared.createHistory( entryJson: HistoryEntryJson( name: name ?? "", diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index b64ac54..2e217d3 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -901,7 +901,7 @@ class ScrapingViewModel: ObservableObject { } } - await toastModel?.updateToastDescription(responseArray.joined()) + await toastModel?.updateToastDescription(responseArray.joined(separator: " ")) PersistenceController.shared.save(backgroundContext) } diff --git a/Ferrite/Views/BatchChoiceView.swift b/Ferrite/Views/BatchChoiceView.swift index 72fa4d0..0a70ac0 100644 --- a/Ferrite/Views/BatchChoiceView.swift +++ b/Ferrite/Views/BatchChoiceView.swift @@ -28,6 +28,7 @@ struct BatchChoiceView: View { if !debridManager.realDebridDownloadUrl.isEmpty { // The download may complete before this sheet dismisses try? await Task.sleep(seconds: 1) + navModel.selectedBatchTitle = file.name navModel.addToHistory(name: searchResult.title, source: searchResult.source, url: debridManager.realDebridDownloadUrl, subName: file.name) navModel.runDebridAction(urlString: debridManager.realDebridDownloadUrl) } diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 3d1b88e..39c475f 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -36,14 +36,13 @@ struct LibraryView: View { var body: some View { NavView { - VStack(spacing: 0) { + VStack { Picker("Segments", selection: $selectedSegment) { Text("Bookmarks").tag(LibraryPickerSegment.bookmarks) Text("History").tag(LibraryPickerSegment.history) } .pickerStyle(.segmented) - .padding(.horizontal) - .padding(.top) + .padding() switch selectedSegment { case .bookmarks: diff --git a/Ferrite/Views/LibraryViews/BookmarksView.swift b/Ferrite/Views/LibraryViews/BookmarksView.swift index aea6c9e..d668b62 100644 --- a/Ferrite/Views/LibraryViews/BookmarksView.swift +++ b/Ferrite/Views/LibraryViews/BookmarksView.swift @@ -49,7 +49,7 @@ struct BookmarksView: View { PersistenceController.shared.save() } } - .id(UUID()) + .inlinedList() .listStyle(.insetGrouped) } } diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/LibraryViews/HistoryButtonView.swift index a6576e1..b359799 100644 --- a/Ferrite/Views/LibraryViews/HistoryButtonView.swift +++ b/Ferrite/Views/LibraryViews/HistoryButtonView.swift @@ -16,6 +16,9 @@ struct HistoryButtonView: View { var body: some View { Button { + navModel.selectedTitle = entry.name + navModel.selectedBatchTitle = entry.subName + if let url = entry.url { if url.starts(with: "https://") { Task { @@ -37,11 +40,13 @@ struct HistoryButtonView: View { VStack(alignment: .leading, spacing: 3) { Text(entry.name ?? "Unknown title") .font(entry.subName == nil ? .body : .subheadline) + .lineLimit(entry.subName == nil ? 2 : 1) if let subName = entry.subName { Text(subName) .foregroundColor(.gray) .font(.subheadline) + .lineLimit(2) } } @@ -67,10 +72,9 @@ struct HistoryButtonView: View { } .font(.caption) } - .lineLimit(1) .disabledAppearance(navModel.currentChoiceSheet != nil, dimmedOpacity: 0.7, animation: .easeOut(duration: 0.2)) } - .backport.tint(.white) + .backport.tint(.primary) .disableInteraction(navModel.currentChoiceSheet != nil) } } diff --git a/Ferrite/Views/MagnetChoiceView.swift b/Ferrite/Views/MagnetChoiceView.swift index df9e253..1ad0c77 100644 --- a/Ferrite/Views/MagnetChoiceView.swift +++ b/Ferrite/Views/MagnetChoiceView.swift @@ -23,6 +23,20 @@ struct MagnetChoiceView: View { var body: some View { NavView { Form { + Section(header: "Now Playing") { + VStack(alignment: .leading, spacing: 5) { + Text(navModel.selectedTitle ?? "No title") + .font(.callout) + .lineLimit(navModel.selectedBatchTitle == nil ? .max : 1) + + if let batchTitle = navModel.selectedBatchTitle { + Text(batchTitle) + .foregroundColor(.gray) + .font(.subheadline) + } + } + } + if !debridManager.realDebridDownloadUrl.isEmpty { Section(header: "Real Debrid options") { ListRowButtonView("Play on Outplayer", systemImage: "arrow.up.forward.app.fill") { diff --git a/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift b/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift index e1504de..9d9d984 100644 --- a/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift +++ b/Ferrite/Views/SearchResultViews/SearchResultButtonView.swift @@ -7,7 +7,6 @@ import SwiftUI -// BUG: iOS 15 cannot refresh the context menu. Debating using swipe actions or adopting a workaround. struct SearchResultButtonView: View { let backgroundContext = PersistenceController.shared.backgroundContext @@ -25,6 +24,7 @@ struct SearchResultButtonView: View { Button { if debridManager.currentDebridTask == nil { navModel.selectedSearchResult = result + navModel.selectedTitle = result.title switch debridManager.matchSearchResult(result: result) { case .full: @@ -56,6 +56,7 @@ struct SearchResultButtonView: View { Text(result.title ?? "No title") .font(.callout) .fixedSize(horizontal: false, vertical: true) + .lineLimit(4) SearchResultRDView(result: result) } -- 2.45.2 From e063b91f3fbb181f1c177beec2e349ba4c1f01cf Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 19 Nov 2022 11:58:02 -0500 Subject: [PATCH 19/22] Ferrite: Format and cleanup Also add swipe to delete support in source lists Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 10 +- .../PersistenceController.swift | 4 +- Ferrite/Extensions/View.swift | 6 +- Ferrite/Models/RealDebridModels.swift | 228 +++++++++--------- Ferrite/ViewModels/BackupManager.swift | 2 +- Ferrite/ViewModels/DebridManager.swift | 2 +- Ferrite/ViewModels/ScrapingViewModel.swift | 32 +-- Ferrite/ViewModels/SourceManager.swift | 4 +- Ferrite/ViewModels/ToastViewModel.swift | 2 +- Ferrite/Views/CommonViews/AlertButton.swift | 4 +- Ferrite/Views/CommonViews/Backport.swift | 4 +- Ferrite/Views/CommonViews/ListRowViews.swift | 2 +- .../SettingsSourceListView.swift | 7 + 13 files changed, 158 insertions(+), 149 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 36097b0..458439c 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0167DB29293FA900B65783 /* RealDebridModels.swift */; }; 0C0D50E5288DFE7F0035ECC8 /* SourceModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */; }; 0C0D50E7288DFF850035ECC8 /* SourcesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */; }; 0C10848B28BD9A38008F0BA6 /* SettingsAppVersionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */; }; @@ -68,7 +69,6 @@ 0CA148DB288903F000DE2211 /* NavView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C1288903F000DE2211 /* NavView.swift */; }; 0CA148DC288903F000DE2211 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C2288903F000DE2211 /* Assets.xcassets */; }; 0CA148DD288903F000DE2211 /* ScrapingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */; }; - 0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C4288903F000DE2211 /* RealDebridModels.swift */; }; 0CA148DF288903F000DE2211 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 0CA148C6288903F000DE2211 /* Preview Assets.xcassets */; }; 0CA148E0288903F000DE2211 /* FerriteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C7288903F000DE2211 /* FerriteApp.swift */; }; 0CA148E1288903F000DE2211 /* Collection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CA148C9288903F000DE2211 /* Collection.swift */; }; @@ -104,6 +104,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 0C0167DB29293FA900B65783 /* RealDebridModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = ""; }; 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceModels.swift; sourceTree = ""; }; 0C0D50E6288DFF850035ECC8 /* SourcesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesView.swift; sourceTree = ""; }; 0C10848A28BD9A38008F0BA6 /* SettingsAppVersionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAppVersionView.swift; sourceTree = ""; }; @@ -160,7 +161,6 @@ 0CA148C1288903F000DE2211 /* NavView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NavView.swift; sourceTree = ""; }; 0CA148C2288903F000DE2211 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 0CA148C3288903F000DE2211 /* ScrapingViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrapingViewModel.swift; sourceTree = ""; }; - 0CA148C4288903F000DE2211 /* RealDebridModels.swift */ = {isa = PBXFileReference; fileEncoding = 4; indentWidth = 3; lastKnownFileType = sourcecode.swift; path = RealDebridModels.swift; sourceTree = ""; }; 0CA148C6288903F000DE2211 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 0CA148C7288903F000DE2211 /* FerriteApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FerriteApp.swift; sourceTree = ""; }; 0CA148C9288903F000DE2211 /* Collection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Collection.swift; sourceTree = ""; }; @@ -243,7 +243,7 @@ children = ( 0C7ED14028D61BBA009E29AD /* BackupModels.swift */, 0C68135128BC1A7C00FAD890 /* GithubModels.swift */, - 0CA148C4288903F000DE2211 /* RealDebridModels.swift */, + 0C0167DB29293FA900B65783 /* RealDebridModels.swift */, 0C41BC6428C2AEB900B47DD6 /* SearchModels.swift */, 0C95D8D928A55BB6005E22B3 /* SettingsModels.swift */, 0C0D50E4288DFE7F0035ECC8 /* SourceModels.swift */, @@ -605,6 +605,7 @@ 0C4CFC4E28970C8B00AD9FAD /* SourceComplexQuery+CoreDataProperties.swift in Sources */, 0CDCB91828C662640098B513 /* EmptyInstructionView.swift in Sources */, 0CA148E2288903F000DE2211 /* Data.swift in Sources */, + 0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */, 0C57D4CC289032ED008534E8 /* SearchResultRDView.swift in Sources */, 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, 0CA05459288EE9E600850554 /* SourceManager.swift in Sources */, @@ -615,7 +616,6 @@ 0CA05457288EE58200850554 /* SettingsSourceListView.swift in Sources */, 0C78041D28BFB3EA001E8CA3 /* String.swift in Sources */, 0C31133C28B1ABFA004DCB0D /* SourceJsonParser+CoreDataClass.swift in Sources */, - 0CA148DE288903F000DE2211 /* RealDebridModels.swift in Sources */, 0CBC76FF288DAAD00054BE44 /* NavigationViewModel.swift in Sources */, 0C44E2AD28D51C63007711AE /* BackupManager.swift in Sources */, 0C4CFC4D28970C8B00AD9FAD /* SourceComplexQuery+CoreDataClass.swift in Sources */, @@ -767,6 +767,7 @@ INFOPLIST_FILE = Ferrite/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Ferrite; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; @@ -801,6 +802,7 @@ INFOPLIST_FILE = Ferrite/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = Ferrite; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.entertainment"; + INFOPLIST_KEY_LSSupportsOpeningDocumentsInPlace = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; diff --git a/Ferrite/DataManagement/PersistenceController.swift b/Ferrite/DataManagement/PersistenceController.swift index a3ac7f0..6983d0a 100644 --- a/Ferrite/DataManagement/PersistenceController.swift +++ b/Ferrite/DataManagement/PersistenceController.swift @@ -45,7 +45,7 @@ struct PersistenceController { description.setOption(true as NSNumber, forKey: NSPersistentStoreRemoteChangeNotificationPostOptionKey) container.loadPersistentStores { _, error in - if let error = error { + if let error { fatalError("CoreData init error: \(error)") } } @@ -203,7 +203,7 @@ struct PersistenceController { let fetchRequest = NSFetchRequest(entityName: "History") - if let predicate = predicate { + if let predicate { fetchRequest.predicate = predicate } else if range != .allTime { throw HistoryDeleteError.noDate("No history date range was provided and you weren't trying to clear everything! Try relaunching the app?") diff --git a/Ferrite/Extensions/View.swift b/Ferrite/Extensions/View.swift index 16092d5..b47adf9 100644 --- a/Ferrite/Extensions/View.swift +++ b/Ferrite/Extensions/View.swift @@ -25,13 +25,13 @@ extension View { // MARK: Modifiers - func conditionalContextMenu(id: ID, - @ViewBuilder _ internalContent: @escaping () -> InternalContent) -> some View + func conditionalContextMenu(id: some Hashable, + @ViewBuilder _ internalContent: @escaping () -> some View) -> some View { modifier(ConditionalContextMenu(internalContent, id: id)) } - func conditionalId(_ id: ID) -> some View { + func conditionalId(_ id: some Hashable) -> some View { modifier(ConditionalId(id: id)) } diff --git a/Ferrite/Models/RealDebridModels.swift b/Ferrite/Models/RealDebridModels.swift index 3d6487e..b58de72 100644 --- a/Ferrite/Models/RealDebridModels.swift +++ b/Ferrite/Models/RealDebridModels.swift @@ -2,7 +2,7 @@ // RealDebridModels.swift // Ferrite // -// Created by Brian Dashore on 7/5/22. +// Created by Brian Dashore on 11/19/22. // // Structures generated from Quicktype @@ -11,184 +11,184 @@ import Foundation // MARK: - device code endpoint public struct DeviceCodeResponse: Codable, Sendable { - let deviceCode, userCode: String - let interval, expiresIn: Int - let verificationURL, directVerificationURL: String + let deviceCode, userCode: String + let interval, expiresIn: Int + let verificationURL, directVerificationURL: String - enum CodingKeys: String, CodingKey { - case deviceCode = "device_code" - case userCode = "user_code" - case interval - case expiresIn = "expires_in" - case verificationURL = "verification_url" - case directVerificationURL = "direct_verification_url" - } + enum CodingKeys: String, CodingKey { + case deviceCode = "device_code" + case userCode = "user_code" + case interval + case expiresIn = "expires_in" + case verificationURL = "verification_url" + case directVerificationURL = "direct_verification_url" + } } // MARK: - device credentials endpoint public struct DeviceCredentialsResponse: Codable, Sendable { - let clientID, clientSecret: String? + let clientID, clientSecret: String? - enum CodingKeys: String, CodingKey { - case clientID = "client_id" - case clientSecret = "client_secret" - } + enum CodingKeys: String, CodingKey { + case clientID = "client_id" + case clientSecret = "client_secret" + } } // MARK: - token endpoint public struct TokenResponse: Codable, Sendable { - let accessToken: String - let expiresIn: Int - let refreshToken, tokenType: String + let accessToken: String + let expiresIn: Int + let refreshToken, tokenType: String - enum CodingKeys: String, CodingKey { - case accessToken = "access_token" - case expiresIn = "expires_in" - case refreshToken = "refresh_token" - case tokenType = "token_type" - } + enum CodingKeys: String, CodingKey { + case accessToken = "access_token" + case expiresIn = "expires_in" + case refreshToken = "refresh_token" + case tokenType = "token_type" + } } // MARK: - instantAvailability endpoint // Thanks Skitty! public struct InstantAvailabilityResponse: Codable, Sendable { - var data: InstantAvailabilityData? + var data: InstantAvailabilityData? - public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() + public init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() - if let data = try? container.decode(InstantAvailabilityData.self) { - self.data = data - } - } -} - -struct InstantAvailabilityData: Codable, Sendable { - var rd: [[String: InstantAvailabilityInfo]] -} - -struct InstantAvailabilityInfo: Codable, Sendable { - var filename: String - var filesize: Int + if let data = try? container.decode(InstantAvailabilityData.self) { + self.data = data + } + } } // MARK: - Instant Availability client side structures +struct InstantAvailabilityData: Codable, Sendable { + var rd: [[String: InstantAvailabilityInfo]] +} + +struct InstantAvailabilityInfo: Codable, Sendable { + var filename: String + var filesize: Int +} + public struct RealDebridIA: Codable, Hashable, Sendable { - let hash: String - let expiryTimeStamp: Double - var files: [RealDebridIAFile] = [] - var batches: [RealDebridIABatch] = [] + let hash: String + let expiryTimeStamp: Double + var files: [RealDebridIAFile] = [] + var batches: [RealDebridIABatch] = [] } public struct RealDebridIABatch: Codable, Hashable, Sendable { - let files: [RealDebridIABatchFile] + let files: [RealDebridIABatchFile] } public struct RealDebridIABatchFile: Codable, Hashable, Sendable { - let id: Int - let fileName: String + let id: Int + let fileName: String } public struct RealDebridIAFile: Codable, Hashable, Sendable { - let name: String - let batchIndex: Int - let batchFileIndex: Int + let name: String + let batchIndex: Int + let batchFileIndex: Int } public enum RealDebridIAStatus: Codable, Hashable, Sendable { - case full - case partial - case none + case full + case partial + case none } // MARK: - addMagnet endpoint public struct AddMagnetResponse: Codable, Sendable { - let id: String - let uri: String + let id: String + let uri: String } // MARK: - torrentInfo endpoint struct TorrentInfoResponse: Codable, Sendable { - let id, filename, originalFilename, hash: String - let bytes, originalBytes: Int - let host: String - let split, progress: Int - let status, added: String - let files: [TorrentInfoFile] - let links: [String] - let ended: String? - let speed: Int? - let seeders: Int? + let id, filename, originalFilename, hash: String + let bytes, originalBytes: Int + let host: String + let split, progress: Int + let status, added: String + let files: [TorrentInfoFile] + let links: [String] + let ended: String? + let speed: Int? + let seeders: Int? - enum CodingKeys: String, CodingKey { - case id, filename - case originalFilename = "original_filename" - case hash, bytes - case originalBytes = "original_bytes" - case host, split, progress, status, added, files, links, ended, speed, seeders - } + enum CodingKeys: String, CodingKey { + case id, filename + case originalFilename = "original_filename" + case hash, bytes + case originalBytes = "original_bytes" + case host, split, progress, status, added, files, links, ended, speed, seeders + } } struct TorrentInfoFile: Codable, Sendable { - let id: Int - let path: String - let bytes, selected: Int + let id: Int + let path: String + let bytes, selected: Int } public struct UserTorrentsResponse: Codable, Sendable { - let id, filename, hash: String - let bytes: Int - let host: String - let split, progress: Int - let status, added: String - let links: [String] - let speed, seeders: Int? - let ended: String? + let id, filename, hash: String + let bytes: Int + let host: String + let split, progress: Int + let status, added: String + let links: [String] + let speed, seeders: Int? + let ended: String? } // MARK: - unrestrictLink endpoint struct UnrestrictLinkResponse: Codable, Sendable { - let id, filename: String - let mimeType: String? - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks, crc: Int - let download: String - let streamable: Int + let id, filename: String + let mimeType: String? + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks, crc: Int + let download: String + let streamable: Int - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, crc, download, streamable - } + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, crc, download, streamable + } } // MARK: - User downloads list public struct UserDownloadsResponse: Codable, Sendable { - let id, filename: String - let mimeType: String? - let filesize: Int - let link: String - let host: String - let hostIcon: String - let chunks: Int - let download: String - let streamable: Int - let generated: String + let id, filename: String + let mimeType: String? + let filesize: Int + let link: String + let host: String + let hostIcon: String + let chunks: Int + let download: String + let streamable: Int + let generated: String - enum CodingKeys: String, CodingKey { - case id, filename, mimeType, filesize, link, host - case hostIcon = "host_icon" - case chunks, download, streamable, generated - } + enum CodingKeys: String, CodingKey { + case id, filename, mimeType, filesize, link, host + case hostIcon = "host_icon" + case chunks, download, streamable, generated + } } diff --git a/Ferrite/ViewModels/BackupManager.swift b/Ferrite/ViewModels/BackupManager.swift index dbc68c9..045ecf9 100644 --- a/Ferrite/ViewModels/BackupManager.swift +++ b/Ferrite/ViewModels/BackupManager.swift @@ -177,7 +177,7 @@ public class BackupManager: ObservableObject { do { try FileManager.default.removeItem(at: backupUrl) - if let index = index { + if let index { backupUrls.remove(at: index) } else { backupUrls.removeAll(where: { $0 == backupUrl }) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index 42ccccd..f5a85bc 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -82,7 +82,7 @@ public class DebridManager: ObservableObject { } public func matchSearchResult(result: SearchResult?) -> RealDebridIAStatus { - guard let result = result else { + guard let result else { return .none } diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index 2e217d3..fce8a84 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -74,7 +74,7 @@ class ScrapingViewModel: ObservableObject { fallbackUrls: source.fallbackUrls ) - if let data = data, + if let data, let html = String(data: data, encoding: .utf8) { let sourceResults = await scrapeHtml(source: source, baseUrl: baseUrl, html: html) @@ -99,7 +99,7 @@ class ScrapingViewModel: ObservableObject { ) } - if let data = data, + if let data, let rss = String(data: data, encoding: .utf8) { let sourceResults = await scrapeRss(source: source, rss: rss) @@ -145,7 +145,7 @@ class ScrapingViewModel: ObservableObject { fallbackUrls: source.fallbackUrls ) - if let data = data { + if let data { let sourceResults = await scrapeJson(source: source, jsonData: data) tempResults += sourceResults } @@ -170,7 +170,7 @@ class ScrapingViewModel: ObservableObject { return data } - if let fallbackUrls = fallbackUrls { + if let fallbackUrls { for fallbackUrl in fallbackUrls { if let data = await fetchWebsiteData(urlString: fallbackUrl + replacedSearchUrl) { return data @@ -359,7 +359,7 @@ class ScrapingViewModel: ObservableObject { } } } else if - let searchResult = searchResult, + let searchResult, let magnetLink = searchResult.magnetLink, magnetLink.starts(with: "magnet:"), !tempResults.contains(searchResult) @@ -404,7 +404,7 @@ class ScrapingViewModel: ObservableObject { if let magnetLinkParser = jsonParser.magnetLink, existingSearchResult?.magnetLink == nil { let rawLink = result[magnetLinkParser.query.components(separatedBy: ".")].rawValue link = rawLink is NSNull ? nil : String(describing: rawLink) - } else if let magnetHash = magnetHash { + } else if let magnetHash { link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: source.trackers) } @@ -506,7 +506,7 @@ class ScrapingViewModel: ObservableObject { discriminator: magnetLinkParser.discriminator, regexString: magnetLinkParser.regex ) - } else if let magnetHash = magnetHash { + } else if let magnetHash { link = generateMagnetLink(magnetHash: magnetHash, title: title, trackers: source.trackers) } else { continue @@ -586,7 +586,7 @@ class ScrapingViewModel: ObservableObject { parsedValue = try item.getElementsByTag(query).first()?.text() default: // If there's a key/value to lookup the attribute with, query it. Othewise assume the value is in the same attribute - if let discriminator = discriminator { + if let discriminator { let containerElement = try item.getElementsByAttributeValue(discriminator, query).first() parsedValue = try containerElement?.attr(attribute) } else { @@ -596,8 +596,8 @@ class ScrapingViewModel: ObservableObject { } // A capture group must be used in the provided regex - if let regexString = regexString, - let parsedValue = parsedValue, + if let regexString, + let parsedValue, let regexValue = try? Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value { return regexValue @@ -777,8 +777,8 @@ class ScrapingViewModel: ObservableObject { } // A capture group must be used in the provided regex - if let regexString = regexString, - let parsedValue = parsedValue, + if let regexString, + let parsedValue, let regexValue = try? Regex(regexString).firstMatch(in: parsedValue)?.groups[safe: 0]?.value { return regexValue @@ -791,10 +791,10 @@ class ScrapingViewModel: ObservableObject { public func fetchMagnetHash(magnetLink: String? = nil, existingHash: String? = nil) -> String? { var magnetHash: String - if let existingHash = existingHash { + if let existingHash { magnetHash = existingHash } else if - let magnetLink = magnetLink, + let magnetLink, let firstSplit = magnetLink.split(separator: ":")[safe: 3], let tempHash = firstSplit.split(separator: "&")[safe: 0] { @@ -839,11 +839,11 @@ class ScrapingViewModel: ObservableObject { magnetLinkArray.append(magnetHash) - if let title = title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) { + if let title, let encodedTitle = title.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) { magnetLinkArray.append("&dn=\(encodedTitle)") } - if let trackers = trackers { + if let trackers { for trackerUrl in trackers { if URL(string: trackerUrl) != nil, let encodedUrlString = trackerUrl.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) diff --git a/Ferrite/ViewModels/SourceManager.swift b/Ferrite/ViewModels/SourceManager.swift index 8c8d0e2..de8989f 100644 --- a/Ferrite/ViewModels/SourceManager.swift +++ b/Ferrite/ViewModels/SourceManager.swift @@ -71,7 +71,7 @@ public class SourceManager: ObservableObject { // Checks if the current app version is supported by the source func checkAppVersion(minVersion: String?) -> Bool { // If there's no min version, assume that every version is supported - guard let minVersion = minVersion else { + guard let minVersion else { return true } @@ -385,7 +385,7 @@ public class SourceManager: ObservableObject { let (data, _) = try await URLSession.shared.data(for: URLRequest(url: URL(string: sourceUrl)!)) let rawResponse = try JSONDecoder().decode(SourceListJson.self, from: data) - if let existingSourceList = existingSourceList { + if let existingSourceList { existingSourceList.urlString = sourceUrl existingSourceList.name = rawResponse.name existingSourceList.author = rawResponse.author diff --git a/Ferrite/ViewModels/ToastViewModel.swift b/Ferrite/ViewModels/ToastViewModel.swift index e35d052..d3f1704 100644 --- a/Ferrite/ViewModels/ToastViewModel.swift +++ b/Ferrite/ViewModels/ToastViewModel.swift @@ -36,7 +36,7 @@ class ToastViewModel: ObservableObject { @Published var showToast: Bool = false public func updateToastDescription(_ description: String, newToastType: ToastType? = nil) { - if let newToastType = newToastType { + if let newToastType { toastType = newToastType } diff --git a/Ferrite/Views/CommonViews/AlertButton.swift b/Ferrite/Views/CommonViews/AlertButton.swift index 19cf66d..664317a 100644 --- a/Ferrite/Views/CommonViews/AlertButton.swift +++ b/Ferrite/Views/CommonViews/AlertButton.swift @@ -37,7 +37,7 @@ struct AlertButton: Identifiable { } func toActionButton() -> Alert.Button { - if let role = role { + if let role { switch role { case .cancel: return .cancel(Text(label)) @@ -57,7 +57,7 @@ struct AlertButton: Identifiable { @available(iOS 15.0, *) func toButtonRole(_ role: Role?) -> ButtonRole? { - if let role = role { + if let role { switch role { case .destructive: return .destructive diff --git a/Ferrite/Views/CommonViews/Backport.swift b/Ferrite/Views/CommonViews/Backport.swift index e5077e1..d57643a 100644 --- a/Ferrite/Views/CommonViews/Backport.swift +++ b/Ferrite/Views/CommonViews/Backport.swift @@ -32,7 +32,7 @@ extension Backport where Content: View { } }, message: { - if let message = message { + if let message { Text(message) } } @@ -75,7 +75,7 @@ extension Backport where Content: View { button.toButtonView() } } message: { - if let message = message { + if let message { Text(message) } } diff --git a/Ferrite/Views/CommonViews/ListRowViews.swift b/Ferrite/Views/CommonViews/ListRowViews.swift index 80e736f..60cf80a 100644 --- a/Ferrite/Views/CommonViews/ListRowViews.swift +++ b/Ferrite/Views/CommonViews/ListRowViews.swift @@ -64,7 +64,7 @@ struct ListRowTextView: View { Spacer() - if let rightText = rightText { + if let rightText { Text(rightText) } else { Image(systemName: rightSymbol!) diff --git a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift index cc1032e..7ab13d4 100644 --- a/Ferrite/Views/SettingsViews/SettingsSourceListView.swift +++ b/Ferrite/Views/SettingsViews/SettingsSourceListView.swift @@ -64,6 +64,13 @@ struct SettingsSourceListView: View { } } } + .onDelete { offsets in + for index in offsets { + if let list = sourceLists[safe: index] { + PersistenceController.shared.delete(list, context: backgroundContext) + } + } + } } .listStyle(.insetGrouped) .inlinedList() -- 2.45.2 From 102b59ab0ac0f583c83d67e58cf2edfbb84d44e1 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 19 Nov 2022 12:13:29 -0500 Subject: [PATCH 20/22] Scraping: Add source specific errors State what source errored when one occurs. Signed-off-by: kingbri --- Ferrite/ViewModels/ScrapingViewModel.swift | 41 +++++++++++----------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/Ferrite/ViewModels/ScrapingViewModel.swift b/Ferrite/ViewModels/ScrapingViewModel.swift index fce8a84..5554516 100644 --- a/Ferrite/ViewModels/ScrapingViewModel.swift +++ b/Ferrite/ViewModels/ScrapingViewModel.swift @@ -29,6 +29,17 @@ class ScrapingViewModel: ObservableObject { searchResults = newResults } + // Utility function to print source specific errors + func sendSourceError(_ description: String, newToastType: ToastViewModel.ToastType? = nil) async { + let newDescription = "\(currentSourceName ?? "No source given"): \(description)" + await toastModel?.updateToastDescription( + newDescription, + newToastType: newToastType + ) + + print(newDescription) + } + public func scanSources(sources: [Source]) async { if sources.isEmpty { await toastModel?.updateToastDescription("There are no sources to search!", newToastType: .info) @@ -56,8 +67,7 @@ class ScrapingViewModel: ObservableObject { let preferredParser = SourcePreferredParser(rawValue: source.preferredParser) ?? .none guard let encodedQuery = searchText.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else { - await toastModel?.updateToastDescription("Could not process search query, invalid characters present.") - print("Could not process search query, invalid characters present") + await sendSourceError("Could not process search query, invalid characters present.") continue } @@ -255,13 +265,11 @@ class ScrapingViewModel: ObservableObject { case -999: await toastModel?.updateToastDescription("Search cancelled", newToastType: .info) case -1001: - await toastModel?.updateToastDescription("Credentials request timed out") + await sendSourceError("Credentials request timed out") default: - await toastModel?.updateToastDescription("Error in fetching an API credential \(error)") + await sendSourceError("Error in fetching an API credential \(error)") } - print("Error in fetching an API credential \(error)") - return nil } } @@ -269,9 +277,7 @@ class ScrapingViewModel: ObservableObject { // Fetches the data for a URL public func fetchWebsiteData(urlString: String) async -> Data? { guard let url = URL(string: urlString) else { - await toastModel?.updateToastDescription("Source doesn't contain a valid URL, contact the source dev!") - - print("Source doesn't contain a valid URL, contact the source dev!") + await sendSourceError("Source doesn't contain a valid URL, contact the source dev!") return nil } @@ -288,13 +294,11 @@ class ScrapingViewModel: ObservableObject { case -999: await toastModel?.updateToastDescription("Search cancelled", newToastType: .info) case -1001: - await toastModel?.updateToastDescription("Data request timed out. Trying fallback URLs if present.") + await sendSourceError("Data request timed out. Trying fallback URLs if present.") default: - await toastModel?.updateToastDescription("Error in fetching website data \(error)") + await sendSourceError("Error in fetching website data \(error)") } - print("Error in fetching data \(error)") - return nil } } @@ -465,8 +469,7 @@ class ScrapingViewModel: ObservableObject { let document = try SwiftSoup.parse(rss, "", Parser.xmlParser()) items = try document.getElementsByTag(rssParser.items) } catch { - await toastModel?.updateToastDescription("RSS scraping error, couldn't fetch items: \(error)") - print("RSS scraping error, couldn't fetch items: \(error)") + await sendSourceError("RSS scraping error, couldn't fetch items: \(error)") return tempResults } @@ -620,8 +623,7 @@ class ScrapingViewModel: ObservableObject { let document = try SwiftSoup.parse(html) rows = try document.select(htmlParser.rows) } catch { - await toastModel?.updateToastDescription("Scraping error, couldn't fetch rows: \(error)") - print("Scraping error, couldn't fetch rows: \(error)") + await sendSourceError("Scraping error, couldn't fetch rows: \(error)") return tempResults } @@ -753,8 +755,7 @@ class ScrapingViewModel: ObservableObject { tempResults.append(result) } } catch { - await toastModel?.updateToastDescription("Scraping error: \(error)") - print("Scraping error: \(error)") + await sendSourceError("Scraping error: \(error)") continue } @@ -901,7 +902,7 @@ class ScrapingViewModel: ObservableObject { } } - await toastModel?.updateToastDescription(responseArray.joined(separator: " ")) + await sendSourceError(responseArray.joined(separator: " ")) PersistenceController.shared.save(backgroundContext) } -- 2.45.2 From f27578be7ae43760b51ec6dc67f7ccd3f1de2f25 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 19 Nov 2022 12:40:27 -0500 Subject: [PATCH 21/22] Settings: Fix debrid button state Make the button published for iOS 14.3 support Signed-off-by: kingbri --- Ferrite/ViewModels/DebridManager.swift | 2 +- Ferrite/Views/LibraryViews/HistoryButtonView.swift | 2 +- Ferrite/Views/SettingsView.swift | 3 +++ 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index f5a85bc..5758b0d 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -22,7 +22,7 @@ public class DebridManager: ObservableObject { var currentDebridTask: Task? // RealDebrid auth variables - var realDebridEnabled: Bool = false { + @Published var realDebridEnabled: Bool = false { didSet { UserDefaults.standard.set(realDebridEnabled, forKey: "RealDebrid.Enabled") } diff --git a/Ferrite/Views/LibraryViews/HistoryButtonView.swift b/Ferrite/Views/LibraryViews/HistoryButtonView.swift index b359799..9399b16 100644 --- a/Ferrite/Views/LibraryViews/HistoryButtonView.swift +++ b/Ferrite/Views/LibraryViews/HistoryButtonView.swift @@ -37,7 +37,7 @@ struct HistoryButtonView: View { } } label: { VStack(alignment: .leading) { - VStack(alignment: .leading, spacing: 3) { + VStack(alignment: .leading) { Text(entry.name ?? "Unknown title") .font(entry.subName == nil ? .body : .subheadline) .lineLimit(entry.subName == nil ? 2 : 1) diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index 292080a..080445f 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -37,6 +37,9 @@ struct SettingsView: View { } label: { Text(debridManager.realDebridEnabled ? "Logout" : (debridManager.realDebridAuthProcessing ? "Processing" : "Login")) .foregroundColor(debridManager.realDebridEnabled ? .red : .blue) + .onChange(of: debridManager.realDebridEnabled) { changed in + print("Debrid enabled changed to \(changed)") + } } } } -- 2.45.2 From 3f588e249d005398a1530e7272cb8724014db6e5 Mon Sep 17 00:00:00 2001 From: kingbri Date: Sat, 19 Nov 2022 12:42:05 -0500 Subject: [PATCH 22/22] Ferrite: Bump version Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 458439c..66e97e2 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -759,7 +759,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\""; DEVELOPMENT_TEAM = 8A74DBQ6S3; ENABLE_PREVIEWS = YES; @@ -778,7 +778,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.4.0; + MARKETING_VERSION = 0.5.0; PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -794,7 +794,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 9; + CURRENT_PROJECT_VERSION = 10; DEVELOPMENT_ASSET_PATHS = "\"Ferrite/Preview Content\""; DEVELOPMENT_TEAM = 8A74DBQ6S3; ENABLE_PREVIEWS = YES; @@ -813,7 +813,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 0.4.0; + MARKETING_VERSION = 0.5.0; PRODUCT_BUNDLE_IDENTIFIER = me.kingbri.Ferrite; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; -- 2.45.2