2023-04-04 15:41:44 -05:00
|
|
|
//
|
|
|
|
|
// SourcesDetailContentViewController.swift
|
|
|
|
|
// AltStore
|
|
|
|
|
//
|
|
|
|
|
// Created by Riley Testut on 3/8/23.
|
|
|
|
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import UIKit
|
2023-04-04 16:17:38 -05:00
|
|
|
import SafariServices
|
2023-04-04 15:41:44 -05:00
|
|
|
|
|
|
|
|
import AltStoreCore
|
|
|
|
|
import Roxas
|
|
|
|
|
|
|
|
|
|
import Nuke
|
|
|
|
|
|
|
|
|
|
private let sectionInset = 20.0
|
|
|
|
|
|
|
|
|
|
extension SourceDetailContentViewController
|
|
|
|
|
{
|
|
|
|
|
private enum Section: Int
|
|
|
|
|
{
|
|
|
|
|
case news
|
|
|
|
|
case featuredApps
|
|
|
|
|
case about
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private enum ElementKind: String
|
|
|
|
|
{
|
|
|
|
|
case title
|
|
|
|
|
case button
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class SourceDetailContentViewController: UICollectionViewController
|
|
|
|
|
{
|
|
|
|
|
let source: Source
|
|
|
|
|
|
|
|
|
|
private lazy var dataSource = self.makeDataSource()
|
|
|
|
|
private lazy var newsDataSource = self.makeNewsDataSource()
|
|
|
|
|
private lazy var appsDataSource = self.makeAppsDataSource()
|
|
|
|
|
private lazy var aboutDataSource = self.makeAboutDataSource()
|
|
|
|
|
|
|
|
|
|
override var collectionViewLayout: UICollectionViewCompositionalLayout {
|
|
|
|
|
return self.collectionView.collectionViewLayout as! UICollectionViewCompositionalLayout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init?(source: Source, coder: NSCoder)
|
|
|
|
|
{
|
|
|
|
|
self.source = source
|
|
|
|
|
|
|
|
|
|
super.init(coder: coder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
required init?(coder: NSCoder) {
|
|
|
|
|
fatalError("init(coder:) has not been implemented")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func viewDidLoad()
|
|
|
|
|
{
|
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
|
2023-10-19 13:39:15 -05:00
|
|
|
self.collectionView.backgroundColor = .altBackground
|
|
|
|
|
|
2023-04-04 15:41:44 -05:00
|
|
|
let collectionViewLayout = self.makeLayout(source: self.source)
|
|
|
|
|
self.collectionView.collectionViewLayout = collectionViewLayout
|
|
|
|
|
|
|
|
|
|
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: "NewsCell")
|
|
|
|
|
self.collectionView.register(TitleCollectionReusableView.self, forSupplementaryViewOfKind: ElementKind.title.rawValue, withReuseIdentifier: ElementKind.title.rawValue)
|
|
|
|
|
self.collectionView.register(ButtonCollectionReusableView.self, forSupplementaryViewOfKind: ElementKind.button.rawValue, withReuseIdentifier: ElementKind.button.rawValue)
|
|
|
|
|
|
|
|
|
|
self.dataSource.proxy = self
|
|
|
|
|
self.collectionView.dataSource = self.dataSource
|
|
|
|
|
self.collectionView.prefetchDataSource = self.dataSource
|
2024-01-23 16:50:55 -06:00
|
|
|
|
|
|
|
|
let context = self.source.managedObjectContext ?? DatabaseManager.shared.viewContext
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(SourceDetailContentViewController.didChangeApps), name: NSManagedObjectContext.didChangeObjectsNotification, object: context)
|
2023-04-04 15:41:44 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override func viewSafeAreaInsetsDidChange()
|
|
|
|
|
{
|
|
|
|
|
super.viewSafeAreaInsetsDidChange()
|
|
|
|
|
|
|
|
|
|
// Add sectionInset to safeAreaInsets.bottom.
|
|
|
|
|
self.collectionView.contentInset = UIEdgeInsets(top: sectionInset, left: 0, bottom: self.view.safeAreaInsets.bottom + sectionInset, right: 0)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension SourceDetailContentViewController
|
|
|
|
|
{
|
|
|
|
|
func makeLayout(source: Source) -> UICollectionViewCompositionalLayout
|
|
|
|
|
{
|
|
|
|
|
let layoutConfig = UICollectionViewCompositionalLayoutConfiguration()
|
|
|
|
|
layoutConfig.interSectionSpacing = 10
|
|
|
|
|
|
|
|
|
|
let layout = UICollectionViewCompositionalLayout(sectionProvider: { (sectionIndex, layoutEnvironment) -> NSCollectionLayoutSection? in
|
|
|
|
|
guard let section = Section(rawValue: sectionIndex) else { return nil }
|
|
|
|
|
|
|
|
|
|
switch section
|
|
|
|
|
{
|
|
|
|
|
case .news:
|
|
|
|
|
guard !source.newsItems.isEmpty else { return nil }
|
|
|
|
|
|
2023-10-16 19:12:42 -05:00
|
|
|
// Underestimate height to prevent jumping size abruptly.
|
|
|
|
|
let heightDimension: NSCollectionLayoutDimension = if #available(iOS 17, *) { .uniformAcrossSiblings(estimate: 50) } else { .estimated(50) }
|
|
|
|
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: heightDimension)
|
2023-04-04 15:41:44 -05:00
|
|
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
|
|
|
|
|
|
|
|
let groupWidth = layoutEnvironment.container.contentSize.width - sectionInset * 2
|
2023-10-16 19:12:42 -05:00
|
|
|
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: heightDimension)
|
2023-04-04 15:41:44 -05:00
|
|
|
let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitems: [item])
|
|
|
|
|
|
|
|
|
|
let buttonSize = NSCollectionLayoutSize(widthDimension: .estimated(60), heightDimension: .estimated(20))
|
|
|
|
|
let sectionFooter = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: buttonSize, elementKind: ElementKind.button.rawValue, alignment: .bottomTrailing)
|
|
|
|
|
|
|
|
|
|
let layoutSection = NSCollectionLayoutSection(group: group)
|
|
|
|
|
layoutSection.interGroupSpacing = 10
|
|
|
|
|
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: sectionInset, bottom: 4, trailing: sectionInset)
|
|
|
|
|
layoutSection.orthogonalScrollingBehavior = .groupPagingCentered
|
|
|
|
|
layoutSection.boundarySupplementaryItems = [sectionFooter]
|
|
|
|
|
return layoutSection
|
|
|
|
|
|
|
|
|
|
case .featuredApps:
|
|
|
|
|
// Always show Featured Apps section, even if there are no apps.
|
|
|
|
|
// guard !source.effectiveFeaturedApps.isEmpty else { return nil }
|
|
|
|
|
|
|
|
|
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .absolute(88))
|
|
|
|
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
|
|
|
|
|
|
|
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
|
|
|
|
|
|
|
|
|
|
let titleSize = NSCollectionLayoutSize(widthDimension: .estimated(75), heightDimension: .estimated(40))
|
|
|
|
|
let titleHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.title.rawValue, alignment: .topLeading)
|
|
|
|
|
|
|
|
|
|
let buttonSize = NSCollectionLayoutSize(widthDimension: .estimated(60), heightDimension: .estimated(20))
|
|
|
|
|
let buttonHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: buttonSize, elementKind: ElementKind.button.rawValue, alignment: .bottomTrailing)
|
|
|
|
|
|
|
|
|
|
let layoutSection = NSCollectionLayoutSection(group: group)
|
|
|
|
|
layoutSection.interGroupSpacing = 15
|
|
|
|
|
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 15 /* independent of sectionInset */, leading: sectionInset, bottom: 4, trailing: sectionInset)
|
|
|
|
|
layoutSection.orthogonalScrollingBehavior = .none
|
|
|
|
|
layoutSection.boundarySupplementaryItems = [titleHeader, buttonHeader]
|
|
|
|
|
return layoutSection
|
|
|
|
|
|
|
|
|
|
case .about:
|
|
|
|
|
guard source.localizedDescription != nil else { return nil }
|
|
|
|
|
|
|
|
|
|
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(200))
|
|
|
|
|
let item = NSCollectionLayoutItem(layoutSize: itemSize)
|
|
|
|
|
|
|
|
|
|
let group = NSCollectionLayoutGroup.vertical(layoutSize: itemSize, subitems: [item])
|
|
|
|
|
|
|
|
|
|
let titleSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(40))
|
|
|
|
|
let titleHeader = NSCollectionLayoutBoundarySupplementaryItem(layoutSize: titleSize, elementKind: ElementKind.title.rawValue, alignment: .topLeading)
|
|
|
|
|
|
|
|
|
|
let layoutSection = NSCollectionLayoutSection(group: group)
|
|
|
|
|
layoutSection.contentInsets = NSDirectionalEdgeInsets(top: 15 /* independent of sectionInset */, leading: sectionInset, bottom: 0, trailing: sectionInset)
|
|
|
|
|
layoutSection.orthogonalScrollingBehavior = .none
|
|
|
|
|
layoutSection.boundarySupplementaryItems = [titleHeader]
|
|
|
|
|
return layoutSection
|
|
|
|
|
}
|
|
|
|
|
}, configuration: layoutConfig)
|
|
|
|
|
|
|
|
|
|
return layout
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeDataSource() -> RSTCompositeCollectionViewPrefetchingDataSource<NSManagedObject, UIImage>
|
|
|
|
|
{
|
|
|
|
|
let newsDataSource = self.newsDataSource as! RSTFetchedResultsCollectionViewDataSource<NSManagedObject>
|
|
|
|
|
let appsDataSource = self.appsDataSource as! RSTArrayCollectionViewPrefetchingDataSource<NSManagedObject, UIImage>
|
|
|
|
|
|
|
|
|
|
let dataSource = RSTCompositeCollectionViewPrefetchingDataSource<NSManagedObject, UIImage>(dataSources: [newsDataSource, appsDataSource, self.aboutDataSource])
|
|
|
|
|
return dataSource
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeNewsDataSource() -> RSTFetchedResultsCollectionViewDataSource<NewsItem>
|
|
|
|
|
{
|
|
|
|
|
let fetchRequest = NewsItem.sortedFetchRequest(for: self.source)
|
|
|
|
|
|
|
|
|
|
let context = self.source.managedObjectContext ?? DatabaseManager.shared.viewContext
|
|
|
|
|
let dataSource = RSTFetchedResultsCollectionViewDataSource(fetchRequest: fetchRequest, managedObjectContext: context)
|
|
|
|
|
dataSource.liveFetchLimit = 5
|
|
|
|
|
dataSource.cellIdentifierHandler = { _ in "NewsCell" }
|
|
|
|
|
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
|
|
|
|
|
let cell = cell as! NewsCollectionViewCell
|
|
|
|
|
|
|
|
|
|
// For some reason, setting cell.layoutMargins = .zero does not update cell.contentView.layoutMargins.
|
|
|
|
|
cell.layoutMargins = .zero
|
|
|
|
|
cell.contentView.layoutMargins = .zero
|
|
|
|
|
|
|
|
|
|
cell.titleLabel.text = newsItem.title
|
|
|
|
|
cell.captionLabel.text = newsItem.caption
|
|
|
|
|
cell.contentBackgroundView.backgroundColor = newsItem.tintColor
|
|
|
|
|
|
|
|
|
|
cell.imageView.image = nil
|
|
|
|
|
cell.imageView.isHidden = true
|
|
|
|
|
|
|
|
|
|
cell.isAccessibilityElement = true
|
|
|
|
|
cell.accessibilityLabel = (cell.titleLabel.text ?? "") + ". " + (cell.captionLabel.text ?? "")
|
|
|
|
|
|
|
|
|
|
if newsItem.storeApp != nil || newsItem.externalURL != nil
|
|
|
|
|
{
|
|
|
|
|
cell.accessibilityTraits.insert(.button)
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
cell.accessibilityTraits.remove(.button)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataSource
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeAppsDataSource() -> RSTArrayCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
|
|
|
|
{
|
|
|
|
|
let featuredApps = self.source.effectiveFeaturedApps
|
|
|
|
|
let limitedFeaturedApps = Array(featuredApps.prefix(5))
|
|
|
|
|
|
|
|
|
|
let dataSource = RSTArrayCollectionViewPrefetchingDataSource<StoreApp, UIImage>(items: limitedFeaturedApps)
|
|
|
|
|
dataSource.cellIdentifierHandler = { _ in "AppCell" }
|
2023-11-29 18:08:42 -06:00
|
|
|
dataSource.predicate = StoreApp.visibleAppsPredicate
|
2023-04-04 15:41:44 -05:00
|
|
|
dataSource.cellConfigurationHandler = { [weak self] (cell, storeApp, indexPath) in
|
|
|
|
|
let cell = cell as! AppBannerCollectionViewCell
|
|
|
|
|
cell.tintColor = storeApp.tintColor
|
|
|
|
|
|
|
|
|
|
// For some reason, setting cell.layoutMargins = .zero does not update cell.contentView.layoutMargins.
|
|
|
|
|
cell.layoutMargins = .zero
|
|
|
|
|
cell.contentView.layoutMargins = .zero
|
|
|
|
|
|
2023-11-30 18:50:54 -06:00
|
|
|
cell.bannerView.button.isIndicatingActivity = false
|
2023-12-07 18:19:42 -06:00
|
|
|
cell.bannerView.configure(for: storeApp, showSourceIcon: false)
|
2023-04-04 15:41:44 -05:00
|
|
|
|
|
|
|
|
cell.bannerView.button.tintColor = storeApp.tintColor
|
2023-11-30 18:50:54 -06:00
|
|
|
cell.bannerView.button.addTarget(self, action: #selector(SourceDetailContentViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
2023-04-04 15:41:44 -05:00
|
|
|
|
2023-12-07 18:19:42 -06:00
|
|
|
cell.bannerView.iconImageView.image = nil
|
2023-11-30 18:50:54 -06:00
|
|
|
cell.bannerView.iconImageView.isIndicatingActivity = true
|
2023-04-04 15:41:44 -05:00
|
|
|
}
|
|
|
|
|
dataSource.prefetchHandler = { (storeApp, indexPath, completion) -> Foundation.Operation? in
|
|
|
|
|
return RSTAsyncBlockOperation { (operation) in
|
|
|
|
|
storeApp.managedObjectContext?.perform {
|
|
|
|
|
ImagePipeline.shared.loadImage(with: storeApp.iconURL, progress: nil) { result in
|
|
|
|
|
guard !operation.isCancelled else { return operation.finish() }
|
|
|
|
|
|
|
|
|
|
switch result
|
|
|
|
|
{
|
|
|
|
|
case .success(let response): completion(response.image, nil)
|
|
|
|
|
case .failure(let error): completion(nil, error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-07 18:19:42 -06:00
|
|
|
dataSource.prefetchCompletionHandler = { [weak dataSource] (cell, image, indexPath, error) in
|
2023-04-04 15:41:44 -05:00
|
|
|
let cell = cell as! AppBannerCollectionViewCell
|
|
|
|
|
cell.bannerView.iconImageView.image = image
|
|
|
|
|
cell.bannerView.iconImageView.isIndicatingActivity = false
|
|
|
|
|
|
2023-12-07 18:19:42 -06:00
|
|
|
if let error, let dataSource
|
2023-04-04 15:41:44 -05:00
|
|
|
{
|
2023-12-07 18:19:42 -06:00
|
|
|
let app = dataSource.item(at: indexPath)
|
|
|
|
|
Logger.main.debug("Failed to fetch app icon from \(app.iconURL, privacy: .public). \(error.localizedDescription, privacy: .public)")
|
2023-04-04 15:41:44 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataSource
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeAboutDataSource() -> RSTDynamicCollectionViewDataSource<NSManagedObject>
|
|
|
|
|
{
|
|
|
|
|
let dataSource = RSTDynamicCollectionViewDataSource<NSManagedObject>()
|
|
|
|
|
dataSource.numberOfSectionsHandler = { 1 }
|
2023-05-24 19:13:40 -05:00
|
|
|
dataSource.numberOfItemsHandler = { [source] _ in source.localizedDescription == nil ? 0 : 1 }
|
2023-04-04 15:41:44 -05:00
|
|
|
dataSource.cellIdentifierHandler = { _ in "AboutCell" }
|
2023-05-24 19:13:40 -05:00
|
|
|
dataSource.cellConfigurationHandler = { [source] (cell, _, indexPath) in
|
2023-04-04 15:41:44 -05:00
|
|
|
let cell = cell as! TextViewCollectionViewCell
|
|
|
|
|
cell.contentView.layoutMargins = .zero // Fixes incorrect margins if not initially on screen.
|
2023-05-24 19:13:40 -05:00
|
|
|
cell.textView.text = source.localizedDescription
|
2023-04-04 15:41:44 -05:00
|
|
|
cell.textView.isCollapsed = false
|
2023-10-19 13:39:15 -05:00
|
|
|
cell.textView.backgroundColor = .altBackground
|
2023-04-04 15:41:44 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dataSource
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 16:17:38 -05:00
|
|
|
private extension SourceDetailContentViewController
|
|
|
|
|
{
|
|
|
|
|
@objc func viewAllNews()
|
|
|
|
|
{
|
|
|
|
|
self.performSegue(withIdentifier: "showAllNews", sender: nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func viewAllApps()
|
|
|
|
|
{
|
|
|
|
|
self.performSegue(withIdentifier: "showAllApps", sender: nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBSegueAction
|
|
|
|
|
func makeNewsViewController(_ coder: NSCoder) -> UIViewController?
|
|
|
|
|
{
|
|
|
|
|
let newsViewController = NewsViewController(source: self.source, coder: coder)
|
|
|
|
|
return newsViewController
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@IBSegueAction
|
|
|
|
|
func makeBrowseViewController(_ coder: NSCoder) -> UIViewController?
|
|
|
|
|
{
|
|
|
|
|
let browseViewController = BrowseViewController(source: self.source, coder: coder)
|
|
|
|
|
return browseViewController
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-04 15:41:44 -05:00
|
|
|
extension SourceDetailContentViewController
|
|
|
|
|
{
|
|
|
|
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
|
|
|
|
{
|
|
|
|
|
let supplementaryView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath)
|
|
|
|
|
|
|
|
|
|
let section = Section(rawValue: indexPath.section)!
|
|
|
|
|
let kind = ElementKind(rawValue: kind)!
|
|
|
|
|
switch (section, kind)
|
|
|
|
|
{
|
|
|
|
|
case (.news, _):
|
|
|
|
|
let buttonView = supplementaryView as! ButtonCollectionReusableView
|
|
|
|
|
buttonView.button.setTitle(NSLocalizedString("View All", comment: ""), for: .normal)
|
|
|
|
|
|
2023-04-04 16:17:38 -05:00
|
|
|
buttonView.button.removeTarget(self, action: nil, for: .primaryActionTriggered)
|
|
|
|
|
buttonView.button.addTarget(self, action: #selector(SourceDetailContentViewController.viewAllNews), for: .primaryActionTriggered)
|
|
|
|
|
|
2023-04-04 15:41:44 -05:00
|
|
|
case (.featuredApps, .title):
|
|
|
|
|
let titleView = supplementaryView as! TitleCollectionReusableView
|
|
|
|
|
titleView.label.text = NSLocalizedString("Featured Apps", comment: "")
|
|
|
|
|
|
|
|
|
|
case (.featuredApps, .button):
|
|
|
|
|
let buttonView = supplementaryView as! ButtonCollectionReusableView
|
|
|
|
|
buttonView.button.setTitle(NSLocalizedString("View All Apps", comment: ""), for: .normal)
|
|
|
|
|
|
2023-04-04 16:17:38 -05:00
|
|
|
buttonView.button.removeTarget(self, action: nil, for: .primaryActionTriggered)
|
|
|
|
|
buttonView.button.addTarget(self, action: #selector(SourceDetailContentViewController.viewAllApps), for: .primaryActionTriggered)
|
|
|
|
|
|
2023-04-04 15:41:44 -05:00
|
|
|
case (.about, _):
|
|
|
|
|
let titleView = supplementaryView as! TitleCollectionReusableView
|
|
|
|
|
titleView.label.text = NSLocalizedString("About", comment: "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return supplementaryView
|
|
|
|
|
}
|
2023-04-04 16:17:38 -05:00
|
|
|
|
|
|
|
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
|
|
|
|
{
|
|
|
|
|
let section = Section(rawValue: indexPath.section)!
|
|
|
|
|
let item = self.dataSource.item(at: indexPath)
|
|
|
|
|
|
|
|
|
|
switch (section, item)
|
|
|
|
|
{
|
|
|
|
|
case (.news, let newsItem as NewsItem):
|
|
|
|
|
if let externalURL = newsItem.externalURL
|
|
|
|
|
{
|
|
|
|
|
let safariViewController = SFSafariViewController(url: externalURL)
|
|
|
|
|
safariViewController.preferredControlTintColor = newsItem.tintColor
|
|
|
|
|
self.present(safariViewController, animated: true, completion: nil)
|
|
|
|
|
}
|
|
|
|
|
else if let storeApp = newsItem.storeApp
|
|
|
|
|
{
|
|
|
|
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
|
|
|
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
case (.featuredApps, let storeApp as StoreApp):
|
|
|
|
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
|
|
|
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
|
|
|
|
|
|
|
|
|
default: break
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-04 15:41:44 -05:00
|
|
|
}
|
|
|
|
|
|
2023-04-04 16:55:55 -05:00
|
|
|
private extension SourceDetailContentViewController
|
|
|
|
|
{
|
2023-11-30 18:50:54 -06:00
|
|
|
@objc func performAppAction(_ sender: PillButton)
|
2023-04-04 16:55:55 -05:00
|
|
|
{
|
|
|
|
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
|
|
|
|
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
|
|
|
|
|
|
|
|
|
|
let storeApp = self.dataSource.item(at: indexPath) as! StoreApp
|
|
|
|
|
|
2023-11-30 18:50:54 -06:00
|
|
|
if let installedApp = storeApp.installedApp, !installedApp.isUpdateAvailable
|
|
|
|
|
{
|
|
|
|
|
self.open(installedApp)
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
sender.isIndicatingActivity = true
|
|
|
|
|
|
|
|
|
|
Task<Void, Never> {
|
2024-01-23 16:43:05 -06:00
|
|
|
await self.downloadApp(storeApp)
|
2023-11-30 18:50:54 -06:00
|
|
|
sender.isIndicatingActivity = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-23 16:43:05 -06:00
|
|
|
@MainActor
|
|
|
|
|
func downloadApp(_ storeApp: StoreApp) async
|
2023-11-30 18:50:54 -06:00
|
|
|
{
|
|
|
|
|
do
|
|
|
|
|
{
|
2024-01-23 16:43:05 -06:00
|
|
|
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<Void, Error>) in
|
|
|
|
|
if let installedApp = storeApp.installedApp, installedApp.isUpdateAvailable
|
|
|
|
|
{
|
|
|
|
|
AppManager.shared.update(installedApp, presentingViewController: self) { result in
|
|
|
|
|
continuation.resume(with: result.map { _ in () })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reload()
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
Task<Void, Never> { @MainActor in
|
|
|
|
|
await AppManager.shared.installAsync(storeApp, presentingViewController: self) { result in
|
|
|
|
|
continuation.resume(with: result.map { _ in () })
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
reload()
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-04 16:55:55 -05:00
|
|
|
}
|
|
|
|
|
}
|
2023-11-30 18:50:54 -06:00
|
|
|
catch is CancellationError {}
|
|
|
|
|
catch
|
|
|
|
|
{
|
2024-01-23 16:43:05 -06:00
|
|
|
let toastView = ToastView(error: error)
|
|
|
|
|
toastView.opensErrorLog = true
|
|
|
|
|
toastView.show(in: self)
|
2023-11-30 18:50:54 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.collectionView.reloadSections([Section.featuredApps.rawValue])
|
2024-01-23 16:43:05 -06:00
|
|
|
|
|
|
|
|
func reload()
|
|
|
|
|
{
|
2023-11-30 18:50:54 -06:00
|
|
|
UIView.performWithoutAnimation {
|
|
|
|
|
guard let index = self.appsDataSource.items.firstIndex(of: storeApp) else {
|
|
|
|
|
self.collectionView.reloadSections([Section.featuredApps.rawValue])
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let indexPath = IndexPath(item: index, section: Section.featuredApps.rawValue)
|
|
|
|
|
self.collectionView.reloadItems(at: [indexPath])
|
|
|
|
|
}
|
2023-04-04 16:55:55 -05:00
|
|
|
}
|
|
|
|
|
}
|
2023-11-30 18:50:54 -06:00
|
|
|
|
|
|
|
|
func open(_ installedApp: InstalledApp)
|
|
|
|
|
{
|
|
|
|
|
UIApplication.shared.open(installedApp.openAppURL)
|
|
|
|
|
}
|
2024-01-23 16:50:55 -06:00
|
|
|
|
|
|
|
|
@objc func didChangeApps(_ notification: Notification)
|
|
|
|
|
{
|
|
|
|
|
// Make sure to include refreshed objects to catch all merged changes.
|
|
|
|
|
let updatedObjects = notification.userInfo?[NSUpdatedObjectsKey] as? Set<NSManagedObject> ?? []
|
|
|
|
|
let refreshedObjects = notification.userInfo?[NSRefreshedObjectsKey] as? Set<NSManagedObject> ?? []
|
|
|
|
|
|
|
|
|
|
let changedAppIndexes = updatedObjects.union(refreshedObjects).compactMap { $0 as? StoreApp }.compactMap { self.appsDataSource.items.firstIndex(of: $0) }
|
|
|
|
|
let indexPaths = changedAppIndexes.map { IndexPath(item: $0, section: Section.featuredApps.rawValue) }
|
|
|
|
|
|
|
|
|
|
guard !indexPaths.isEmpty else { return }
|
|
|
|
|
|
|
|
|
|
// Async so that AppManager.installationProgress(for:) is nil when we update.
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.collectionView.reloadItems(at: indexPaths)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-04-04 16:55:55 -05:00
|
|
|
}
|
|
|
|
|
|
2023-04-04 15:41:44 -05:00
|
|
|
extension SourceDetailContentViewController: ScrollableContentViewController
|
|
|
|
|
{
|
|
|
|
|
var scrollView: UIScrollView { self.collectionView }
|
|
|
|
|
}
|