mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
* Change error from Swift.Error to NSError
* Adds ResultOperation.localizedFailure
* Finish Riley's monster commit
3b38d725d7
May the Gods have mercy on my soul.
* Fix format strings I broke
* Include "Enable JIT" errors in Error Log
* Fix minimuxer status checking
* [skip ci] Update the no wifi message to include VPN
* Opens Error Log when tapping ToastView
* Fixes Error Log context menu covering cell content
* Fixes Error Log context menu appearing while scrolling
* Fixes incorrect Search FAQ URL
* Fix Error Log showing UIAlertController on iOS 14+
* Fix Error Log not showing UIAlertController on iOS <=13
* Fix wrong color in AuthenticationViewController
* Fix typo
* Fixes logging non-AltServerErrors as AltServerError.underlyingError
* Limits quitting other AltStore/SideStore processes to database migrations
* Skips logging cancelled errors
* Replaces StoreApp.latestVersion with latestSupportedVersion + latestAvailableVersion
We now store the latest supported version as a relationship on StoreApp, rather than the latest available version. This allows us to reference the latest supported version in predicates and sort descriptors.
However, we kept the underlying Core Data property name the same to avoid extra migration.
* Conforms OperatingSystemVersion to Comparable
* Parses AppVersion.minOSVersion/maxOSVersion from source JSON
* Supports non-NSManagedObjects for @Managed properties
This allows us to use @Managed with properties that may or may not be NSManagedObjects at runtime (e.g. protocols). If they are, Managed will keep strong reference to context like before.
* Supports optional @Managed properties
* Conforms AppVersion to AppProtocol
* Verifies min/max OS version before downloading app + asks user to download older app version if necessary
* Improves error message when file does not exist at AppVersion.downloadURL
* Removes unnecessary StoreApp convenience properties
* Removes unnecessary StoreApp convenience properties as well as fix other issues
* Remove Settings bundle, add SwiftUI view instead
Fix refresh all shortcut intent
* Update AuthenticationOperation.swift
Signed-off-by: June Park <rjp2030@outlook.com>
* Fix build issues given by develop
* Add availability check to fix CI build(?)
* If it's gonna be that way...
---------
Signed-off-by: June Park <rjp2030@outlook.com>
Co-authored-by: nythepegasus <nythepegasus84@gmail.com>
Co-authored-by: Riley Testut <riley@rileytestut.com>
Co-authored-by: ny <me@nythepegas.us>
247 lines
9.4 KiB
Swift
247 lines
9.4 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.version)
|
|
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, 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, 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: 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
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|