source-code

This commit is contained in:
Thunder7yoshi
2020-06-09 23:38:02 +02:00
parent 3e3f9ab752
commit 06a8d21b98
346 changed files with 26230 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
//
// NewsCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
class NewsCollectionViewCell: UICollectionViewCell
{
@IBOutlet var titleLabel: UILabel!
@IBOutlet var captionLabel: UILabel!
@IBOutlet var imageView: UIImageView!
@IBOutlet var contentBackgroundView: UIView!
override func awakeFromNib()
{
super.awakeFromNib()
self.contentView.preservesSuperviewLayoutMargins = true
self.contentBackgroundView.layer.cornerRadius = 30
self.contentBackgroundView.clipsToBounds = true
self.imageView.layer.cornerRadius = 30
self.imageView.clipsToBounds = true
}
}

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15400" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15404"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="wRF-2R-NUG" customClass="NewsCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
<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="335" height="299"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="azr-Ea-luN">
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
</view>
<view contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
<rect key="frame" x="16" y="0.0" width="303" height="299"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="tNk-9u-1tk">
<rect key="frame" x="0.0" y="0.0" width="303" height="298.5"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="akF-Tr-G5M">
<rect key="frame" x="0.0" y="0.0" width="303" height="117.5"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
<rect key="frame" x="25" y="25" width="54.5" height="26.5"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
<rect key="frame" x="25" y="61.5" width="35.5" height="36"/>
<fontDescription key="fontDescription" type="system" pointSize="14"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/>
</stackView>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="335" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="l36-Bm-De0">
<rect key="frame" x="0.0" y="117.5" width="303" height="181"/>
<constraints>
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" priority="999" id="QGD-YE-Hw2"/>
</constraints>
</imageView>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="tNk-9u-1tk" firstAttribute="top" secondItem="Xba-Qs-SQo" secondAttribute="top" id="Dw8-lF-Fzl"/>
<constraint firstAttribute="trailing" secondItem="tNk-9u-1tk" secondAttribute="trailing" id="Zt8-Wa-oB9"/>
<constraint firstItem="tNk-9u-1tk" firstAttribute="leading" secondItem="Xba-Qs-SQo" secondAttribute="leading" id="m6p-Ee-dTh"/>
<constraint firstAttribute="bottom" secondItem="tNk-9u-1tk" secondAttribute="bottom" constant="0.5" id="v9g-yC-db9"/>
</constraints>
</view>
</subviews>
</view>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Xba-Qs-SQo" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="0xe-Rt-MhF"/>
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="5MO-c0-5rG"/>
<constraint firstItem="azr-Ea-luN" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leadingMargin" id="8Ck-dI-nJy"/>
<constraint firstAttribute="trailingMargin" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
<constraint firstAttribute="bottom" secondItem="Xba-Qs-SQo" secondAttribute="bottom" id="Ecj-fN-hZv"/>
<constraint firstAttribute="bottom" secondItem="azr-Ea-luN" secondAttribute="bottom" priority="999" id="e56-UD-DRT"/>
<constraint firstItem="azr-Ea-luN" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="h2k-WE-Esg"/>
<constraint firstAttribute="trailingMargin" secondItem="azr-Ea-luN" secondAttribute="trailing" priority="999" id="hsS-zC-A58"/>
</constraints>
<connections>
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/>
<outlet property="contentBackgroundView" destination="azr-Ea-luN" id="2Pl-11-YvR"/>
<outlet property="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
</connections>
<point key="canvasLocation" x="138" y="153"/>
</collectionViewCell>
</objects>
</document>

View File

