[AltStore] Adds redesigned AppViewController to view/download AltStore apps

This commit is contained in:
Riley Testut
2019-07-24 12:23:54 -07:00
parent 711dd69b74
commit fc44dfb19c
37 changed files with 1583 additions and 399 deletions

View File

@@ -27,12 +27,18 @@
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="app" inverseEntity="InstalledApp" syncable="YES"/>
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
<uniquenessConstraints>
<uniquenessConstraint>
<constraint value="identifier"/>
</uniquenessConstraint>
</uniquenessConstraints>
</entity>
<entity name="AppPermission" representedClassName="AppPermission" syncable="YES">
<attribute name="type" attributeType="String" syncable="YES"/>
<attribute name="usageDescription" attributeType="String" syncable="YES"/>
<relationship name="app" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="App" inverseName="permissions" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="InstalledApp" representedClassName="InstalledApp" syncable="YES">
<attribute name="bundleIdentifier" attributeType="String" syncable="YES"/>
<attribute name="expirationDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
@@ -59,8 +65,9 @@
</entity>
<elements>
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="240"/>
<element name="App" positionX="-63" positionY="-18" width="128" height="255"/>
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="120"/>
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
</elements>
</model>

View File

@@ -39,6 +39,11 @@ class App: NSManagedObject, Decodable, Fetchable
/* Relationships */
@NSManaged private(set) var installedApp: InstalledApp?
@objc(permissions) @NSManaged var _permissions: NSOrderedSet
@nonobjc var permissions: [AppPermission] {
return self._permissions.array as! [AppPermission]
}
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
@@ -59,6 +64,7 @@ class App: NSManagedObject, Decodable, Fetchable
case downloadURL
case tintColor
case subtitle
case permissions
}
required init(from decoder: Decoder) throws
@@ -93,7 +99,12 @@ class App: NSManagedObject, Decodable, Fetchable
self.tintColor = tintColor
}
let permissions = try container.decodeIfPresent([AppPermission].self, forKey: .permissions) ?? []
context.insert(self)
// Must assign after we're inserted into context.
self._permissions = NSOrderedSet(array: permissions)
}
}

View File

@@ -0,0 +1,88 @@
//
// AppPermission.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import UIKit
extension ALTAppPermissionType
{
var localizedShortName: String? {
switch self
{
case .photos: return NSLocalizedString("Photos", comment: "")
case .backgroundAudio: return NSLocalizedString("Audio (BG)", comment: "")
case .backgroundFetch: return NSLocalizedString("Fetch (BG)", comment: "")
default: return nil
}
}
var localizedName: String? {
switch self
{
case .photos: return NSLocalizedString("Photos", comment: "")
case .backgroundAudio: return NSLocalizedString("Background Audio", comment: "")
case .backgroundFetch: return NSLocalizedString("Background Fetch", comment: "")
default: return nil
}
}
var icon: UIImage? {
switch self
{
case .photos: return UIImage(named: "PhotosPermission")
case .backgroundAudio: return UIImage(named: "BackgroundAudioPermission")
case .backgroundFetch: return UIImage(named: "BackgroundFetchPermission")
default: return nil
}
}
}
@objc(AppPermission)
class AppPermission: NSManagedObject, Decodable, Fetchable
{
/* Properties */
@NSManaged var type: ALTAppPermissionType
@NSManaged var usageDescription: String
/* Relationships */
@NSManaged private(set) var app: App!
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
{
super.init(entity: entity, insertInto: context)
}
private enum CodingKeys: String, CodingKey
{
case type
case usageDescription
}
required init(from decoder: Decoder) throws
{
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
super.init(entity: AppPermission.entity(), insertInto: nil)
let container = try decoder.container(keyedBy: CodingKeys.self)
self.usageDescription = try container.decode(String.self, forKey: .usageDescription)
let rawType = try container.decode(String.self, forKey: .type)
self.type = ALTAppPermissionType(rawValue: rawType)
context.insert(self)
}
}
extension AppPermission
{
@nonobjc class func fetchRequest() -> NSFetchRequest<AppPermission>
{
return NSFetchRequest<AppPermission>(entityName: "AppPermission")
}
}

View File

@@ -22,6 +22,7 @@ public class DatabaseManager
private init()
{
self.persistentContainer = RSTPersistentContainer(name: "AltStore")
self.persistentContainer.preferredMergePolicy = MergePolicy()
}
}

View File

@@ -0,0 +1,39 @@
//
// MergePolicy.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import CoreData
import Roxas
open class MergePolicy: RSTRelationshipPreservingMergePolicy
{
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
{
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
return try super.resolve(constraintConflicts: conflicts)
}
for conflict in conflicts
{
switch conflict.databaseObject
{
case let databaseObject as App:
// Delete previous permissions
for permission in databaseObject.permissions
{
permission.managedObjectContext?.delete(permission)
}
default: break
}
}
try super.resolve(constraintConflicts: conflicts)
}
}