mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Shows source/category icon next to BrowseViewController’s title
Also tints all navigation bar buttons to match source/category tint color. # Conflicts: # AltStore/Browse/BrowseViewController.swift
This commit is contained in:
@@ -39,10 +39,17 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
private let prototypeCell = AppCardCollectionViewCell(frame: .zero)
|
||||
|
||||
private var sortButton: UIBarButtonItem?
|
||||
private var categoriesMenu: UIMenu?
|
||||
|
||||
private var preferredAppSorting: AppSorting = UserDefaults.shared.preferredAppSorting
|
||||
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
private var titleStackView: UIStackView!
|
||||
private var titleSourceIconView: AppIconImageView!
|
||||
private var titleCategoryIconView: UIImageView!
|
||||
private var titleLabel: UILabel!
|
||||
|
||||
init?(source: Source?, coder: NSCoder)
|
||||
{
|
||||
self.source = source
|
||||
@@ -78,6 +85,7 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
self.collectionView.backgroundColor = .altBackground
|
||||
self.collectionView.alwaysBounceVertical = true
|
||||
|
||||
self.dataSource.searchController.delegate = self
|
||||
self.dataSource.searchController.searchableKeyPaths = [#keyPath(StoreApp.name),
|
||||
#keyPath(StoreApp.subtitle),
|
||||
#keyPath(StoreApp.developerName),
|
||||
@@ -101,33 +109,31 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
})
|
||||
self.collectionView.refreshControl = refreshControl
|
||||
|
||||
if let source = self.source
|
||||
if self.category != nil, #available(iOS 16, *)
|
||||
{
|
||||
let tintColor = source.effectiveTintColor ?? .altPrimary
|
||||
self.view.tintColor = tintColor
|
||||
|
||||
let appearance = NavigationBarAppearance()
|
||||
appearance.configureWithTintColor(tintColor)
|
||||
appearance.configureWithDefaultBackground()
|
||||
|
||||
let edgeAppearance = appearance.copy()
|
||||
edgeAppearance.configureWithTransparentBackground()
|
||||
|
||||
self.navigationItem.standardAppearance = appearance
|
||||
self.navigationItem.scrollEdgeAppearance = edgeAppearance
|
||||
}
|
||||
else if self.category != nil, #available(iOS 16, *)
|
||||
{
|
||||
let menu = UIMenu(children: [
|
||||
let categoriesMenu = UIMenu(children: [
|
||||
UIDeferredMenuElement.uncached { [weak self] completion in
|
||||
let actions = self?.makeCategoryActions() ?? []
|
||||
completion(actions)
|
||||
}
|
||||
])
|
||||
|
||||
self.navigationItem.titleMenuProvider = { _ in menu }
|
||||
self.navigationItem.titleMenuProvider = { _ in categoriesMenu }
|
||||
|
||||
self.categoriesMenu = categoriesMenu
|
||||
}
|
||||
|
||||
self.titleSourceIconView = AppIconImageView(style: .circular)
|
||||
|
||||
self.titleCategoryIconView = UIImageView(frame: .zero)
|
||||
self.titleCategoryIconView.contentMode = .scaleAspectFit
|
||||
|
||||
self.titleLabel = UILabel()
|
||||
self.titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
|
||||
|
||||
self.titleStackView = UIStackView(arrangedSubviews: [self.titleSourceIconView, self.titleCategoryIconView, self.titleLabel])
|
||||
self.titleStackView.spacing = 4
|
||||
|
||||
self.navigationItem.largeTitleDisplayMode = .never
|
||||
|
||||
if #available(iOS 16, *)
|
||||
@@ -142,6 +148,15 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
|
||||
self.preparePipeline()
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
// Source icon = equal width and height
|
||||
self.titleSourceIconView.heightAnchor.constraint(equalToConstant: 26),
|
||||
self.titleSourceIconView.widthAnchor.constraint(equalTo: self.titleSourceIconView.heightAnchor),
|
||||
|
||||
// Category icon = constant height, variable widths
|
||||
self.titleCategoryIconView.heightAnchor.constraint(equalToConstant: 26)
|
||||
])
|
||||
|
||||
self.updateDataSource()
|
||||
self.update()
|
||||
}
|
||||
@@ -152,6 +167,13 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
|
||||
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.navigationController?.navigationBar.tintColor = nil
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrowseViewController
|
||||
@@ -346,13 +368,74 @@ private extension BrowseViewController
|
||||
}
|
||||
}
|
||||
|
||||
if let category = self.category
|
||||
let tintColor: UIColor
|
||||
|
||||
if let source = self.source
|
||||
{
|
||||
tintColor = source.effectiveTintColor?.adjustedForDisplay ?? .altPrimary
|
||||
|
||||
self.title = source.name
|
||||
|
||||
self.titleSourceIconView.backgroundColor = tintColor
|
||||
self.titleSourceIconView.isHidden = false
|
||||
|
||||
self.titleCategoryIconView.isHidden = true
|
||||
|
||||
if let iconURL = source.effectiveIconURL
|
||||
{
|
||||
Nuke.loadImage(with: iconURL, into: self.titleSourceIconView) { result in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error): Logger.main.error("Failed to fetch source icon at \(iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
||||
case .success: self.titleSourceIconView.backgroundColor = .white
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let category = self.category
|
||||
{
|
||||
tintColor = category.tintColor
|
||||
|
||||
self.title = category.localizedName
|
||||
|
||||
let image = UIImage(systemName: category.filledSymbolName)?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
|
||||
self.titleCategoryIconView.image = image
|
||||
self.titleCategoryIconView.isHidden = false
|
||||
|
||||
self.titleSourceIconView.isHidden = true
|
||||
}
|
||||
else
|
||||
{
|
||||
tintColor = .altPrimary
|
||||
|
||||
self.title = NSLocalizedString("Browse", comment: "")
|
||||
|
||||
self.titleSourceIconView.isHidden = true
|
||||
self.titleCategoryIconView.isHidden = true
|
||||
}
|
||||
|
||||
self.titleLabel.text = self.title
|
||||
self.titleStackView.sizeToFit()
|
||||
self.navigationItem.titleView = self.titleStackView
|
||||
|
||||
self.view.tintColor = tintColor
|
||||
|
||||
let appearance = NavigationBarAppearance()
|
||||
appearance.configureWithTintColor(tintColor)
|
||||
appearance.configureWithDefaultBackground()
|
||||
|
||||
let edgeAppearance = appearance.copy()
|
||||
edgeAppearance.configureWithTransparentBackground()
|
||||
|
||||
self.navigationItem.standardAppearance = appearance
|
||||
self.navigationItem.scrollEdgeAppearance = edgeAppearance
|
||||
|
||||
// Necessary to tint UISearchController's inline bar button.
|
||||
self.navigationController?.navigationBar.tintColor = tintColor
|
||||
|
||||
if let sortButton
|
||||
{
|
||||
sortButton.image = sortButton.image?.withTintColor(tintColor, renderingMode: .alwaysOriginal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,11 +461,17 @@ private extension BrowseViewController
|
||||
return StoreCategory(rawValue: rawCategory) ?? .other
|
||||
}
|
||||
|
||||
let sortedCategories = Set(categories).sorted(by: { $0.localizedName.localizedStandardCompare($1.localizedName) == .orderedAscending })
|
||||
var sortedCategories = Set(categories).sorted(by: { $0.localizedName.localizedStandardCompare($1.localizedName) == .orderedAscending })
|
||||
if let otherIndex = sortedCategories.firstIndex(of: .other)
|
||||
{
|
||||
// Ensure "Other" is always last
|
||||
sortedCategories.move(fromOffsets: [otherIndex], toOffset: sortedCategories.count)
|
||||
}
|
||||
|
||||
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
|
||||
let image = UIImage(systemName: category.symbolName)?.withTintColor(category.tintColor, renderingMode: .alwaysOriginal)
|
||||
return UIAction(title: category.localizedName, image: image, state: state) { _ in
|
||||
handler(category)
|
||||
}
|
||||
}
|
||||
@@ -589,6 +678,30 @@ extension BrowseViewController: UIViewControllerPreviewingDelegate
|
||||
}
|
||||
}
|
||||
|
||||
extension BrowseViewController: UISearchControllerDelegate
|
||||
{
|
||||
func willPresentSearchController(_ searchController: UISearchController)
|
||||
{
|
||||
// Hide titleView + menu to ensure search bar is as large as possible.
|
||||
self.navigationItem.titleView = nil
|
||||
|
||||
if #available(iOS 16, *)
|
||||
{
|
||||
self.navigationItem.titleMenuProvider = nil
|
||||
}
|
||||
}
|
||||
|
||||
func willDismissSearchController(_ searchController: UISearchController)
|
||||
{
|
||||
self.navigationItem.titleView = self.titleStackView
|
||||
|
||||
if let categoriesMenu, #available(iOS 16, *)
|
||||
{
|
||||
self.navigationItem.titleMenuProvider = { _ in categoriesMenu }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 17, *)
|
||||
#Preview(traits: .portrait) {
|
||||
DatabaseManager.shared.startForPreview()
|
||||
|
||||
@@ -25,10 +25,24 @@ class AppIconImageView: UIImageView
|
||||
}
|
||||
}
|
||||
|
||||
override func awakeFromNib()
|
||||
init(style: Style)
|
||||
{
|
||||
super.awakeFromNib()
|
||||
self.style = style
|
||||
|
||||
super.init(image: nil)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder)
|
||||
{
|
||||
super.init(coder: coder)
|
||||
|
||||
self.initialize()
|
||||
}
|
||||
|
||||
private func initialize()
|
||||
{
|
||||
self.contentMode = .scaleAspectFill
|
||||
self.clipsToBounds = true
|
||||
self.backgroundColor = .white
|
||||
|
||||
Reference in New Issue
Block a user