mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-10 15:23:27 +01:00
@@ -0,0 +1,225 @@
|
||||
//
|
||||
// AppContentViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/22/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import RoxasUIKit
|
||||
import OSLog
|
||||
#if canImport(Logging)
|
||||
import Logging
|
||||
#endif
|
||||
|
||||
import Nuke
|
||||
|
||||
extension AppContentViewController {
|
||||
private enum Row: Int, CaseIterable {
|
||||
case subtitle
|
||||
case screenshots
|
||||
case description
|
||||
case versionDescription
|
||||
case permissions
|
||||
}
|
||||
}
|
||||
|
||||
final class AppContentViewController: UITableViewController {
|
||||
var app: StoreApp!
|
||||
|
||||
private lazy var screenshotsDataSource = self.makeScreenshotsDataSource()
|
||||
private lazy var permissionsDataSource = self.makePermissionsDataSource()
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .medium
|
||||
dateFormatter.timeStyle = .none
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
private lazy var byteCountFormatter: ByteCountFormatter = {
|
||||
let formatter = ByteCountFormatter()
|
||||
return formatter
|
||||
}()
|
||||
|
||||
@IBOutlet private var subtitleLabel: UILabel!
|
||||
@IBOutlet private var descriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionDescriptionTextView: CollapsingTextView!
|
||||
@IBOutlet private var versionLabel: UILabel!
|
||||
@IBOutlet private var versionDateLabel: UILabel!
|
||||
@IBOutlet private var sizeLabel: UILabel!
|
||||
|
||||
@IBOutlet private var screenshotsCollectionView: UICollectionView!
|
||||
@IBOutlet private var permissionsCollectionView: UICollectionView!
|
||||
|
||||
var preferredScreenshotSize: CGSize? {
|
||||
let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
|
||||
let aspectRatio: CGFloat = 16.0 / 9.0 // Hardcoded for now.
|
||||
|
||||
let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2)
|
||||
|
||||
let itemWidth = width / 1.5
|
||||
let itemHeight = itemWidth * aspectRatio
|
||||
|
||||
return CGSize(width: itemWidth, height: itemHeight)
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
tableView.contentInset.bottom = 20
|
||||
|
||||
screenshotsCollectionView.dataSource = screenshotsDataSource
|
||||
screenshotsCollectionView.prefetchDataSource = screenshotsDataSource
|
||||
|
||||
permissionsCollectionView.dataSource = permissionsDataSource
|
||||
|
||||
subtitleLabel.text = app.subtitle
|
||||
descriptionTextView.text = app.localizedDescription
|
||||
|
||||
if let version = app.latestVersion {
|
||||
versionDescriptionTextView.text = version.localizedDescription
|
||||
versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.version)
|
||||
versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: dateFormatter)
|
||||
sizeLabel.text = byteCountFormatter.string(fromByteCount: version.size)
|
||||
} else {
|
||||
versionDescriptionTextView.text = nil
|
||||
versionLabel.text = nil
|
||||
versionDateLabel.text = nil
|
||||
sizeLabel.text = byteCountFormatter.string(fromByteCount: 0)
|
||||
}
|
||||
|
||||
descriptionTextView.maximumNumberOfLines = 5
|
||||
descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
|
||||
versionDescriptionTextView.maximumNumberOfLines = 3
|
||||
versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
||||
}
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
guard var size = preferredScreenshotSize else { return }
|
||||
size.height = min(size.height, screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
||||
|
||||
let layout = screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout
|
||||
layout.itemSize = size
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
|
||||
guard segue.identifier == "showPermission" else { return }
|
||||
|
||||
guard let cell = sender as? UICollectionViewCell, let indexPath = permissionsCollectionView.indexPath(for: cell) else { return }
|
||||
|
||||
let permission = permissionsDataSource.item(at: indexPath)
|
||||
|
||||
let maximumWidth = view.bounds.width - 20
|
||||
|
||||
let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController
|
||||
permissionPopoverViewController.permission = permission
|
||||
permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true
|
||||
|
||||
let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||
permissionPopoverViewController.preferredContentSize = size
|
||||
|
||||
permissionPopoverViewController.popoverPresentationController?.delegate = self
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame
|
||||
permissionPopoverViewController.popoverPresentationController?.sourceView = permissionsCollectionView
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppContentViewController {
|
||||
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage> {
|
||||
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: app.screenshotURLs as [NSURL])
|
||||
dataSource.cellConfigurationHandler = { cell, _, _ in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.image = nil
|
||||
cell.imageView.isIndicatingActivity = true
|
||||
}
|
||||
dataSource.prefetchHandler = { imageURL, _, completionHandler in
|
||||
RSTAsyncBlockOperation { operation in
|
||||
let request = ImageRequest(url: imageURL as URL, processor: .screenshot)
|
||||
ImagePipeline.shared.loadImage(with: request, progress: nil, completion: { response, error in
|
||||
guard !operation.isCancelled else { return operation.finish() }
|
||||
|
||||
if let image = response?.image {
|
||||
completionHandler(image, nil)
|
||||
} else {
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { cell, image, _, error in
|
||||
let cell = cell as! ScreenshotCollectionViewCell
|
||||
cell.imageView.isIndicatingActivity = false
|
||||
cell.imageView.image = image
|
||||
|
||||
if let error = error {
|
||||
os_log("Error loading image: %@", type: .error, error.localizedDescription)
|
||||
}
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission> {
|
||||
let dataSource = RSTArrayCollectionViewDataSource(items: app.permissions)
|
||||
dataSource.cellConfigurationHandler = { cell, permission, _ in
|
||||
let cell = cell as! PermissionCollectionViewCell
|
||||
cell.button.setImage(permission.type.icon, for: .normal)
|
||||
cell.button.tintColor = .label
|
||||
cell.textLabel.text = permission.type.localizedShortName ?? permission.type.localizedName
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppContentViewController {
|
||||
@objc func toggleCollapsingSection(_ sender: UIButton) {
|
||||
let indexPath: IndexPath
|
||||
|
||||
switch sender {
|
||||
case descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
||||
case versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0)
|
||||
default: return
|
||||
}
|
||||
|
||||
// Disable animations to prevent some potentially strange ones.
|
||||
UIView.performWithoutAnimation {
|
||||
self.tableView.reloadRows(at: [indexPath], with: .none)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppContentViewController {
|
||||
override func tableView(_: UITableView, willDisplay cell: UITableViewCell, forRowAt _: IndexPath) {
|
||||
cell.tintColor = app.tintColor
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
switch Row.allCases[indexPath.row] {
|
||||
case .screenshots:
|
||||
guard let size = preferredScreenshotSize else { return 0.0 }
|
||||
return size.height
|
||||
|
||||
case .permissions:
|
||||
guard !app.permissions.isEmpty else { return 0.0 }
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
|
||||
default:
|
||||
return super.tableView(tableView, heightForRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension AppContentViewController: UIPopoverPresentationControllerDelegate {
|
||||
func adaptivePresentationStyle(for _: UIPresentationController, traitCollection _: UITraitCollection) -> UIModalPresentationStyle {
|
||||
.none
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// AppContentViewControllerCells.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/24/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@objc
|
||||
final class PermissionCollectionViewCell: UICollectionViewCell {
|
||||
@IBOutlet var button: UIButton!
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
button.layer.cornerRadius = button.bounds.midY
|
||||
}
|
||||
|
||||
override func tintColorDidChange() {
|
||||
super.tintColorDidChange()
|
||||
|
||||
button.backgroundColor = tintColor.withAlphaComponent(0.15)
|
||||
textLabel.textColor = tintColor
|
||||
}
|
||||
}
|
||||
|
||||
@objc
|
||||
final class AppContentTableViewCell: UITableViewCell {
|
||||
override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {
|
||||
// Ensure cell is laid out so it will report correct size.
|
||||
layoutIfNeeded()
|
||||
|
||||
let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,505 @@
|
||||
//
|
||||
// AppViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/22/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
import RoxasUIKit
|
||||
|
||||
import Nuke
|
||||
|
||||
@objc
|
||||
@objcMembers
|
||||
public final class AppViewController: UIViewController {
|
||||
var app: StoreApp!
|
||||
|
||||
private var contentViewController: AppContentViewController!
|
||||
private var contentViewControllerShadowView: UIView!
|
||||
|
||||
private var blurAnimator: UIViewPropertyAnimator?
|
||||
private var navigationBarAnimator: UIViewPropertyAnimator?
|
||||
|
||||
private var contentSizeObservation: NSKeyValueObservation?
|
||||
|
||||
@IBOutlet private var scrollView: UIScrollView!
|
||||
@IBOutlet private var contentView: UIView!
|
||||
|
||||
@IBOutlet private var bannerView: AppBannerView!
|
||||
|
||||
@IBOutlet private var backButton: UIButton!
|
||||
@IBOutlet private var backButtonContainerView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var backgroundAppIconImageView: UIImageView!
|
||||
@IBOutlet private var backgroundBlurView: UIVisualEffectView!
|
||||
|
||||
@IBOutlet private var navigationBarTitleView: UIView!
|
||||
@IBOutlet private var navigationBarDownloadButton: PillButton!
|
||||
@IBOutlet private var navigationBarAppIconImageView: UIImageView!
|
||||
@IBOutlet private var navigationBarAppNameLabel: UILabel!
|
||||
|
||||
private var _shouldResetLayout = false
|
||||
private var _backgroundBlurEffect: UIBlurEffect?
|
||||
private var _backgroundBlurTintColor: UIColor?
|
||||
|
||||
private var _preferredStatusBarStyle: UIStatusBarStyle = .default
|
||||
|
||||
public override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
_preferredStatusBarStyle
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
navigationBarTitleView.sizeToFit()
|
||||
navigationItem.titleView = navigationBarTitleView
|
||||
|
||||
contentViewControllerShadowView = UIView()
|
||||
contentViewControllerShadowView.backgroundColor = .white
|
||||
contentViewControllerShadowView.layer.cornerRadius = 38
|
||||
contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor
|
||||
contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1)
|
||||
contentViewControllerShadowView.layer.shadowRadius = 10
|
||||
contentViewControllerShadowView.layer.shadowOpacity = 0.3
|
||||
contentViewController.view.superview?.insertSubview(contentViewControllerShadowView, at: 0)
|
||||
|
||||
contentView.addGestureRecognizer(scrollView.panGestureRecognizer)
|
||||
|
||||
contentViewController.view.layer.cornerRadius = 38
|
||||
contentViewController.view.layer.masksToBounds = true
|
||||
|
||||
contentViewController.tableView.panGestureRecognizer.require(toFail: scrollView.panGestureRecognizer)
|
||||
contentViewController.tableView.showsVerticalScrollIndicator = false
|
||||
|
||||
// Bring to front so the scroll indicators are visible.
|
||||
view.bringSubviewToFront(scrollView)
|
||||
scrollView.isUserInteractionEnabled = false
|
||||
|
||||
bannerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93)
|
||||
bannerView.backgroundEffectView.effect = UIBlurEffect(style: .regular)
|
||||
bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
bannerView.iconImageView.image = nil
|
||||
bannerView.iconImageView.tintColor = app.tintColor
|
||||
bannerView.button.tintColor = app.tintColor
|
||||
bannerView.tintColor = app.tintColor
|
||||
|
||||
bannerView.configure(for: app)
|
||||
bannerView.accessibilityTraits.remove(.button)
|
||||
|
||||
bannerView.button.addTarget(self, action: #selector(AppViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||
|
||||
backButtonContainerView.tintColor = app.tintColor
|
||||
|
||||
navigationController?.navigationBar.tintColor = app.tintColor
|
||||
navigationBarDownloadButton.tintColor = app.tintColor
|
||||
navigationBarAppNameLabel.text = app.name
|
||||
navigationBarAppIconImageView.tintColor = app.tintColor
|
||||
|
||||
contentSizeObservation = contentViewController.tableView.observe(\.contentSize) { [weak self] _, _ in
|
||||
self?.view.setNeedsLayout()
|
||||
self?.view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
update()
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
|
||||
_backgroundBlurEffect = backgroundBlurView.effect as? UIBlurEffect
|
||||
_backgroundBlurTintColor = backgroundBlurView.contentView.backgroundColor
|
||||
|
||||
// Load Images
|
||||
for imageView in [bannerView.iconImageView!, backgroundAppIconImageView!, navigationBarAppIconImageView!] {
|
||||
imageView.isIndicatingActivity = true
|
||||
|
||||
Nuke.loadImage(with: app.iconURL, options: .shared, into: imageView, progress: nil) { [weak imageView] response, _ in
|
||||
if response?.image != nil {
|
||||
imageView?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
prepareBlur()
|
||||
|
||||
// Update blur immediately.
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
|
||||
transitionCoordinator?.animate(alongsideTransition: { _ in
|
||||
self.hideNavigationBar()
|
||||
}, completion: nil)
|
||||
}
|
||||
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
_shouldResetLayout = true
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
|
||||
public override func viewWillDisappear(_ animated: Bool) {
|
||||
super.viewWillDisappear(animated)
|
||||
|
||||
// Guard against "dismissing" when presenting via 3D Touch pop.
|
||||
guard self.navigationController != nil else { return }
|
||||
|
||||
// Store reference since self.navigationController will be nil after disappearing.
|
||||
let navigationController = self.navigationController
|
||||
navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up.
|
||||
|
||||
transitionCoordinator?.animate(alongsideTransition: { _ in
|
||||
self.showNavigationBar(for: navigationController)
|
||||
}, completion: { context in
|
||||
if !context.isCancelled {
|
||||
self.showNavigationBar(for: navigationController)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
if navigationController == nil {
|
||||
resetNavigationBarAnimation()
|
||||
}
|
||||
}
|
||||
|
||||
public override func prepare(for segue: UIStoryboardSegue, sender _: Any?) {
|
||||
guard segue.identifier == "embedAppContentViewController" else { return }
|
||||
|
||||
contentViewController = segue.destination as? AppContentViewController
|
||||
contentViewController.app = app
|
||||
|
||||
if #available(iOS 15, *) {
|
||||
// Fix navigation bar + tab bar appearance on iOS 15.
|
||||
self.setContentScrollView(self.scrollView)
|
||||
self.navigationItem.scrollEdgeAppearance = self.navigationController?.navigationBar.standardAppearance
|
||||
}
|
||||
}
|
||||
|
||||
public override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
if _shouldResetLayout {
|
||||
// Various events can cause UI to mess up, so reset affected components now.
|
||||
|
||||
if navigationController?.topViewController == self {
|
||||
hideNavigationBar()
|
||||
}
|
||||
|
||||
prepareBlur()
|
||||
|
||||
// Reset navigation bar animation, and create a new one later in this method if necessary.
|
||||
resetNavigationBarAnimation()
|
||||
|
||||
_shouldResetLayout = false
|
||||
}
|
||||
|
||||
let statusBarHeight = UIApplication.shared.statusBarFrame.height
|
||||
let cornerRadius = contentViewControllerShadowView.layer.cornerRadius
|
||||
|
||||
let inset = 12 as CGFloat
|
||||
let padding = 20 as CGFloat
|
||||
|
||||
let backButtonSize = backButton.sizeThatFits(CGSize(width: 1000, height: 1000))
|
||||
var backButtonFrame = CGRect(x: inset, y: statusBarHeight,
|
||||
width: backButtonSize.width + 20, height: backButtonSize.height + 20)
|
||||
|
||||
var headerFrame = CGRect(x: inset, y: 0, width: view.bounds.width - inset * 2, height: bannerView.bounds.height)
|
||||
var contentFrame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.height)
|
||||
var backgroundIconFrame = CGRect(x: 0, y: 0, width: view.bounds.width, height: view.bounds.width)
|
||||
|
||||
let minimumHeaderY = backButtonFrame.maxY + 8
|
||||
|
||||
let minimumContentY = minimumHeaderY + headerFrame.height + padding
|
||||
let maximumContentY = view.bounds.width * 0.667
|
||||
|
||||
// A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur.
|
||||
let minimumBlurFraction = 0.3 as CGFloat
|
||||
|
||||
contentFrame.origin.y = maximumContentY - scrollView.contentOffset.y
|
||||
headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height
|
||||
|
||||
// Stretch the app icon image to fill additional vertical space if necessary.
|
||||
let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height)
|
||||
backgroundIconFrame.size.height = height
|
||||
|
||||
let blurThreshold = 0 as CGFloat
|
||||
if scrollView.contentOffset.y < blurThreshold {
|
||||
// Determine how much to lessen blur by.
|
||||
|
||||
let range = 75 as CGFloat
|
||||
let difference = -scrollView.contentOffset.y
|
||||
|
||||
let fraction = min(difference, range) / range
|
||||
|
||||
let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction
|
||||
blurAnimator?.fractionComplete = fractionComplete
|
||||
} else {
|
||||
// Set blur to default.
|
||||
|
||||
blurAnimator?.fractionComplete = minimumBlurFraction
|
||||
}
|
||||
|
||||
// Animate navigation bar.
|
||||
let showNavigationBarThreshold = (maximumContentY - minimumContentY) + backButtonFrame.origin.y
|
||||
if scrollView.contentOffset.y > showNavigationBarThreshold {
|
||||
if navigationBarAnimator == nil {
|
||||
prepareNavigationBarAnimation()
|
||||
}
|
||||
|
||||
let difference = scrollView.contentOffset.y - showNavigationBarThreshold
|
||||
let range = (headerFrame.height + padding) - (navigationController?.navigationBar.bounds.height ?? view.safeAreaInsets.top)
|
||||
|
||||
let fractionComplete = min(difference, range) / range
|
||||
navigationBarAnimator?.fractionComplete = fractionComplete
|
||||
} else {
|
||||
resetNavigationBarAnimation()
|
||||
}
|
||||
|
||||
let beginMovingBackButtonThreshold = (maximumContentY - minimumContentY)
|
||||
if scrollView.contentOffset.y > beginMovingBackButtonThreshold {
|
||||
let difference = scrollView.contentOffset.y - beginMovingBackButtonThreshold
|
||||
backButtonFrame.origin.y -= difference
|
||||
}
|
||||
|
||||
let pinContentToTopThreshold = maximumContentY
|
||||
if scrollView.contentOffset.y > pinContentToTopThreshold {
|
||||
contentFrame.origin.y = 0
|
||||
backgroundIconFrame.origin.y = 0
|
||||
|
||||
let difference = scrollView.contentOffset.y - pinContentToTopThreshold
|
||||
contentViewController.tableView.contentOffset.y = difference
|
||||
} else {
|
||||
// Keep content table view's content offset at the top.
|
||||
contentViewController.tableView.contentOffset.y = 0
|
||||
}
|
||||
|
||||
// Keep background app icon centered in gap between top of content and top of screen.
|
||||
backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2
|
||||
|
||||
// Set frames.
|
||||
contentViewController.view.superview?.frame = contentFrame
|
||||
bannerView.frame = headerFrame
|
||||
backgroundAppIconImageView.frame = backgroundIconFrame
|
||||
backgroundBlurView.frame = backgroundIconFrame
|
||||
backButtonContainerView.frame = backButtonFrame
|
||||
|
||||
contentViewControllerShadowView.frame = contentViewController.view.frame
|
||||
|
||||
backButtonContainerView.layer.cornerRadius = backButtonContainerView.bounds.midY
|
||||
|
||||
scrollView.scrollIndicatorInsets.top = statusBarHeight
|
||||
|
||||
// Adjust content offset + size.
|
||||
let contentOffset = scrollView.contentOffset
|
||||
|
||||
var contentSize = contentViewController.tableView.contentSize
|
||||
contentSize.height += maximumContentY
|
||||
|
||||
scrollView.contentSize = contentSize
|
||||
scrollView.contentOffset = contentOffset
|
||||
|
||||
bannerView.backgroundEffectView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
public override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
super.traitCollectionDidChange(previousTraitCollection)
|
||||
_shouldResetLayout = true
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.blurAnimator?.stopAnimation(true)
|
||||
self.navigationBarAnimator?.stopAnimation(true)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppViewController {
|
||||
final class func makeAppViewController(app: StoreApp) -> AppViewController {
|
||||
let storyboard = UIStoryboard(name: "Main", bundle: Bundle.init(for: AppViewController.self))
|
||||
|
||||
let appViewController = storyboard.instantiateViewController(withIdentifier: "appViewController") as! AppViewController
|
||||
appViewController.app = app
|
||||
return appViewController
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppViewController {
|
||||
func update() {
|
||||
for button in [bannerView.button!, navigationBarDownloadButton!] {
|
||||
button.tintColor = app.tintColor
|
||||
button.isIndicatingActivity = false
|
||||
|
||||
if app.installedApp == nil {
|
||||
button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||
} else {
|
||||
button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||
}
|
||||
|
||||
let progress = AppManager.shared.installationProgress(for: app)
|
||||
button.progress = progress
|
||||
}
|
||||
|
||||
if let versionDate = app.latestVersion?.date, versionDate > Date() {
|
||||
bannerView.button.countdownDate = versionDate
|
||||
navigationBarDownloadButton.countdownDate = versionDate
|
||||
} else {
|
||||
bannerView.button.countdownDate = nil
|
||||
navigationBarDownloadButton.countdownDate = nil
|
||||
}
|
||||
|
||||
let barButtonItem = navigationItem.rightBarButtonItem
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
navigationItem.rightBarButtonItem = barButtonItem
|
||||
}
|
||||
|
||||
func showNavigationBar(for navigationController: UINavigationController? = nil) {
|
||||
let navigationController = navigationController ?? self.navigationController
|
||||
navigationController?.navigationBar.alpha = 1.0
|
||||
navigationController?.navigationBar.tintColor = .altPrimary
|
||||
navigationController?.navigationBar.setNeedsLayout()
|
||||
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
_preferredStatusBarStyle = .lightContent
|
||||
} else {
|
||||
_preferredStatusBarStyle = .default
|
||||
}
|
||||
|
||||
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
|
||||
func hideNavigationBar(for navigationController: UINavigationController? = nil) {
|
||||
let navigationController = navigationController ?? self.navigationController
|
||||
navigationController?.navigationBar.alpha = 0.0
|
||||
|
||||
_preferredStatusBarStyle = .lightContent
|
||||
navigationController?.setNeedsStatusBarAppearanceUpdate()
|
||||
}
|
||||
|
||||
func prepareBlur() {
|
||||
if let animator = blurAnimator {
|
||||
animator.stopAnimation(true)
|
||||
}
|
||||
|
||||
backgroundBlurView.effect = _backgroundBlurEffect
|
||||
backgroundBlurView.contentView.backgroundColor = _backgroundBlurTintColor
|
||||
|
||||
blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||
self?.backgroundBlurView.effect = nil
|
||||
self?.backgroundBlurView.contentView.backgroundColor = .clear
|
||||
}
|
||||
|
||||
blurAnimator?.startAnimation()
|
||||
blurAnimator?.pauseAnimation()
|
||||
}
|
||||
|
||||
func prepareNavigationBarAnimation() {
|
||||
resetNavigationBarAnimation()
|
||||
|
||||
navigationBarAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { [weak self] in
|
||||
self?.showNavigationBar()
|
||||
self?.navigationController?.navigationBar.tintColor = self?.app.tintColor
|
||||
self?.navigationController?.navigationBar.barTintColor = nil
|
||||
self?.contentViewController.view.layer.cornerRadius = 0
|
||||
}
|
||||
|
||||
navigationBarAnimator?.startAnimation()
|
||||
navigationBarAnimator?.pauseAnimation()
|
||||
|
||||
update()
|
||||
}
|
||||
|
||||
func resetNavigationBarAnimation() {
|
||||
navigationBarAnimator?.stopAnimation(true)
|
||||
navigationBarAnimator = nil
|
||||
|
||||
hideNavigationBar()
|
||||
|
||||
contentViewController.view.layer.cornerRadius = contentViewControllerShadowView.layer.cornerRadius
|
||||
}
|
||||
}
|
||||
|
||||
extension AppViewController {
|
||||
@IBAction func popViewController(_: UIButton) {
|
||||
navigationController?.popViewController(animated: true)
|
||||
}
|
||||
|
||||
@IBAction func performAppAction(_: PillButton) {
|
||||
if let installedApp = app.installedApp {
|
||||
open(installedApp)
|
||||
} else {
|
||||
downloadApp()
|
||||
}
|
||||
}
|
||||
|
||||
func downloadApp() {
|
||||
guard app.installedApp == nil else { return }
|
||||
|
||||
let group = AppManager.shared.install(app, presentingViewController: self) { result in
|
||||
do {
|
||||
_ = try result.get()
|
||||
} catch OperationError.cancelled {
|
||||
// Ignore
|
||||
} catch {
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.bannerView.button.progress = nil
|
||||
self.navigationBarDownloadButton.progress = nil
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
bannerView.button.progress = group.progress
|
||||
navigationBarDownloadButton.progress = group.progress
|
||||
}
|
||||
|
||||
func open(_ installedApp: InstalledApp) {
|
||||
UIApplication.shared.open(installedApp.openAppURL)
|
||||
}
|
||||
}
|
||||
|
||||
private extension AppViewController {
|
||||
@objc func didChangeApp(_: Notification) {
|
||||
// Async so that AppManager.installationProgress(for:) is nil when we update.
|
||||
DispatchQueue.main.async {
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func willEnterForeground(_: Notification) {
|
||||
guard let navigationController = navigationController, navigationController.topViewController == self else { return }
|
||||
|
||||
_shouldResetLayout = true
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
|
||||
@objc func didBecomeActive(_: Notification) {
|
||||
guard let navigationController = navigationController, navigationController.topViewController == self else { return }
|
||||
|
||||
// Fixes Navigation Bar appearing after app becomes inactive -> active again.
|
||||
_shouldResetLayout = true
|
||||
view.setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
extension AppViewController: UIScrollViewDelegate {
|
||||
public func scrollViewDidScroll(_: UIScrollView) {
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// PermissionPopoverViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 7/23/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
import SideStoreCore
|
||||
|
||||
final class PermissionPopoverViewController: UIViewController {
|
||||
var permission: AppPermission!
|
||||
|
||||
@IBOutlet private var nameLabel: UILabel!
|
||||
@IBOutlet private var descriptionLabel: UILabel!
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
nameLabel.text = permission.type.localizedName
|
||||
descriptionLabel.text = permission.usageDescription
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user