From 87ced5523e1f4e81529c616bfc88abebf2ba2c9f Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Tue, 30 Jul 2019 17:00:04 -0700 Subject: [PATCH] [AltStore] Refactors fetch apps logic to use Source model objects --- AltStore.xcodeproj/project.pbxproj | 12 +- AltStore/AppDelegate.swift | 14 +- AltStore/Browse/BrowseViewController.swift | 22 ++- AltStore/Managing Apps/AppManager.swift | 32 ++-- .../AltStore.xcdatamodel/contents | 16 +- AltStore/Model/App.swift | 3 + AltStore/Model/DatabaseManager.swift | 3 + AltStore/Model/MergePolicy.swift | 14 ++ AltStore/Model/Source.swift | 92 +++++++++++ AltStore/My Apps/MyAppsViewController.swift | 4 +- ...ation.swift => FetchSourceOperation.swift} | 23 +-- AltStore/Operations/OperationError.swift | 3 + AltStore/Resources/Apps-Dev.json | 149 +++++++++--------- 13 files changed, 272 insertions(+), 115 deletions(-) create mode 100644 AltStore/Model/Source.swift rename AltStore/Operations/{FetchAppsOperation.swift => FetchSourceOperation.swift} (70%) diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 1911a671..31580ccd 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -109,7 +109,6 @@ BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E5722BC3D0F002A40FE /* OperationGroup.swift */; }; BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */; }; BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */ = {isa = PBXBuildFile; fileRef = BF770E6822BD57DD002A40FE /* Silence.m4a */; }; - BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */; }; BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C122E659F700049BA1 /* AppContentViewController.swift */; }; BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF8F69C322E662D300049BA1 /* AppViewController.swift */; }; BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */; }; @@ -176,6 +175,8 @@ BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */; }; BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */; }; BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */; }; + BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DC22F0E7F3002E24B9 /* Source.swift */; }; + BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */; }; BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE338E722F10E56002E24B9 /* LaunchViewController.swift */; }; BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFE6325922A83BEB00F30809 /* Authentication.storyboard */; }; BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */; }; @@ -356,7 +357,6 @@ BF770E5722BC3D0F002A40FE /* OperationGroup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationGroup.swift; sourceTree = ""; }; BF770E6622BD57C3002A40FE /* BackgroundTaskManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BackgroundTaskManager.swift; sourceTree = ""; }; BF770E6822BD57DD002A40FE /* Silence.m4a */ = {isa = PBXFileReference; lastKnownFileType = file; path = Silence.m4a; sourceTree = ""; }; - BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAppsOperation.swift; sourceTree = ""; }; BF8F69C122E659F700049BA1 /* AppContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewController.swift; sourceTree = ""; }; BF8F69C322E662D300049BA1 /* AppViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppViewController.swift; sourceTree = ""; }; BF9ABA4422DCFF43008935CF /* BrowseViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseViewController.swift; sourceTree = ""; }; @@ -427,6 +427,8 @@ BFDB6A0A22AAEDB7007EA6D6 /* Operation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Operation.swift; sourceTree = ""; }; BFDB6A0C22AAFC19007EA6D6 /* OperationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationError.swift; sourceTree = ""; }; BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendAppOperation.swift; sourceTree = ""; }; + BFE338DC22F0E7F3002E24B9 /* Source.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Source.swift; sourceTree = ""; }; + BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchSourceOperation.swift; sourceTree = ""; }; BFE338E722F10E56002E24B9 /* LaunchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LaunchViewController.swift; sourceTree = ""; }; BFE6325922A83BEB00F30809 /* Authentication.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Authentication.storyboard; sourceTree = ""; }; BFE6325B22A83C0100F30809 /* AuthenticationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationViewController.swift; sourceTree = ""; }; @@ -851,6 +853,7 @@ BFBBE2DE22931F73002097FA /* App.swift */, BF3D648722E79A3700E9056B /* AppPermission.swift */, BFBBE2E022931F81002097FA /* InstalledApp.swift */, + BFE338DC22F0E7F3002E24B9 /* Source.swift */, BFE6326522A857C100F30809 /* Team.swift */, ); path = Model; @@ -905,7 +908,7 @@ BFDB6A0722AAED73007EA6D6 /* ResignAppOperation.swift */, BFDB6A0E22AB2776007EA6D6 /* SendAppOperation.swift */, BF770E5022BB1CF6002A40FE /* InstallAppOperation.swift */, - BF7B9EF222B82B1F0042C873 /* FetchAppsOperation.swift */, + BFE338DE22F0EADB002E24B9 /* FetchSourceOperation.swift */, ); path = Operations; sourceTree = ""; @@ -1274,7 +1277,6 @@ files = ( BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, - BF7B9EF322B82B1F0042C873 /* FetchAppsOperation.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, @@ -1283,11 +1285,13 @@ BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */, BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */, BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */, + BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */, BFBBE2DF22931F73002097FA /* App.swift in Sources */, BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */, BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, + BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */, diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index e35f3ce1..4252ef1f 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -176,22 +176,22 @@ extension AppDelegate return } - var fetchAppsResult: Result<[App], Error>? + var fetchSourceResult: Result? var serversResult: Result? let dispatchGroup = DispatchGroup() dispatchGroup.enter() dispatchGroup.enter() - AppManager.shared.fetchApps() { (result) in - fetchAppsResult = result + AppManager.shared.fetchSource() { (result) in + fetchSourceResult = result dispatchGroup.leave() do { - let apps = try result.get() + let source = try result.get() - guard let context = apps.first?.managedObjectContext else { return } + guard let context = source.managedObjectContext else { return } let updatesFetchRequest = InstalledApp.updatesFetchRequest() updatesFetchRequest.includesPendingChanges = true @@ -230,13 +230,13 @@ extension AppDelegate } dispatchGroup.notify(queue: .main) { - guard let fetchAppsResult = fetchAppsResult, let serversResult = serversResult else { + guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else { completionHandler(.failed) return } // Call completionHandler early to improve chances of refreshing in the background again. - switch (fetchAppsResult, serversResult) + switch (fetchSourceResult, serversResult) { case (.success, .success): completionHandler(.newData) case (.success, .failure(ConnectionError.serverNotFound)): completionHandler(.newData) diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 29c871ea..f840d7ae 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -36,7 +36,7 @@ class BrowseViewController: UICollectionViewController { super.viewWillAppear(animated) - self.fetchApps() + self.fetchSource() } override func prepare(for segue: UIStoryboardSegue, sender: Any?) @@ -57,10 +57,18 @@ private extension BrowseViewController func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource { let fetchRequest = App.fetchRequest() as NSFetchRequest - fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.name, ascending: false)] - fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.bundleIdentifier), App.altstoreAppID) + fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \App.sortIndex, ascending: true), NSSortDescriptor(keyPath: \App.name, ascending: true)] fetchRequest.returnsObjectsAsFaults = false + if let source = Source.fetchAltStoreSource(in: DatabaseManager.shared.viewContext) + { + fetchRequest.predicate = NSPredicate(format: "%K != %@ AND %K == %@", #keyPath(App.bundleIdentifier), App.altstoreAppID, #keyPath(App.source), source) + } + else + { + fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(App.bundleIdentifier), App.altstoreAppID) + } + let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) dataSource.cellConfigurationHandler = { (cell, app, indexPath) in let cell = cell as! BrowseCollectionViewCell @@ -98,13 +106,13 @@ private extension BrowseViewController return dataSource } - func fetchApps() + func fetchSource() { - AppManager.shared.fetchApps() { (result) in + AppManager.shared.fetchSource() { (result) in do { - let apps = try result.get() - try apps.first?.managedObjectContext?.save() + let source = try result.get() + try source.managedObjectContext?.save() } catch { diff --git a/AltStore/Managing Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift index 493c53e9..3ff5a7c2 100644 --- a/AltStore/Managing Apps/AppManager.swift +++ b/AltStore/Managing Apps/AppManager.swift @@ -16,7 +16,7 @@ import Roxas extension AppManager { - static let didFetchAppsNotification = Notification.Name("com.altstore.AppManager.didFetchApps") + static let didFetchSourceNotification = Notification.Name("com.altstore.AppManager.didFetchSource") } class AppManager @@ -86,21 +86,27 @@ extension AppManager extension AppManager { - func fetchApps(completionHandler: @escaping (Result<[App], Error>) -> Void) + func fetchSource(completionHandler: @escaping (Result) -> Void) { - let fetchAppsOperation = FetchAppsOperation() - fetchAppsOperation.resultHandler = { (result) in - switch result - { - case .failure(let error): - completionHandler(.failure(error)) - - case .success(let apps): - completionHandler(.success(apps)) - NotificationCenter.default.post(name: AppManager.didFetchAppsNotification, object: self) + DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in + guard let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) else { + return completionHandler(.failure(OperationError.noSources)) } + + let fetchSourceOperation = FetchSourceOperation(sourceURL: source.sourceURL) + fetchSourceOperation.resultHandler = { (result) in + switch result + { + case .failure(let error): + completionHandler(.failure(error)) + + case .success(let source): + completionHandler(.success(source)) + NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self) + } + } + self.operationQueue.addOperation(fetchSourceOperation) } - self.operationQueue.addOperation(fetchAppsOperation) } } diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index 5342def1..2f6a06e6 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -22,6 +22,7 @@ + @@ -29,6 +30,7 @@ + @@ -54,6 +56,17 @@ + + + + + + + + + + + @@ -68,9 +81,10 @@ - + + \ No newline at end of file diff --git a/AltStore/Model/App.swift b/AltStore/Model/App.swift index a985e119..7064fe7e 100644 --- a/AltStore/Model/App.swift +++ b/AltStore/Model/App.swift @@ -39,8 +39,11 @@ class App: NSManagedObject, Decodable, Fetchable @NSManaged private(set) var downloadURL: URL @NSManaged private(set) var tintColor: UIColor? + @NSManaged var sortIndex: Int32 + /* Relationships */ @NSManaged var installedApp: InstalledApp? + @NSManaged var source: Source? @objc(permissions) @NSManaged var _permissions: NSOrderedSet @nonobjc var permissions: [AppPermission] { diff --git a/AltStore/Model/DatabaseManager.swift b/AltStore/Model/DatabaseManager.swift index 464b8c7a..33576501 100644 --- a/AltStore/Model/DatabaseManager.swift +++ b/AltStore/Model/DatabaseManager.swift @@ -118,8 +118,11 @@ private extension DatabaseManager } else { + let source = Source.makeAltStoreSource(in: context) + storeApp = App.makeAltStoreApp(in: context) storeApp.version = localApp.version + storeApp.source = source } let installedApp: InstalledApp diff --git a/AltStore/Model/MergePolicy.swift b/AltStore/Model/MergePolicy.swift index e7e25e4e..e7516231 100644 --- a/AltStore/Model/MergePolicy.swift +++ b/AltStore/Model/MergePolicy.swift @@ -30,6 +30,20 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy permission.managedObjectContext?.delete(permission) } + case let databaseObject as Source: + guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break } + + let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier }) + + for app in databaseObject.apps + { + if !bundleIdentifiers.contains(app.bundleIdentifier) + { + // No longer listed in Source, so remove it from database. + app.managedObjectContext?.delete(app) + } + } + default: break } } diff --git a/AltStore/Model/Source.swift b/AltStore/Model/Source.swift new file mode 100644 index 00000000..f28d4242 --- /dev/null +++ b/AltStore/Model/Source.swift @@ -0,0 +1,92 @@ +// +// Source.swift +// AltStore +// +// Created by Riley Testut on 7/30/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import CoreData + +extension Source +{ + static let altStoreIdentifier = "com.rileytestut.AltStore" +} + +@objc(Source) +class Source: NSManagedObject, Fetchable, Decodable +{ + /* Properties */ + @NSManaged var name: String + @NSManaged var identifier: String + @NSManaged var sourceURL: URL + + /* Relationships */ + @objc(apps) @NSManaged private(set) var _apps: NSOrderedSet + + @nonobjc var apps: [App] { + get { + return self._apps.array as! [App] + } + set { + self._apps = NSOrderedSet(array: newValue) + } + } + + private enum CodingKeys: String, CodingKey + { + case name + case identifier + case sourceURL + case apps + } + + private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?) + { + super.init(entity: entity, insertInto: context) + } + + required init(from decoder: Decoder) throws + { + guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") } + + super.init(entity: Source.entity(), insertInto: nil) + + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.identifier = try container.decode(String.self, forKey: .identifier) + self.sourceURL = try container.decode(URL.self, forKey: .sourceURL) + + let apps = try container.decodeIfPresent([App].self, forKey: .apps) ?? [] + for (index, app) in apps.enumerated() + { + app.sortIndex = Int32(index) + } + + context.insert(self) + + // Must assign after we're inserted into context. + self._apps = NSMutableOrderedSet(array: apps) + + print("Downloaded Order:", self.apps.map { $0.bundleIdentifier }) + } +} + +extension Source +{ + class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source + { + let source = Source(context: context) + source.name = "AltStore" + source.identifier = Source.altStoreIdentifier + source.sourceURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")! + + return source + } + + class func fetchAltStoreSource(in context: NSManagedObjectContext) -> Source? + { + let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) + return source + } +} diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 21291d2b..8c6bc09b 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -57,7 +57,7 @@ class MyAppsViewController: UICollectionViewController { super.init(coder: aDecoder) - NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchApps(_:)), name: AppManager.didFetchAppsNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(MyAppsViewController.didFetchSource(_:)), name: AppManager.didFetchSourceNotification, object: nil) } override func viewDidLoad() @@ -522,7 +522,7 @@ private extension MyAppsViewController private extension MyAppsViewController { - @objc func didFetchApps(_ notification: Notification) + @objc func didFetchSource(_ notification: Notification) { DispatchQueue.main.async { if self.updatesDataSource.fetchedResultsController.fetchedObjects == nil diff --git a/AltStore/Operations/FetchAppsOperation.swift b/AltStore/Operations/FetchSourceOperation.swift similarity index 70% rename from AltStore/Operations/FetchAppsOperation.swift rename to AltStore/Operations/FetchSourceOperation.swift index eb3ead59..a47e1651 100644 --- a/AltStore/Operations/FetchAppsOperation.swift +++ b/AltStore/Operations/FetchSourceOperation.swift @@ -1,17 +1,19 @@ // -// FetchAppsOperation.swift +// FetchSourceOperation.swift // AltStore // -// Created by Riley Testut on 6/17/19. +// Created by Riley Testut on 7/30/19. // Copyright © 2019 Riley Testut. All rights reserved. // import Foundation import Roxas -@objc(FetchAppsOperation) -class FetchAppsOperation: ResultOperation<[App]> +@objc(FetchSourceOperation) +class FetchSourceOperation: ResultOperation { + let sourceURL: URL + private let session = URLSession(configuration: .default) private lazy var dateFormatter: DateFormatter = { @@ -20,13 +22,16 @@ class FetchAppsOperation: ResultOperation<[App]> return dateFormatter }() + init(sourceURL: URL) + { + self.sourceURL = sourceURL + } + override func main() { super.main() - let appsURL = URL(string: "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1")! - - let dataTask = self.session.dataTask(with: appsURL) { (data, response, error) in + let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in do { @@ -36,8 +41,8 @@ class FetchAppsOperation: ResultOperation<[App]> decoder.dateDecodingStrategy = .formatted(self.dateFormatter) decoder.managedObjectContext = context - let apps = try decoder.decode([App].self, from: data) - self.finish(.success(apps)) + let source = try decoder.decode(Source.self, from: data) + self.finish(.success(source)) } catch { diff --git a/AltStore/Operations/OperationError.swift b/AltStore/Operations/OperationError.swift index 4412d215..1fe70acc 100644 --- a/AltStore/Operations/OperationError.swift +++ b/AltStore/Operations/OperationError.swift @@ -25,6 +25,8 @@ enum OperationError: LocalizedError case iOSVersionNotSupported(ALTApplication) + case noSources + var errorDescription: String? { switch self { case .unknown: return NSLocalizedString("An unknown error occured.", comment: "") @@ -35,6 +37,7 @@ enum OperationError: LocalizedError case .unknownUDID: return NSLocalizedString("Unknown device UDID.", comment: "") case .invalidApp: return NSLocalizedString("The app is invalid.", comment: "") case .invalidParameters: return NSLocalizedString("Invalid parameters.", comment: "") + case .noSources: return NSLocalizedString("There are no AltStore sources.", comment: "") case .iOSVersionNotSupported(let app): let name = app.name diff --git a/AltStore/Resources/Apps-Dev.json b/AltStore/Resources/Apps-Dev.json index 132c531d..c7ff30af 100644 --- a/AltStore/Resources/Apps-Dev.json +++ b/AltStore/Resources/Apps-Dev.json @@ -1,73 +1,78 @@ -[ - { - "name": "AltStore", - "bundleIdentifier": "com.rileytestut.AltStore", - "developerName": "Riley Testut", - "version": "0.8", - "versionDate": "2019-07-16", - "versionDescription": "AltStore has been updated with bug fixes and improvements and other nice goodies for you to enjoy.", - "downloadURL": "https://www.dropbox.com/s/w1gn9iztlqvltyp/AltStore.ipa?dl=1", - "localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.", - "iconName": "AppIcon", - "size": 11796480, - "permissions": [ - { - "type": "background-fetch", - "usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring." - }, - { - "type": "background-audio", - "usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background." - } - ] - }, - { - "name": "Delta", - "bundleIdentifier": "com.rileytestut.Delta", - "developerName": "Riley Testut", - "subtitle": "Classic games in your pocket.", - "version": "0.8", - "versionDate": "2019-07-11", - "versionDescription": "Finally, after over five years of waiting, Delta is out of beta and ready for everyone to enjoy!\n\nCurrently supports NES, SNES, N64, GB(C), and GBA games, with more to come in the future.", - "downloadURL": "https://www.dropbox.com/s/31i4hcqnorucrxi/Delta.ipa?dl=1", - "localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.", - "iconName": "DeltaIcon", - "tintColor": "8A28F7", - "size": 23624417, - "permissions": [ - { - "type": "photos", - "usageDescription": "Allows Delta to use images from your Photo Library as game artwork." - } - ], - "screenshotNames": [ - "Delta1", - "Delta2", - "Delta3", - ] - }, - { - "name": "Clip", - "bundleIdentifier": "com.rileytestut.ClipboardManager", - "subtitle": "Manage your clipboard history with ease.", - "developerName": "Riley Testut", - "version": "0.8", - "versionDate": "2019-07-29", - "versionDescription": "Bug fixes and improvements.", - "downloadURL": "https://www.dropbox.com/s/rqopivl22iz4ldw/Clipboard.ipa?dl=1", - "localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.", - "iconName": "ClipboardIcon", - "tintColor": "EC008C", - "size": 10401873, - "permissions": [ - { - "type": "background-audio", - "usageDescription": "Allows Clipboard Manager to continuously monitor your clipboard in the background." - } - ], - "screenshotNames": [ +{ + "name": "AltStore", + "identifier": "com.rileytestut.AltStore", + "sourceURL": "https://www.dropbox.com/s/6qi1vt6hsi88lv6/Apps-Dev.json?dl=1", + "apps": [ + { + "name": "AltStore", + "bundleIdentifier": "com.rileytestut.AltStore", + "developerName": "Riley Testut", + "version": "0.8", + "versionDate": "2019-07-16", + "versionDescription": "AltStore has been updated with bug fixes and improvements and other nice goodies for you to enjoy.", + "downloadURL": "https://www.dropbox.com/s/w1gn9iztlqvltyp/AltStore.ipa?dl=1", + "localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.", + "iconName": "AppIcon", + "size": 11796480, + "permissions": [ + { + "type": "background-fetch", + "usageDescription": "AltStore periodically refreshes apps in the background to prevent them from expiring." + }, + { + "type": "background-audio", + "usageDescription": "Allows AltStore to run longer than 30 seconds when refreshing apps in background." + } + ] + }, + { + "name": "Delta", + "bundleIdentifier": "com.rileytestut.Delta", + "developerName": "Riley Testut", + "subtitle": "Classic games in your pocket.", + "version": "0.8", + "versionDate": "2019-07-11", + "versionDescription": "Finally, after over five years of waiting, Delta is out of beta and ready for everyone to enjoy!\n\nCurrently supports NES, SNES, N64, GB(C), and GBA games, with more to come in the future.", + "downloadURL": "https://www.dropbox.com/s/31i4hcqnorucrxi/Delta.ipa?dl=1", + "localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.", + "iconName": "DeltaIcon", + "tintColor": "8A28F7", + "size": 23624417, + "permissions": [ + { + "type": "photos", + "usageDescription": "Allows Delta to use images from your Photo Library as game artwork." + } + ], + "screenshotNames": [ + "Delta1", + "Delta2", + "Delta3" + ] + }, + { + "name": "Clip", + "bundleIdentifier": "com.rileytestut.ClipboardManager", + "subtitle": "Manage your clipboard history with ease.", + "developerName": "Riley Testut", + "version": "0.8", + "versionDate": "2019-07-29", + "versionDescription": "Bug fixes and improvements.", + "downloadURL": "https://www.dropbox.com/s/rqopivl22iz4ldw/Clipboard.ipa?dl=1", + "localizedDescription": "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Sed enim ut sem viverra aliquet eget sit amet. Viverra mauris in aliquam sem fringilla ut. Egestas erat imperdiet sed euismod nisi porta. Sit amet dictum sit amet justo donec enim diam vulputate. Phasellus vestibulum lorem sed risus ultricies tristique nulla. Elit pellentesque habitant morbi tristique. Ut lectus arcu bibendum at. Ullamcorper a lacus vestibulum sed. Mi tempus imperdiet nulla malesuada pellentesque elit eget gravida. Nec feugiat nisl pretium fusce id velit ut. Amet nulla facilisi morbi tempus. Ut sem nulla pharetra diam sit amet nisl.\n\nTortor at auctor urna nunc id cursus metus. Commodo ullamcorper a lacus vestibulum sed arcu non odio. Faucibus turpis in eu mi bibendum neque egestas. Auctor augue mauris augue neque gravida in fermentum et sollicitudin. Aliquam vestibulum morbi blandit cursus risus at ultrices mi tempus. Placerat in egestas erat imperdiet sed euismod nisi. Aliquam id diam maecenas ultricies mi eget mauris. Nunc faucibus a pellentesque sit amet porttitor eget dolor. Sed faucibus turpis in eu mi. Tortor vitae purus faucibus ornare suspendisse sed nisi lacus sed. Id semper risus in hendrerit gravida rutrum quisque. At lectus urna duis convallis convallis. Egestas maecenas pharetra convallis posuere. Id velit ut tortor pretium viverra. Quam pellentesque nec nam aliquam sem et tortor consequat. Risus pretium quam vulputate dignissim. Urna nec tincidunt praesent semper feugiat nibh sed pulvinar.\n\nTempor orci eu lobortis elementum nibh tellus. Mattis rhoncus urna neque viverra justo nec. Maecenas pharetra convallis posuere morbi leo. Rhoncus mattis rhoncus urna neque viverra justo nec. Gravida dictum fusce ut placerat orci nulla pellentesque dignissim enim. At imperdiet dui accumsan sit amet. Elit sed vulputate mi sit amet mauris commodo. Pellentesque habitant morbi tristique senectus. Tortor id aliquet lectus proin nibh. Magna etiam tempor orci eu lobortis elementum. Est pellentesque elit ullamcorper dignissim. Dapibus ultrices in iaculis nunc. Commodo quis imperdiet massa tincidunt nunc pulvinar sapien et ligula. Diam vel quam elementum pulvinar. Vel turpis nunc eget lorem dolor sed viverra. Tortor pretium viverra suspendisse potenti nullam ac tortor vitae. Euismod nisi porta lorem mollis. Massa id neque aliquam vestibulum morbi blandit.\n\nMassa massa ultricies mi quis hendrerit dolor magna eget est. Augue interdum velit euismod in pellentesque massa. Sed risus ultricies tristique nulla aliquet enim. Risus viverra adipiscing at in tellus. Donec adipiscing tristique risus nec feugiat. Eget sit amet tellus cras adipiscing enim eu turpis. Auctor neque vitae tempus quam pellentesque nec. Sit amet tellus cras adipiscing enim eu turpis egestas. Dui faucibus in ornare quam viverra. Fermentum iaculis eu non diam phasellus vestibulum lorem. Odio ut enim blandit volutpat maecenas. Dolor sit amet consectetur adipiscing elit pellentesque habitant morbi tristique. Pellentesque diam volutpat commodo sed egestas egestas. Aliquam purus sit amet luctus venenatis lectus magna fringilla. Viverra mauris in aliquam sem fringilla ut morbi tincidunt. Elit duis tristique sollicitudin nibh sit. Fermentum dui faucibus in ornare quam viverra orci sagittis. Aliquet eget sit amet tellus cras adipiscing.", + "iconName": "ClipboardIcon", + "tintColor": "EC008C", + "size": 10401873, + "permissions": [ + { + "type": "background-audio", + "usageDescription": "Allows Clipboard Manager to continuously monitor your clipboard in the background." + } + ], + "screenshotNames": [ "Clip1", - "Clip2", - ] - } -] + "Clip2" + ] + } + ] +}