diff --git a/Ferrite/Classes/Application.swift b/Ferrite/Classes/Application.swift index cf9832f..2f70175 100644 --- a/Ferrite/Classes/Application.swift +++ b/Ferrite/Classes/Application.swift @@ -32,4 +32,6 @@ public class Application { } #endif } + + let osVersion: OperatingSystemVersion = ProcessInfo().operatingSystemVersion } diff --git a/Ferrite/ViewModels/DebridManager.swift b/Ferrite/ViewModels/DebridManager.swift index b4a0255..9dfad81 100644 --- a/Ferrite/ViewModels/DebridManager.swift +++ b/Ferrite/ViewModels/DebridManager.swift @@ -544,6 +544,9 @@ public class DebridManager: ObservableObject { } else { throw RealDebrid.RDError.FailedRequest(description: "Could not fetch your file from RealDebrid's cache or API") } + + // Fetch one more time to add updated data into the RD cloud cache + await fetchRdCloud(bypassTTL: true) } catch { switch error { case RealDebrid.RDError.EmptyTorrents: @@ -640,6 +643,9 @@ public class DebridManager: ObservableObject { } else { throw AllDebrid.ADError.FailedRequest(description: "Could not fetch your file from AllDebrid's cache or API") } + + // Fetch one more time to add updated data into the AD cloud cache + await fetchAdCloud(bypassTTL: true) } catch { await sendDebridError(error, prefix: "AllDebrid download error", cancelString: "Download cancelled") } @@ -650,7 +656,6 @@ public class DebridManager: ObservableObject { if bypassTTL || Date().timeIntervalSince1970 > allDebridCloudTTL { do { allDebridCloudMagnets = try await allDebrid.userMagnets() - realDebridCloudDownloads = try await realDebrid.userDownloads() // 5 minutes allDebridCloudTTL = Date().timeIntervalSince1970 + 300 @@ -685,6 +690,9 @@ public class DebridManager: ObservableObject { throw Premiumize.PMError.FailedRequest(description: "There were no items or files found!") } + // Fetch one more time to add updated data into the PM cloud cache + await fetchPmCloud(bypassTTL: true) + // Add a PM transfer if the item exists if let premiumizeItem = selectedPremiumizeItem { try await premiumize.createTransfer(magnet: premiumizeItem.magnet) diff --git a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift index ef8743e..7cd8189 100644 --- a/Ferrite/Views/ComponentViews/Library/BookmarksView.swift +++ b/Ferrite/Views/ComponentViews/Library/BookmarksView.swift @@ -15,14 +15,15 @@ struct BookmarksView: View { let backgroundContext = PersistenceController.shared.backgroundContext - var bookmarks: FetchedResults + @Binding var searchText: String @State private var viewTask: Task? + @State private var bookmarkPredicate: NSPredicate? var body: some View { - ZStack { - if !bookmarks.isEmpty { - List { + DynamicFetchRequest(predicate: bookmarkPredicate) { (bookmarks: FetchedResults) in + List { + if !bookmarks.isEmpty { ForEach(bookmarks, id: \.self) { bookmark in SearchResultButtonView(result: bookmark.toSearchResult(), existingBookmark: bookmark) } @@ -30,43 +31,53 @@ struct BookmarksView: View { for index in offsets { if let bookmark = bookmarks[safe: index] { PersistenceController.shared.delete(bookmark, context: backgroundContext) - + NotificationCenter.default.post(name: .didDeleteBookmark, object: bookmark) } } } .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() } } - .inlinedList() - .listStyle(.insetGrouped) + } + .inlinedList() + .listStyle(.insetGrouped) + .onAppear { + if debridManager.enabledDebrids.count > 0 { + viewTask = Task { + let magnets = bookmarks.compactMap { + if let magnetHash = $0.magnetHash { + return Magnet(hash: magnetHash, link: $0.magnetLink) + } else { + return nil + } + } + await debridManager.populateDebridIA(magnets) + } + } + } + .onDisappear { + viewTask?.cancel() } } .onAppear { - if debridManager.enabledDebrids.count > 0 { - viewTask = Task { - let magnets = bookmarks.compactMap { - if let magnetHash = $0.magnetHash { - return Magnet(hash: magnetHash, link: $0.magnetLink) - } else { - return nil - } - } - await debridManager.populateDebridIA(magnets) - } - } + applyPredicate() } - .onDisappear { - viewTask?.cancel() + .onChange(of: searchText) { _ in + applyPredicate() } } + + func applyPredicate() { + bookmarkPredicate = searchText.isEmpty ? nil : NSPredicate(format: "title CONTAINS[cd] %@", searchText) + } } diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift index 2476f27..4aed0f7 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/AllDebridCloudView.swift @@ -11,11 +11,15 @@ struct AllDebridCloudView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel + @Binding var searchText: String + @State private var viewTask: Task? var body: some View { DisclosureGroup("Magnets") { - ForEach(debridManager.allDebridCloudMagnets, id: \.id) { magnet in + ForEach(debridManager.allDebridCloudMagnets.filter { + searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) + }, id: \.id) { magnet in Button { if magnet.status == "Ready" && !magnet.links.isEmpty { navModel.resultFromCloud = true @@ -38,8 +42,9 @@ struct AllDebridCloudView: View { } } } else { - debridManager.clearIAValues() let magnet = Magnet(hash: magnet.hash, link: nil) + + // Do not clear old IA values await debridManager.populateDebridIA([magnet]) if debridManager.selectDebridResult(magnet: magnet) { @@ -85,9 +90,3 @@ struct AllDebridCloudView: View { } } } - -struct AllDebridCloudView_Previews: PreviewProvider { - static var previews: some View { - AllDebridCloudView() - } -} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift index 14166cc..96e902c 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/PremiumizeCloudView.swift @@ -12,11 +12,15 @@ struct PremiumizeCloudView: View { @EnvironmentObject var debridManager: DebridManager @EnvironmentObject var navModel: NavigationViewModel + @Binding var searchText: String + @State private var viewTask: Task? var body: some View { DisclosureGroup("Items") { - ForEach(debridManager.premiumizeCloudItems, id: \.id) { item in + ForEach(debridManager.premiumizeCloudItems.filter { + searchText.isEmpty ? true : $0.name.lowercased().contains(searchText.lowercased()) + }, id: \.id) { item in Button(item.name) { Task { navModel.resultFromCloud = true @@ -60,9 +64,3 @@ struct PremiumizeCloudView: View { } } } - -struct PremiumizeCloudView_Previews: PreviewProvider { - static var previews: some View { - PremiumizeCloudView() - } -} diff --git a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift index 79f5d12..45e1239 100644 --- a/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/Cloud/RealDebridCloudView.swift @@ -11,12 +11,16 @@ struct RealDebridCloudView: View { @EnvironmentObject var navModel: NavigationViewModel @EnvironmentObject var debridManager: DebridManager + @Binding var searchText: String + @State private var viewTask: Task? var body: some View { Group { DisclosureGroup("Downloads") { - ForEach(debridManager.realDebridCloudDownloads, id: \.self) { downloadResponse in + ForEach(debridManager.realDebridCloudDownloads.filter { + searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) + }, id: \.self) { downloadResponse in Button(downloadResponse.filename) { navModel.resultFromCloud = true navModel.selectedTitle = downloadResponse.filename @@ -46,7 +50,9 @@ struct RealDebridCloudView: View { } DisclosureGroup("Torrents") { - ForEach(debridManager.realDebridCloudTorrents, id: \.self) { torrentResponse in + ForEach(debridManager.realDebridCloudTorrents.filter { + searchText.isEmpty ? true : $0.filename.lowercased().contains(searchText.lowercased()) + }, id: \.self) { torrentResponse in Button { if torrentResponse.status == "downloaded" && !torrentResponse.links.isEmpty { navModel.resultFromCloud = true @@ -69,8 +75,9 @@ struct RealDebridCloudView: View { } } } else { - debridManager.clearIAValues() let magnet = Magnet(hash: torrentResponse.hash, link: nil) + + // Do not clear old IA values await debridManager.populateDebridIA([magnet]) if debridManager.selectDebridResult(magnet: magnet) { @@ -119,9 +126,3 @@ struct RealDebridCloudView: View { } } } - -struct RealDebridCloudView_Previews: PreviewProvider { - static var previews: some View { - RealDebridCloudView() - } -} diff --git a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift index 8ae90a8..fb435bb 100644 --- a/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift +++ b/Ferrite/Views/ComponentViews/Library/DebridCloudView.swift @@ -10,30 +10,21 @@ import SwiftUI struct DebridCloudView: View { @EnvironmentObject var debridManager: DebridManager + @Binding var searchText: String + var body: some View { - NavView { - VStack { - List { - switch debridManager.selectedDebridType { - case .realDebrid: - RealDebridCloudView() - case .premiumize: - PremiumizeCloudView() - case .allDebrid: - AllDebridCloudView() - case .none: - EmptyView() - } - } - .inlinedList() - .listStyle(.grouped) + List { + switch debridManager.selectedDebridType { + case .realDebrid: + RealDebridCloudView(searchText: $searchText) + case .premiumize: + PremiumizeCloudView(searchText: $searchText) + case .allDebrid: + AllDebridCloudView(searchText: $searchText) + case .none: + EmptyView() } } - } -} - -struct DebridCloudView_Previews: PreviewProvider { - static var previews: some View { - DebridCloudView() + .listStyle(.plain) } } diff --git a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift index 3a52d2d..8e85859 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryButtonView.swift @@ -36,7 +36,7 @@ struct HistoryButtonView: View { toastModel.updateToastDescription("URL invalid. Cannot load this history entry. Please delete it.") } } label: { - VStack(alignment: .leading) { + VStack(alignment: .leading, spacing: 10) { VStack(alignment: .leading) { Text(entry.name ?? "Unknown title") .font(entry.subName == nil ? .body : .subheadline) diff --git a/Ferrite/Views/ComponentViews/Library/HistoryView.swift b/Ferrite/Views/ComponentViews/Library/HistoryView.swift index e53ffb5..ca273db 100644 --- a/Ferrite/Views/ComponentViews/Library/HistoryView.swift +++ b/Ferrite/Views/ComponentViews/Library/HistoryView.swift @@ -10,44 +10,89 @@ 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 + @Binding var searchText: String - 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() } - } + @State private var historyPredicate: NSPredicate? var body: some View { - if !history.isEmpty { + DynamicFetchRequest(predicate: historyPredicate) { (allEntries: FetchedResults) in 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) - } - } + if !history.isEmpty { + ForEach(groupedHistory(history), id: \.self) { historyGroup in + HistorySectionView(allEntries: allEntries, historyGroup: historyGroup) } } } .listStyle(.insetGrouped) } + .onAppear { + applyPredicate() + } + .onChange(of: searchText) { _ in + applyPredicate() + } + } + + func applyPredicate() { + if searchText.isEmpty { + historyPredicate = nil + } else { + let namePredicate = NSPredicate(format: "name CONTAINS[cd] %@", searchText.lowercased()) + let subNamePredicate = NSPredicate(format: "subName CONTAINS[cd] %@", searchText.lowercased()) + historyPredicate = NSCompoundPredicate(type: .or, subpredicates: [namePredicate, subNamePredicate]) + } + } + + func groupedHistory(_ result: FetchedResults) -> [[History]] { + return Dictionary(grouping: result) { (element: History) in + element.dateString ?? "" + } + .values + .sorted { $0[0].date ?? Date() > $1[0].date ?? Date() } + } +} + +struct HistorySectionView: View { + let backgroundContext = PersistenceController.shared.backgroundContext + + var formatter: DateFormatter = .init() + var allEntries: FetchedResults + var historyGroup: [History] + + init(allEntries: FetchedResults, historyGroup: [History]) { + self.allEntries = allEntries + self.historyGroup = historyGroup + + formatter.dateStyle = .medium + formatter.timeStyle = .none + } + + var body: some View { + if compareGroup(historyGroup) > 0 { + Section(header: Text(formatter.string(from: historyGroup[0].date ?? Date()))) { + ForEach(historyGroup, id: \.self) { history in + ForEach(history.entryArray.filter { allEntries.contains($0) }, id: \.self) { entry in + HistoryButtonView(entry: entry) + } + .onDelete { offsets in + removeEntry(at: offsets, from: history) + } + } + } + } + } + + func compareGroup(_ group: [History]) -> Int { + var totalCount = 0 + for history in group { + totalCount += history.entryArray.reduce(0, { result, item in + result + (allEntries.contains { $0.name == item.name || (item.subName.map { return !$0.isEmpty } ?? false && $0.subName == item.subName) } ? 1 : 0) + }) + } + + return totalCount } func removeEntry(at offsets: IndexSet, from history: History) { diff --git a/Ferrite/Views/ContentView.swift b/Ferrite/Views/ContentView.swift index b691ba5..28471f6 100644 --- a/Ferrite/Views/ContentView.swift +++ b/Ferrite/Views/ContentView.swift @@ -19,6 +19,8 @@ struct ContentView: View { sortDescriptors: [] ) var sources: FetchedResults + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true + @State private var selectedSource: Source? { didSet { scrapingModel.filteredSource = selectedSource @@ -72,7 +74,9 @@ struct ContentView: View { SearchResultsView() } .navigationTitle("Search") - .navigationBarTitleDisplayMode(navModel.isSearching ? .inline : .large) + .navigationBarTitleDisplayMode( + navModel.isSearching && Application.shared.osVersion.majorVersion > 14 ? .inline : .large + ) .navigationSearchBar { SearchBar("Search", text: $scrapingModel.searchText, @@ -114,8 +118,8 @@ struct ContentView: View { } .introspectSearchController { searchController in searchController.hidesNavigationBarDuringPresentation = false - searchController.searchBar.autocorrectionType = .no - searchController.searchBar.autocapitalizationType = .none + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none } .toolbar { ToolbarItemGroup(placement: .navigationBarTrailing) { diff --git a/Ferrite/Views/LibraryView.swift b/Ferrite/Views/LibraryView.swift index 07604a7..3c494c1 100644 --- a/Ferrite/Views/LibraryView.swift +++ b/Ferrite/Views/LibraryView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import SwiftUIX struct LibraryView: View { enum LibraryPickerSegment { @@ -19,9 +20,7 @@ struct LibraryView: View { @FetchRequest( entity: Bookmark.entity(), - sortDescriptors: [ - NSSortDescriptor(keyPath: \Bookmark.orderNum, ascending: true) - ] + sortDescriptors: [] ) var bookmarks: FetchedResults @FetchRequest( @@ -31,11 +30,15 @@ struct LibraryView: View { ] ) var history: FetchedResults - @State private var historyEmpty = true + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true @State private var selectedSegment: LibraryPickerSegment = .bookmarks @State private var editMode: EditMode = .inactive + @State private var searchText: String = "" + @State private var isEditingSearch = false + @State private var isSearching = false + var body: some View { NavView { VStack { @@ -48,19 +51,34 @@ struct LibraryView: View { } } .pickerStyle(.segmented) - .padding() + .padding(.horizontal) + .padding(.vertical, 5) switch selectedSegment { case .bookmarks: - BookmarksView(bookmarks: bookmarks) + BookmarksView(searchText: $searchText) case .history: - HistoryView(history: history) + HistoryView(history: history, searchText: $searchText) case .debridCloud: - DebridCloudView() + DebridCloudView(searchText: $searchText) } Spacer() } + .navigationSearchBar { + SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { + isSearching = true + }) + .showsCancelButton(isEditingSearch || isSearching) + .onCancel { + searchText = "" + isSearching = false + } + } + .introspectSearchController { searchController in + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none + } .overlay { switch selectedSegment { case .bookmarks: @@ -80,7 +98,8 @@ struct LibraryView: View { .navigationTitle("Library") .toolbar { ToolbarItem(placement: .navigationBarTrailing) { - HStack { + HStack(spacing: Application.shared.osVersion.majorVersion > 14 ? 10 : 18) { + Spacer() EditButton() switch selectedSegment { @@ -90,6 +109,7 @@ struct LibraryView: View { HistoryActionsView() } } + .animation(.none) } } .environment(\.editMode, $editMode) diff --git a/Ferrite/Views/SettingsView.swift b/Ferrite/Views/SettingsView.swift index ed777c3..f4656c0 100644 --- a/Ferrite/Views/SettingsView.swift +++ b/Ferrite/Views/SettingsView.swift @@ -15,6 +15,8 @@ struct SettingsView: View { let backgroundContext = PersistenceController.shared.backgroundContext + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true + @AppStorage("Updates.AutomaticNotifs") var autoUpdateNotifs = true @AppStorage("Actions.DefaultDebrid") var defaultDebridAction: DefaultDebridActionType = .none @@ -76,6 +78,12 @@ struct SettingsView: View { } } + Section(header: Text("Behavior")) { + Toggle(isOn: $autocorrectSearch) { + Text("Autocorrect search") + } + } + Section(header: Text("Source management")) { NavigationLink("Source lists", destination: SettingsSourceListView()) } diff --git a/Ferrite/Views/SourcesView.swift b/Ferrite/Views/SourcesView.swift index aa60a27..eb9c708 100644 --- a/Ferrite/Views/SourcesView.swift +++ b/Ferrite/Views/SourcesView.swift @@ -15,13 +15,11 @@ struct SourcesView: View { let backgroundContext = PersistenceController.shared.backgroundContext - @FetchRequest( - entity: Source.entity(), - sortDescriptors: [] - ) var sources: FetchedResults + @AppStorage("Behavior.AutocorrectSearch") var autocorrectSearch = true @State private var checkedForSources = false - @State private var isEditing = false + @State private var isEditingSearch = false + @State private var isSearching = false @State private var viewTask: Task? = nil @State private var searchText: String = "" @@ -35,7 +33,7 @@ struct SourcesView: View { ZStack { if !checkedForSources { ProgressView() - } else if sources.isEmpty, sourceManager.availableSources.isEmpty { + } else if installedSources.isEmpty, sourceManager.availableSources.isEmpty { EmptyInstructionView(title: "No Sources", message: "Add a source list in Settings") } else { List { @@ -119,11 +117,18 @@ struct SourcesView: View { } .navigationTitle("Sources") .navigationSearchBar { - SearchBar("Search", text: $searchText, isEditing: $isEditing) - .showsCancelButton(isEditing) - .onCancel { - searchText = "" - } + SearchBar("Search", text: $searchText, isEditing: $isEditingSearch, onCommit: { + isSearching = true + }) + .showsCancelButton(isEditingSearch || isSearching) + .onCancel { + searchText = "" + isSearching = false + } + } + .introspectSearchController { searchController in + searchController.searchBar.autocorrectionType = autocorrectSearch ? .default : .no + searchController.searchBar.autocapitalizationType = autocorrectSearch ? .sentences : .none } } }