Files
SideStore/AltStore/My Apps/MyAppsViewController.swift
2019-06-06 12:56:13 -07:00

231 lines
8.6 KiB
Swift

//
// MyAppsViewController.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class MyAppsViewController: UITableViewController
{
private var refreshErrors = [String: Error]()
private lazy var dataSource = self.makeDataSource()
private lazy var dateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .short
return dateFormatter
}()
override func viewDidLoad()
{
super.viewDidLoad()
self.tableView.dataSource = self.dataSource
self.update()
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
self.update()
}
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
}
}
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)
dataSource.proxy = self
dataSource.cellConfigurationHandler = { [weak self] (cell, installedApp, indexPath) in
guard let app = installedApp.app else { return }
cell.textLabel?.text = app.name
let detailText =
"""
Expires: \(self?.dateFormatter.string(from: installedApp.expirationDate) ?? "-")
"""
cell.detailTextLabel?.numberOfLines = 1
cell.detailTextLabel?.text = detailText
cell.detailTextLabel?.textColor = .red
if let _ = self?.refreshErrors[installedApp.bundleIdentifier]
{
cell.accessoryType = .detailButton
cell.tintColor = .red
}
else
{
cell.accessoryType = .none
cell.tintColor = nil
}
}
return dataSource
}
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
AppManager.shared.refreshAllApps(presentingViewController: self) { (result) in
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
}
sender.isIndicatingActivity = false
self.update()
}
}
}
}
extension MyAppsViewController
{
override func tableView(_ tableView: UITableView, editActionsForRowAt indexPath: IndexPath) -> [UITableViewRowAction]?
{
let deleteAction = UITableViewRowAction(style: .destructive, title: "Delete") { (action, indexPath) in
let installedApp = self.dataSource.item(at: indexPath)
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)
}
}
}
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)
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)
self.update()
}
}
}
}
return [deleteAction, refreshAction]
}
override func tableView(_ tableView: UITableView, commit editingStyle: UITableViewCell.EditingStyle, forRowAt indexPath: IndexPath)
{
}
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)
}
}