mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
411 lines
14 KiB
Swift
411 lines
14 KiB
Swift
//
|
|
// AppBannerView.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 8/29/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
import AltStoreCore
|
|
import Roxas
|
|
|
|
import Nuke
|
|
|
|
extension AppBannerView
|
|
{
|
|
enum Style
|
|
{
|
|
case app
|
|
case source
|
|
}
|
|
|
|
enum AppAction
|
|
{
|
|
case install
|
|
case open
|
|
case update
|
|
case custom(String)
|
|
}
|
|
}
|
|
|
|
class AppBannerView: RSTNibView
|
|
{
|
|
override var accessibilityLabel: String? {
|
|
get { return self.accessibilityView?.accessibilityLabel }
|
|
set { self.accessibilityView?.accessibilityLabel = newValue }
|
|
}
|
|
|
|
override open var accessibilityAttributedLabel: NSAttributedString? {
|
|
get { return self.accessibilityView?.accessibilityAttributedLabel }
|
|
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
|
|
}
|
|
|
|
override var accessibilityValue: String? {
|
|
get { return self.accessibilityView?.accessibilityValue }
|
|
set { self.accessibilityView?.accessibilityValue = newValue }
|
|
}
|
|
|
|
override open var accessibilityAttributedValue: NSAttributedString? {
|
|
get { return self.accessibilityView?.accessibilityAttributedValue }
|
|
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
|
|
}
|
|
|
|
override open var accessibilityTraits: UIAccessibilityTraits {
|
|
get { return self.accessibilityView?.accessibilityTraits ?? [] }
|
|
set { self.accessibilityView?.accessibilityTraits = newValue }
|
|
}
|
|
|
|
var style: Style = .app
|
|
|
|
private var originalTintColor: UIColor?
|
|
|
|
@IBOutlet var titleLabel: UILabel!
|
|
@IBOutlet var subtitleLabel: UILabel!
|
|
@IBOutlet var iconImageView: AppIconImageView!
|
|
@IBOutlet var button: PillButton!
|
|
@IBOutlet var buttonLabel: UILabel!
|
|
@IBOutlet var betaBadgeView: UIView!
|
|
@IBOutlet var sourceIconImageView: AppIconImageView!
|
|
|
|
@IBOutlet var backgroundEffectView: UIVisualEffectView!
|
|
|
|
@IBOutlet private var vibrancyView: UIVisualEffectView!
|
|
@IBOutlet private var stackView: UIStackView!
|
|
@IBOutlet private var accessibilityView: UIView!
|
|
|
|
@IBOutlet private var iconImageViewHeightConstraint: NSLayoutConstraint!
|
|
|
|
override init(frame: CGRect)
|
|
{
|
|
super.init(frame: frame)
|
|
|
|
self.initialize()
|
|
}
|
|
|
|
required init?(coder: NSCoder)
|
|
{
|
|
super.init(coder: coder)
|
|
|
|
self.initialize()
|
|
}
|
|
|
|
private func initialize()
|
|
{
|
|
self.accessibilityView.accessibilityTraits.formUnion(.button)
|
|
|
|
self.isAccessibilityElement = false
|
|
self.accessibilityElements = [self.accessibilityView, self.button].compactMap { $0 }
|
|
|
|
self.betaBadgeView.isHidden = true
|
|
|
|
self.sourceIconImageView.style = .circular
|
|
self.sourceIconImageView.isHidden = true
|
|
|
|
self.layoutMargins = self.stackView.layoutMargins
|
|
self.insetsLayoutMarginsFromSafeArea = false
|
|
|
|
self.stackView.isLayoutMarginsRelativeArrangement = true
|
|
self.stackView.preservesSuperviewLayoutMargins = true
|
|
}
|
|
|
|
override func tintColorDidChange()
|
|
{
|
|
super.tintColorDidChange()
|
|
|
|
if self.tintAdjustmentMode != .dimmed
|
|
{
|
|
self.originalTintColor = self.tintColor
|
|
}
|
|
|
|
self.update()
|
|
}
|
|
}
|
|
|
|
extension AppBannerView
|
|
{
|
|
func configure(for app: AppProtocol, action: AppAction? = nil, showSourceIcon: Bool = true)
|
|
{
|
|
struct AppValues
|
|
{
|
|
var name: String
|
|
var developerName: String? = nil
|
|
var isBeta: Bool = false
|
|
|
|
init(app: AppProtocol)
|
|
{
|
|
self.name = app.name
|
|
|
|
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
|
|
self.developerName = storeApp.developerName
|
|
|
|
if storeApp.isBeta
|
|
{
|
|
self.name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
|
|
self.isBeta = true
|
|
}
|
|
}
|
|
}
|
|
|
|
self.style = .app
|
|
|
|
let values = AppValues(app: app)
|
|
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
|
|
self.betaBadgeView.isHidden = !values.isBeta
|
|
|
|
if let developerName = values.developerName
|
|
{
|
|
self.subtitleLabel.text = developerName
|
|
self.accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
|
|
}
|
|
else
|
|
{
|
|
self.subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
|
|
self.accessibilityLabel = values.name
|
|
}
|
|
|
|
if let storeApp = app.storeApp, storeApp.isPledgeRequired
|
|
{
|
|
// Always show button label for Patreon apps.
|
|
self.buttonLabel.isHidden = false
|
|
self.buttonLabel.text = storeApp.isPledged ? NSLocalizedString("Pledged", comment: "") : NSLocalizedString("Join Patreon", comment: "")
|
|
}
|
|
else
|
|
{
|
|
self.buttonLabel.isHidden = true
|
|
}
|
|
|
|
if let source = app.storeApp?.source, showSourceIcon
|
|
{
|
|
self.sourceIconImageView.isHidden = false
|
|
self.sourceIconImageView.backgroundColor = source.effectiveTintColor?.adjustedForDisplay ?? .altPrimary
|
|
|
|
if let iconURL = source.effectiveIconURL
|
|
{
|
|
if let image = ImageCache.shared[iconURL]
|
|
{
|
|
self.sourceIconImageView.backgroundColor = .white
|
|
self.sourceIconImageView.image = image.image
|
|
}
|
|
else
|
|
{
|
|
self.sourceIconImageView.image = nil
|
|
|
|
Nuke.loadImage(with: iconURL, into: self.sourceIconImageView) { result in
|
|
switch result
|
|
{
|
|
case .failure(let error): Logger.main.error("Failed to fetch source icon from \(iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
|
case .success: self.sourceIconImageView.backgroundColor = .white // In case icon has transparent background.
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self.sourceIconImageView.isHidden = true
|
|
}
|
|
|
|
let buttonAction: AppAction
|
|
|
|
if let action
|
|
{
|
|
buttonAction = action
|
|
}
|
|
else if let storeApp = app.storeApp
|
|
{
|
|
if let installedApp = storeApp.installedApp
|
|
{
|
|
// App is installed
|
|
|
|
if installedApp.isUpdateAvailable && (!storeApp.isPledgeRequired || storeApp.isPledged)
|
|
{
|
|
buttonAction = .update
|
|
}
|
|
else
|
|
{
|
|
buttonAction = .open
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// App is not installed
|
|
buttonAction = .install
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// App is not from a source, fall back to .open
|
|
buttonAction = .open
|
|
}
|
|
|
|
switch buttonAction
|
|
{
|
|
case .open:
|
|
let buttonTitle = NSLocalizedString("Open", comment: "")
|
|
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
|
self.button.accessibilityLabel = String(format: NSLocalizedString("Open %@", comment: ""), values.name)
|
|
self.button.accessibilityValue = buttonTitle
|
|
|
|
self.button.countdownDate = nil
|
|
|
|
case .update:
|
|
let buttonTitle = NSLocalizedString("Update", comment: "")
|
|
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
|
self.button.accessibilityLabel = String(format: NSLocalizedString("Update %@", comment: ""), values.name)
|
|
self.button.accessibilityValue = buttonTitle
|
|
|
|
self.button.countdownDate = nil
|
|
|
|
case .custom(let buttonTitle):
|
|
self.button.setTitle(buttonTitle, for: .normal)
|
|
self.button.accessibilityLabel = buttonTitle
|
|
self.button.accessibilityValue = buttonTitle
|
|
|
|
self.button.countdownDate = nil
|
|
|
|
case .install:
|
|
if let storeApp = app.storeApp, storeApp.isPledgeRequired
|
|
{
|
|
// Pledge required
|
|
|
|
if storeApp.isPledged
|
|
{
|
|
let buttonTitle = NSLocalizedString("Install", comment: "")
|
|
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
|
self.button.accessibilityLabel = String(format: NSLocalizedString("Install %@", comment: ""), app.name)
|
|
self.button.accessibilityValue = buttonTitle
|
|
}
|
|
else if let amount = storeApp.pledgeAmount, let currencyCode = storeApp.pledgeCurrency, #available(iOS 15, *)
|
|
{
|
|
let price = amount.formatted(.currency(code: currencyCode).presentation(.narrow).precision(.fractionLength(0...2)))
|
|
|
|
let buttonTitle = String(format: NSLocalizedString("%@/mo", comment: ""), price)
|
|
self.button.setTitle(buttonTitle, for: .normal)
|
|
self.button.accessibilityLabel = String(format: NSLocalizedString("Pledge %@ a month", comment: ""), price)
|
|
self.button.accessibilityValue = String(format: NSLocalizedString("%@ a month", comment: ""), price)
|
|
}
|
|
else
|
|
{
|
|
let buttonTitle = NSLocalizedString("Pledge", comment: "")
|
|
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
|
self.button.accessibilityLabel = buttonTitle
|
|
self.button.accessibilityValue = buttonTitle
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Free app
|
|
|
|
let buttonTitle = NSLocalizedString("Free", comment: "")
|
|
self.button.setTitle(buttonTitle.uppercased(), for: .normal)
|
|
self.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), app.name)
|
|
self.button.accessibilityValue = buttonTitle
|
|
}
|
|
|
|
if let versionDate = app.storeApp?.latestSupportedVersion?.date, versionDate > Date()
|
|
{
|
|
self.button.countdownDate = versionDate
|
|
}
|
|
else
|
|
{
|
|
self.button.countdownDate = nil
|
|
}
|
|
}
|
|
|
|
// Ensure PillButton is correct size before assigning progress.
|
|
self.layoutIfNeeded()
|
|
|
|
if let progress = AppManager.shared.installationProgress(for: app), progress.fractionCompleted < 1.0
|
|
{
|
|
self.button.progress = progress
|
|
}
|
|
else
|
|
{
|
|
self.button.progress = nil
|
|
}
|
|
}
|
|
|
|
func configure(for source: Source)
|
|
{
|
|
self.style = .source
|
|
|
|
let subtitle: String
|
|
if let text = source.subtitle
|
|
{
|
|
subtitle = text
|
|
}
|
|
else if let scheme = source.sourceURL.scheme
|
|
{
|
|
subtitle = source.sourceURL.absoluteString.replacingOccurrences(of: scheme + "://", with: "")
|
|
}
|
|
else
|
|
{
|
|
subtitle = source.sourceURL.absoluteString
|
|
}
|
|
|
|
self.titleLabel.text = source.name
|
|
self.subtitleLabel.text = subtitle
|
|
|
|
let tintColor = source.effectiveTintColor ?? .altPrimary
|
|
self.tintColor = tintColor
|
|
|
|
let accessibilityLabel = source.name + "\n" + subtitle
|
|
self.accessibilityLabel = accessibilityLabel
|
|
}
|
|
}
|
|
|
|
private extension AppBannerView
|
|
{
|
|
func update()
|
|
{
|
|
self.clipsToBounds = true
|
|
self.layer.cornerRadius = 22
|
|
|
|
let tintColor = self.originalTintColor ?? self.tintColor
|
|
self.subtitleLabel.textColor = tintColor
|
|
|
|
switch self.style
|
|
{
|
|
case .app:
|
|
self.directionalLayoutMargins.trailing = self.stackView.directionalLayoutMargins.trailing
|
|
|
|
self.iconImageViewHeightConstraint.constant = 60
|
|
self.iconImageView.style = .icon
|
|
|
|
self.titleLabel.textColor = .label
|
|
|
|
self.button.style = .pill
|
|
|
|
self.backgroundEffectView.contentView.backgroundColor = UIColor(resource: .blurTint)
|
|
self.backgroundEffectView.backgroundColor = tintColor
|
|
|
|
case .source:
|
|
self.directionalLayoutMargins.trailing = 20
|
|
|
|
self.iconImageViewHeightConstraint.constant = 44
|
|
self.iconImageView.style = .circular
|
|
|
|
self.titleLabel.textColor = .white
|
|
|
|
self.button.style = .custom
|
|
|
|
self.backgroundEffectView.contentView.backgroundColor = tintColor?.adjustedForDisplay
|
|
self.backgroundEffectView.backgroundColor = nil
|
|
|
|
if let tintColor, tintColor.isTooBright
|
|
{
|
|
let textVibrancyEffect = UIVibrancyEffect(blurEffect: .init(style: .systemChromeMaterialLight), style: .fill)
|
|
self.vibrancyView.effect = textVibrancyEffect
|
|
}
|
|
else
|
|
{
|
|
// Thinner == more dull
|
|
let textVibrancyEffect = UIVibrancyEffect(blurEffect: .init(style: .systemThinMaterialDark), style: .secondaryLabel)
|
|
self.vibrancyView.effect = textVibrancyEffect
|
|
}
|
|
}
|
|
}
|
|
}
|