[AltStore] Adds redesigned BrowseViewController to browse and install apps

This commit is contained in:
Riley Testut
2019-07-16 14:25:09 -07:00
parent 800ec11ae1
commit 129ae15a54
16 changed files with 689 additions and 22 deletions

View File

@@ -9,12 +9,6 @@
import UIKit
import Roxas
@objc(ScreenshotCollectionViewCell)
private class ScreenshotCollectionViewCell: UICollectionViewCell
{
@IBOutlet var imageView: UIImageView!
}
extension AppDetailViewController
{
private enum Row: Int

View File

@@ -4,7 +4,9 @@
<adaptation id="fullscreen"/>
</device>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -18,10 +20,11 @@
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
</tabBar>
<connections>
<segue destination="XF0-gk-CxQ" kind="relationship" relationship="viewControllers" id="uf8-vX-M3B"/>
<segue destination="v8c-d9-T9x" kind="relationship" relationship="viewControllers" id="fqx-5p-YCD"/>
<segue destination="gJ2-NJ-8DO" kind="relationship" relationship="viewControllers" id="J4s-tm-3YV"/>
<segue destination="MGm-Zy-ffn" kind="relationship" relationship="viewControllers" id="quv-RY-0rM"/>
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="kCE-KJ-sWv"/>
<segue destination="XF0-gk-CxQ" kind="relationship" relationship="viewControllers" id="t3q-Lp-gsl"/>
<segue destination="v8c-d9-T9x" kind="relationship" relationship="viewControllers" id="DmU-oI-nsr"/>
<segue destination="gJ2-NJ-8DO" kind="relationship" relationship="viewControllers" id="WBS-ya-y8Z"/>
<segue destination="MGm-Zy-ffn" kind="relationship" relationship="viewControllers" id="wxx-9a-d9q"/>
</connections>
</tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
@@ -86,12 +89,156 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="nb5-5T-hHT" userLabel="First Responder" sceneMemberID="firstResponder"/>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="CuF-K7-fn8">
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
<rect key="frame" x="0.0" y="-1" width="150" height="2.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressView>
</objects>
<point key="canvasLocation" x="1518" y="420"/>
</scene>
<!--Browse-->
<scene sceneID="rXq-UR-qQp">
<objects>
<collectionViewController id="e3L-BF-iXp" customClass="BrowseViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="CaT-1q-qnx">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="10" id="e0H-IH-rng">
<size key="itemSize" width="375" height="372"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="ofi-ID-YeW" customClass="BrowseCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="375" height="372"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="372"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="Qmy-fI-0do" userLabel="App Info">
<rect key="frame" x="20" y="20" width="335" height="65"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="CXK-rw-bWe" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="65" height="65"/>
<constraints>
<constraint firstAttribute="width" secondItem="CXK-rw-bWe" secondAttribute="height" multiplier="1:1" id="GRJ-Zv-m2F"/>
<constraint firstAttribute="height" constant="65" id="Y9m-Am-Kjl"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="M2b-41-kPM">
<rect key="frame" x="76" y="14" width="176" height="37"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Bdj-E8-Jzp">
<rect key="frame" x="0.0" y="0.0" width="176" height="20.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lpO-8T-aGe">
<rect key="frame" x="0.0" y="22.5" width="176" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
<color key="textColor" white="0.66666666666666663" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
</stackView>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="xCW-7H-lc3" customClass="ProgressButton" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="263" y="17" width="72" height="31"/>
<color key="backgroundColor" red="0.22352941176470589" green="0.49411764705882355" blue="0.396078431372549" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="width" constant="72" id="LDp-OV-97J"/>
<constraint firstAttribute="height" constant="31" id="ZUE-0U-ROX"/>
</constraints>
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
<state key="normal" title="OPEN"/>
<connections>
<action selector="performAppAction:" destination="e3L-BF-iXp" eventType="primaryActionTriggered" id="0jZ-Ql-ALs"/>
</connections>
</button>
</subviews>
</stackView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="P9V-7N-kV7" userLabel="Screenshots">
<rect key="frame" x="15" y="100" width="345" height="252"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="K2h-4K-tEf">
<rect key="frame" x="0.0" y="0.0" width="345" height="252"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Classic Nintendo games in your pocket." textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="L3d-OX-f9e">
<rect key="frame" x="20" y="15" width="305" height="17"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" red="1" green="0.14901960780000001" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<collectionView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" contentInsetAdjustmentBehavior="never" dataMode="prototypes" translatesAutoresizingMaskIntoConstraints="NO" id="wnA-ZZ-fwF">
<rect key="frame" x="20" y="47" width="305" height="185"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="185" id="x3q-LQ-mXd"/>
</constraints>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="0.0" minimumInteritemSpacing="0.0" id="g2J-u0-NvJ">
<size key="itemSize" width="50" height="0.0"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="Cell" id="xlX-GE-9nO" customClass="ScreenshotCollectionViewCell">
<rect key="frame" x="0.0" y="0.0" width="50" height="0.0"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="50" height="0.0"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</collectionViewCell>
</cells>
</collectionView>
</subviews>
<edgeInsets key="layoutMargins" top="15" left="20" bottom="20" right="20"/>
</stackView>
</subviews>
<color key="backgroundColor" red="1" green="0.14901960780000001" blue="0.0" alpha="0.050000000000000003" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="bottom" secondItem="K2h-4K-tEf" secondAttribute="bottom" id="RPX-HI-VGa"/>
<constraint firstAttribute="trailing" secondItem="K2h-4K-tEf" secondAttribute="trailing" id="mLS-R0-ZeU"/>
<constraint firstItem="K2h-4K-tEf" firstAttribute="top" secondItem="P9V-7N-kV7" secondAttribute="top" id="r7P-xH-pzs"/>
<constraint firstItem="K2h-4K-tEf" firstAttribute="leading" secondItem="P9V-7N-kV7" secondAttribute="leading" id="tY4-Ta-ec2"/>
</constraints>
</view>
</subviews>
</view>
<constraints>
<constraint firstItem="P9V-7N-kV7" firstAttribute="leading" secondItem="ofi-ID-YeW" secondAttribute="leading" constant="15" id="WeY-x3-kYd"/>
<constraint firstItem="P9V-7N-kV7" firstAttribute="top" secondItem="Qmy-fI-0do" secondAttribute="bottom" constant="15" id="b4n-9X-Sfq"/>
<constraint firstAttribute="trailing" secondItem="Qmy-fI-0do" secondAttribute="trailing" constant="20" id="dar-0I-2Zt"/>
<constraint firstAttribute="bottom" secondItem="P9V-7N-kV7" secondAttribute="bottom" constant="20" id="qxV-1c-5fe"/>
<constraint firstItem="Qmy-fI-0do" firstAttribute="leading" secondItem="ofi-ID-YeW" secondAttribute="leading" constant="20" id="r1c-ZM-cIP"/>
<constraint firstItem="Qmy-fI-0do" firstAttribute="top" secondItem="ofi-ID-YeW" secondAttribute="top" constant="20" id="uYg-jP-c9b"/>
<constraint firstAttribute="trailing" secondItem="P9V-7N-kV7" secondAttribute="trailing" constant="15" id="umj-rz-TS3"/>
</constraints>
<connections>
<outlet property="actionButton" destination="xCW-7H-lc3" id="7iL-lM-AEP"/>
<outlet property="appIconImageView" destination="CXK-rw-bWe" id="UIK-Ps-dlD"/>
<outlet property="developerLabel" destination="lpO-8T-aGe" id="U0r-qS-Ify"/>
<outlet property="nameLabel" destination="Bdj-E8-Jzp" id="dlr-bl-tOq"/>
<outlet property="screenshotsCollectionView" destination="wnA-ZZ-fwF" id="dcp-DC-GtH"/>
<outlet property="screenshotsContentView" destination="P9V-7N-kV7" id="7mg-7o-ckH"/>
<outlet property="subtitleLabel" destination="L3d-OX-f9e" id="ERX-nn-g4g"/>
</connections>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="e3L-BF-iXp" id="ARv-GZ-Gc2"/>
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"/>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1517.5999999999999" y="-1013.3433283358322"/>
</scene>
<!--Apps-->
<scene sceneID="JlP-x7-lBT">
<objects>
@@ -477,7 +624,7 @@
</tableViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="H7V-ct-fO1" userLabel="First Responder" sceneMemberID="firstResponder"/>
<progressView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="750" progressViewStyle="bar" id="QaM-dt-nHj">
<rect key="frame" x="0.0" y="0.0" width="150" height="2.5"/>
<rect key="frame" x="0.0" y="-1" width="150" height="2.5"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</progressView>
</objects>
@@ -636,14 +783,37 @@
</objects>
<point key="canvasLocation" x="750" y="1124"/>
</scene>
<!--Browse-->
<scene sceneID="VHa-uP-bFU">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="faz-B4-Sub" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Browse" id="Uwh-Bg-Ymq"/>
<toolbarItems/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="dIv-qd-9L5" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
<autoresizingMask key="autoresizingMask"/>
<color key="tintColor" name="Green"/>
</navigationBar>
<nil name="viewControllers"/>
<connections>
<segue destination="e3L-BF-iXp" kind="relationship" relationship="rootViewController" id="EVp-fA-PvU"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="OkH-49-O0J" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="750" y="-1013"/>
</scene>
</scenes>
<resources>
<image name="DeltaIcon" width="512" height="512"/>
<image name="first" width="30" height="30"/>
<image name="second" width="30" height="30"/>
<namedColor name="Green">
<color red="0.22352941176470589" green="0.49411764705882355" blue="0.396078431372549" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
<inferredMetricsTieBreakers>
<segue reference="wBX-0o-Ywz"/>
</inferredMetricsTieBreakers>
<color key="tintColor" name="Purple"/>
<color key="tintColor" name="Green"/>
</document>

View File

@@ -0,0 +1,101 @@
//
// BrowseCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
@objc class BrowseCollectionViewCell: UICollectionViewCell
{
var imageNames: [String] = [] {
didSet {
self.dataSource.items = self.imageNames.map { $0 as NSString }
}
}
private lazy var dataSource = self.makeDataSource()
private lazy var imageSizes = [NSString: CGSize]()
@IBOutlet var nameLabel: UILabel!
@IBOutlet var developerLabel: UILabel!
@IBOutlet var appIconImageView: UIImageView!
@IBOutlet var actionButton: ProgressButton!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet private var screenshotsContentView: UIView!
@IBOutlet private var screenshotsCollectionView: UICollectionView!
override func awakeFromNib()
{
super.awakeFromNib()
self.screenshotsCollectionView.delegate = self
self.screenshotsCollectionView.dataSource = self.dataSource
self.screenshotsCollectionView.prefetchDataSource = self.dataSource
self.screenshotsContentView.layer.cornerRadius = 20
self.screenshotsContentView.layer.masksToBounds = true
self.update()
}
override func tintColorDidChange()
{
super.tintColorDidChange()
self.update()
}
}
private extension BrowseCollectionViewCell
{
func makeDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>
{
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<NSString, UIImage>(items: [])
dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = true
}
dataSource.prefetchHandler = { (imageName, indexPath, completion) in
return BlockOperation {
let image = UIImage(named: imageName as String)
completion(image, nil)
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! ScreenshotCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
}
return dataSource
}
private func update()
{
self.subtitleLabel.textColor = self.tintColor
self.screenshotsContentView.backgroundColor = self.tintColor.withAlphaComponent(0.1)
}
}
extension BrowseCollectionViewCell: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let imageURL = self.dataSource.item(at: indexPath)
let dimensions = self.imageSizes[imageURL] ?? UIScreen.main.nativeBounds.size
let aspectRatio = dimensions.width / dimensions.height
let height = self.screenshotsCollectionView.bounds.height
let width = (self.screenshotsCollectionView.bounds.height * aspectRatio).rounded(.down)
let size = CGSize(width: width, height: height)
return size
}
}