@@ -0,0 +1,497 @@
//
// NewsViewController.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import SafariServices
import Roxas
import Nuke
private class AppBannerFooterView: UICollectionReusableView
{
let bannerView = AppBannerView(frame: .zero)
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
override init(frame: CGRect)
{
super.init(frame: frame)
self.addGestureRecognizer(self.tapGestureRecognizer)
self.bannerView.translatesAutoresizingMaskIntoConstraints = false
self.addSubview(self.bannerView)
NSLayoutConstraint.activate([
self.bannerView.topAnchor.constraint(equalTo: self.topAnchor),
self.bannerView.bottomAnchor.constraint(equalTo: self.bottomAnchor),
self.bannerView.leadingAnchor.constraint(equalTo: self.layoutMarginsGuide.leadingAnchor),
self.bannerView.trailingAnchor.constraint(equalTo: self.layoutMarginsGuide.trailingAnchor)
])
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class NewsViewController: UICollectionViewController
{
private lazy var dataSource = self.makeDataSource()
private lazy var placeholderView = RSTPlaceholderView(frame: .zero)
private var prototypeCell: NewsCollectionViewCell!
private var loadingState: LoadingState = .loading {
didSet {
self.update()
}
}
// Cache
private var cachedCellSizes = [String: CGSize]()
required init?(coder: NSCoder)
{
super.init(coder: coder)
NotificationCenter.default.addObserver(self, selector: #selector(NewsViewController.importApp(_:)), name: AppDelegate.importAppDeepLinkNotification, object: nil)
}
override func viewDidLoad()
{
super.viewDidLoad()
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
self.collectionView.register(AppBannerFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner")
self.registerForPreviewing(with: self, sourceView: self.collectionView)
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.fetchSource()
}
override func viewWillLayoutSubviews()
{
super.viewWillLayoutSubviews()
if self.collectionView.contentInset.bottom != 20
{
// Triggers collection view update in iOS 13, which crashes if we do it in viewDidLoad()
// since the database might not be loaded yet.
self.collectionView.contentInset.bottom = 20
}
}
}
private extension NewsViewController
{
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
{
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)]
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.objectID), cacheName: nil)
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
let cell = cell as! NewsCollectionViewCell
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.titleLabel.text = newsItem.title
cell.captionLabel.text = newsItem.caption
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
cell.imageView.image = nil
if newsItem.imageURL != nil
{
cell.imageView.isIndicatingActivity = true
cell.imageView.isHidden = false
}
else
{
cell.imageView.isIndicatingActivity = false
cell.imageView.isHidden = true
}
}
dataSource.prefetchHandler = { (newsItem, indexPath, completionHandler) in
guard let imageURL = newsItem.imageURL else { return nil }
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: imageURL, 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! NewsCollectionViewCell
cell.imageView.isIndicatingActivity = false
cell.imageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
dataSource.placeholderView = self.placeholderView
return dataSource
}
func fetchSource()
{
self.loadingState = .loading
AppManager.shared.fetchSources() { (result) in
do
{
let sources = try result.get()
try sources.first?.managedObjectContext?.save()
DispatchQueue.main.async {
self.loadingState = .finished(.success(()))
}
}
catch let error as NSError
{
DispatchQueue.main.async {
if self.dataSource.itemCount > 0
{
let error = error.withLocalizedFailure(NSLocalizedString("Failed to Fetch Sources", comment: ""))
let toastView = ToastView(error: error)
toastView.show(in: self)
}
self.loadingState = .finished(.failure(error))
}
}
}
}
func update()
{
switch self.loadingState
{
case .loading:
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.detailTextLabel.text = NSLocalizedString("Loading...", comment: "")
self.placeholderView.activityIndicatorView.startAnimating()
case .finished(.failure(let error)):
self.placeholderView.textLabel.isHidden = false
self.placeholderView.detailTextLabel.isHidden = false
self.placeholderView.textLabel.text = NSLocalizedString("Unable to Fetch News", comment: "")
self.placeholderView.detailTextLabel.text = error.localizedDescription
self.placeholderView.activityIndicatorView.stopAnimating()
case .finished(.success):
self.placeholderView.textLabel.isHidden = true
self.placeholderView.detailTextLabel.isHidden = true
self.placeholderView.activityIndicatorView.stopAnimating()
}
}
}
private extension NewsViewController
{
@objc func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer)
{
guard let footerView = gestureRecognizer.view as? UICollectionReusableView else { return }
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
return supplementaryView == footerView
}) else { return }
let item = self.dataSource.item(at: indexPath)
guard let storeApp = item.storeApp else { return }
let appViewController = AppViewController.makeAppViewController(app: storeApp)
self.navigationController?.pushViewController(appViewController, animated: true)
}
@objc func performAppAction(_ sender: PillButton)
{
let point = self.collectionView.convert(sender.center, from: sender.superview)
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
return supplementaryView?.frame.contains(point) ?? false
}) else { return }
let app = self.dataSource.item(at: indexPath)
guard let storeApp = app.storeApp else { return }
if let installedApp = app.storeApp?.installedApp
{
self.open(installedApp)
}
else
{
self.install(storeApp, at: indexPath)
}
}
@objc func install(_ storeApp: StoreApp, at indexPath: IndexPath)
{
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
guard previousProgress == nil else {
previousProgress?.cancel()
return
}
_ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in
DispatchQueue.main.async {
switch result
{
case .failure(OperationError.cancelled): break // Ignore
case .failure(let error):
let toastView = ToastView(error: error)
toastView.show(in: self)
case .success: print("Installed app:", storeApp.bundleIdentifier)
}
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
}
}
}
UIView.performWithoutAnimation {
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
}
}
func open(_ installedApp: InstalledApp)
{
UIApplication.shared.open(installedApp.openAppURL)
}
}
private extension NewsViewController
{
@objc func importApp(_ notification: Notification)
{
self.presentedViewController?.dismiss(animated: true, completion: nil)
}
}
extension NewsViewController
{
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
{
let newsItem = self.dataSource.item(at: indexPath)
if let externalURL = newsItem.externalURL
{
let safariViewController = SFSafariViewController(url: externalURL)
safariViewController.preferredControlTintColor = newsItem.tintColor
self.present(safariViewController, animated: true, completion: nil)
}
else if let storeApp = newsItem.storeApp
{
let appViewController = AppViewController.makeAppViewController(app: storeApp)
self.navigationController?.pushViewController(appViewController, animated: true)
}
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{
let item = self.dataSource.item(at: indexPath)
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
guard let storeApp = item.storeApp else { return footerView }
footerView.layoutMargins.left = self.view.layoutMargins.left
footerView.layoutMargins.right = self.view.layoutMargins.right
footerView.bannerView.titleLabel.text = storeApp.name
footerView.bannerView.subtitleLabel.text = storeApp.developerName
footerView.bannerView.tintColor = storeApp.tintColor
footerView.bannerView.betaBadgeView.isHidden = !storeApp.isBeta
footerView.bannerView.button.addTarget(self, action: #selector(NewsViewController.performAppAction(_:)), for: .primaryActionTriggered)
footerView.tapGestureRecognizer.addTarget(self, action: #selector(NewsViewController.handleTapGesture(_:)))
footerView.bannerView.button.isIndicatingActivity = false
if storeApp.installedApp == nil
{
footerView.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
let progress = AppManager.shared.installationProgress(for: storeApp)
footerView.bannerView.button.progress = progress
if Date() < storeApp.versionDate
{
footerView.bannerView.button.countdownDate = storeApp.versionDate
}
else
{
footerView.bannerView.button.countdownDate = nil
}
}
else
{
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
footerView.bannerView.button.progress = nil
footerView.bannerView.button.countdownDate = nil
}
Nuke.loadImage(with: storeApp.iconURL, into: footerView.bannerView.iconImageView)
return footerView
}
}
extension NewsViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
let item = self.dataSource.item(at: indexPath)
if let previousSize = self.cachedCellSizes[item.identifier]
{
return previousSize
}
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: collectionView.bounds.width)
NSLayoutConstraint.activate([widthConstraint])
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
self.cachedCellSizes[item.identifier] = size
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
let item = self.dataSource.item(at: IndexPath(row: 0, section: section))
if item.storeApp != nil
{
return CGSize(width: 88, height: 88)
}
else
{
return .zero
}
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
{
var insets = UIEdgeInsets(top: 30, left: 0, bottom: 13, right: 0)
if section == 0
{
insets.top = 10
}
return insets
}
}
extension NewsViewController: UIViewControllerPreviewingDelegate
{
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
{
if let indexPath = self.collectionView.indexPathForItem(at: location), let cell = self.collectionView.cellForItem(at: indexPath)
{
// Previewing news item.
previewingContext.sourceRect = cell.frame
let newsItem = self.dataSource.item(at: indexPath)
if let externalURL = newsItem.externalURL
{
let safariViewController = SFSafariViewController(url: externalURL)
safariViewController.preferredControlTintColor = newsItem.tintColor
return safariViewController
}
else if let storeApp = newsItem.storeApp
{
let appViewController = AppViewController.makeAppViewController(app: storeApp)
return appViewController
}
return nil
}
else
{
// Previewing app banner (or nothing).
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath)
return layoutAttributes?.frame.contains(location) ?? false
}) else { return nil }
guard let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath) else { return nil }
previewingContext.sourceRect = layoutAttributes.frame
let item = self.dataSource.item(at: indexPath)
guard let storeApp = item.storeApp else { return nil }
let appViewController = AppViewController.makeAppViewController(app: storeApp)
return appViewController
}
}
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
{
if let safariViewController = viewControllerToCommit as? SFSafariViewController
{
self.present(safariViewController, animated: true, completion: nil)
}
else
{
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
}
}
}