mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-14 09:13: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 */; };
|
D5DAE0962804DF430034D8D4 /* UpdatePatronsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DAE0952804DF430034D8D4 /* UpdatePatronsOperation.swift */; };
|
||||||
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
D5DB145A28F9DC5A00A8F606 /* ALTLocalizedError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DB145828F9DC1000A8F606 /* ALTLocalizedError.swift */; };
|
||||||
D5DB145B28F9DC5C00A8F606 /* 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 */; };
|
D5E1E7C128077DE90016FC96 /* UpdateKnownSourcesOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5E1E7C028077DE90016FC96 /* UpdateKnownSourcesOperation.swift */; };
|
||||||
D5E3FB9828FDFAD90034B72C /* NSError+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+AltStore.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
D5F48B4729CCF21B002B52A4 /* AltStore+Async.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AltStore+Async.swift"; sourceTree = "<group>"; };
|
||||||
@@ -1654,6 +1656,7 @@
|
|||||||
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
|
BFB39B5B252BC10E00D1BE50 /* Managed.swift */,
|
||||||
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
|
D5F48B4929CD0B67002B52A4 /* AsyncManaged.swift */,
|
||||||
D5893F812A141E4900E767CD /* KnownSource.swift */,
|
D5893F812A141E4900E767CD /* KnownSource.swift */,
|
||||||
|
D5DB81632B0410BC003F5F8B /* AppSorting.swift */,
|
||||||
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
|
BF66EE8E2501AEBC007EE018 /* ALTAppPermissions.h */,
|
||||||
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
|
BF66EE912501AEBC007EE018 /* ALTAppPermissions.m */,
|
||||||
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */,
|
BF66EE922501AEBC007EE018 /* ALTPatreonBenefitID.h */,
|
||||||
@@ -3158,6 +3161,7 @@
|
|||||||
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
|
D5CA0C4E280E249E00469595 /* AltStore9ToAltStore10.xcmappingmodel in Sources */,
|
||||||
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
|
D51AD27F29356B7B00967AAA /* ALTWrappedError.m in Sources */,
|
||||||
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
|
BF989184250AACFC002ACF50 /* Date+RelativeDate.swift in Sources */,
|
||||||
|
D5DB81642B0410BC003F5F8B /* AppSorting.swift in Sources */,
|
||||||
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */,
|
BF66EE962501AEBC007EE018 /* ALTPatreonBenefitID.m in Sources */,
|
||||||
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
|
BFAECC5A2501B0A400528F27 /* NetworkConnection.swift in Sources */,
|
||||||
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
|
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */,
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
|||||||
|
|
||||||
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
|
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
|
||||||
|
|
||||||
|
private var sortButton: UIBarButtonItem?
|
||||||
|
private var preferredAppSorting: AppSorting = UserDefaults.shared.preferredAppSorting
|
||||||
|
|
||||||
private var cancellables = Set<AnyCancellable>()
|
private var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
init?(source: Source?, coder: NSCoder)
|
init?(source: Source?, coder: NSCoder)
|
||||||
@@ -94,8 +97,14 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
|||||||
self.navigationItem.preferredSearchBarPlacement = .inline
|
self.navigationItem.preferredSearchBarPlacement = .inline
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if #available(iOS 15, *)
|
||||||
|
{
|
||||||
|
self.prepareAppSorting()
|
||||||
|
}
|
||||||
|
|
||||||
self.preparePipeline()
|
self.preparePipeline()
|
||||||
|
|
||||||
|
self.updateDataSource()
|
||||||
self.update()
|
self.update()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,13 +128,9 @@ private extension BrowseViewController
|
|||||||
.store(in: &self.cancellables)
|
.store(in: &self.cancellables)
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
func makeFetchRequest() -> NSFetchRequest<StoreApp>
|
||||||
{
|
{
|
||||||
let fetchRequest = StoreApp.fetchRequest() as 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
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
|
||||||
let predicate = StoreApp.visibleAppsPredicate
|
let predicate = StoreApp.visibleAppsPredicate
|
||||||
@@ -140,6 +145,38 @@ private extension BrowseViewController
|
|||||||
fetchRequest.predicate = predicate
|
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 context = self.source?.managedObjectContext ?? DatabaseManager.shared.viewContext
|
||||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: context)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: context)
|
||||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||||
@@ -192,7 +229,12 @@ private extension BrowseViewController
|
|||||||
|
|
||||||
func updateDataSource()
|
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()
|
func updateSources()
|
||||||
@@ -239,6 +281,52 @@ private extension BrowseViewController
|
|||||||
|
|
||||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
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 trustedServerURL: String?
|
||||||
@NSManaged var skipPatreonDownloads: Bool
|
@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
|
@nonobjc
|
||||||
var activeAppsLimit: Int? {
|
var activeAppsLimit: Int? {
|
||||||
get {
|
get {
|
||||||
@@ -103,6 +114,10 @@ public extension UserDefaults
|
|||||||
let permissionCheckingDisabled = false
|
let permissionCheckingDisabled = false
|
||||||
#endif
|
#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 = [
|
let defaults = [
|
||||||
#keyPath(UserDefaults.isAppLimitDisabled): false,
|
#keyPath(UserDefaults.isAppLimitDisabled): false,
|
||||||
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
#keyPath(UserDefaults.isBackgroundRefreshEnabled): true,
|
||||||
@@ -117,7 +132,8 @@ public extension UserDefaults
|
|||||||
#keyPath(UserDefaults.ignoreActiveAppsLimit): false,
|
#keyPath(UserDefaults.ignoreActiveAppsLimit): false,
|
||||||
#keyPath(UserDefaults.isMacDirtyCowSupported): isMacDirtyCowSupported
|
#keyPath(UserDefaults.isMacDirtyCowSupported): isMacDirtyCowSupported
|
||||||
#keyPath(UserDefaults.permissionCheckingDisabled): permissionCheckingDisabled,
|
#keyPath(UserDefaults.permissionCheckingDisabled): permissionCheckingDisabled,
|
||||||
] as [String : Any]
|
#keyPath(UserDefaults._preferredAppSorting): preferredAppSorting.rawValue,
|
||||||
|
] as [String: Any]
|
||||||
|
|
||||||
UserDefaults.standard.register(defaults: defaults)
|
UserDefaults.standard.register(defaults: defaults)
|
||||||
UserDefaults.shared.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