View File

@@ -0,0 +1,172 @@
//
// BrowseViewController.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class BrowseViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
override func viewDidLoad()
{
super.viewDidLoad()
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.fetchApps()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
let collectionViewLayout = self.collectionViewLayout as! UICollectionViewFlowLayout
collectionViewLayout.itemSize.width = self.view.bounds.width
}
}
private extension BrowseViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>
{
let fetchRequest = App.fetchRequest() as NSFetchRequest<App>
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.name, ascending: false)]
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.identifier), App.altstoreAppID)
fetchRequest.returnsObjectsAsFaults = false
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<App, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
dataSource.cellConfigurationHandler = { [weak self] (cell, app, indexPath) in
guard let `self` = self else { return }
let cell = cell as! BrowseCollectionViewCell
cell.nameLabel.text = app.name
cell.developerLabel.text = app.developerName
cell.subtitleLabel.text = app.subtitle
cell.imageNames = Array(app.screenshotNames.prefix(3))
cell.appIconImageView.image = UIImage(named: app.iconName)
cell.actionButton.tag = indexPath.item
cell.actionButton.activityIndicatorView.style = .white
// Explicitly set to false to ensure we're starting from a non-activity indicating state.
// Otherwise, cell reuse can mess up some cached values.
cell.actionButton.isIndicatingActivity = false
let tintColor = app.tintColor ?? self.collectionView.tintColor!
cell.tintColor = tintColor
cell.actionButton.progressTintColor = tintColor
if app.installedApp == nil
{
cell.actionButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
cell.actionButton.setTitleColor(.altGreen, for: .normal)
cell.actionButton.backgroundColor = UIColor.altGreen.withAlphaComponent(0.1)
if let progress = AppManager.shared.installationProgress(for: app)
{
cell.actionButton.progress = progress
cell.actionButton.isIndicatingActivity = true
cell.actionButton.activityIndicatorView.isUserInteractionEnabled = false
cell.actionButton.isUserInteractionEnabled = true
}
else
{
cell.actionButton.progress = nil
cell.actionButton.isIndicatingActivity = false
}
}
else
{
cell.actionButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
cell.actionButton.setTitleColor(.white, for: .normal)
cell.actionButton.backgroundColor = .altGreen
}
}
return dataSource
}
func fetchApps()
{
AppManager.shared.fetchApps() { (result) in
do
{
let apps = try result.get()
try apps.first?.managedObjectContext?.save()
}
catch
{
DispatchQueue.main.async {
let toastView = RSTToastView(text: NSLocalizedString("Failed to Fetch Apps", comment: ""), detailText: error.localizedDescription)
toastView.tintColor = .altGreen
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
}
}
}
}
}
private extension BrowseViewController
{
@IBAction func performAppAction(_ sender: ProgressButton)
{
let indexPath = IndexPath(item: sender.tag, section: 0)
let app = self.dataSource.item(at: indexPath)
if let installedApp = app.installedApp
{
self.open(installedApp)
}
else
{
self.install(app, at: indexPath)
}
}
func install(_ app: App, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: app)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
_ = AppManager.shared.install(app, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = RSTToastView(text: "Failed to install \(app.name)", detailText: error.localizedDescription)
toastView.tintColor = .altGreen
toastView.show(in: self.navigationController!.view, duration: 2)
case .success(let installedApp): print("Installed app:", installedApp.app.identifier)
}
self.collectionView.reloadItems(at: [indexPath])
}
}
self.collectionView.reloadItems(at: [indexPath])
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}

