diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 8f6a0943..9f47cc33 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -14,9 +14,10 @@ BFB1169D22932DB100BB457C /* Apps.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps.json */; }; BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; }; BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; }; + BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; }; BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; }; BFD247702284B9A500981D42 /* AppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476F2284B9A500981D42 /* AppsViewController.swift */; }; - BFD247722284B9A500981D42 /* SecondViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* SecondViewController.swift */; }; + BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD247712284B9A500981D42 /* MyAppsViewController.swift */; }; BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; }; BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; }; BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247782284B9A700981D42 /* LaunchScreen.storyboard */; }; @@ -51,10 +52,11 @@ BFB1169C22932DB100BB457C /* Apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = Apps.json; sourceTree = ""; }; BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = ""; }; BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + BFBBE2E022931F81002097FA /* InstalledApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledApp.swift; sourceTree = ""; }; BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; }; BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; BFD2476F2284B9A500981D42 /* AppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppsViewController.swift; sourceTree = ""; }; - BFD247712284B9A500981D42 /* SecondViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SecondViewController.swift; sourceTree = ""; }; + BFD247712284B9A500981D42 /* MyAppsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MyAppsViewController.swift; sourceTree = ""; }; BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; BFD247762284B9A700981D42 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; BFD247792284B9A700981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; @@ -80,6 +82,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + BFBBE2E2229320A2002097FA /* My Apps */ = { + isa = PBXGroup; + children = ( + BFD247712284B9A500981D42 /* MyAppsViewController.swift */, + ); + path = "My Apps"; + sourceTree = ""; + }; BFD247612284B9A500981D42 = { isa = PBXGroup; children = ( @@ -103,10 +113,10 @@ BFD2476D2284B9A500981D42 /* AppDelegate.swift */, BFD247732284B9A500981D42 /* Main.storyboard */, BFD2478A2284C49000981D42 /* Apps */, + BFBBE2E2229320A2002097FA /* My Apps */, BFD247982284D7FC00981D42 /* Model */, BFD2478D2284C4C700981D42 /* Components */, BFD2479D2284FBBD00981D42 /* Extensions */, - BFD247712284B9A500981D42 /* SecondViewController.swift */, BFD247962284D7C100981D42 /* Resources */, BFD247972284D7D800981D42 /* Supporting Files */, ); @@ -165,6 +175,7 @@ BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */, BFB11691229322E400BB457C /* DatabaseManager.swift */, BFBBE2DE22931F73002097FA /* App.swift */, + BFBBE2E022931F81002097FA /* InstalledApp.swift */, ); path = Model; sourceTree = ""; @@ -252,8 +263,9 @@ buildActionMask = 2147483647; files = ( BFD2478F2284C8F900981D42 /* Button.swift in Sources */, - BFD247722284B9A500981D42 /* SecondViewController.swift in Sources */, + BFD247722284B9A500981D42 /* MyAppsViewController.swift in Sources */, BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, + BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, BFBBE2DF22931F73002097FA /* App.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */, diff --git a/AltStore/Apps/AppDetailViewController.swift b/AltStore/Apps/AppDetailViewController.swift index 17fe3d3c..d006d8e9 100644 --- a/AltStore/Apps/AppDetailViewController.swift +++ b/AltStore/Apps/AppDetailViewController.swift @@ -48,6 +48,8 @@ class AppDetailViewController: UITableViewController self.tableView.delegate = self self.screenshotsCollectionView.dataSource = self.screenshotsDataSource + self.downloadButton.activityIndicatorView.style = .white + self.update() } @@ -95,6 +97,39 @@ private extension AppDetailViewController } } +private extension AppDetailViewController +{ + @IBAction func downloadApp(_ sender: UIButton) + { + guard self.app.installedApp == nil else { return } + + sender.isIndicatingActivity = true + + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + let app = context.object(with: self.app.objectID) as! App + + _ = InstalledApp(app: app, + bundleIdentifier: app.identifier, + signedDate: Date(), + expirationDate: Date().addingTimeInterval(60 * 60 * 24 * 7), + context: context) + + do + { + try context.save() + } + catch + { + print("Failed to download app.", error) + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { + sender.isIndicatingActivity = false + } + } + } +} + extension AppDetailViewController { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat diff --git a/AltStore/Apps/AppsViewController.swift b/AltStore/Apps/AppsViewController.swift index 48813036..4b06f562 100644 --- a/AltStore/Apps/AppsViewController.swift +++ b/AltStore/Apps/AppsViewController.swift @@ -59,6 +59,15 @@ private extension AppsViewController cell.developerLabel.text = app.developerName cell.appIconImageView.image = UIImage(named: app.iconName) + if app.installedApp != nil + { + cell.button.isEnabled = false + cell.button.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal) + } + else + { + cell.button.isEnabled = true + cell.button.setTitle(NSLocalizedString("Download", comment: ""), for: .normal) } } diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index 2438eae7..6ded4674 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -6,47 +6,9 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - @@ -58,13 +20,58 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -219,6 +226,9 @@ + + + @@ -376,6 +386,25 @@ + + + + + + + + + + + + + + + + + + + diff --git a/AltStore/Components/Button.swift b/AltStore/Components/Button.swift index aa2860ea..7963c2cf 100644 --- a/AltStore/Components/Button.swift +++ b/AltStore/Components/Button.swift @@ -41,12 +41,25 @@ class Button: UIButton self.update() } } + + override var isEnabled: Bool { + didSet { + self.update() + } + } } private extension Button { func update() { - self.backgroundColor = self.tintColor + if self.isEnabled + { + self.backgroundColor = self.tintColor + } + else + { + self.backgroundColor = .lightGray + } } } diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index fe0ea2d0..8f3cc203 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -7,13 +7,22 @@ + + + + + + + + - + + \ No newline at end of file diff --git a/AltStore/Model/App.swift b/AltStore/Model/App.swift index 1995cdbc..b7e93eab 100644 --- a/AltStore/Model/App.swift +++ b/AltStore/Model/App.swift @@ -22,6 +22,9 @@ class App: NSManagedObject, Decodable @NSManaged private(set) var iconName: String @NSManaged private(set) var screenshotNames: [String] + /* Relationships */ + @NSManaged private(set) var installedApp: InstalledApp? + private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) { super.init(entity: entity, insertInto: context) diff --git a/AltStore/Model/InstalledApp.swift b/AltStore/Model/InstalledApp.swift new file mode 100644 index 00000000..20944e76 --- /dev/null +++ b/AltStore/Model/InstalledApp.swift @@ -0,0 +1,50 @@ +// +// InstalledApp.swift +// AltStore +// +// Created by Riley Testut on 5/20/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import Foundation +import CoreData + +@objc(InstalledApp) +class InstalledApp: NSManagedObject +{ + /* Properties */ + @NSManaged var bundleIdentifier: String + + @NSManaged var signedDate: Date + @NSManaged var expirationDate: Date + + @NSManaged var isBeta: Bool + + /* Relationships */ + @NSManaged private(set) var app: App? + + private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) + { + super.init(entity: entity, insertInto: context) + } + + init(app: App, bundleIdentifier: String, signedDate: Date, expirationDate: Date, context: NSManagedObjectContext) + { + super.init(entity: InstalledApp.entity(), insertInto: context) + + let app = context.object(with: app.objectID) as! App + self.app = app + + self.bundleIdentifier = bundleIdentifier + self.signedDate = signedDate + self.expirationDate = expirationDate + } +} + +extension InstalledApp +{ + @nonobjc class func fetchRequest() -> NSFetchRequest + { + return NSFetchRequest(entityName: "InstalledApp") + } +} diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift new file mode 100644 index 00000000..5fe2cbd7 --- /dev/null +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -0,0 +1,58 @@ +// +// 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 lazy var dataSource = self.makeDataSource() + + private lazy var dateFormatter: DateFormatter = { + let dateFormatter = DateFormatter() + dateFormatter.dateStyle = .short + dateFormatter.timeStyle = .none + return dateFormatter + }() + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.dataSource = self.dataSource + } +} + +private extension MyAppsViewController +{ + func makeDataSource() -> RSTFetchedResultsTableViewDataSource + { + let fetchRequest = InstalledApp.fetchRequest() as NSFetchRequest + 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.cellConfigurationHandler = { (cell, installedApp, indexPath) in + guard let app = installedApp.app else { return } + + cell.textLabel?.text = app.name + + let detailText = + """ + Signed: \(self.dateFormatter.string(from: installedApp.signedDate)) + Expires: \(self.dateFormatter.string(from: installedApp.expirationDate)) + """ + + cell.detailTextLabel?.numberOfLines = 2 + cell.detailTextLabel?.text = detailText + } + + return dataSource + } +}