mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
AltStore will now consider an update available if either: * The source’s marketing version doesn’t match installed app’s version * The source declares a build version AND it doesn’t match the install app’s build version The installed app matches an app version if both maketing versions match, and the build versions match (if provided by the source).
249 lines
9.5 KiB
Swift
249 lines
9.5 KiB
Swift
//
|
|
// AppContentViewController.swift
|
|
// AltStore
|
|
//
|
|
// Created by Riley Testut on 7/22/19.
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
|
|
import AltStoreCore
|
|
import Roxas
|
|
|
|
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()
|
|
|
|
self.tableView.contentInset.bottom = 20
|
|
|
|
self.screenshotsCollectionView.dataSource = self.screenshotsDataSource
|
|
self.screenshotsCollectionView.prefetchDataSource = self.screenshotsDataSource
|
|
|
|
self.permissionsCollectionView.dataSource = self.permissionsDataSource
|
|
|
|
self.subtitleLabel.text = self.app.subtitle
|
|
self.descriptionTextView.text = self.app.localizedDescription
|
|
|
|
if let version = self.app.latestAvailableVersion
|
|
{
|
|
self.versionDescriptionTextView.text = version.localizedDescription
|
|
self.versionLabel.text = String(format: NSLocalizedString("Version %@", comment: ""), version.localizedVersion)
|
|
self.versionDateLabel.text = Date().relativeDateString(since: version.date, dateFormatter: self.dateFormatter)
|
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: version.size)
|
|
}
|
|
else
|
|
{
|
|
self.versionDescriptionTextView.text = nil
|
|
self.versionLabel.text = nil
|
|
self.versionDateLabel.text = nil
|
|
self.sizeLabel.text = self.byteCountFormatter.string(fromByteCount: 0)
|
|
}
|
|
|
|
self.descriptionTextView.maximumNumberOfLines = 5
|
|
self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
|
|
|
self.versionDescriptionTextView.maximumNumberOfLines = 3
|
|
self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered)
|
|
}
|
|
|
|
override func viewDidLayoutSubviews()
|
|
{
|
|
super.viewDidLayoutSubviews()
|
|
|
|
guard var size = self.preferredScreenshotSize else { return }
|
|
size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning.
|
|
|
|
let layout = self.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 = self.permissionsCollectionView.indexPath(for: cell) else { return }
|
|
|
|
let permission = self.permissionsDataSource.item(at: indexPath)
|
|
|
|
let maximumWidth = self.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 = self.permissionsCollectionView
|
|
}
|
|
}
|
|
|
|
private extension AppContentViewController
|
|
{
|
|
func makeScreenshotsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>
|
|
{
|
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSURL, UIImage>(items: self.app.screenshotURLs as [NSURL])
|
|
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
|
|
let cell = cell as! ScreenshotCollectionViewCell
|
|
cell.imageView.image = nil
|
|
cell.imageView.isIndicatingActivity = true
|
|
}
|
|
dataSource.prefetchHandler = { (imageURL, indexPath, completionHandler) in
|
|
return RSTAsyncBlockOperation() { (operation) in
|
|
let request = ImageRequest(url: imageURL as URL, processors: [.screenshot])
|
|
ImagePipeline.shared.loadImage(with: request, progress: nil) { result in
|
|
guard !operation.isCancelled else { return operation.finish() }
|
|
|
|
switch result
|
|
{
|
|
case .success(let response): completionHandler(response.image, nil)
|
|
case .failure(let error): completionHandler(nil, error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
|
let cell = cell as! ScreenshotCollectionViewCell
|
|
cell.imageView.isIndicatingActivity = false
|
|
cell.imageView.image = image
|
|
|
|
if let error = error
|
|
{
|
|
print("Error loading image:", error)
|
|
}
|
|
}
|
|
|
|
return dataSource
|
|
}
|
|
|
|
func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource<AppPermission>
|
|
{
|
|
let dataSource = RSTArrayCollectionViewDataSource(items: Array(self.app.permissions))
|
|
dataSource.cellConfigurationHandler = { (cell, permission, indexPath) 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
|
|
|
|
let icon = UIImage(systemName: permission.symbolName ?? "lock")
|
|
cell.button.setImage(icon, for: .normal)
|
|
|
|
cell.textLabel.text = permission.localizedDisplayName
|
|
}
|
|
|
|
return dataSource
|
|
}
|
|
}
|
|
|
|
private extension AppContentViewController
|
|
{
|
|
@objc func toggleCollapsingSection(_ sender: UIButton)
|
|
{
|
|
let indexPath: IndexPath
|
|
|
|
switch sender
|
|
{
|
|
case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0)
|
|
case self.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(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath)
|
|
{
|
|
cell.tintColor = self.app.tintColor
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat
|
|
{
|
|
switch Row.allCases[indexPath.row]
|
|
{
|
|
case .screenshots:
|
|
guard let size = self.preferredScreenshotSize else { return 0.0 }
|
|
return size.height
|
|
|
|
case .permissions:
|
|
guard !self.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 controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle
|
|
{
|
|
return .none
|
|
}
|
|
}
|