diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index 71f3bb4e..aab89a3c 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -180,7 +180,6 @@ BFD52C2222A1A9EC000B7ED1 /* cnary.c in Sources */ = {isa = PBXBuildFile; fileRef = BFD52C1F22A1A9EC000B7ED1 /* cnary.c */; }; BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6E7230CC961007955AB /* PatreonAPI.swift */; }; BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */; }; - BFD5D6EC230CCDA1007955AB /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6EB230CCDA1007955AB /* PatreonViewController.swift */; }; BFD5D6EE230D8A86007955AB /* Patron.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6ED230D8A86007955AB /* Patron.swift */; }; BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F1230DD974007955AB /* Benefit.swift */; }; BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD5D6F3230DDB0A007955AB /* Campaign.swift */; }; @@ -208,6 +207,9 @@ BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; }; BFE6326A22A85DAF00F30809 /* ReplaceCertificateViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */; }; BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; }; + BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; }; + BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; }; + BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; }; DBAC68F8EC03F4A41D62EDE1 /* Pods_AltStore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1039C07E517311FC499A0B64 /* Pods_AltStore.framework */; }; /* End PBXBuildFile section */ @@ -456,7 +458,6 @@ BFD52C1F22A1A9EC000B7ED1 /* cnary.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = cnary.c; path = Dependencies/libplist/libcnary/cnary.c; sourceTree = SOURCE_ROOT; }; BFD5D6E7230CC961007955AB /* PatreonAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonAPI.swift; sourceTree = ""; }; BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonAccount.swift; sourceTree = ""; }; - BFD5D6EB230CCDA1007955AB /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = ""; }; BFD5D6ED230D8A86007955AB /* Patron.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Patron.swift; sourceTree = ""; }; BFD5D6F1230DD974007955AB /* Benefit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Benefit.swift; sourceTree = ""; }; BFD5D6F3230DDB0A007955AB /* Campaign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Campaign.swift; sourceTree = ""; }; @@ -484,6 +485,9 @@ BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = ""; }; BFE6326922A85DAF00F30809 /* ReplaceCertificateViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaceCertificateViewController.swift; sourceTree = ""; }; BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = ""; }; + BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = ""; }; + BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = ""; }; + BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = ""; }; EA79A60285C6AF5848AA16E9 /* Pods-AltStore.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AltStore.debug.xcconfig"; path = "Target Support Files/Pods-AltStore/Pods-AltStore.debug.xcconfig"; sourceTree = ""; }; FC3822AB1C4CF1D4CDF7445D /* Pods_AltServer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltServer.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -971,7 +975,9 @@ BFE60741231B07E6002B0E8E /* SettingsHeaderFooterView.swift */, BFE6073B231AE1E7002B0E8E /* SettingsHeaderFooterView.xib */, BF02419522F2199300129732 /* RefreshAttemptsViewController.swift */, - BFD5D6EB230CCDA1007955AB /* PatreonViewController.swift */, + BFF0B68D23219520007A79E1 /* PatreonViewController.swift */, + BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */, + BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */, ); path = Settings; sourceTree = ""; @@ -1223,6 +1229,7 @@ BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */, BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */, BFD247772284B9A700981D42 /* Assets.xcassets in Resources */, + BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */, BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */, BFD247752284B9A500981D42 /* Main.storyboard in Resources */, BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */, @@ -1393,7 +1400,6 @@ BFDB6A0F22AB2776007EA6D6 /* SendAppOperation.swift in Sources */, BFDB6A0D22AAFC1A007EA6D6 /* OperationError.swift in Sources */, BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */, - BFD5D6EC230CCDA1007955AB /* PatreonViewController.swift in Sources */, BFE6325E22A8497000F30809 /* SelectTeamViewController.swift in Sources */, BFD2478F2284C8F900981D42 /* Button.swift in Sources */, BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */, @@ -1416,6 +1422,7 @@ BFD2478C2284C4C300981D42 /* AppIconImageView.swift in Sources */, BFE338DD22F0E7F3002E24B9 /* Source.swift in Sources */, BF8F69C422E662D300049BA1 /* AppViewController.swift in Sources */, + BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */, BFD5D6EA230CCAE5007955AB /* PatreonAccount.swift in Sources */, BFE6326822A858F300F30809 /* Account.swift in Sources */, BFE6326622A857C200F30809 /* Team.swift in Sources */, @@ -1458,6 +1465,7 @@ BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */, BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */, BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */, + BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */, BF770E5622BC3C03002A40FE /* Server.swift in Sources */, BF43003022A71C960051E2BC /* UserDefaults+AltStore.swift in Sources */, ); diff --git a/AltStore/Components/NavigationBar.swift b/AltStore/Components/NavigationBar.swift index b12276e3..890813c1 100644 --- a/AltStore/Components/NavigationBar.swift +++ b/AltStore/Components/NavigationBar.swift @@ -12,6 +12,8 @@ import Roxas class NavigationBar: UINavigationBar { + @IBInspectable var automaticallyAdjustsItemPositions: Bool = true + private let backgroundColorView = UIView() override init(frame: CGRect) @@ -55,11 +57,14 @@ class NavigationBar: UINavigationBar self.insertSubview(self.backgroundColorView, at: 1) } - // We can't easily shift just the back button up, so we shift the entire content view slightly. - for contentView in self.subviews + if self.automaticallyAdjustsItemPositions { - guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue } - contentView.center.y -= 2 + // We can't easily shift just the back button up, so we shift the entire content view slightly. + for contentView in self.subviews + { + guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue } + contentView.center.y -= 2 + } } } } diff --git a/AltStore/Settings/AboutPatreonHeaderView.xib b/AltStore/Settings/AboutPatreonHeaderView.xib new file mode 100644 index 00000000..5f2954e5 --- /dev/null +++ b/AltStore/Settings/AboutPatreonHeaderView.xib @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/AltStore/Settings/PatreonComponents.swift b/AltStore/Settings/PatreonComponents.swift new file mode 100644 index 00000000..2bfc60fb --- /dev/null +++ b/AltStore/Settings/PatreonComponents.swift @@ -0,0 +1,68 @@ +// +// PatreonComponents.swift +// AltStore +// +// Created by Riley Testut on 9/5/19. +// Copyright © 2019 Riley Testut. All rights reserved. +// + +import UIKit + +class PatronCollectionViewCell: UICollectionViewCell +{ + @IBOutlet var textLabel: UILabel! +} + +class PatronsHeaderView: UICollectionReusableView +{ + let textLabel = UILabel() + + override init(frame: CGRect) + { + super.init(frame: frame) + + self.textLabel.font = UIFont.boldSystemFont(ofSize: 17) + self.textLabel.textColor = .white + self.addSubview(self.textLabel, pinningEdgesWith: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20)) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class PatronsFooterView: UICollectionReusableView +{ + let button = UIButton(type: .system) + + override init(frame: CGRect) + { + super.init(frame: frame) + + self.button.translatesAutoresizingMaskIntoConstraints = false + self.button.activityIndicatorView.style = .white + self.button.titleLabel?.textColor = .white + self.addSubview(self.button) + + NSLayoutConstraint.activate([self.button.centerXAnchor.constraint(equalTo: self.centerXAnchor), + self.button.centerYAnchor.constraint(equalTo: self.centerYAnchor)]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +class AboutPatreonHeaderView: UICollectionReusableView +{ + @IBOutlet var supportButton: UIButton! + + override func awakeFromNib() + { + super.awakeFromNib() + + self.supportButton.clipsToBounds = true + self.supportButton.layer.cornerRadius = 16 + } +} + diff --git a/AltStore/Settings/PatreonViewController.swift b/AltStore/Settings/PatreonViewController.swift index 62ef4f02..a569dc8b 100644 --- a/AltStore/Settings/PatreonViewController.swift +++ b/AltStore/Settings/PatreonViewController.swift @@ -2,18 +2,33 @@ // PatreonViewController.swift // AltStore // -// Created by Riley Testut on 8/20/19. +// Created by Riley Testut on 9/5/19. // Copyright © 2019 Riley Testut. All rights reserved. // import UIKit +import SafariServices import AuthenticationServices import Roxas -class PatreonViewController: UITableViewController +extension PatreonViewController +{ + private enum Section: Int, CaseIterable + { + case about + case patrons + } +} + +class PatreonViewController: UICollectionViewController { private lazy var dataSource = self.makeDataSource() + private lazy var patronsDataSource = self.makePatronsDataSource() + + private var prototypeAboutHeader: AboutPatreonHeaderView! + + private var patronsResult: Result<[Patron], Error>? @IBOutlet private var signInButton: UIBarButtonItem! @IBOutlet private var signOutButton: UIBarButtonItem! @@ -22,7 +37,14 @@ class PatreonViewController: UITableViewController { super.viewDidLoad() - self.tableView.dataSource = self.dataSource + let aboutHeaderNib = UINib(nibName: "AboutPatreonHeaderView", bundle: nil) + self.prototypeAboutHeader = aboutHeaderNib.instantiate(withOwner: nil, options: nil)[0] as? AboutPatreonHeaderView + + self.collectionView.dataSource = self.dataSource + + self.collectionView.register(aboutHeaderNib, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "AboutHeader") + self.collectionView.register(PatronsHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "PatronsHeader") + self.collectionView.register(PatronsFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "PatronsFooter") self.update() } @@ -32,19 +54,45 @@ class PatreonViewController: UITableViewController super.viewWillAppear(animated) self.fetchPatrons() + + self.update() + } + + override func viewDidLayoutSubviews() + { + super.viewDidLayoutSubviews() + + let layout = self.collectionViewLayout as! UICollectionViewFlowLayout + + var itemWidth = (self.collectionView.bounds.width - (layout.sectionInset.left + layout.sectionInset.right + layout.minimumInteritemSpacing)) / 2 + itemWidth.round(.down) + + layout.itemSize = CGSize(width: itemWidth, height: layout.itemSize.height) } } private extension PatreonViewController { - func makeDataSource() -> RSTArrayTableViewDataSource + func makeDataSource() -> RSTCompositeCollectionViewDataSource { - let dataSource = RSTArrayTableViewDataSource(items: []) - dataSource.cellConfigurationHandler = { (cell, patron, indexPath) in - cell.textLabel?.text = patron.name + let aboutDataSource = RSTDynamicCollectionViewDataSource() + aboutDataSource.numberOfSectionsHandler = { 1 } + aboutDataSource.numberOfItemsHandler = { _ in 0 } + + let dataSource = RSTCompositeCollectionViewDataSource(dataSources: [aboutDataSource, self.patronsDataSource]) + dataSource.proxy = self + return dataSource + } + + func makePatronsDataSource() -> RSTArrayCollectionViewDataSource + { + let patronsDataSource = RSTArrayCollectionViewDataSource(items: []) + patronsDataSource.cellConfigurationHandler = { (cell, patron, indexPath) in + let cell = cell as! PatronCollectionViewCell + cell.textLabel.text = patron.name } - return dataSource + return patronsDataSource } func update() @@ -58,28 +106,48 @@ private extension PatreonViewController self.navigationItem.rightBarButtonItem = self.signInButton } } - - func fetchPatrons() - { - PatreonAPI.shared.fetchPatrons { (result) in - do - { - let patrons = try result.get() - self.dataSource.items = patrons - } - catch - { - DispatchQueue.main.async { - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) - } - } - } - } } private extension PatreonViewController { + @objc func fetchPatrons() + { + if let result = self.patronsResult, case .failure = result + { + self.patronsResult = nil + self.collectionView.reloadData() + } + + PatreonAPI.shared.fetchPatrons { (result) in + self.patronsResult = result + + do + { + let patrons = try result.get() + let sortedPatrons = patrons.sorted { $0.name < $1.name } + + self.patronsDataSource.items = sortedPatrons + } + catch + { + print("Failed to fetch patrons:", error) + + DispatchQueue.main.async { + self.collectionView.reloadData() + } + } + } + } + + @objc func openPatreonURL(_ sender: UIButton) + { + let patreonURL = URL(string: "https://www.patreon.com/rileytestut")! + + let safariViewController = SFSafariViewController(url: patreonURL) + safariViewController.preferredControlTintColor = self.view.tintColor + self.present(safariViewController, animated: true, completion: nil) + } + @IBAction func authenticate(_ sender: UIBarButtonItem) { PatreonAPI.shared.authenticate { (result) in @@ -108,22 +176,100 @@ private extension PatreonViewController @IBAction func signOut(_ sender: UIBarButtonItem) { - PatreonAPI.shared.signOut { (result) in - do - { - try result.get() - - DispatchQueue.main.async { - self.update() + func signOut() + { + PatreonAPI.shared.signOut { (result) in + do + { + try result.get() + + DispatchQueue.main.async { + self.update() + } + } + catch + { + DispatchQueue.main.async { + let toastView = ToastView(text: error.localizedDescription, detailText: nil) + toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + } } } - catch + } + + let alertController = UIAlertController(title: NSLocalizedString("Are you sure you want to sign out?", comment: ""), message: NSLocalizedString("You will no longer have access to beta versions of apps once you sign out.", comment: ""), preferredStyle: .actionSheet) + alertController.addAction(UIAlertAction(title: NSLocalizedString("Sign Out", comment: ""), style: .destructive) { _ in signOut() }) + alertController.addAction(.cancel) + self.present(alertController, animated: true, completion: nil) + } +} + +extension PatreonViewController +{ + override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView + { + let section = Section.allCases[indexPath.section] + switch section + { + case .about: + let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "AboutHeader", for: indexPath) as! AboutPatreonHeaderView + headerView.supportButton.addTarget(self, action: #selector(PatreonViewController.openPatreonURL(_:)), for: .primaryActionTriggered) + return headerView + + case .patrons: + if kind == UICollectionView.elementKindSectionHeader { - DispatchQueue.main.async { - let toastView = ToastView(text: error.localizedDescription, detailText: nil) - toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0) + let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "PatronsHeader", for: indexPath) as! PatronsHeaderView + headerView.textLabel.text = NSLocalizedString("Special thanks to...", comment: "") + return headerView + } + else + { + let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "PatronsFooter", for: indexPath) as! PatronsFooterView + footerView.button.isIndicatingActivity = false + footerView.button.isHidden = false + footerView.button.addTarget(self, action: #selector(PatreonViewController.fetchPatrons), for: .primaryActionTriggered) + + switch self.patronsResult + { + case .none: footerView.button.isIndicatingActivity = true + case .success?: footerView.button.isHidden = true + case .failure?: footerView.button.setTitle(NSLocalizedString("Error Loading Patrons", comment: ""), for: .normal) } + + return footerView } } } } + +extension PatreonViewController: UICollectionViewDelegateFlowLayout +{ + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize + { + let section = Section.allCases[section] + switch section + { + case .about: + let widthConstraint = self.prototypeAboutHeader.widthAnchor.constraint(equalToConstant: collectionView.bounds.width) + NSLayoutConstraint.activate([widthConstraint]) + defer { NSLayoutConstraint.deactivate([widthConstraint]) } + + let size = self.prototypeAboutHeader.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize) + return size + + case .patrons: + return CGSize(width: 320, height: 20) + } + } + + func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize + { + let section = Section.allCases[section] + switch section + { + case .about: return .zero + case .patrons: return CGSize(width: 320, height: 20) + } + } +} diff --git a/AltStore/Settings/Settings.storyboard b/AltStore/Settings/Settings.storyboard index 9b405fbf..e37d5533 100644 --- a/AltStore/Settings/Settings.storyboard +++ b/AltStore/Settings/Settings.storyboard @@ -186,7 +186,7 @@ - + @@ -300,7 +300,11 @@ + + + + @@ -381,56 +385,71 @@ - + - - + + - - - - - - - + + + + + + + + + + + + + + - - - - + + + + + + + + + + + + - - + + - - - + + + - + - - + + - - - - + + + - + + @@ -441,8 +460,5 @@ - - -