2019-05-20 21:26:01 +02:00
|
|
|
//
|
|
|
|
|
// MyAppsViewController.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 5/9/19.
|
|
|
|
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import UIKit
|
|
|
|
|
import Roxas
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
import AltSign
|
|
|
|
|
|
2019-05-20 21:26:01 +02:00
|
|
|
class MyAppsViewController: UITableViewController
|
|
|
|
|
{
|
2019-06-06 12:56:13 -07:00
|
|
|
private var refreshErrors = [String: Error]()
|
|
|
|
|
|
2019-05-20 21:26:01 +02:00
|
|
|
private lazy var dataSource = self.makeDataSource()
|
|
|
|
|
|
|
|
|
|
private lazy var dateFormatter: DateFormatter = {
|
|
|
|
|
let dateFormatter = DateFormatter()
|
|
|
|
|
dateFormatter.dateStyle = .short
|
2019-06-05 11:03:49 -07:00
|
|
|
dateFormatter.timeStyle = .short
|
2019-05-20 21:26:01 +02:00
|
|
|
return dateFormatter
|
|
|
|
|
}()
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
@IBOutlet private var progressView: UIProgressView!
|
|
|
|
|
|
2019-05-20 21:26:01 +02:00
|
|
|
override func viewDidLoad()
|
|
|
|
|
{
|
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
|
|
|
|
self.tableView.dataSource = self.dataSource
|
2019-06-06 12:56:13 -07:00
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
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)])
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-06 12:56:13 -07:00
|
|
|
self.update()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func viewWillAppear(_ animated: Bool)
|
|
|
|
|
{
|
|
|
|
|
super.viewWillAppear(animated)
|
|
|
|
|
|
|
|
|
|
self.update()
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
2019-05-20 21:40:04 +02:00
|
|
|
|
|
|
|
|
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
|
|
|
|
{
|
|
|
|
|
guard segue.identifier == "showAppDetail" else { return }
|
|
|
|
|
|
|
|
|
|
guard let cell = sender as? UITableViewCell, let indexPath = self.tableView.indexPath(for: cell) else { return }
|
|
|
|
|
|
|
|
|
|
let installedApp = self.dataSource.item(at: indexPath)
|
|
|
|
|
guard let app = installedApp.app else { return }
|
|
|
|
|
|
|
|
|
|
let appDetailViewController = segue.destination as! AppDetailViewController
|
|
|
|
|
appDetailViewController.app = app
|
|
|
|
|
}
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension MyAppsViewController
|
|
|
|
|
{
|
|
|
|
|
func makeDataSource() -> RSTFetchedResultsTableViewDataSource<InstalledApp>
|
|
|
|
|
{
|
|
|
|
|
let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest<InstalledApp>
|
|
|
|
|
fetchRequest.relationshipKeyPathsForPrefetching = [#keyPath(InstalledApp.app)]
|
|
|
|
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \InstalledApp.app?.name, ascending: true)]
|
|
|
|
|
fetchRequest.returnsObjectsAsFaults = false
|
|
|
|
|
|
|
|
|
|
let dataSource = RSTFetchedResultsTableViewDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
2019-05-20 21:36:39 +02:00
|
|
|
dataSource.proxy = self
|
2019-06-06 12:56:13 -07:00
|
|
|
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
|
2019-05-20 21:26:01 +02:00
|
|
|
guard let app = installedApp.app else { return }
|
|
|
|
|
|
2019-06-17 14:49:23 -07:00
|
|
|
cell.textLabel?.text = app.name + " (\(installedApp.version))"
|
2019-05-20 21:26:01 +02:00
|
|
|
|
|
|
|
|
let detailText =
|
|
|
|
|
"""
|
2019-06-06 12:56:13 -07:00
|
|
|
Expires: \(self?.dateFormatter.string(from: installedApp.expirationDate) ?? "-")
|
2019-05-20 21:26:01 +02:00
|
|
|
"""
|
|
|
|
|
|
2019-06-04 18:50:55 -07:00
|
|
|
cell.detailTextLabel?.numberOfLines = 1
|
2019-05-20 21:26:01 +02:00
|
|
|
cell.detailTextLabel?.text = detailText
|
2019-06-04 18:50:55 -07:00
|
|
|
cell.detailTextLabel?.textColor = .red
|
2019-06-06 12:56:13 -07:00
|
|
|
|
|
|
|
|
if let _ = self?.refreshErrors[installedApp.bundleIdentifier]
|
|
|
|
|
{
|
|
|
|
|
cell.accessoryType = .detailButton
|
|
|
|
|
cell.tintColor = .red
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
cell.accessoryType = .none
|
|
|
|
|
cell.tintColor = nil
|
|
|
|
|
}
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataSource
|
|
|
|
|
}
|
2019-06-06 12:56:13 -07:00
|
|
|
|
|
|
|
|
func update()
|
|
|
|
|
{
|
|
|
|
|
self.navigationItem.rightBarButtonItem?.isEnabled = !(self.dataSource.fetchedResultsController.fetchedObjects?.isEmpty ?? true)
|
|
|
|
|
|
|
|
|
|
self.tableView.reloadData()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension MyAppsViewController
|
|
|
|
|
{
|
|
|
|
|
@IBAction func refreshAllApps(_ sender: UIBarButtonItem)
|
|
|
|
|
{
|
|
|
|
|
sender.isIndicatingActivity = true
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
let installedApps = InstalledApp.all(in: DatabaseManager.shared.viewContext)
|
|
|
|
|
|
|
|
|
|
let progress = AppManager.shared.refresh(installedApps, presentingViewController: self) { (result) in
|
2019-06-06 12:56:13 -07:00
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .failure(let error):
|
|
|
|
|
let toastView = RSTToastView(text: error.localizedDescription, detailText: nil)
|
|
|
|
|
toastView.tintColor = .red
|
|
|
|
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
|
|
|
|
|
|
|
|
|
self.refreshErrors = [:]
|
|
|
|
|
|
|
|
|
|
case .success(let results):
|
|
|
|
|
let failures = results.compactMapValues { $0.error }
|
|
|
|
|
|
|
|
|
|
if failures.isEmpty
|
|
|
|
|
{
|
|
|
|
|
let toastView = RSTToastView(text: NSLocalizedString("Successfully refreshed apps!", comment: ""), detailText: nil)
|
|
|
|
|
toastView.tintColor = .altPurple
|
|
|
|
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
let localizedText: String
|
|
|
|
|
if failures.count == 1
|
|
|
|
|
{
|
|
|
|
|
localizedText = String(format: NSLocalizedString("Failed to refresh %@ app.", comment: ""), NSNumber(value: failures.count))
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
localizedText = String(format: NSLocalizedString("Failed to refresh %@ apps.", comment: ""), NSNumber(value: failures.count))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let toastView = RSTToastView(text: localizedText, detailText: nil)
|
|
|
|
|
toastView.tintColor = .red
|
|
|
|
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.refreshErrors = failures
|
|
|
|
|
}
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
self.progressView.observedProgress = nil
|
|
|
|
|
self.progressView.progress = 0.0
|
|
|
|
|
|
2019-06-06 12:56:13 -07:00
|
|
|
sender.isIndicatingActivity = false
|
|
|
|
|
self.update()
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
|
|
|
|
self.progressView.observedProgress = progress
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func refresh(_ installedApp: InstalledApp)
|
|
|
|
|
{
|
|
|
|
|
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
let app = try result.get()
|
|
|
|
|
try app.managedObjectContext?.save()
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", 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 refresh \(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
|
2019-06-06 12:56:13 -07:00
|
|
|
}
|
2019-05-20 21:26:01 +02:00
|
|
|
}
|
2019-05-20 21:36:39 +02:00
|
|
|
|
|
|
|
|
extension MyAppsViewController
|
|
|
|
|
{
|
2019-06-05 11:03:49 -07:00
|
|
|
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?
|
2019-05-20 21:36:39 +02:00
|
|
|
{
|
2019-06-05 11:03:49 -07:00
|
|
|
let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
|
|
|
|
|
let installedApp = self.dataSource.item(at: indexPath)
|
2019-05-20 21:36:39 +02:00
|
|
|
|
2019-06-05 11:03:49 -07:00
|
|
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
|
|
|
|
let installedApp = context.object(with: installedApp.objectID) as! InstalledApp
|
|
|
|
|
context.delete(installedApp)
|
|
|
|
|
|
|
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
try context.save()
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
print("Failed to delete installed app.", error)
|
|
|
|
|
}
|
2019-05-20 21:36:39 +02:00
|
|
|
}
|
2019-06-05 11:03:49 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let refreshAction = UITableViewRowAction(style: .normal, title: "Refresh") { (action, indexPath) in
|
|
|
|
|
let installedApp = self.dataSource.item(at: indexPath)
|
|
|
|
|
|
|
|
|
|
let toastView = RSTToastView(text: "Refreshing...", detailText: nil)
|
|
|
|
|
toastView.tintColor = .altPurple
|
|
|
|
|
toastView.activityIndicatorView.startAnimating()
|
|
|
|
|
toastView.show(in: self.navigationController?.view ?? self.view)
|
|
|
|
|
|
2019-06-10 15:03:47 -07:00
|
|
|
let progress = AppManager.shared.refresh(installedApp, presentingViewController: self) { (result) in
|
2019-06-05 11:03:49 -07:00
|
|
|
do
|
|
|
|
|
{
|
|
|
|
|
let app = try result.get()
|
|
|
|
|
try app.managedObjectContext?.save()
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
let toastView = RSTToastView(text: "Refreshed \(installedApp.app.name)!", detailText: nil)
|
|
|
|
|
toastView.tintColor = .altPurple
|
|
|
|
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
2019-06-06 12:56:13 -07:00
|
|
|
|
|
|
|
|
self.update()
|
2019-06-05 11:03:49 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
let toastView = RSTToastView(text: "Failed to refresh \(installedApp.app.name)", detailText: error.localizedDescription)
|
|
|
|
|
toastView.tintColor = .altPurple
|
|
|
|
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.progressView.observedProgress = nil
|
|
|
|
|
self.progressView.progress = 0.0
|
|
|
|
|
}
|
2019-05-20 21:36:39 +02:00
|
|
|
}
|
2019-06-10 15:03:47 -07:00
|
|
|
|
|
|
|
|
self.progressView.observedProgress = progress
|
2019-05-20 21:36:39 +02:00
|
|
|
}
|
2019-06-05 11:03:49 -07:00
|
|
|
|
|
|
|
|
return [deleteAction, refreshAction]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
|
|
|
|
|
{
|
2019-05-20 21:36:39 +02:00
|
|
|
}
|
2019-06-06 12:56:13 -07:00
|
|
|
|
|
|
|
|
override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath)
|
|
|
|
|
{
|
|
|
|
|
let installedApp = self.dataSource.item(at: indexPath)
|
|
|
|
|
|
|
|
|
|
guard let error = self.refreshErrors[installedApp.bundleIdentifier] else { return }
|
|
|
|
|
|
|
|
|
|
let alertController = UIAlertController(title: "Failed to Refresh \(installedApp.app.name)", message: error.localizedDescription, preferredStyle: .alert)
|
|
|
|
|
alertController.addAction(.ok)
|
|
|
|
|
self.present(alertController, animated: true, completion: nil)
|
|
|
|
|
}
|
2019-05-20 21:36:39 +02:00
|
|
|
}
|