View File

@@ -0,0 +1,28 @@
//
// ScreenshotCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
@objc(ScreenshotCollectionViewCell)
class ScreenshotCollectionViewCell: UICollectionViewCell
{
let imageView: UIImageView
required init?(coder aDecoder: NSCoder)
{
self.imageView = UIImageView(image: nil)
self.imageView.layer.cornerRadius = 8
self.imageView.layer.masksToBounds = true
super.init(coder: aDecoder)
self.addSubview(self.imageView, pinningEdgesWith: .zero)
}
}

View File

@@ -0,0 +1,34 @@
//
// NavigationBar.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class NavigationBar: UINavigationBar
{
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder aDecoder: NSCoder)
{
super.init(coder: aDecoder)
self.initialize()
}
private func initialize()
{
self.barTintColor = .white
self.shadowImage = UIImage()
}
}

View File

@@ -0,0 +1,63 @@
//
// ProgressButton.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
class ProgressButton: UIButton
{
var progress: Progress? {
didSet {
self.progressView.progress = Float(self.progress?.fractionCompleted ?? 0)
self.progressView.observedProgress = self.progress
}
}
var progressTintColor: UIColor? {
get {
return self.progressView.progressTintColor
}
set {
self.progressView.progressTintColor = newValue
}
}
private let progressView = UIProgressView(progressViewStyle: .default)
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += 32
size.height += 4
return size
}
override func awakeFromNib()
{
super.awakeFromNib()
self.layer.masksToBounds = true
self.progressView.progress = 0
self.progressView.trackImage = UIImage()
self.progressView.isUserInteractionEnabled = false
self.addSubview(self.progressView)
}
override func layoutSubviews()
{
super.layoutSubviews()
self.progressView.bounds.size.width = self.bounds.width
let scale = self.bounds.height / self.progressView.bounds.height
self.progressView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: scale)
self.progressView.center = CGPoint(x: self.bounds.midX, y: self.bounds.midY)
self.layer.cornerRadius = self.bounds.midY
}
}

