diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 2e9eceb5..98309fc4 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -22,6 +22,12 @@ BF1E315A22A0620000370A3C /* NSError+ALTServerError.m in Sources */ = {isa = PBXBuildFile; fileRef = BF1E314922A060F400370A3C /* NSError+ALTServerError.m */; }; BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; }; BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; }; + BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; }; + BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; }; + BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; }; + BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */; }; + BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D64A122E8031100E9056B /* MergePolicy.swift */; }; + BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D64AF22E8D4B800E9056B /* AppContentViewControllerCells.swift */; }; BF3F786422CAA41E008FBD20 /* ALTDeviceManager+Installation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */; }; BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002D22A714AF0051E2BC /* Keychain.swift */; }; BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */; }; @@ -103,6 +109,8 @@ 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 */; }; BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */; }; BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */; }; @@ -125,7 +133,6 @@ BFD247882284BB4200981D42 /* Roxas.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BFD247862284BB3B00981D42 /* Roxas.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478B2284C4C300981D42 /* AppIconImageView.swift */; }; BFD2478F2284C8F900981D42 /* Button.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2478E2284C8F900981D42 /* Button.swift */; }; - BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */; }; BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */; }; BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BD322A0800A000B7ED1 /* ServerManager.swift */; }; BFD52C0122A1A9CB000B7ED1 /* ptrarray.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52BE522A1A9CA000B7ED1 /* ptrarray.c */; }; @@ -252,6 +259,13 @@ BF1E314922A060F400370A3C /* NSError+ALTServerError.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+ALTServerError.m"; sourceTree = ""; }; BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; }; BF219A7E22CAC431007676A6 /* AltStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltStore.entitlements; sourceTree = ""; }; + BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = ""; }; + BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = ""; }; + BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = ""; }; + BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionPopoverViewController.swift; sourceTree = ""; }; + BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollapsingTextView.swift; sourceTree = ""; }; + BF3D64A122E8031100E9056B /* MergePolicy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MergePolicy.swift; sourceTree = ""; }; + BF3D64AF22E8D4B800E9056B /* AppContentViewControllerCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppContentViewControllerCells.swift; sourceTree = ""; }; BF3F786322CAA41E008FBD20 /* ALTDeviceManager+Installation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ALTDeviceManager+Installation.swift"; sourceTree = ""; }; BF43002D22A714AF0051E2BC /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = ""; }; BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserDefaults+AltStore.swift"; sourceTree = ""; }; @@ -338,6 +352,8 @@ 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 = ""; }; BF9ABA4622DD0638008935CF /* BrowseCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseCollectionViewCell.swift; sourceTree = ""; }; BF9ABA4822DD0742008935CF /* ScreenshotCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScreenshotCollectionViewCell.swift; sourceTree = ""; }; @@ -363,7 +379,6 @@ BFD247862284BB3B00981D42 /* Roxas.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Roxas.framework; sourceTree = BUILT_PRODUCTS_DIR; }; BFD2478B2284C4C300981D42 /* AppIconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIconImageView.swift; sourceTree = ""; }; BFD2478E2284C8F900981D42 /* Button.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Button.swift; sourceTree = ""; }; - BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDetailViewController.swift; sourceTree = ""; }; BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIColor+AltStore.swift"; sourceTree = ""; }; BFD52BD222A06EFB000B7ED1 /* AltKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AltKit.h; sourceTree = ""; }; BFD52BD322A0800A000B7ED1 /* ServerManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerManager.swift; sourceTree = ""; }; @@ -479,6 +494,26 @@ path = AltKit; sourceTree = ""; }; + BF3D648922E79A7700E9056B /* Types */ = { + isa = PBXGroup; + children = ( + BF3D648B22E79AC800E9056B /* ALTAppPermission.h */, + BF3D648C22E79AC800E9056B /* ALTAppPermission.m */, + ); + path = Types; + sourceTree = ""; + }; + BF3D64A022E7FAD800E9056B /* App Detail */ = { + isa = PBXGroup; + children = ( + BF8F69C322E662D300049BA1 /* AppViewController.swift */, + BF8F69C122E659F700049BA1 /* AppContentViewController.swift */, + BF3D64AF22E8D4B800E9056B /* AppContentViewControllerCells.swift */, + BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */, + ); + path = "App Detail"; + sourceTree = ""; + }; BF45868E229872EA00BD7491 /* AltServer */ = { isa = PBXGroup; children = ( @@ -721,13 +756,15 @@ BFD247732284B9A500981D42 /* Main.storyboard */, BFE6325822A83BA800F30809 /* Authentication */, BF9ABA4322DCFF33008935CF /* Browse */, - BFD2478A2284C49000981D42 /* Apps */, + BF3D64A022E7FAD800E9056B /* App Detail */, BFBBE2E2229320A2002097FA /* My Apps */, BFDB69FB22A9A7A6007EA6D6 /* Account */, + BFD2478A2284C49000981D42 /* Managing Apps */, BFC51D7922972F1F00388324 /* Server */, BFD247982284D7FC00981D42 /* Model */, BFDB6A0922AAEDA1007EA6D6 /* Operations */, BFD2478D2284C4C700981D42 /* Components */, + BF3D648922E79A7700E9056B /* Types */, BFDB6A0622A9B114007EA6D6 /* Protocols */, BFD2479D2284FBBD00981D42 /* Extensions */, BFD247962284D7C100981D42 /* Resources */, @@ -751,13 +788,12 @@ name = Frameworks; sourceTree = ""; }; - BFD2478A2284C49000981D42 /* Apps */ = { + BFD2478A2284C49000981D42 /* Managing Apps */ = { isa = PBXGroup; children = ( - BFD2479B2284E19A00981D42 /* AppDetailViewController.swift */, BF0C4EBC22A1BD8B009A2DD7 /* AppManager.swift */, ); - path = Apps; + path = "Managing Apps"; sourceTree = ""; }; BFD2478D2284C4C700981D42 /* Components */ = { @@ -770,6 +806,7 @@ BF9ABA4A22DD137F008935CF /* NavigationBar.swift */, BF9ABA4C22DD16DE008935CF /* PillButton.swift */, BF18B0F022E25DF9005C4CF5 /* ToastView.swift */, + BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */, ); path = Components; sourceTree = ""; @@ -799,8 +836,10 @@ children = ( BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */, BFB11691229322E400BB457C /* DatabaseManager.swift */, + BF3D64A122E8031100E9056B /* MergePolicy.swift */, BFE6326722A858F300F30809 /* Account.swift */, BFBBE2DE22931F73002097FA /* App.swift */, + BF3D648722E79A3700E9056B /* AppPermission.swift */, BFBBE2E022931F81002097FA /* InstalledApp.swift */, BFE6326522A857C100F30809 /* Team.swift */, ); @@ -1223,6 +1262,7 @@ 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 */, BFDB69FD22A9A7B7007EA6D6 /* AccountViewController.swift in Sources */, @@ -1235,27 +1275,33 @@ BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */, BF770E5422BC044E002A40FE /* AppOperationContext.swift in Sources */, BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, + BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */, - BFD2479C2284E19A00981D42 /* AppDetailViewController.swift in Sources */, BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */, BFDB6A0B22AAEDB7007EA6D6 /* Operation.swift in Sources */, BF770E6722BD57C4002A40FE /* BackgroundTaskManager.swift in Sources */, + BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */, BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */, BFE6325C22A83C0100F30809 /* AuthenticationViewController.swift in Sources */, BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */, BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */, + BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */, BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */, + BF8F69C222E659F700049BA1 /* AppContentViewController.swift in Sources */, BF08858522DE7EC800DE9F1E /* UpdateCollectionViewCell.swift in Sources */, BF770E5822BC3D0F002A40FE /* OperationGroup.swift in Sources */, BF18B0F122E25DF9005C4CF5 /* ToastView.swift in Sources */, + BF3D649F22E7B24C00E9056B /* CollapsingTextView.swift in Sources */, BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */, BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */, BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, + BF3D64A222E8031100E9056B /* MergePolicy.swift in Sources */, BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */, + BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */, BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */, BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */, BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */, @@ -1646,6 +1692,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "AltStore/AltStore-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; @@ -1671,6 +1718,7 @@ PRODUCT_BUNDLE_IDENTIFIER = com.rileytestut.AltStore; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OBJC_BRIDGING_HEADER = "AltStore/AltStore-Bridging-Header.h"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 1; }; diff --git a/AltStore/AltStore-Bridging-Header.h b/AltStore/AltStore-Bridging-Header.h index 83dbb109..52a2d835 100644 --- a/AltStore/AltStore-Bridging-Header.h +++ b/AltStore/AltStore-Bridging-Header.h @@ -3,3 +3,4 @@ // #import "NSError+ALTServerError.h" +#import "ALTAppPermission.h" diff --git a/AltStore/App Detail/AppContentViewController.swift b/AltStore/App Detail/AppContentViewController.swift new file mode 100644 index 00000000..fee28b12 --- /dev/null +++ b/AltStore/App Detail/AppContentViewController.swift @@ -0,0 +1,178 @@ +// +// AppContentViewController.swift +// AltStore +// +// Created by Riley Testut on 7/22/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +import Roxas + +extension AppContentViewController +{ + private enum Row: Int, CaseIterable + { + case subtitle + case screenshots + case description + case versionDescription + case permissions + } +} + +class AppContentViewController: UITableViewController +{ + var app: App! + + private lazy var screenshotsDataSource = self.makeScreenshotsDataSource() + private lazy var permissionsDataSource = self.makePermissionsDataSource() + + @IBOutlet private var subtitleLabel: UILabel! + @IBOutlet private var descriptionTextView: CollapsingTextView! + @IBOutlet private var versionDescriptionTextView: CollapsingTextView! + + @IBOutlet private var screenshotsCollectionView: UICollectionView! + @IBOutlet private var permissionsCollectionView: UICollectionView! + + var preferredScreenshotSize: CGSize? { + guard let image = self.screenshotsDataSource.items.first else { return nil } + + let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout + + let aspectRatio = image.size.height / image.size.width + + let width = self.screenshotsCollectionView.bounds.width - (layout.minimumInteritemSpacing * 2) + + let itemWidth = width / 1.5 + let itemHeight = itemWidth * aspectRatio + + return CGSize(width: itemWidth, height: itemHeight) + } + + override func viewDidLoad() + { + super.viewDidLoad() + + self.tableView.contentInset.bottom = 20 + + self.screenshotsCollectionView.dataSource = self.screenshotsDataSource + self.permissionsCollectionView.dataSource = self.permissionsDataSource + + self.subtitleLabel.text = self.app.subtitle + self.descriptionTextView.text = self.app.localizedDescription + self.versionDescriptionTextView.text = self.app.versionDescription + + self.descriptionTextView.maximumNumberOfLines = 5 + self.descriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) + + self.versionDescriptionTextView.maximumNumberOfLines = 3 + self.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(AppContentViewController.toggleCollapsingSection(_:)), for: .primaryActionTriggered) + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + guard var size = self.preferredScreenshotSize else { return } + size.height = min(size.height, self.screenshotsCollectionView.bounds.height) // Silence temporary "item too tall" warning. + + let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout + layout.itemSize = size + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) + { + guard segue.identifier == "showPermission" else { return } + + guard let cell = sender as? UICollectionViewCell, let indexPath = self.permissionsCollectionView.indexPath(for: cell) else { return } + + let permission = self.permissionsDataSource.item(at: indexPath) + + let maximumWidth = self.view.bounds.width - 20 + + let permissionPopoverViewController = segue.destination as! PermissionPopoverViewController + permissionPopoverViewController.permission = permission + permissionPopoverViewController.view.widthAnchor.constraint(lessThanOrEqualToConstant: maximumWidth).isActive = true + + let size = permissionPopoverViewController.view.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + permissionPopoverViewController.preferredContentSize = size + + permissionPopoverViewController.popoverPresentationController?.delegate = self + permissionPopoverViewController.popoverPresentationController?.sourceRect = cell.frame + permissionPopoverViewController.popoverPresentationController?.sourceView = self.permissionsCollectionView + } +} + +private extension AppContentViewController +{ + func makeScreenshotsDataSource() -> RSTArrayCollectionViewDataSource + { + let screenshots = self.app.screenshotNames.compactMap(UIImage.init(named:)) + + let dataSource = RSTArrayCollectionViewDataSource(items: screenshots) + dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in + let cell = cell as! ScreenshotCollectionViewCell + cell.imageView.image = screenshot + } + + return dataSource + } + + func makePermissionsDataSource() -> RSTArrayCollectionViewDataSource + { + let dataSource = RSTArrayCollectionViewDataSource(items: self.app.permissions) + dataSource.cellConfigurationHandler = { (cell, permission, indexPath) in + let cell = cell as! PermissionCollectionViewCell + cell.button.setImage(permission.type.icon, for: .normal) + cell.textLabel.text = permission.type.localizedShortName + } + + return dataSource + } +} + +private extension AppContentViewController +{ + @objc func toggleCollapsingSection(_ sender: UIButton) + { + let indexPath: IndexPath + + switch sender + { + case self.descriptionTextView.moreButton: indexPath = IndexPath(row: Row.description.rawValue, section: 0) + case self.versionDescriptionTextView.moreButton: indexPath = IndexPath(row: Row.versionDescription.rawValue, section: 0) + default: return + } + + // Disable animations to prevent some potentially strange ones. + UIView.performWithoutAnimation { + self.tableView.reloadRows(at: [indexPath], with: .none) + } + } +} + +extension AppContentViewController +{ + override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) + { + cell.tintColor = self.app.tintColor + } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat + { + guard indexPath.row == Row.screenshots.rawValue else { return super.tableView(tableView, heightForRowAt: indexPath) } + + guard let size = self.preferredScreenshotSize else { return 0.0 } + return size.height + } +} + +extension AppContentViewController: UIPopoverPresentationControllerDelegate +{ + func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle + { + return .none + } +} diff --git a/AltStore/App Detail/AppContentViewControllerCells.swift b/AltStore/App Detail/AppContentViewControllerCells.swift new file mode 100644 index 00000000..923b5d6c --- /dev/null +++ b/AltStore/App Detail/AppContentViewControllerCells.swift @@ -0,0 +1,43 @@ +// +// AppContentViewControllerCells.swift +// AltStore +// +// Created by Riley Testut on 7/24/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +class PermissionCollectionViewCell: UICollectionViewCell +{ + @IBOutlet var button: UIButton! + @IBOutlet var textLabel: UILabel! + + override func layoutSubviews() + { + super.layoutSubviews() + + self.button.layer.cornerRadius = self.button.bounds.midY + } + + override func tintColorDidChange() + { + super.tintColorDidChange() + + self.button.backgroundColor = self.tintColor.withAlphaComponent(0.15) + self.textLabel.textColor = self.tintColor + } +} + +class AppContentTableViewCell: UITableViewCell +{ + override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize + { + // Ensure cell is laid out so it will report correct size. + self.layoutIfNeeded() + + let size = super.systemLayoutSizeFitting(targetSize, withHorizontalFittingPriority: horizontalFittingPriority, verticalFittingPriority: verticalFittingPriority) + + return size + } +} diff --git a/AltStore/App Detail/AppViewController.swift b/AltStore/App Detail/AppViewController.swift new file mode 100644 index 00000000..6f8467e1 --- /dev/null +++ b/AltStore/App Detail/AppViewController.swift @@ -0,0 +1,406 @@ +// +// AppViewController.swift +// AltStore +// +// Created by Riley Testut on 7/22/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +import Roxas + +class AppViewController: UIViewController +{ + var app: App! + + private var contentViewController: AppContentViewController! + private var contentViewControllerShadowView: UIView! + + private var blurAnimator: UIViewPropertyAnimator? + + private var contentSizeObservation: NSKeyValueObservation? + + @IBOutlet private var scrollView: UIScrollView! + @IBOutlet private var contentView: UIView! + + @IBOutlet private var headerView: UIView! + @IBOutlet private var headerContentView: UIView! + + @IBOutlet private var backButton: UIButton! + @IBOutlet private var backButtonContainerView: UIVisualEffectView! + + @IBOutlet private var nameLabel: UILabel! + @IBOutlet private var developerLabel: UILabel! + @IBOutlet private var downloadButton: PillButton! + @IBOutlet private var appIconImageView: UIImageView! + + @IBOutlet private var backgroundAppIconImageView: UIImageView! + @IBOutlet private var backgroundBlurView: UIVisualEffectView! + + private var _isEnteringForeground = false + private var _backgroundBlurEffect: UIBlurEffect? + private var _backgroundBlurTintColor: UIColor? + + override func viewDidLoad() + { + super.viewDidLoad() + + self.contentViewControllerShadowView = UIView() + self.contentViewControllerShadowView.backgroundColor = .white + self.contentViewControllerShadowView.layer.cornerRadius = 38 + self.contentViewControllerShadowView.layer.shadowColor = UIColor.black.cgColor + self.contentViewControllerShadowView.layer.shadowOffset = CGSize(width: 0, height: -1) + self.contentViewControllerShadowView.layer.shadowRadius = 10 + self.contentViewControllerShadowView.layer.shadowOpacity = 0.3 + self.contentViewController.view.superview?.insertSubview(self.contentViewControllerShadowView, at: 0) + + self.contentView.addGestureRecognizer(self.scrollView.panGestureRecognizer) + + self.contentViewController.view.layer.cornerRadius = 38 + self.contentViewController.view.layer.masksToBounds = true + + self.contentViewController.tableView.panGestureRecognizer.require(toFail: self.scrollView.panGestureRecognizer) + self.contentViewController.tableView.showsVerticalScrollIndicator = false + + self.headerView.frame = CGRect(x: 0, y: 0, width: 300, height: 93) + self.headerView.layer.cornerRadius = 24 + self.headerView.layer.masksToBounds = true + + // Bring to front so the scroll indicators are visible. + self.view.bringSubviewToFront(self.scrollView) + self.scrollView.isUserInteractionEnabled = false + + self.nameLabel.text = self.app.name + self.developerLabel.text = self.app.developerName + self.developerLabel.textColor = self.app.tintColor + self.appIconImageView.image = UIImage(named: self.app.iconName) + self.downloadButton.tintColor = self.app.tintColor + self.backgroundAppIconImageView.image = UIImage(named: self.app.iconName) + + self.backButtonContainerView.tintColor = self.app.tintColor + + self.contentSizeObservation = self.contentViewController.tableView.observe(\.contentSize) { (tableView, change) in + self.view.setNeedsLayout() + self.view.layoutIfNeeded() + } + + self.update() + + NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.didChangeApp(_:)), name: .NSManagedObjectContextObjectsDidChange, object: DatabaseManager.shared.viewContext) + NotificationCenter.default.addObserver(self, selector: #selector(AppViewController.willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) + + self._backgroundBlurEffect = self.backgroundBlurView.effect as? UIBlurEffect + self._backgroundBlurTintColor = self.backgroundBlurView.contentView.backgroundColor + } + + override func viewWillAppear(_ animated: Bool) + { + super.viewWillAppear(animated) + + self.prepareBlur() + + // Update blur immediately. + self.view.setNeedsLayout() + self.view.layoutIfNeeded() + + self.transitionCoordinator?.animate(alongsideTransition: { (context) in + self.hideNavigationBar() + }, completion: nil) + } + + override func viewDidAppear(_ animated: Bool) + { + super.viewDidAppear(animated) + + self.hideNavigationBar() + } + + override func viewWillDisappear(_ animated: Bool) + { + super.viewWillDisappear(animated) + + // Guard against "dismissing" when presenting via 3D Touch pop. + guard self.navigationController != nil else { return } + + // Store reference since self.navigationController will be nil after disappearing. + let navigationController = self.navigationController + navigationController?.navigationBar.barStyle = .default // Don't animate, or else status bar might appear messed-up. + + self.transitionCoordinator?.animate(alongsideTransition: { (context) in + self.showNavigationBar(for: navigationController) + }, completion: { (context) in + if context.isCancelled + { + self.hideNavigationBar(for: navigationController) + } + else + { + self.showNavigationBar(for: navigationController) + } + }) + } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) + { + guard segue.identifier == "embedAppContentViewController" else { return } + + self.contentViewController = segue.destination as? AppContentViewController + self.contentViewController.app = self.app + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + if self._isEnteringForeground + { + // Returning from background messes up some of our UI, so reset affected components now. + + if self.navigationController?.topViewController == self + { + self.hideNavigationBar() + } + + self.prepareBlur() + + self._isEnteringForeground = false + } + + let statusBarHeight = UIApplication.shared.statusBarFrame.height + + let inset = 12 as CGFloat + let padding = 20 as CGFloat + + let backButtonSize = self.backButton.sizeThatFits(CGSize(width: 1000, height: 1000)) + let backButtonFrame = CGRect(x: inset, y: statusBarHeight, + width: backButtonSize.width + 20, height: backButtonSize.height + 20) + + var headerFrame = CGRect(x: inset, y: 0, width: self.view.bounds.width - inset * 2, height: self.headerView.bounds.height) + var contentFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.height) + var backgroundIconFrame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: self.view.bounds.width) + + let minimumHeaderY = backButtonFrame.maxY + 8 + + let minimumContentY = minimumHeaderY + headerFrame.height + padding + let maximumContentY = self.view.bounds.width * 0.75 + + // A full blur is too much, so we reduce the visible blur by 0.3, resulting in 70% blur. + let minimumBlurFraction = 0.3 as CGFloat + + let difference = (maximumContentY - minimumContentY) + + if self.scrollView.contentOffset.y > difference + { + // Full screen + + headerFrame.origin.y = minimumHeaderY + contentFrame.origin.y = minimumContentY + backgroundIconFrame.origin.y = 0 + + self.contentViewController.tableView.contentOffset.y = self.scrollView.contentOffset.y - difference + } + else + { + // Partial screen + + contentFrame.origin.y = maximumContentY - self.scrollView.contentOffset.y + headerFrame.origin.y = contentFrame.origin.y - padding - headerFrame.height + + let cornerRadius = self.contentViewControllerShadowView.layer.cornerRadius + + // Stretch the app icon image to fill additional vertical space if necessary. + let height = max(contentFrame.origin.y + cornerRadius * 2, backgroundIconFrame.height) + backgroundIconFrame.size.height = height + + // Keep content table view's content offset at the top. + self.contentViewController.tableView.contentOffset.y = 0 + + if self.scrollView.contentOffset.y < 0 + { + // Determine how much to lessen blur by. + + let range = 75 as CGFloat + + let fraction = min(-self.scrollView.contentOffset.y, range) / range + + let fractionComplete = (fraction * (1.0 - minimumBlurFraction)) + minimumBlurFraction + self.blurAnimator?.fractionComplete = fractionComplete + } + else + { + // Set blur to default. + + self.blurAnimator?.fractionComplete = minimumBlurFraction + } + } + + // Keep background app icon centered in gap between top of content and top of screen. + backgroundIconFrame.origin.y = (contentFrame.origin.y / 2) - backgroundIconFrame.height / 2 + + // Set frames. + self.contentViewController.view.superview?.frame = contentFrame + self.headerView.frame = headerFrame + self.backgroundAppIconImageView.frame = backgroundIconFrame + self.backgroundBlurView.frame = backgroundIconFrame + self.backButtonContainerView.frame = backButtonFrame + + self.headerContentView.frame = CGRect(x: 0, y: 0, width: self.headerView.bounds.width, height: self.headerView.bounds.height) + self.contentViewControllerShadowView.frame = self.contentViewController.view.frame + + self.backButtonContainerView.layer.cornerRadius = self.backButtonContainerView.bounds.midY + + self.scrollView.scrollIndicatorInsets.top = statusBarHeight + + // Adjust content offset + size. + let contentOffset = self.scrollView.contentOffset + + var contentSize = self.contentViewController.tableView.contentSize + contentSize.height += minimumContentY + self.view.safeAreaInsets.bottom + self.contentViewController.tableView.contentInset.bottom + + self.scrollView.contentSize = contentSize + self.scrollView.contentOffset = contentOffset + } + + deinit + { + self.blurAnimator?.stopAnimation(true) + } +} + +private extension AppViewController +{ + func update() + { + self.downloadButton.isIndicatingActivity = false + + if self.app.installedApp == nil + { + self.downloadButton.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal) + self.downloadButton.isInverted = false + } + else + { + self.downloadButton.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal) + self.downloadButton.isInverted = true + } + + let progress = AppManager.shared.installationProgress(for: self.app) + self.downloadButton.progress = progress + } + + func showNavigationBar(for navigationController: UINavigationController? = nil) + { + let navigationController = navigationController ?? self.navigationController + navigationController?.navigationBar.barStyle = .default + navigationController?.navigationBar.alpha = 1.0 + navigationController?.navigationBar.setBackgroundImage(nil, for: .default) + } + + func hideNavigationBar(for navigationController: UINavigationController? = nil) + { + let navigationController = navigationController ?? self.navigationController + navigationController?.navigationBar.barStyle = .black + navigationController?.navigationBar.alpha = 0.0 + navigationController?.navigationBar.setBackgroundImage(UIImage(), for: .default) + } + + func prepareBlur() + { + if let animator = self.blurAnimator + { + animator.stopAnimation(true) + } + + self.backgroundBlurView.effect = self._backgroundBlurEffect + self.backgroundBlurView.contentView.backgroundColor = self._backgroundBlurTintColor + + self.blurAnimator = UIViewPropertyAnimator(duration: 1.0, curve: .linear) { + self.backgroundBlurView.effect = nil + self.backgroundBlurView.contentView.backgroundColor = .clear + } + + self.blurAnimator?.startAnimation() + self.blurAnimator?.pauseAnimation() + } +} + +extension AppViewController +{ + @IBAction func popViewController(_ sender: UIButton) + { + self.navigationController?.popViewController(animated: true) + } + + @IBAction func performAppAction(_ sender: PillButton) + { + if let installedApp = self.app.installedApp + { + self.open(installedApp) + } + else + { + self.downloadApp() + } + } + + func downloadApp() + { + guard self.app.installedApp == nil else { return } + + let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in + do + { + _ = try result.get() + } + catch OperationError.cancelled + { + // Ignore + } + catch + { + DispatchQueue.main.async { + let toastView = ToastView(text: error.localizedDescription, detailText: nil) + toastView.show(in: self.navigationController!.view, duration: 2) + } + } + + DispatchQueue.main.async { + self.downloadButton.progress = nil + self.update() + } + } + + self.downloadButton.progress = progress + } + + func open(_ installedApp: InstalledApp) + { + UIApplication.shared.open(installedApp.openAppURL) + } +} + +private extension AppViewController +{ + @objc func didChangeApp(_ notification: Notification) + { + self.update() + } + + @objc func willEnterForeground(_ notification: Notification) + { + guard let navigationController = self.navigationController, navigationController.topViewController == self else { return } + + self._isEnteringForeground = true + self.view.setNeedsLayout() + } +} + +extension AppViewController: UIScrollViewDelegate +{ + func scrollViewDidScroll(_ scrollView: UIScrollView) + { + self.view.setNeedsLayout() + self.view.layoutIfNeeded() + } +} diff --git a/AltStore/App Detail/PermissionPopoverViewController.swift b/AltStore/App Detail/PermissionPopoverViewController.swift new file mode 100644 index 00000000..b34e2c68 --- /dev/null +++ b/AltStore/App Detail/PermissionPopoverViewController.swift @@ -0,0 +1,25 @@ +// +// PermissionPopoverViewController.swift +// AltStore +// +// Created by Riley Testut on 7/23/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +class PermissionPopoverViewController: UIViewController +{ + var permission: AppPermission! + + @IBOutlet private var nameLabel: UILabel! + @IBOutlet private var descriptionLabel: UILabel! + + override func viewDidLoad() + { + super.viewDidLoad() + + self.nameLabel.text = self.permission.type.localizedName + self.descriptionLabel.text = self.permission.usageDescription + } +} diff --git a/AltStore/AppDelegate.swift b/AltStore/AppDelegate.swift index 81928f17..22b42489 100644 --- a/AltStore/AppDelegate.swift +++ b/AltStore/AppDelegate.swift @@ -49,6 +49,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate { { self.isLaunching = true + self.setTintColor() + ServerManager.shared.startDiscovering() DatabaseManager.shared.start { (error) in @@ -93,6 +95,14 @@ class AppDelegate: UIResponder, UIApplicationDelegate { } } +private extension AppDelegate +{ + func setTintColor() + { + self.window?.tintColor = .altGreen + } +} + extension AppDelegate { private func prepareForBackgroundFetch() diff --git a/AltStore/Apps/AppDetailViewController.swift b/AltStore/Apps/AppDetailViewController.swift deleted file mode 100644 index 6d423240..00000000 --- a/AltStore/Apps/AppDetailViewController.swift +++ /dev/null @@ -1,183 +0,0 @@ -// -// AppDetailViewController.swift -// AltStore -// -// Created by Riley Testut on 5/9/19. -// Copyright © 2019 Riley Testut. All rights reserved. -// - -import UIKit -import Roxas - -extension AppDetailViewController -{ - private enum Row: Int - { - case general - case screenshots - case description - } -} - -class AppDetailViewController: UITableViewController -{ - var app: App! - - private lazy var screenshotsDataSource = self.makeScreenshotsDataSource() - - @IBOutlet private var nameLabel: UILabel! - @IBOutlet private var developerButton: UIButton! - @IBOutlet private var appIconImageView: UIImageView! - - @IBOutlet private var downloadButton: UIButton! - - @IBOutlet private var screenshotsCollectionView: UICollectionView! - - @IBOutlet private var descriptionLabel: UILabel! - - override func viewDidLoad() - { - super.viewDidLoad() - - self.tableView.delegate = self - self.screenshotsCollectionView.dataSource = self.screenshotsDataSource - - self.downloadButton.activityIndicatorView.style = .white - - self.update() - } - - override func viewWillAppear(_ animated: Bool) - { - super.viewWillAppear(animated) - - self.update() - } - - override func viewDidLayoutSubviews() - { - super.viewDidLayoutSubviews() - - guard let image = self.screenshotsDataSource.items.first else { return } - - let aspectRatio = image.size.width / image.size.height - - let height = self.screenshotsCollectionView.bounds.height - let width = self.screenshotsCollectionView.bounds.height * aspectRatio - - let layout = self.screenshotsCollectionView.collectionViewLayout as! UICollectionViewFlowLayout - layout.itemSize = CGSize(width: width, height: height) - } -} - -private extension AppDetailViewController -{ - func update() - { - self.nameLabel.text = self.app.name - self.developerButton.setTitle(self.app.developerName, for: .normal) - self.appIconImageView.image = UIImage(named: self.app.iconName) - - self.descriptionLabel.text = self.app.localizedDescription - - if !self.downloadButton.isIndicatingActivity - { - if self.app.installedApp == nil - { - let text = String(format: NSLocalizedString("Download %@", comment: ""), self.app.name) - self.downloadButton.setTitle(text, for: .normal) - self.downloadButton.isEnabled = true - } - else - { - self.downloadButton.setTitle(NSLocalizedString("Installed", comment: ""), for: .normal) - self.downloadButton.isEnabled = false - } - } - } - - func makeScreenshotsDataSource() -> RSTArrayCollectionViewDataSource - { - let screenshots = self.app.screenshotNames.compactMap(UIImage.init(named:)) - - let dataSource = RSTArrayCollectionViewDataSource(items: screenshots) - dataSource.cellConfigurationHandler = { (cell, screenshot, indexPath) in - let cell = cell as! ScreenshotCollectionViewCell - cell.imageView.image = screenshot - } - - return dataSource - } -} - -private extension AppDetailViewController -{ - @IBAction func downloadApp(_ sender: UIButton) - { - guard self.app.installedApp == nil else { return } - - sender.isIndicatingActivity = true - - let progressView = UIProgressView(progressViewStyle: .bar) - progressView.translatesAutoresizingMaskIntoConstraints = false - progressView.progress = 0.0 - - let progress = AppManager.shared.install(self.app, presentingViewController: self) { (result) in - do - { - _ = try result.get() - - DispatchQueue.main.async { - let toastView = RSTToastView(text: "Installed \(self.app.name)!", detailText: nil) - toastView.tintColor = .altPurple - toastView.show(in: self.navigationController!.view, duration: 2) - } - } - catch OperationError.cancelled - { - // Ignore - } - catch - { - DispatchQueue.main.async { - let toastView = RSTToastView(text: "Failed to install \(self.app.name)", detailText: error.localizedDescription) - toastView.tintColor = .altPurple - toastView.show(in: self.navigationController!.view, duration: 2) - } - } - - DispatchQueue.main.async { - UIView.animate(withDuration: 0.4, animations: { - progressView.alpha = 0.0 - }) { _ in - progressView.removeFromSuperview() - } - - sender.isIndicatingActivity = false - self.update() - } - } - - progressView.observedProgress = progress - - if let navigationBar = self.navigationController?.navigationBar - { - navigationBar.addSubview(progressView) - - NSLayoutConstraint.activate([progressView.widthAnchor.constraint(equalTo: navigationBar.widthAnchor), - progressView.bottomAnchor.constraint(equalTo: navigationBar.bottomAnchor)]) - } - } -} - -extension AppDetailViewController -{ - override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat - { - guard indexPath.row == Row.screenshots.rawValue else { return super.tableView(tableView, heightForRowAt: indexPath) } - guard !self.screenshotsDataSource.items.isEmpty else { return 0.0 } - - let height = self.view.bounds.height * 0.67 - return height - } -} diff --git a/AltStore/Base.lproj/Main.storyboard b/AltStore/Base.lproj/Main.storyboard index 7536f716..d26f1c95 100644 --- a/AltStore/Base.lproj/Main.storyboard +++ b/AltStore/Base.lproj/Main.storyboard @@ -7,6 +7,7 @@ + @@ -105,7 +106,7 @@ - + @@ -158,6 +159,10 @@ + + + + @@ -172,177 +177,370 @@ - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - + + + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - + - - - + + + + + + + - - - - - + + + - + + + + + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + @@ -351,23 +549,69 @@ - - + + - + + - - - - - - + + + + + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -621,6 +865,10 @@ + + + + @@ -664,11 +912,14 @@ - + + + + diff --git a/AltStore/Browse/BrowseViewController.swift b/AltStore/Browse/BrowseViewController.swift index 5af484d5..1323db8f 100644 --- a/AltStore/Browse/BrowseViewController.swift +++ b/AltStore/Browse/BrowseViewController.swift @@ -36,6 +36,18 @@ class BrowseViewController: UICollectionViewController let collectionViewLayout = self.collectionViewLayout as! UICollectionViewFlowLayout collectionViewLayout.itemSize.width = self.view.bounds.width } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) + { + guard segue.identifier == "showApp" else { return } + + guard let cell = sender as? UICollectionViewCell, let indexPath = self.collectionView.indexPath(for: cell) else { return } + + let app = self.dataSource.item(at: indexPath) + + let appViewController = segue.destination as! AppViewController + appViewController.app = app + } } private extension BrowseViewController diff --git a/AltStore/Browse/ScreenshotCollectionViewCell.swift b/AltStore/Browse/ScreenshotCollectionViewCell.swift index bc329a5a..a0bb049c 100644 --- a/AltStore/Browse/ScreenshotCollectionViewCell.swift +++ b/AltStore/Browse/ScreenshotCollectionViewCell.swift @@ -18,11 +18,25 @@ class ScreenshotCollectionViewCell: UICollectionViewCell required init?(coder aDecoder: NSCoder) { self.imageView = UIImageView(image: nil) - self.imageView.layer.cornerRadius = 8 self.imageView.layer.masksToBounds = true super.init(coder: aDecoder) self.addSubview(self.imageView, pinningEdgesWith: .zero) } + + override func layoutSubviews() + { + super.layoutSubviews() + + if let image = self.imageView.image, (image.size.height / image.size.width) > ((16.0 / 9.0) + 0.1) + { + // Image aspect ratio is taller than 16:9, so assume it's an X-style screenshot and set corner radius. + self.imageView.layer.cornerRadius = max(self.imageView.bounds.width / 9.8, 8) + } + else + { + self.imageView.layer.cornerRadius = 0 + } + } } diff --git a/AltStore/Components/CollapsingTextView.swift b/AltStore/Components/CollapsingTextView.swift new file mode 100644 index 00000000..b5b73378 --- /dev/null +++ b/AltStore/Components/CollapsingTextView.swift @@ -0,0 +1,114 @@ +// +// CollapsingTextView.swift +// AltStore +// +// Created by Riley Testut on 7/23/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +class CollapsingTextView: UITextView +{ + var isCollapsed = true { + didSet { + self.setNeedsLayout() + } + } + + var maximumNumberOfLines = 2 { + didSet { + self.setNeedsLayout() + } + } + + var lineSpacing: CGFloat = 2 { + didSet { + self.setNeedsLayout() + } + } + + let moreButton = UIButton(type: .system) + + override func awakeFromNib() + { + super.awakeFromNib() + + self.layoutManager.delegate = self + + self.textContainerInset = .zero + self.textContainer.lineFragmentPadding = 0 + self.textContainer.lineBreakMode = .byTruncatingTail + self.textContainer.heightTracksTextView = true + self.textContainer.widthTracksTextView = true + + self.moreButton.setTitle(NSLocalizedString("More", comment: ""), for: .normal) + self.moreButton.addTarget(self, action: #selector(CollapsingTextView.toggleCollapsed(_:)), for: .primaryActionTriggered) + self.addSubview(self.moreButton) + + self.setNeedsLayout() + } + + override func layoutSubviews() + { + super.layoutSubviews() + + guard let font = self.font else { return } + + let buttonFont = UIFont.systemFont(ofSize: font.pointSize, weight: .medium) + self.moreButton.titleLabel?.font = buttonFont + + let size = self.moreButton.sizeThatFits(CGSize(width: 1000, height: 1000)) + + let moreButtonFrame = CGRect(x: self.bounds.width - self.moreButton.bounds.width, + y: self.bounds.height - self.moreButton.bounds.height - self.lineSpacing, + width: size.width, + height: font.lineHeight) + self.moreButton.frame = moreButtonFrame + + if self.isCollapsed + { + var exclusionFrame = moreButtonFrame + exclusionFrame.origin.y += self.moreButton.bounds.midY + exclusionFrame.size.width = self.bounds.width // Extra wide to make sure it wraps to next line. + + self.textContainer.maximumNumberOfLines = self.maximumNumberOfLines + self.textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)] + + let maximumCollapsedHeight = font.lineHeight * CGFloat(self.maximumNumberOfLines) + if self.bounds.height > maximumCollapsedHeight + { + self.moreButton.isHidden = false + } + else + { + self.moreButton.isHidden = true + } + } + else + { + self.textContainer.maximumNumberOfLines = 0 + self.textContainer.exclusionPaths = [] + + self.moreButton.isHidden = true + } + + self.invalidateIntrinsicContentSize() + } +} + +private extension CollapsingTextView +{ + @objc func toggleCollapsed(_ sender: UIButton) + { + self.isCollapsed.toggle() + } +} + +extension CollapsingTextView: NSLayoutManagerDelegate +{ + func layoutManager(_ layoutManager: NSLayoutManager, lineSpacingAfterGlyphAt glyphIndex: Int, withProposedLineFragmentRect rect: CGRect) -> CGFloat + { + return self.lineSpacing + } +} diff --git a/AltStore/Components/ToastView.swift b/AltStore/Components/ToastView.swift index 6961dc62..1e9f07c0 100644 --- a/AltStore/Components/ToastView.swift +++ b/AltStore/Components/ToastView.swift @@ -10,6 +10,17 @@ import Roxas class ToastView: RSTToastView { + override init(text: String, detailText detailedText: String?) + { + super.init(text: text, detailText: detailedText) + + self.layoutMargins = UIEdgeInsets(top: 6, left: 12, bottom: 6, right: 12) + } + + required init(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func layoutSubviews() { super.layoutSubviews() diff --git a/AltStore/Apps/AppManager.swift b/AltStore/Managing Apps/AppManager.swift similarity index 100% rename from AltStore/Apps/AppManager.swift rename to AltStore/Managing Apps/AppManager.swift diff --git a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents index 595ff761..ed9210cf 100644 --- a/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents +++ b/AltStore/Model/AltStore.xcdatamodeld/AltStore.xcdatamodel/contents @@ -27,12 +27,18 @@ + + + + + + @@ -59,8 +65,9 @@ - + + \ No newline at end of file diff --git a/AltStore/Model/App.swift b/AltStore/Model/App.swift index 14094d52..d506cff4 100644 --- a/AltStore/Model/App.swift +++ b/AltStore/Model/App.swift @@ -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) } } diff --git a/AltStore/Model/AppPermission.swift b/AltStore/Model/AppPermission.swift new file mode 100644 index 00000000..7dc75373 --- /dev/null +++ b/AltStore/Model/AppPermission.swift @@ -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 + { + return NSFetchRequest(entityName: "AppPermission") + } +} diff --git a/AltStore/Model/DatabaseManager.swift b/AltStore/Model/DatabaseManager.swift index 017f35c1..58082454 100644 --- a/AltStore/Model/DatabaseManager.swift +++ b/AltStore/Model/DatabaseManager.swift @@ -22,6 +22,7 @@ public class DatabaseManager private init() { self.persistentContainer = RSTPersistentContainer(name: "AltStore") + self.persistentContainer.preferredMergePolicy = MergePolicy() } } diff --git a/AltStore/Model/MergePolicy.swift b/AltStore/Model/MergePolicy.swift new file mode 100644 index 00000000..e7e25e4e --- /dev/null +++ b/AltStore/Model/MergePolicy.swift @@ -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) + } +} diff --git a/AltStore/My Apps/MyAppsViewController.swift b/AltStore/My Apps/MyAppsViewController.swift index 95a8fe19..6da2618f 100644 --- a/AltStore/My Apps/MyAppsViewController.swift +++ b/AltStore/My Apps/MyAppsViewController.swift @@ -79,6 +79,18 @@ class MyAppsViewController: UICollectionViewController self.collectionView.register(UpdateCollectionViewCell.nib, forCellWithReuseIdentifier: "UpdateCell") self.collectionView.register(UpdatesCollectionHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "UpdatesHeader") } + + override func prepare(for segue: UIStoryboardSegue, sender: Any?) + { + guard segue.identifier == "showApp" else { return } + + guard let cell = sender as? UICollectionViewCell, let indexPath = self.collectionView.indexPath(for: cell) else { return } + + let installedApp = self.dataSource.item(at: indexPath) + + let appViewController = segue.destination as! AppViewController + appViewController.app = installedApp.app + } } private extension MyAppsViewController @@ -122,7 +134,7 @@ private extension MyAppsViewController cell.mode = .collapsed } - cell.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered) + cell.versionDescriptionTextView.moreButton.addTarget(self, action: #selector(MyAppsViewController.toggleUpdateCellMode(_:)), for: .primaryActionTriggered) let progress = AppManager.shared.installationProgress(for: app) cell.updateButton.progress = progress diff --git a/AltStore/My Apps/UpdateCollectionViewCell.swift b/AltStore/My Apps/UpdateCollectionViewCell.swift index f610cc80..486665eb 100644 --- a/AltStore/My Apps/UpdateCollectionViewCell.swift +++ b/AltStore/My Apps/UpdateCollectionViewCell.swift @@ -30,10 +30,8 @@ extension UpdateCollectionViewCell @IBOutlet var dateLabel: UILabel! @IBOutlet var updateButton: PillButton! @IBOutlet var versionDescriptionTitleLabel: UILabel! - @IBOutlet var versionDescriptionTextView: UITextView! - - @IBOutlet var moreButton: UIButton! - + @IBOutlet var versionDescriptionTextView: CollapsingTextView! + override func awakeFromNib() { super.awakeFromNib() @@ -41,11 +39,6 @@ extension UpdateCollectionViewCell self.contentView.layer.cornerRadius = 20 self.contentView.layer.masksToBounds = true - self.versionDescriptionTextView.textContainerInset = .zero - self.versionDescriptionTextView.textContainer.lineFragmentPadding = 0 - self.versionDescriptionTextView.textContainer.lineBreakMode = .byTruncatingTail - self.versionDescriptionTextView.textContainer.heightTracksTextView = true - self.update() } @@ -56,44 +49,6 @@ extension UpdateCollectionViewCell self.update() } - override func layoutSubviews() - { - super.layoutSubviews() - - let textContainer = self.versionDescriptionTextView.textContainer - - switch self.mode - { - case .collapsed: - // Extra wide to make sure it wraps to next line. - let frame = CGRect(x: textContainer.size.width - self.moreButton.bounds.width - 8, - y: textContainer.size.height - 4, - width: textContainer.size.width, - height: textContainer.size.height) - - textContainer.maximumNumberOfLines = 2 - textContainer.exclusionPaths = [UIBezierPath(rect: frame)] - - if let font = self.versionDescriptionTextView.font, self.versionDescriptionTextView.bounds.height > font.lineHeight * 1.5 - { - self.moreButton.isHidden = false - } - else - { - // One (or less) lines, so hide more button. - self.moreButton.isHidden = true - } - - case .expanded: - textContainer.maximumNumberOfLines = 10 - textContainer.exclusionPaths = [] - - self.moreButton.isHidden = true - } - - self.versionDescriptionTextView.invalidateIntrinsicContentSize() - } - override func apply(_ layoutAttributes: UICollectionViewLayoutAttributes) { // Animates transition to new attributes. @@ -108,6 +63,12 @@ private extension UpdateCollectionViewCell { func update() { + switch self.mode + { + case .collapsed: self.versionDescriptionTextView.isCollapsed = true + case .expanded: self.versionDescriptionTextView.isCollapsed = false + } + self.versionDescriptionTitleLabel.textColor = self.tintColor self.contentView.backgroundColor = self.tintColor.withAlphaComponent(0.1) diff --git a/AltStore/My Apps/UpdateCollectionViewCell.xib b/AltStore/My Apps/UpdateCollectionViewCell.xib index 035acc78..3ec8c953 100644 --- a/AltStore/My Apps/UpdateCollectionViewCell.xib +++ b/AltStore/My Apps/UpdateCollectionViewCell.xib @@ -76,7 +76,7 @@ - + @@ -90,22 +90,12 @@ - - - @@ -124,7 +114,6 @@ - diff --git a/AltStore/Resources/Apps-Dev.json b/AltStore/Resources/Apps-Dev.json index 0b7ef4ae..8158fe95 100644 --- a/AltStore/Resources/Apps-Dev.json +++ b/AltStore/Resources/Apps-Dev.json @@ -3,25 +3,41 @@ "name": "AltStore", "identifier": "com.rileytestut.AltStore", "developerName": "Riley Testut", - "version": "1.0", - "versionDate": "2019-05-20", - "versionDescription": "AltStore has been updated with bug fixes and improvements.", + "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": "DeltaIcon" + "iconName": "AppIcon", + "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", "identifier": "com.rileytestut.Delta", "developerName": "Riley Testut", - "subtitle": "Classic Nintendo games in your pocket.", - "version": "1.0", - "versionDate": "2019-05-20", + "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", + "permissions": [ + { + "type": "photos", + "usageDescription": "Allows Delta to use images from your Photo Library as game artwork." + } + ], "screenshotNames": [ "Delta1", "Delta2", @@ -34,10 +50,17 @@ "identifier": "com.rileytestut.ClipboardManager", "subtitle": "Manage your clipboard history with ease.", "developerName": "Riley Testut", - "version": "1.0", + "version": "0.8", "versionDate": "2019-06-20", + "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" + "iconName": "ClipboardIcon", + "permissions": [ + { + "type": "background-audio", + "usageDescription": "Allows Clipboard Manager to continuously monitor your clipboard in the background." + } + ] } ] diff --git a/AltStore/Resources/Assets.xcassets/Back.imageset/Back@2x.png b/AltStore/Resources/Assets.xcassets/Back.imageset/Back@2x.png new file mode 100644 index 00000000..b03046a3 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Back.imageset/Back@2x.png differ diff --git a/AltStore/Resources/Assets.xcassets/Back.imageset/Contents.json b/AltStore/Resources/Assets.xcassets/Back.imageset/Contents.json new file mode 100644 index 00000000..02a54137 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Back.imageset/Contents.json @@ -0,0 +1,24 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "Back@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + }, + "properties" : { + "template-rendering-intent" : "template" + } +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/Contents.json b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/Contents.json new file mode 100644 index 00000000..fcc90b1f --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "sound@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "sound@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@2x.png b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@2x.png new file mode 100644 index 00000000..f7121412 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@2x.png differ diff --git a/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@3x.png b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@3x.png new file mode 100644 index 00000000..23bbdff5 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundAudioPermission.imageset/sound@3x.png differ diff --git a/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/Contents.json b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/Contents.json new file mode 100644 index 00000000..04671484 --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "fetch@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "fetch@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@2x.png b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@2x.png new file mode 100644 index 00000000..c44c3dee Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@2x.png differ diff --git a/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@3x.png b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@3x.png new file mode 100644 index 00000000..6c05606a Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Permissions/BackgroundFetchPermission.imageset/fetch@3x.png differ diff --git a/AltStore/Resources/Assets.xcassets/Permissions/Contents.json b/AltStore/Resources/Assets.xcassets/Permissions/Contents.json new file mode 100644 index 00000000..da4a164c --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Permissions/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/Contents.json b/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/Contents.json new file mode 100644 index 00000000..de4d09de --- /dev/null +++ b/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "photos@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "photos@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} \ No newline at end of file diff --git a/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@2x.png b/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@2x.png new file mode 100644 index 00000000..63dcd177 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@2x.png differ diff --git a/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@3x.png b/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@3x.png new file mode 100644 index 00000000..522b8bd8 Binary files /dev/null and b/AltStore/Resources/Assets.xcassets/Permissions/PhotosPermission.imageset/photos@3x.png differ diff --git a/AltStore/Types/ALTAppPermission.h b/AltStore/Types/ALTAppPermission.h new file mode 100644 index 00000000..5c68b1b1 --- /dev/null +++ b/AltStore/Types/ALTAppPermission.h @@ -0,0 +1,14 @@ +// +// ALTAppPermission.h +// AltStore +// +// Created by Riley Testut on 7/23/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import + +typedef NSString *ALTAppPermissionType NS_TYPED_EXTENSIBLE_ENUM; +extern ALTAppPermissionType const ALTAppPermissionTypePhotos; +extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio; +extern ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch; diff --git a/AltStore/Types/ALTAppPermission.m b/AltStore/Types/ALTAppPermission.m new file mode 100644 index 00000000..7eee2905 --- /dev/null +++ b/AltStore/Types/ALTAppPermission.m @@ -0,0 +1,13 @@ +// +// ALTAppPermission.m +// AltStore +// +// Created by Riley Testut on 7/23/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +#import "ALTAppPermission.h" + +ALTAppPermissionType const ALTAppPermissionTypePhotos = @"photos"; +ALTAppPermissionType const ALTAppPermissionTypeBackgroundAudio = @"background-audio"; +ALTAppPermissionType const ALTAppPermissionTypeBackgroundFetch = @"background-fetch";