mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Allows changing BrowseViewController sort order
This commit is contained in:
@@ -439,6 +439,7 @@
|
||||
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
||||
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
||||
D5DB145B28F9DC5C00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
||||
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB81632B0410BC003F5F8B /* AppSorting.swift */; };
|
||||
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */; };
|
||||
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.swift */; };
|
||||
D5F2F6A92720B7C20081CCF5 /* PatchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */; };
|
||||
@@ -1115,6 +1116,7 @@
|
||||
D5CF56812A0D83F9006D93E2 /* VerificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VerificationError.swift; sourceTree = "<group>"; };
|
||||
D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdatePatronsOperation.swift; sourceTree = "<group>"; };
|
||||
D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ALTLocalizedError.swift; sourceTree = "<group>"; };
|
||||
D5DB81632B0410BC003F5F8B /* AppSorting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSorting.swift; sourceTree = "<group>"; };
|
||||
D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateKnownSourcesOperation.swift; sourceTree = "<group>"; };
|
||||
D5F2F6A82720B7C20081CCF5 /* PatchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatchViewController.swift; sourceTree = "<group>"; };
|
||||
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltStore+Async.swift"; sourceTree = "<group>"; };
|
||||
@@ -1654,6 +1656,7 @@
|
||||
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
|
||||
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
|
||||
D5893F812A141E4900E767CD /* KnownSource.swift */,
|
||||
D5DB81632B0410BC003F5F8B /* AppSorting.swift */,
|
||||
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
|
||||
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
|
||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */,
|
||||
@@ -3158,6 +3161,7 @@
|
||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
|
||||
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
|
||||
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
|
||||
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */,
|
||||
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */,
|
||||
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
|
||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
|
||||
|
||||
@@ -25,6 +25,9 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
|
||||
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
|
||||
|
||||
private var sortButton: UIBarButtonItem?
|
||||
private var preferredAppSorting: AppSorting = UserDefaults.shared.preferredAppSorting
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init?(source: Source?, coder: NSCoder)
|
||||
@@ -94,8 +97,14 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
self.navigationItem.preferredSearchBarPlacement = .inline
|
||||
}
|
||||
|
||||
if #available(iOS 15, *)
|
||||
{
|
||||
self.prepareAppSorting()
|
||||
}
|
||||
|
||||
self.preparePipeline()
|
||||
|
||||
self.updateDataSource()
|
||||
self.update()
|
||||
}
|
||||
|
||||
@@ -119,13 +128,9 @@ private extension BrowseViewController
|
||||
.store(in: &self.cancellables)
|
||||
}
|
||||
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||
func makeFetchRequest() -> NSFetchRequest<StoreApp>
|
||||
{
|
||||
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
let predicate = StoreApp.visibleAppsPredicate
|
||||
@@ -140,6 +145,38 @@ private extension BrowseViewController
|
||||
fetchRequest.predicate = predicate
|
||||
}
|
||||
|
||||
var sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true)]
|
||||
|
||||
switch self.preferredAppSorting
|
||||
{
|
||||
case .default:
|
||||
let descriptor = NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: self.preferredAppSorting.isAscending)
|
||||
sortDescriptors.insert(descriptor, at: 0)
|
||||
|
||||
case .name:
|
||||
// Already sorting by name, no need to prepend additional sort descriptor.
|
||||
break
|
||||
|
||||
case .developer:
|
||||
let descriptor = NSSortDescriptor(keyPath: \StoreApp.developerName, ascending: self.preferredAppSorting.isAscending)
|
||||
sortDescriptors.insert(descriptor, at: 0)
|
||||
|
||||
case .lastUpdated:
|
||||
let descriptor = NSSortDescriptor(keyPath: \StoreApp.latestSupportedVersion?.date, ascending: self.preferredAppSorting.isAscending)
|
||||
sortDescriptors.insert(descriptor, at: 0)
|
||||
}
|
||||
|
||||
fetchRequest.sortDescriptors = sortDescriptors
|
||||
|
||||
return fetchRequest
|
||||
}
|
||||
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||
{
|
||||
let fetchRequest = self.makeFetchRequest()
|
||||
|
||||
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: context)
|
||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||
@@ -192,7 +229,12 @@ private extension BrowseViewController
|
||||
|
||||
func updateDataSource()
|
||||
{
|
||||
self.dataSource.predicate = nil
|
||||
self.dataSource.predicate = nil
|
||||
let fetchRequest = self.makeFetchRequest()
|
||||
|
||||
let context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: context, sectionNameKeyPath: nil, cacheName: nil)
|
||||
self.dataSource.fetchedResultsController = fetchedResultsController
|
||||
}
|
||||
|
||||
func updateSources()
|
||||
@@ -239,6 +281,52 @@ private extension BrowseViewController
|
||||
|
||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||
}
|
||||
|
||||
@available(iOS 15, *)
|
||||
func prepareAppSorting()
|
||||
{
|
||||
if self.preferredAppSorting == .default && self.source == nil
|
||||
{
|
||||
// Only allow `default` sorting if source is non-nil.
|
||||
// Otherwise, fall back to `lastUpdated` sorting.
|
||||
self.preferredAppSorting = .lastUpdated
|
||||
|
||||
// Don't update UserDefaults unless explicitly changed by user.
|
||||
// UserDefaults.shared.preferredAppSorting = .lastUpdated
|
||||
}
|
||||
|
||||
let children = UIDeferredMenuElement.uncached { [weak self] completion in
|
||||
guard let self else { return completion([]) }
|
||||
|
||||
var sortingOptions = AppSorting.allCases
|
||||
if self.source == nil
|
||||
{
|
||||
// Only allow `default` sorting when source is non-nil.
|
||||
sortingOptions = sortingOptions.filter { $0 != .default }
|
||||
}
|
||||
|
||||
let actions = sortingOptions.map { sorting in
|
||||
let state: UIMenuElement.State = (sorting == self.preferredAppSorting) ? .on : .off
|
||||
let action = UIAction(title: sorting.localizedName, image: nil, state: state) { action in
|
||||
self.preferredAppSorting = sorting
|
||||
UserDefaults.shared.preferredAppSorting = sorting // Update separately to save change.
|
||||
|
||||
self.updateDataSource()
|
||||
}
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
completion(actions)
|
||||
}
|
||||
|
||||
let sortMenu = UIMenu(title: NSLocalizedString("Sort by…", comment: ""), options: [.singleSelection], children: [children])
|
||||
let sortIcon = UIImage(systemName: "arrow.up.arrow.down")
|
||||
|
||||
let sortButton = UIBarButtonItem(title: NSLocalizedString("Sort by…", comment: ""), image: sortIcon, primaryAction: nil, menu: sortMenu)
|
||||
self.sortButton = sortButton
|
||||
|
||||
self.navigationItem.rightBarButtonItems = [sortButton, .flexibleSpace()] // flexibleSpace() required to prevent showing full search bar inline.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,6 +53,17 @@ public extension UserDefaults
|
||||
@NSManaged var trustedServerURL: String?
|
||||
@NSManaged var skipPatreonDownloads: Bool
|
||||
|
||||
@nonobjc var preferredAppSorting: AppSorting {
|
||||
get {
|
||||
let sorting = _preferredAppSorting.flatMap { AppSorting(rawValue: $0) } ?? .default
|
||||
return sorting
|
||||
}
|
||||
set {
|
||||
_preferredAppSorting = newValue.rawValue
|
||||
}
|
||||
}
|
||||
@NSManaged @objc(preferredAppSorting) private var _preferredAppSorting: String?
|
||||
|
||||
@nonobjc
|
||||
var activeAppsLimit: Int? {
|
||||
get {
|
||||
@@ -103,6 +114,10 @@ public extension UserDefaults
|
||||
let permissionCheckingDisabled = false
|
||||
#endif
|
||||
|
||||
// Pre-iOS 15 doesn't support custom sorting, so default to sorting by name.
|
||||
// Otherwise, default to `default` sorting (a.k.a. "source order").
|
||||
let preferredAppSorting: AppSorting = if #available(iOS 15, *) { .default } else { .name }
|
||||
|
||||
let defaults = [
|
||||
#keyPath(UserDefaults.isAppLimitDisabled): false,
|
||||
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||
@@ -117,7 +132,8 @@ public extension UserDefaults
|
||||
#keyPath(UserDefaults.ignoreActiveAppsLimit): false,
|
||||
#keyPath(UserDefaults.isMacDirtyCowSupported): isMacDirtyCowSupported
|
||||
#keyPath(UserDefaults.permissionCheckingDisabled): permissionCheckingDisabled,
|
||||
] as [String : Any]
|
||||
#keyPath(UserDefaults._preferredAppSorting): preferredAppSorting.rawValue,
|
||||
] as [String: Any]
|
||||
|
||||
UserDefaults.standard.register(defaults: defaults)
|
||||
UserDefaults.shared.register(defaults: defaults)
|
||||
|
||||
35
AltStoreCore/Types/AppSorting.swift
Normal file
35
AltStoreCore/Types/AppSorting.swift
Normal file
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// AppSorting.swift
|
||||
// AltStoreCore
|
||||
//
|
||||
// Created by Riley Testut on 11/14/23.
|
||||
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum AppSorting: String, CaseIterable
|
||||
{
|
||||
case `default`
|
||||
case name
|
||||
case developer
|
||||
case lastUpdated
|
||||
|
||||
public var localizedName: String {
|
||||
switch self
|
||||
{
|
||||
case .default: return NSLocalizedString("Default", comment: "")
|
||||
case .name: return NSLocalizedString("Name", comment: "")
|
||||
case .developer: return NSLocalizedString("Developer", comment: "")
|
||||
case .lastUpdated: return NSLocalizedString("Last Updated", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
public var isAscending: Bool {
|
||||
switch self
|
||||
{
|
||||
case .default, .name, .developer: return true
|
||||
case .lastUpdated: return false
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user