diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index ab21a85b..084010f4 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -18,7 +18,14 @@ import Nuke class BrowseViewController: UICollectionViewController, PeekPopPreviewing { // 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 placeholderView = RSTPlaceholderView(frame: .zero) @@ -33,12 +40,24 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing init?(source: Source?, coder: NSCoder) { 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) } required init?(coder: NSCoder) { + self.source = nil + self.category = nil + super.init(coder: coder) } @@ -91,6 +110,19 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing self.navigationItem.standardAppearance = appearance 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, *) { @@ -140,6 +172,14 @@ private extension BrowseViewController let filterPredicate = NSPredicate(format: "%K == %@", #keyPath(StoreApp._source), source) 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 { fetchRequest.predicate = predicate @@ -281,6 +321,57 @@ private extension BrowseViewController 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 + 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, *) func prepareAppSorting() diff --git a/AltStoreCore/Model/StoreApp.swift b/AltStoreCore/Model/StoreApp.swift index 2f1b7c88..6c1a6947 100644 --- a/AltStoreCore/Model/StoreApp.swift +++ b/AltStoreCore/Model/StoreApp.swift @@ -594,6 +594,13 @@ public extension StoreApp 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 { return NSFetchRequest(entityName: "StoreApp")