mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Revises Entitlements UI on app detail page
This commit is contained in:
@@ -116,6 +116,12 @@ final class AppContentViewController: UITableViewController
|
||||
if self.appDetailCollectionViewHeightConstraint.constant != permissionsHeight && permissionsHeight > 0
|
||||
{
|
||||
self.appDetailCollectionViewHeightConstraint.constant = permissionsHeight
|
||||
|
||||
UIView.performWithoutAnimation {
|
||||
// Update row height without animation.
|
||||
self.tableView.beginUpdates()
|
||||
self.tableView.endUpdates()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ extension AppDetailCollectionViewController
|
||||
private enum Section: Int
|
||||
{
|
||||
case privacy
|
||||
case entitlements
|
||||
case knownEntitlements
|
||||
case unknownEntitlements
|
||||
}
|
||||
|
||||
private enum ElementKind: String
|
||||
@@ -45,7 +46,8 @@ class AppDetailCollectionViewController: UICollectionViewController
|
||||
{
|
||||
let app: StoreApp
|
||||
private let privacyPermissions: [AppPermission]
|
||||
private let entitlementPermissions: [AppPermission]
|
||||
private let knownEntitlementPermissions: [AppPermission]
|
||||
private let unknownEntitlementPermissions: [AppPermission]
|
||||
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
private lazy var privacyDataSource = self.makePrivacyDataSource()
|
||||
@@ -79,7 +81,10 @@ class AppDetailCollectionViewController: UICollectionViewController
|
||||
}
|
||||
|
||||
self.privacyPermissions = app.permissions.filter { $0.type == .privacy }.sorted(by: comparator)
|
||||
self.entitlementPermissions = app.permissions.filter { $0.type == .entitlement }.sorted(by: comparator)
|
||||
|
||||
let entitlementPermissions = app.permissions.lazy.filter { $0.type == .entitlement }
|
||||
self.knownEntitlementPermissions = entitlementPermissions.filter { $0.isKnown }.sorted(by: comparator)
|
||||
self.unknownEntitlementPermissions = entitlementPermissions.filter { !$0.isKnown }.sorted(by: comparator)
|
||||
|
||||
super.init(coder: coder)
|
||||
}
|
||||
@@ -101,11 +106,35 @@ class AppDetailCollectionViewController: UICollectionViewController
|
||||
self.collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "PrivacyCell")
|
||||
self.collectionView.register(UICollectionViewListCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||
|
||||
self.headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, elementKind, indexPath) in
|
||||
self.headerRegistration = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [weak self] (headerView, elementKind, indexPath) in
|
||||
var configuration = UIListContentConfiguration.plainHeader()
|
||||
configuration.text = NSLocalizedString("Entitlements", comment: "")
|
||||
configuration.directionalLayoutMargins.bottom = 15
|
||||
|
||||
switch Section(rawValue: indexPath.section)!
|
||||
{
|
||||
case .privacy: break
|
||||
case .knownEntitlements:
|
||||
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).withSymbolicTraits(.traitBold) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3)
|
||||
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||
configuration.text = NSLocalizedString("Entitlements", comment: "")
|
||||
|
||||
configuration.secondaryTextProperties.font = UIFont.preferredFont(forTextStyle: .callout)
|
||||
configuration.textToSecondaryTextVerticalPadding = 8
|
||||
configuration.secondaryText = NSLocalizedString("Entitlements are additional permissions that grant access to certain system services, including potentially sensitive information. We recommend reviewing these before sideloading.", comment: "")
|
||||
|
||||
case .unknownEntitlements:
|
||||
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitBold) ?? UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
configuration.textProperties.font = UIFont(descriptor: fontDescriptor, size: 0.0)
|
||||
configuration.text = NSLocalizedString("Other Entitlements", comment: "")
|
||||
|
||||
let action = UIAction(image: UIImage(systemName: "questionmark.circle")) { _ in
|
||||
self?.showUnknownEntitlementsAlert()
|
||||
}
|
||||
|
||||
let helpButton = UIButton(primaryAction: action)
|
||||
let customAccessory = UICellAccessory.customView(configuration: .init(customView: helpButton, placement: .trailing(), tintColor: self?.app.tintColor ?? .altPrimary))
|
||||
headerView.accessories = [customAccessory]
|
||||
}
|
||||
|
||||
headerView.contentConfiguration = configuration
|
||||
headerView.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
||||
}
|
||||
@@ -120,7 +149,7 @@ private extension AppDetailCollectionViewController
|
||||
{
|
||||
func makeLayout() -> UICollectionViewCompositionalLayout
|
||||
{
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [privacyPermissions, entitlementPermissions] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
let layout = UICollectionViewCompositionalLayout(sectionProvider: { [privacyPermissions, knownEntitlementPermissions, unknownEntitlementPermissions] (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
||||
guard let section = Section(rawValue: sectionIndex) else { return nil }
|
||||
switch section
|
||||
{
|
||||
@@ -137,15 +166,18 @@ private extension AppDetailCollectionViewController
|
||||
layoutSection.interGroupSpacing = 10
|
||||
return layoutSection
|
||||
|
||||
case .entitlements:
|
||||
guard !entitlementPermissions.isEmpty else { return nil }
|
||||
|
||||
case .knownEntitlements where !knownEntitlementPermissions.isEmpty: fallthrough
|
||||
case .unknownEntitlements where !unknownEntitlementPermissions.isEmpty:
|
||||
var configuration = UICollectionLayoutListConfiguration(appearance: .plain)
|
||||
configuration.headerMode = .supplementary
|
||||
configuration.showsSeparators = false
|
||||
configuration.backgroundColor = .altBackground
|
||||
|
||||
let layoutSection = NSCollectionLayoutSection.list(using: configuration, layoutEnvironment: layoutEnvironment)
|
||||
layoutSection.contentInsets.top = 4
|
||||
return layoutSection
|
||||
|
||||
case .knownEntitlements, .unknownEntitlements: return nil
|
||||
}
|
||||
})
|
||||
|
||||
@@ -186,49 +218,61 @@ private extension AppDetailCollectionViewController
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makeEntitlementsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
||||
|
||||
func makeEntitlementsDataSource() -> RSTCompositeCollectionViewDataSource<AppPermission>
|
||||
{
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: self.entitlementPermissions)
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, appPermission, indexPath) in
|
||||
let knownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.knownEntitlementPermissions)
|
||||
let unknownEntitlementsDataSource = RSTArrayCollectionViewDataSource(items: self.unknownEntitlementPermissions)
|
||||
|
||||
let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [knownEntitlementsDataSource, unknownEntitlementsDataSource])
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, appPermission, _) in
|
||||
let cell = cell as! UICollectionViewListCell
|
||||
let tintColor = self?.app.tintColor ?? .altPrimary
|
||||
|
||||
var content = cell.defaultContentConfiguration()
|
||||
content.image = UIImage(systemName: appPermission.effectiveSymbolName)
|
||||
content.text = appPermission.localizedDisplayName
|
||||
content.secondaryText = appPermission.permission.rawValue
|
||||
content.secondaryTextProperties.color = .secondaryLabel
|
||||
|
||||
let tintColor = self?.app.tintColor ?? .altPrimary
|
||||
content.imageProperties.tintColor = tintColor
|
||||
|
||||
if let name = appPermission.localizedName
|
||||
if appPermission.isKnown
|
||||
{
|
||||
content.text = name
|
||||
content.secondaryText = appPermission.permission.rawValue
|
||||
content.secondaryTextProperties.color = UIColor.secondaryLabel
|
||||
}
|
||||
else
|
||||
{
|
||||
content.text = appPermission.permission.rawValue
|
||||
content.image = UIImage(systemName: appPermission.effectiveSymbolName)
|
||||
content.imageProperties.tintColor = tintColor
|
||||
|
||||
if #available(iOS 15.4, *) /*, let self */ // Capturing self leads to strong-reference cycle.
|
||||
{
|
||||
let detailAccessory = UICellAccessory.detail(options: .init(tintColor: tintColor)) {
|
||||
self?.showPermissionAlert(for: appPermission)
|
||||
}
|
||||
cell.accessories = [detailAccessory]
|
||||
}
|
||||
}
|
||||
|
||||
cell.contentConfiguration = content
|
||||
cell.backgroundConfiguration = UIBackgroundConfiguration.clear()
|
||||
|
||||
if #available(iOS 15.4, *) /*, let self */ // Capturing self leads to strong-reference cycle.
|
||||
{
|
||||
let detailAccessory = UICellAccessory.detail(displayed: .always, options: .init(tintColor: tintColor)) {
|
||||
let alertController = UIAlertController(title: appPermission.localizedDisplayName, message: appPermission.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self?.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
cell.accessories = [detailAccessory]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppDetailCollectionViewController
|
||||
{
|
||||
func showPermissionAlert(for permission: AppPermission)
|
||||
{
|
||||
let alertController = UIAlertController(title: permission.localizedDisplayName, message: permission.localizedDescription, preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
func showUnknownEntitlementsAlert()
|
||||
{
|
||||
let alertController = UIAlertController(title: NSLocalizedString("Other Entitlements", comment: ""), message: NSLocalizedString("AltStore does not have detailed information for these entitlements.", comment: ""), preferredStyle: .alert)
|
||||
alertController.addAction(.ok)
|
||||
self.present(alertController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDetailCollectionViewController
|
||||
{
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
|
||||
@@ -28,21 +28,33 @@ public protocol ALTAppPermission: RawRepresentable<String>, Hashable
|
||||
var symbolName: String? { get }
|
||||
|
||||
var localizedName: String? { get }
|
||||
var synthesizedName: String? { get } // Kupo!
|
||||
|
||||
var localizedDescription: String? { get }
|
||||
|
||||
// Default implementations
|
||||
// Convenience properties with default implementations.
|
||||
// Would normally just be in extension, except that crashes Swift 5.8 compiler ¯\_(ツ)_/¯
|
||||
var isKnown: Bool { get }
|
||||
var effectiveSymbolName: String { get }
|
||||
var localizedDisplayName: String { get }
|
||||
}
|
||||
|
||||
public extension ALTAppPermission
|
||||
{
|
||||
var isKnown: Bool {
|
||||
// Assume all known permissions have non-nil localizedDescriptions.
|
||||
return self.localizedDescription != nil
|
||||
}
|
||||
|
||||
var effectiveSymbolName: String { self.symbolName ?? "lock" }
|
||||
|
||||
var localizedDisplayName: String {
|
||||
return self.localizedName ?? self.rawValue
|
||||
return self.localizedName ?? self.synthesizedName ?? self.rawValue
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public extension ALTAppPermission
|
||||
{
|
||||
func isEqual(_ permission: any ALTAppPermission) -> Bool
|
||||
{
|
||||
guard let permission = permission as? Self else { return false }
|
||||
@@ -61,6 +73,8 @@ public struct UnknownAppPermission: ALTAppPermission
|
||||
public var symbolName: String? { nil }
|
||||
|
||||
public var localizedName: String? { nil }
|
||||
public var synthesizedName: String? { nil }
|
||||
|
||||
public var localizedDescription: String? { nil }
|
||||
|
||||
public var rawValue: String
|
||||
@@ -77,6 +91,25 @@ extension ALTEntitlement: ALTAppPermission
|
||||
public var symbolName: String? { nil }
|
||||
|
||||
public var localizedName: String? { nil }
|
||||
|
||||
public var synthesizedName: String? {
|
||||
// Attempt to convert last component of entitlement to human-readable string.
|
||||
// e.g. com.apple.developer.kernel.increased-memory-limit -> "Increased Memory Limit"
|
||||
let components = self.rawValue.components(separatedBy: ".")
|
||||
guard let rawName = components.last else { return nil }
|
||||
|
||||
let words = rawName.components(separatedBy: "-").map { word in
|
||||
switch word.lowercased()
|
||||
{
|
||||
case "carplay": return NSLocalizedString("CarPlay", comment: "")
|
||||
default: return word.localizedCapitalized
|
||||
}
|
||||
}
|
||||
|
||||
let synthesizedName = words.joined(separator: " ")
|
||||
return synthesizedName
|
||||
}
|
||||
|
||||
public var localizedDescription: String? { nil }
|
||||
}
|
||||
|
||||
@@ -99,6 +132,7 @@ extension ALTAppPrivacyPermission: ALTAppPermission
|
||||
}
|
||||
}
|
||||
|
||||
public var synthesizedName: String? { nil }
|
||||
public var localizedDescription: String? { nil }
|
||||
|
||||
public var symbolName: String? {
|
||||
@@ -123,5 +157,7 @@ extension ALTAppBackgroundMode: ALTAppPermission
|
||||
public var symbolName: String? { nil }
|
||||
|
||||
public var localizedName: String? { nil }
|
||||
public var synthesizedName: String? { nil }
|
||||
|
||||
public var localizedDescription: String? { nil }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user