From f7d2f1ce602ebd1eb399d50fc748c423c7c37d75 Mon Sep 17 00:00:00 2001 From: kingbri Date: Thu, 20 Apr 2023 16:37:26 -0400 Subject: [PATCH] Filters: Add result sorting Sort by seeders, leechers, and size. Also supports ascending and descending options. Signed-off-by: kingbri --- Ferrite.xcodeproj/project.pbxproj | 4 ++ Ferrite/Models/FilterModels.swift | 6 ++ Ferrite/Models/SearchModels.swift | 34 +++++++++ Ferrite/ViewModels/NavigationViewModel.swift | 27 +++++++ .../Filters/SortFilterView.swift | 72 +++++++++++++++++++ .../SearchResult/SearchFilterHeaderView.swift | 4 ++ .../SearchResult/SearchResultsView.swift | 7 +- 7 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 Ferrite/Views/ComponentViews/Filters/SortFilterView.swift diff --git a/Ferrite.xcodeproj/project.pbxproj b/Ferrite.xcodeproj/project.pbxproj index 3c46056..70d93a5 100644 --- a/Ferrite.xcodeproj/project.pbxproj +++ b/Ferrite.xcodeproj/project.pbxproj @@ -21,6 +21,7 @@ 0C1A3E5629C9488C00DA9730 /* CodableWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */; }; 0C2886D22960AC2800D6FC16 /* DebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */; }; 0C2886D72960C50900D6FC16 /* RealDebridCloudView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */; }; + 0C2B028F29E9E61E00DCF127 /* SortFilterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */; }; 0C2D9653299316CC00A504B6 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2D9652299316CC00A504B6 /* Tag.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 */; }; @@ -170,6 +171,7 @@ 0C1A3E5529C9488C00DA9730 /* CodableWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CodableWrapper.swift; sourceTree = ""; }; 0C2886D12960AC2800D6FC16 /* DebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DebridCloudView.swift; sourceTree = ""; }; 0C2886D62960C50900D6FC16 /* RealDebridCloudView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RealDebridCloudView.swift; sourceTree = ""; }; + 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SortFilterView.swift; sourceTree = ""; }; 0C2D9652299316CC00A504B6 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.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 = ""; }; @@ -510,6 +512,7 @@ 0C84FCE429E4B43200B0DFE4 /* SelectedDebridFilterView.swift */, 0C871BDE29994D9D005279AC /* FilterLabelView.swift */, 0C84FCE829E5ADEF00B0DFE4 /* FilterAmountLabelView.swift */, + 0C2B028E29E9E61E00DCF127 /* SortFilterView.swift */, ); path = Filters; sourceTree = ""; @@ -899,6 +902,7 @@ 0C0167DC29293FA900B65783 /* RealDebridModels.swift in Sources */, 0C57D4CC289032ED008534E8 /* SearchResultInfoView.swift in Sources */, 0C42B5982932F6DD008057A0 /* Set.swift in Sources */, + 0C2B028F29E9E61E00DCF127 /* SortFilterView.swift in Sources */, 0C41BC6328C2AD0F00B47DD6 /* SearchResultButtonView.swift in Sources */, 0CA05459288EE9E600850554 /* PluginManager.swift in Sources */, 0C84F4772895BE680074B7C9 /* FerriteDB.xcdatamodeld in Sources */, diff --git a/Ferrite/Models/FilterModels.swift b/Ferrite/Models/FilterModels.swift index 03cf910..611a780 100644 --- a/Ferrite/Models/FilterModels.swift +++ b/Ferrite/Models/FilterModels.swift @@ -12,3 +12,9 @@ enum FilterType { case IA case sort } + +enum SortFilter: String, Hashable, CaseIterable { + case seeders = "Seeders" + case leechers = "Leechers" + case size = "Size" +} diff --git a/Ferrite/Models/SearchModels.swift b/Ferrite/Models/SearchModels.swift index 3fcc861..e2a9598 100644 --- a/Ferrite/Models/SearchModels.swift +++ b/Ferrite/Models/SearchModels.swift @@ -15,6 +15,40 @@ public struct SearchResult: Codable, Hashable, Sendable { let magnet: Magnet let seeders: String? let leechers: String? + + // Converts size to a double + func rawSize() -> Double? { + guard let size else { + return nil + } + + let splitSize = size.split(separator: " ") + + guard + let bytesString = splitSize.first, + let multipliedBytes = Double(bytesString), + let units = splitSize.last + else { + return nil + } + + switch units.lowercased() { + case "gb": + return multipliedBytes * 1e9 + case "gib": + return multipliedBytes * pow(1024, 3) + case "mb": + return multipliedBytes * 1e6 + case "mib": + return multipliedBytes * pow(1024, 2) + case "kb": + return multipliedBytes * 1e3 + case "kib": + return multipliedBytes * 1024 + default: + return nil + } + } } extension ScrapingViewModel { diff --git a/Ferrite/ViewModels/NavigationViewModel.swift b/Ferrite/ViewModels/NavigationViewModel.swift index fdebe78..910ce27 100644 --- a/Ferrite/ViewModels/NavigationViewModel.swift +++ b/Ferrite/ViewModels/NavigationViewModel.swift @@ -50,6 +50,33 @@ public class NavigationViewModel: ObservableObject { // For filters @Published var enabledFilters: Set = [] + @Published var currentSortFilter: SortFilter? + @Published var currentSortOrder: SortOrder = .forward + + public func compareSearchResult(lhs: SearchResult, rhs: SearchResult) -> Bool { + switch currentSortFilter { + case .leechers: + guard let lhsLeechers = lhs.leechers, let rhsLeechers = rhs.leechers else { + return false + } + + return currentSortOrder == .forward ? lhsLeechers > rhsLeechers : lhsLeechers < rhsLeechers + case .seeders: + guard let lhsSeeders = lhs.seeders, let rhsSeeders = rhs.seeders else { + return false + } + + return currentSortOrder == .forward ? lhsSeeders > rhsSeeders : lhsSeeders < rhsSeeders + case .size: + guard let lhsSize = lhs.rawSize(), let rhsSize = rhs.rawSize() else { + return false + } + + return currentSortOrder == .forward ? lhsSize > rhsSize : lhsSize < rhsSize + case .none: + return false + } + } @Published var kodiExpanded: Bool = false diff --git a/Ferrite/Views/ComponentViews/Filters/SortFilterView.swift b/Ferrite/Views/ComponentViews/Filters/SortFilterView.swift new file mode 100644 index 0000000..f803a38 --- /dev/null +++ b/Ferrite/Views/ComponentViews/Filters/SortFilterView.swift @@ -0,0 +1,72 @@ +// +// SortFilterView.swift +// Ferrite +// +// Created by Brian Dashore on 4/14/23. +// + +import SwiftUI + +struct SortFilterView: View { + @EnvironmentObject var navModel: NavigationViewModel + + var body: some View { + Menu { + Button { + navModel.currentSortFilter = nil + navModel.currentSortOrder = .forward + } label: { + HStack { + Text("None") + + if navModel.currentSortFilter == nil { + Image(systemName: "checkmark") + } + } + } + + ForEach(SortFilter.allCases, id: \.self) { sortFilter in + Button { + navModel.currentSortFilter = sortFilter + navModel.currentSortOrder = navModel.currentSortOrder == .forward ? .reverse : .forward + } label: { + HStack { + Text(sortFilter.rawValue) + + if navModel.currentSortFilter == sortFilter { + Image(systemName: navModel.currentSortOrder == .forward ? "chevron.down" : "chevron.up") + } + } + } + } + } label: { + FilterLabelView( + name: "Sort\(navModel.currentSortFilter.map { ": \($0.rawValue)" } ?? "")", + count: navModel.currentSortFilter == nil ? 0 : 1 + ) + } + .id(navModel.currentSortFilter) + .onChange(of: navModel.currentSortFilter) { newFilter in + navModel.currentSortOrder = .forward + if newFilter == nil { + navModel.enabledFilters.remove(.sort) + } else { + navModel.enabledFilters.insert(.sort) + } + } + .onChange(of: navModel.enabledFilters) { newFilters in + if newFilters.isEmpty { + Task { + try? await Task.sleep(seconds: 0.25) + navModel.currentSortFilter = nil + } + } + } + } +} + +struct SortFilterView_Previews: PreviewProvider { + static var previews: some View { + SortFilterView() + } +} diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift index 13e0938..64a2a7a 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchFilterHeaderView.swift @@ -60,6 +60,10 @@ struct SearchFilterHeaderView: View { if !debridManager.enabledDebrids.isEmpty { IAFilterView() } + + // MARK: - Sort filter picker + + SortFilterView() } .padding(.horizontal, verticalSizeClass == .compact ? 65 : 18) .animation(.easeInOut, value: navModel.enabledFilters) diff --git a/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift b/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift index 09d9987..77b748b 100644 --- a/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift +++ b/Ferrite/Views/ComponentViews/SearchResult/SearchResultsView.swift @@ -21,7 +21,12 @@ struct SearchResultsView: View { @Binding var searchText: String var body: some View { - ForEach(scrapingModel.searchResults, id: \.self) { result in + ForEach( + scrapingModel.searchResults.sorted { + navModel.compareSearchResult(lhs: $0, rhs: $1) + } + , id: \.self + ) { result in let debridIAStatus = debridManager.matchMagnetHash(result.magnet) if (pluginManager.filteredInstalledSources.isEmpty ||