View File

@@ -11,4 +11,5 @@ import UIKit
extension UIColor
{
static let altPurple = UIColor(named: "Purple")!
static let altGreen = UIColor(named: "Green")!
}

View File

@@ -0,0 +1,32 @@
//
// UIColor+Hex.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
extension UIColor
{
// Borrowed from https://stackoverflow.com/a/33397427
convenience init?(hexString: String)
{
let hex = hexString.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int = UInt32()
Scanner(string: hex).scanHexInt32(&int)
let a, r, g, b: UInt32
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
return nil
}
self.init(red: CGFloat(r) / 255, green: CGFloat(g) / 255, blue: CGFloat(b) / 255, alpha: CGFloat(a) / 255)
}
}

View File

@@ -21,6 +21,8 @@
<attribute name="localizedDescription" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="screenshotNames" attributeType="Transformable" syncable="YES"/>
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
<attribute name="version" attributeType="String" syncable="YES"/>
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
@@ -57,7 +59,7 @@
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="210"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="240"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
</elements>

View File

@@ -22,6 +22,7 @@ class App: NSManagedObject, Decodable, Fetchable
/* Properties */
@NSManaged private(set) var name: String
@NSManaged private(set) var identifier: String
@NSManaged private(set) var subtitle: String?
@NSManaged private(set) var developerName: String
@NSManaged private(set) var localizedDescription: String
@@ -34,6 +35,7 @@ class App: NSManagedObject, Decodable, Fetchable
@NSManaged private(set) var versionDescription: String?
@NSManaged private(set) var downloadURL: URL
@NSManaged private(set) var tintColor: UIColor?
/* Relationships */
@NSManaged private(set) var installedApp: InstalledApp?
@@ -55,6 +57,8 @@ class App: NSManagedObject, Decodable, Fetchable
case iconName
case screenshotNames
case downloadURL
case tintColor
case subtitle
}
required init(from decoder: Decoder) throws
@@ -69,6 +73,8 @@ class App: NSManagedObject, Decodable, Fetchable
self.developerName = try container.decode(String.self, forKey: .developerName)
self.localizedDescription = try container.decode(String.self, forKey: .localizedDescription)
self.subtitle = try container.decodeIfPresent(String.self, forKey: .subtitle)
self.version = try container.decode(String.self, forKey: .version)
self.versionDate = try container.decode(Date.self, forKey: .versionDate)
self.versionDescription = try container.decodeIfPresent(String.self, forKey: .versionDescription)
@@ -78,6 +84,15 @@ class App: NSManagedObject, Decodable, Fetchable
self.downloadURL = try container.decode(URL.self, forKey: .downloadURL)
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
{
guard let tintColor = UIColor(hexString: tintColorHex) else {
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
}
self.tintColor = tintColor
}
context.insert(self)
}
}

