mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-19 19:53:25 +01:00
Supports filtering apps in BrowseViewController by category
This commit is contained in:
@@ -18,7 +18,14 @@ import Nuke
|
|||||||
class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||||
{
|
{
|
||||||
// Nil == Show apps from all sources.
|
// Nil == Show apps from all sources.
|
||||||
var source: Source?
|
let source: Source?
|
||||||
|
|
||||||
|
private(set) var category: StoreCategory? {
|
||||||
|
didSet {
|
||||||
|
self.updateDataSource()
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private lazy var dataSource = self.makeDataSource()
|
private lazy var dataSource = self.makeDataSource()
|
||||||
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
|
||||||
@@ -33,12 +40,24 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
|||||||
init?(source: Source?, coder: NSCoder)
|
init?(source: Source?, coder: NSCoder)
|
||||||
{
|
{
|
||||||
self.source = source
|
self.source = source
|
||||||
|
self.category = nil
|
||||||
|
|
||||||
|
super.init(coder: coder)
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(category: StoreCategory?, coder: NSCoder)
|
||||||
|
{
|
||||||
|
self.source = nil
|
||||||
|
self.category = category
|
||||||
|
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder)
|
required init?(coder: NSCoder)
|
||||||
{
|
{
|
||||||
|
self.source = nil
|
||||||
|
self.category = nil
|
||||||
|
|
||||||
super.init(coder: coder)
|
super.init(coder: coder)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +110,19 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
|||||||
self.navigationItem.standardAppearance = appearance
|
self.navigationItem.standardAppearance = appearance
|
||||||
self.navigationItem.scrollEdgeAppearance = edgeAppearance
|
self.navigationItem.scrollEdgeAppearance = edgeAppearance
|
||||||
}
|
}
|
||||||
|
else if self.category != nil, #available(iOS 16, *)
|
||||||
|
{
|
||||||
|
let menu = UIMenu(children: [
|
||||||
|
UIDeferredMenuElement.uncached { [weak self] completion in
|
||||||
|
let actions = self?.makeCategoryActions() ?? []
|
||||||
|
completion(actions)
|
||||||
|
}
|
||||||
|
])
|
||||||
|
|
||||||
|
self.navigationItem.titleMenuProvider = { _ in menu }
|
||||||
|
}
|
||||||
|
|
||||||
|
self.navigationItem.largeTitleDisplayMode = .never
|
||||||
|
|
||||||
if #available(iOS 16, *)
|
if #available(iOS 16, *)
|
||||||
{
|
{
|
||||||
@@ -140,6 +172,14 @@ private extension BrowseViewController
|
|||||||
let filterPredicate = NSPredicate(format: "%K == %@", #keyPath(StoreApp._source), source)
|
let filterPredicate = NSPredicate(format: "%K == %@", #keyPath(StoreApp._source), source)
|
||||||
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [filterPredicate, predicate])
|
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [filterPredicate, predicate])
|
||||||
}
|
}
|
||||||
|
else if let category = self.category
|
||||||
|
{
|
||||||
|
let categoryPredicate = switch category {
|
||||||
|
case .other: StoreApp.otherCategoryPredicate
|
||||||
|
default: NSPredicate(format: "%K == %@", #keyPath(StoreApp._category), category.rawValue)
|
||||||
|
}
|
||||||
|
fetchRequest.predicate = NSCompoundPredicate(andPredicateWithSubpredicates: [categoryPredicate, predicate])
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
fetchRequest.predicate = predicate
|
fetchRequest.predicate = predicate
|
||||||
@@ -281,6 +321,57 @@ private extension BrowseViewController
|
|||||||
|
|
||||||
self.placeholderView.activityIndicatorView.stopAnimating()
|
self.placeholderView.activityIndicatorView.stopAnimating()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let category = self.category
|
||||||
|
{
|
||||||
|
self.title = category.localizedName
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.title = NSLocalizedString("Browse", comment: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeCategoryActions() -> [UIAction]
|
||||||
|
{
|
||||||
|
let handler = { [weak self] (category: StoreCategory) in
|
||||||
|
self?.category = category
|
||||||
|
}
|
||||||
|
|
||||||
|
let fetchRequest = NSFetchRequest(entityName: StoreApp.entity().name!) as NSFetchRequest<NSDictionary>
|
||||||
|
fetchRequest.resultType = .dictionaryResultType
|
||||||
|
fetchRequest.returnsDistinctResults = true
|
||||||
|
fetchRequest.propertiesToFetch = [#keyPath(StoreApp._category)]
|
||||||
|
fetchRequest.predicate = StoreApp.visibleAppsPredicate
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let dictionaries = try DatabaseManager.shared.viewContext.fetch(fetchRequest)
|
||||||
|
|
||||||
|
// Keep nil values
|
||||||
|
let categories = dictionaries.map { $0[#keyPath(StoreApp._category)] as? String? ?? nil }.map { rawCategory -> StoreCategory in
|
||||||
|
guard let rawCategory else { return .other }
|
||||||
|
return StoreCategory(rawValue: rawCategory) ?? .other
|
||||||
|
}
|
||||||
|
|
||||||
|
let sortedCategories = Set(categories).sorted(by: { $0.localizedName.localizedStandardCompare($1.localizedName) == .orderedAscending })
|
||||||
|
|
||||||
|
let actions = sortedCategories.map { category in
|
||||||
|
let state: UIAction.State = (category == self.category) ? .on : .off
|
||||||
|
return UIAction(title: category.localizedName, image: UIImage(systemName: category.symbolName), state: state) { _ in
|
||||||
|
handler(category)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return actions
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Logger.main.error("Failed to fetch categories. \(error.localizedDescription, privacy: .public)")
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@available(iOS 15, *)
|
@available(iOS 15, *)
|
||||||
func prepareAppSorting()
|
func prepareAppSorting()
|
||||||
|
|||||||
@@ -594,6 +594,13 @@ public extension StoreApp
|
|||||||
return predicate
|
return predicate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class var otherCategoryPredicate: NSPredicate {
|
||||||
|
let knownCategories = StoreCategory.allCases.lazy.filter { $0 != .other }.map { $0.rawValue }
|
||||||
|
|
||||||
|
let predicate = NSPredicate(format: "%K == nil OR NOT (%K IN %@)", #keyPath(StoreApp._category), #keyPath(StoreApp._category), Array(knownCategories))
|
||||||
|
return predicate
|
||||||
|
}
|
||||||
|
|
||||||
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
@nonobjc class func fetchRequest() -> NSFetchRequest<StoreApp>
|
||||||
{
|
{
|
||||||
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
return NSFetchRequest<StoreApp>(entityName: "StoreApp")
|
||||||
|
|||||||
Reference in New Issue
Block a user