mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
[AltStore] Sideloads apps from Files
This commit is contained in:
@@ -927,7 +927,13 @@ World</string>
|
||||
<outlet property="delegate" destination="hv7-Ar-voT" id="1PN-pf-cZK"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="My Apps" id="zLJ-Cg-ijh"/>
|
||||
<navigationItem key="navigationItem" title="My Apps" id="zLJ-Cg-ijh">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="add" id="4MN-JN-Knh">
|
||||
<connections>
|
||||
<action selector="sideloadApp:" destination="hv7-Ar-voT" id="Tjv-oi-2H0"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="kiO-UO-esV" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
|
||||
@@ -26,6 +26,10 @@ class InstalledApp: NSManagedObject, Fetchable
|
||||
/* Relationships */
|
||||
@NSManaged var storeApp: App?
|
||||
|
||||
var isSideloaded: Bool {
|
||||
return self.storeApp == nil
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
{
|
||||
super.init(entity: entity, insertInto: context)
|
||||
|
||||
@@ -11,6 +11,8 @@ import UIKit
|
||||
import AltKit
|
||||
import Roxas
|
||||
|
||||
import AltSign
|
||||
|
||||
private let maximumCollapsedUpdatesCount = 2
|
||||
|
||||
extension MyAppsViewController
|
||||
@@ -43,12 +45,15 @@ class MyAppsViewController: UICollectionViewController
|
||||
private lazy var installedAppsDataSource = self.makeInstalledAppsDataSource()
|
||||
|
||||
private var prototypeUpdateCell: UpdateCollectionViewCell!
|
||||
private var longPressGestureRecognizer: UILongPressGestureRecognizer!
|
||||
private var sideloadingProgressView: UIProgressView!
|
||||
|
||||
// State
|
||||
private var isUpdateSectionCollapsed = true
|
||||
private var expandedAppUpdates = Set<String>()
|
||||
private var isRefreshingAllApps = false
|
||||
private var refreshGroup: OperationGroup?
|
||||
private var sideloadingProgress: Progress?
|
||||
|
||||
// Cache
|
||||
private var cachedUpdateSizes = [String: CGSize]()
|
||||
@@ -83,6 +88,23 @@ class MyAppsViewController: UICollectionViewController
|
||||
|
||||
self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell")
|
||||
self.collectionView.register(UpdatesCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader")
|
||||
|
||||
self.sideloadingProgressView = UIProgressView(progressViewStyle: .bar)
|
||||
self.sideloadingProgressView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.sideloadingProgressView.progressTintColor = .altGreen
|
||||
self.sideloadingProgressView.progress = 0
|
||||
|
||||
if let navigationBar = self.navigationController?.navigationBar
|
||||
{
|
||||
navigationBar.addSubview(self.sideloadingProgressView)
|
||||
NSLayoutConstraint.activate([self.sideloadingProgressView.leadingAnchor.constraint(equalTo: navigationBar.leadingAnchor),
|
||||
self.sideloadingProgressView.trailingAnchor.constraint(equalTo: navigationBar.trailingAnchor),
|
||||
self.sideloadingProgressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)])
|
||||
}
|
||||
|
||||
// Gestures
|
||||
self.longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(MyAppsViewController.handleLongPressGesture(_:)))
|
||||
self.collectionView.addGestureRecognizer(self.longPressGestureRecognizer)
|
||||
}
|
||||
|
||||
override func prepare(for segue: UIStoryboardSegue, sender: Any?)
|
||||
@@ -96,6 +118,16 @@ class MyAppsViewController: UICollectionViewController
|
||||
let appViewController = segue.destination as! AppViewController
|
||||
appViewController.app = installedApp.storeApp
|
||||
}
|
||||
|
||||
override func shouldPerformSegue(withIdentifier identifier: String, sender: Any?) -> Bool
|
||||
{
|
||||
guard identifier == "showApp" else { return true }
|
||||
|
||||
guard let cell = sender as? UICollectionViewCell, let indexPath = self.collectionView.indexPath(for: cell) else { return true }
|
||||
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
return !installedApp.isSideloaded
|
||||
}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
@@ -187,13 +219,12 @@ private extension MyAppsViewController
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<InstalledApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
||||
dataSource.cellConfigurationHandler = { (cell, installedApp, indexPath) in
|
||||
guard let app = installedApp.storeApp else { return }
|
||||
|
||||
let tintColor = app.tintColor ?? .altGreen
|
||||
let tintColor = installedApp.storeApp?.tintColor ?? .altGreen
|
||||
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.tintColor = tintColor
|
||||
cell.appIconImageView.image = UIImage(named: app.iconName)
|
||||
cell.appIconImageView.isIndicatingActivity = true
|
||||
|
||||
cell.refreshButton.isIndicatingActivity = false
|
||||
cell.refreshButton.addTarget(self, action: #selector(MyAppsViewController.refreshApp(_:)), for: .primaryActionTriggered)
|
||||
|
||||
@@ -210,8 +241,8 @@ private extension MyAppsViewController
|
||||
cell.refreshButton.setTitle(String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays)), for: .normal)
|
||||
}
|
||||
|
||||
cell.nameLabel.text = app.name
|
||||
cell.developerLabel.text = app.developerName
|
||||
cell.nameLabel.text = installedApp.name
|
||||
cell.developerLabel.text = installedApp.storeApp?.developerName ?? NSLocalizedString("Sideloaded", comment: "")
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
@@ -224,7 +255,7 @@ private extension MyAppsViewController
|
||||
default: cell.refreshButton.tintColor = .refreshRed
|
||||
}
|
||||
|
||||
if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: app), progress.fractionCompleted < 1.0
|
||||
if let refreshGroup = self.refreshGroup, let progress = refreshGroup.progress(for: installedApp), progress.fractionCompleted < 1.0
|
||||
{
|
||||
cell.refreshButton.progress = progress
|
||||
}
|
||||
@@ -233,6 +264,24 @@ private extension MyAppsViewController
|
||||
cell.refreshButton.progress = nil
|
||||
}
|
||||
}
|
||||
dataSource.prefetchHandler = { (item, indexPath, completion) in
|
||||
let fileURL = item.fileURL
|
||||
|
||||
return BlockOperation {
|
||||
guard let application = ALTApplication(fileURL: fileURL) else {
|
||||
completion(nil, OperationError.invalidApp)
|
||||
return
|
||||
}
|
||||
|
||||
let icon = application.icon
|
||||
completion(icon, nil)
|
||||
}
|
||||
}
|
||||
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||
let cell = cell as! InstalledAppCollectionViewCell
|
||||
cell.appIconImageView.image = image
|
||||
cell.appIconImageView.isIndicatingActivity = false
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
@@ -465,6 +514,35 @@ private extension MyAppsViewController
|
||||
self.collectionView.reloadItems(at: [indexPath])
|
||||
}
|
||||
|
||||
@IBAction func sideloadApp(_ sender: UIBarButtonItem)
|
||||
{
|
||||
let iOSAppUTI = "com.apple.itunes.ipa" // Declared by the system.
|
||||
|
||||
let documentPickerViewController = UIDocumentPickerViewController(documentTypes: [iOSAppUTI], in: .import)
|
||||
documentPickerViewController.delegate = self
|
||||
self.present(documentPickerViewController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func presentAlert(for installedApp: InstalledApp)
|
||||
{
|
||||
let alertController = UIAlertController(title: nil, message: NSLocalizedString("Removing a sideloaded app only removes it from AltStore. You must also delete it from the home screen to fully uninstall the app.", comment: ""), preferredStyle: .actionSheet)
|
||||
alertController.addAction(.cancel)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove", comment: ""), style: .destructive, handler: { (action) in
|
||||
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 remove sideloaded app.", error) }
|
||||
}
|
||||
}))
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private extension MyAppsViewController
|
||||
{
|
||||
@objc func didFetchApps(_ notification: Notification)
|
||||
{
|
||||
DispatchQueue.main.async {
|
||||
@@ -477,6 +555,23 @@ private extension MyAppsViewController
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func handleLongPressGesture(_ gestureRecognizer: UILongPressGestureRecognizer)
|
||||
{
|
||||
guard gestureRecognizer.state == .began else { return }
|
||||
|
||||
let point = gestureRecognizer.location(in: self.collectionView)
|
||||
|
||||
guard
|
||||
let indexPath = self.collectionView.indexPathForItem(at: point),
|
||||
indexPath.section == Section.installedApps.rawValue
|
||||
else { return }
|
||||
|
||||
let installedApp = self.dataSource.item(at: indexPath)
|
||||
guard installedApp.storeApp == nil else { return }
|
||||
|
||||
self.presentAlert(for: installedApp)
|
||||
}
|
||||
}
|
||||
|
||||
extension MyAppsViewController
|
||||
@@ -638,3 +733,58 @@ extension MyAppsViewController: NSFetchedResultsControllerDelegate
|
||||
self.updatesDataSource.controllerDidChangeContent(controller)
|
||||
}
|
||||
}
|
||||
|
||||
extension MyAppsViewController: UIDocumentPickerDelegate
|
||||
{
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL])
|
||||
{
|
||||
guard let fileURL = urls.first else { return }
|
||||
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
|
||||
|
||||
DispatchQueue.global().async {
|
||||
let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||
|
||||
do
|
||||
{
|
||||
try FileManager.default.createDirectory(at: temporaryDirectory, withIntermediateDirectories: true, attributes: nil)
|
||||
|
||||
let unzippedApplicationURL = try FileManager.default.unzipAppBundle(at: fileURL, toDirectory: temporaryDirectory)
|
||||
|
||||
guard let application = ALTApplication(fileURL: unzippedApplicationURL) else { return }
|
||||
|
||||
self.sideloadingProgress = AppManager.shared.install(application, presentingViewController: self) { (result) in
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
if let error = result.error
|
||||
{
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
}
|
||||
else
|
||||
{
|
||||
print("Successfully installed app:", application.bundleIdentifier)
|
||||
}
|
||||
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
self.sideloadingProgressView.observedProgress = nil
|
||||
self.sideloadingProgressView.setHidden(true, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.sideloadingProgressView.progress = 0
|
||||
self.sideloadingProgressView.isHidden = false
|
||||
self.sideloadingProgressView.observedProgress = self.sideloadingProgress
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
try? FileManager.default.removeItem(at: temporaryDirectory)
|
||||
|
||||
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user