View File

@@ -24,7 +24,7 @@ class FetchAppsOperation: ResultOperation<[App]>
{
super.main()
let appsURL = URL(string: "https://www.dropbox.com/s/z5tj1tx8zgeqbms/Apps.json?dl=1")!
let appsURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")!
let dataTask = self.session.dataTask(with: appsURL) { (data, response, error) in
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in

View File

@@ -14,12 +14,14 @@
"name": "Delta",
"identifier": "com.rileytestut.Delta",
"developerName": "Riley Testut",
"subtitle": "Classic Nintendo games in your pocket.",
"version": "1.0",
"versionDate": "2019-05-20",
"versionDescription": "Finally, after over five years of waiting, Delta is out of beta and ready for everyone to enjoy!\n\nCurrently supports NES, SNES, N64, GB(C), and GBA games, with more to come in the future.",
"downloadURL": "https://www.dropbox.com/s/31i4hcqnorucrxi/Delta.ipa?dl=1",
"localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.",
"iconName": "DeltaIcon",
"tintColor": "8A28F7",
"screenshotNames": [
"Delta1",
"Delta2",
@@ -30,6 +32,7 @@
{
"name": "Clipboard Manager",
"identifier": "com.rileytestut.ClipboardManager",
"subtitle": "Manage your clipboard history with ease.",
"developerName": "Riley Testut",
"version": "1.0",
"versionDate": "2019-06-20",

View File

@@ -0,0 +1,20 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
},
"colors" : [
{
"idiom" : "universal",
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "57",
"alpha" : "1.000",
"blue" : "101",
"green" : "126"
}
}
}
]
}

View File

@@ -9,10 +9,10 @@
"color" : {
"color-space" : "srgb",
"components" : {
"red" : "0.545",
"red" : "0x8A",
"alpha" : "1.000",
"blue" : "0.969",
"green" : "0.157"
"blue" : "0xF7",
"green" : "0x28"
}
}
}