mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStore] Update apps from UpdatesViewController
This commit is contained in:
@@ -90,6 +90,7 @@
|
||||
BF4713A622976D1E00784A2F /* openssl.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF4713A422976CFC00784A2F /* openssl.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF5AB3A82285FE7500DC914B /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; };
|
||||
BF5AB3A92285FE7500DC914B /* AltSign.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BF5AB3A72285FE6C00DC914B /* AltSign.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */; };
|
||||
BF9B63C6229DD44E002F0A62 /* AltSign.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF9B63C5229DD44D002F0A62 /* AltSign.framework */; };
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
|
||||
@@ -310,6 +311,7 @@
|
||||
BF4588962298DE6E00BD7491 /* libzip.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = libzip.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF4713A422976CFC00784A2F /* openssl.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = openssl.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF5AB3A72285FE6C00DC914B /* AltSign.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppsOperation.swift; sourceTree = "<group>"; };
|
||||
BF9B63C5229DD44D002F0A62 /* AltSign.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = AltSign.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
|
||||
@@ -738,8 +740,8 @@
|
||||
BFD247962284D7C100981D42 /* Resources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFD247762284B9A700981D42 /* Assets.xcassets */,
|
||||
BFB1169C22932DB100BB457C /* Apps.json */,
|
||||
BFD247762284B9A700981D42 /* Assets.xcassets */,
|
||||
);
|
||||
path = Resources;
|
||||
sourceTree = "<group>";
|
||||
@@ -810,6 +812,7 @@
|
||||
BFC1F38C22AEE3A4003AC21A /* DownloadAppOperation.swift */,
|
||||
BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */,
|
||||
BFDB6A0E22AB2776007EA6D6 /* InstallAppOperation.swift */,
|
||||
BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */,
|
||||
);
|
||||
path = Operations;
|
||||
sourceTree = "<group>";
|
||||
@@ -1170,6 +1173,7 @@
|
||||
files = (
|
||||
BFDB6A0F22AB2776007EA6D6 /* InstallAppOperation.swift in Sources */,
|
||||
BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */,
|
||||
BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */,
|
||||
BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */,
|
||||
BFD2478F2284C8F900981D42 /* Button.swift in Sources */,
|
||||
BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */,
|
||||
|
||||
@@ -14,6 +14,11 @@ import AltKit
|
||||
|
||||
import Roxas
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
static let didFetchAppsNotification = Notification.Name("com.altstore.AppManager.didFetchApps")
|
||||
}
|
||||
|
||||
class AppManager
|
||||
{
|
||||
static let shared = AppManager()
|
||||
@@ -75,6 +80,26 @@ extension AppManager
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
func fetchApps(completionHandler: @escaping (Result<[App], Error>) -> Void)
|
||||
{
|
||||
let fetchAppsOperation = FetchAppsOperation()
|
||||
fetchAppsOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success(let apps):
|
||||
completionHandler(.success(apps))
|
||||
NotificationCenter.default.post(name: AppManager.didFetchAppsNotification, object: self)
|
||||
}
|
||||
}
|
||||
self.operationQueue.addOperation(fetchAppsOperation)
|
||||
}
|
||||
}
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
func install(_ app: App, presentingViewController: UIViewController, completionHandler: @escaping (Result<InstalledApp, Error>) -> Void) -> Progress
|
||||
|
||||
@@ -11,13 +11,7 @@ import Roxas
|
||||
|
||||
class AppsViewController: UITableViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
return dateFormatter
|
||||
}()
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
@@ -82,23 +76,19 @@ private extension AppsViewController
|
||||
|
||||
func fetchApps()
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let appsFileURL = Bundle.main.url(forResource: "Apps", withExtension: "json")!
|
||||
|
||||
AppManager.shared.fetchApps { (result) in
|
||||
do
|
||||
{
|
||||
let data = try Data(contentsOf: appsFileURL)
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
|
||||
decoder.managedObjectContext = context
|
||||
|
||||
_ = try decoder.decode([App].self, from: data)
|
||||
try context.save()
|
||||
let apps = try result.get()
|
||||
try apps.first?.managedObjectContext?.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
fatalError("Failed to save fetched apps. \(error)")
|
||||
DispatchQueue.main.async {
|
||||
let toastView = RSTToastView(text: NSLocalizedString("Failed to fetch apps", comment: ""), detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,8 +471,15 @@
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Updates" id="hvX-Ly-Y2C"/>
|
||||
<connections>
|
||||
<outlet property="progressView" destination="QaM-dt-nHj" id="Tlq-vj-Bo9"/>
|
||||
</connections>
|
||||
</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"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</progressView>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1518" y="1124"/>
|
||||
</scene>
|
||||
@@ -636,7 +643,7 @@
|
||||
<image name="second" width="30" height="30"/>
|
||||
</resources>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="8jj-zE-2hk"/>
|
||||
<segue reference="wBX-0o-Ywz"/>
|
||||
</inferredMetricsTieBreakers>
|
||||
<color key="tintColor" name="Purple"/>
|
||||
</document>
|
||||
|
||||
@@ -34,9 +34,13 @@
|
||||
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
|
||||
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
|
||||
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="isBeta" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="version" attributeType="String" syncable="YES"/>
|
||||
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="installedApp" inverseEntity="App" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
<entity name="Team" representedClassName="Team" syncable="YES">
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
@@ -53,7 +57,7 @@
|
||||
<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="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="105"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -18,8 +18,6 @@ class InstalledApp: NSManagedObject, Fetchable
|
||||
|
||||
@NSManaged var expirationDate: Date
|
||||
|
||||
@NSManaged var isBeta: Bool
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged private(set) var app: App!
|
||||
|
||||
@@ -34,7 +32,7 @@ class InstalledApp: NSManagedObject, Fetchable
|
||||
|
||||
let app = context.object(with: app.objectID) as! App
|
||||
self.app = app
|
||||
self.version = "0.9"
|
||||
self.version = app.version
|
||||
|
||||
self.bundleIdentifier = bundleIdentifier
|
||||
self.expirationDate = expirationDate
|
||||
|
||||
@@ -79,7 +79,7 @@ private extension MyAppsViewController
|
||||
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
|
||||
guard let app = installedApp.app else { return }
|
||||
|
||||
cell.textLabel?.text = app.name
|
||||
cell.textLabel?.text = app.name + " (\(installedApp.version))"
|
||||
|
||||
let detailText =
|
||||
"""
|
||||
|
||||
@@ -45,11 +45,22 @@ class DownloadAppOperation: ResultOperation<InstalledApp>
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let app = context.object(with: self.app.objectID) as! App
|
||||
|
||||
let installedApp = InstalledApp(app: app,
|
||||
let installedApp: InstalledApp
|
||||
|
||||
if let app = app.installedApp
|
||||
{
|
||||
installedApp = app
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
installedApp = InstalledApp(app: app,
|
||||
bundleIdentifier: app.identifier,
|
||||
expirationDate: Date(),
|
||||
context: context)
|
||||
}
|
||||
|
||||
installedApp.version = app.version
|
||||
self.finish(.success(installedApp))
|
||||
}
|
||||
}
|
||||
|
||||
53
AltStore/Operations/FetchAppsOperation.swift
Normal file
53
AltStore/Operations/FetchAppsOperation.swift
Normal file
@@ -0,0 +1,53 @@
|
||||
//
|
||||
// FetchAppsOperation.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 6/17/19.
|
||||
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Roxas
|
||||
|
||||
@objc(FetchAppsOperation)
|
||||
class FetchAppsOperation: ResultOperation<[App]>
|
||||
{
|
||||
private let session = URLSession(configuration: .default)
|
||||
|
||||
private lazy var dateFormatter: DateFormatter = {
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd"
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
override func main()
|
||||
{
|
||||
super.main()
|
||||
|
||||
let appsURL = URL(string: "https://www.dropbox.com/s/z5tj1tx8zgeqbms/Apps.json?dl=1")!
|
||||
|
||||
let dataTask = self.session.dataTask(with: appsURL) { (data, response, error) in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
do
|
||||
{
|
||||
let (data, _) = try Result((data, response), error).get()
|
||||
|
||||
let decoder = JSONDecoder()
|
||||
decoder.dateDecodingStrategy = .formatted(self.dateFormatter)
|
||||
decoder.managedObjectContext = context
|
||||
|
||||
let apps = try decoder.decode([App].self, from: data)
|
||||
self.finish(.success(apps))
|
||||
}
|
||||
catch
|
||||
{
|
||||
self.finish(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.progress.addChild(dataTask.progress, withPendingUnitCount: 1)
|
||||
|
||||
dataTask.resume()
|
||||
}
|
||||
}
|
||||
@@ -21,19 +21,36 @@ class UpdatesViewController: UITableViewController
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
@IBOutlet private var progressView: UIProgressView!
|
||||
|
||||
required init?(coder aDecoder: NSCoder)
|
||||
{
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(UpdatesViewController.didFetchApps(_:)), name: AppManager.didFetchAppsNotification, object: nil)
|
||||
}
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.tableView.dataSource = self.dataSource
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
self.progressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
navigationBar.addSubview(self.progressView)
|
||||
|
||||
NSLayoutConstraint.activate([self.progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor),
|
||||
self.progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool)
|
||||
{
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
let count = self.tableView.numberOfRows(inSection: 0)
|
||||
self.navigationController?.tabBarItem.badgeValue = count > 0 ? String(describing: count) : nil
|
||||
self.update()
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
@@ -71,6 +88,98 @@ private extension UpdatesViewController
|
||||
cell.detailTextLabel?.text = detailText
|
||||
}
|
||||
|
||||
let placeholderView = RSTPlaceholderView()
|
||||
placeholderView.textLabel.text = NSLocalizedString("No Updates", comment: "")
|
||||
placeholderView.detailTextLabel.text = NSLocalizedString("There are no app updates at this time.", comment: "")
|
||||
dataSource.placeholderView = placeholderView
|
||||
|
||||
return dataSource
|
||||
}
|
||||
|
||||
func update()
|
||||
{
|
||||
if let count = self.dataSource.fetchedResultsController.fetchedObjects?.count, count > 0
|
||||
{
|
||||
self.navigationController?.tabBarItem.badgeValue = String(describing: count)
|
||||
}
|
||||
else
|
||||
{
|
||||
self.navigationController?.tabBarItem.badgeValue = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension UpdatesViewController
|
||||
{
|
||||
func update(_ installedApp: InstalledApp)
|
||||
{
|
||||
let toastView = RSTToastView(text: "Updating...", detailText: nil)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.activityIndicatorView.startAnimating()
|
||||
toastView.show(in: self.navigationController?.view ?? self.view)
|
||||
|
||||
let progress = AppManager.shared.install(installedApp.app, presentingViewController: self) { (result) in
|
||||
do
|
||||
{
|
||||
let app = try result.get()
|
||||
try app.managedObjectContext?.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let installedApp = DatabaseManager.shared.persistentContainer.viewContext.object(with: installedApp.objectID) as! InstalledApp
|
||||
|
||||
let toastView = RSTToastView(text: "Updated \(installedApp.app.name) to version \(installedApp.version)!", detailText: nil)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
let toastView = RSTToastView(text: "Failed to update \(installedApp.app.name)", detailText: error.localizedDescription)
|
||||
toastView.tintColor = .altPurple
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.progressView.observedProgress = nil
|
||||
self.progressView.progress = 0.0
|
||||
}
|
||||
}
|
||||
|
||||
self.progressView.observedProgress = progress
|
||||
}
|
||||
|
||||
@objc func didFetchApps(_ notification: Notification)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
if self.dataSource.fetchedResultsController.fetchedObjects == nil
|
||||
{
|
||||
do { try self.dataSource.fetchedResultsController.performFetch() }
|
||||
catch { print("Error fetching:", error) }
|
||||
}
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UpdatesViewController
|
||||
{
|
||||
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?
|
||||
{
|
||||
let updateAction = UITableViewRowAction(style: .normal, title: "Update") { [weak self] (action, indexPath) in
|
||||
guard let installedApp = self?.dataSource.item(at: indexPath) else { return }
|
||||
self?.update(installedApp)
|
||||
}
|
||||
updateAction.backgroundColor = .altPurple
|
||||
|
||||
return [updateAction]
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user