Shows detailed source “About” page when adding 3rd-party sources

Allows users to preview sources before adding them to their AltStore.
This commit is contained in:
Riley Testut
2023-04-04 15:41:44 -05:00
committed by Magesh K
parent 5145e355ce
commit 654f73f4ee
12 changed files with 1718 additions and 31 deletions

View File

@@ -0,0 +1,341 @@
//
// SourcesDetailContentViewController.swift
// AltStore
//
// Created by Riley Testut on 3/8/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
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()
self.view.tintColor = self.source.effectiveTintColor
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
}
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 }
let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(50)) // Underestimate height to prevent jumping size abruptly.
let item = NSCollectionLayoutItem(layoutSize: itemSize)
let groupWidth = layoutEnvironment.container.contentSize.width - sectionInset * 2
let groupSize = NSCollectionLayoutSize(widthDimension: .absolute(groupWidth), heightDimension: .estimated(50))
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" }
dataSource.predicate = NSPredicate(format: "%K == NO", #keyPath(StoreApp.isBeta)) // Never show beta apps (at least until we support betas for other sources).
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
cell.bannerView.configure(for: storeApp)
cell.bannerView.iconImageView.isIndicatingActivity = true
cell.bannerView.buttonLabel.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
cell.bannerView.button.tintColor = storeApp.tintColor
let buttonTitle = NSLocalizedString("Free", comment: "")
cell.bannerView.button.setTitle(buttonTitle.uppercased(), for: .normal)
cell.bannerView.button.accessibilityLabel = String(format: NSLocalizedString("Download %@", comment: ""), storeApp.name)
cell.bannerView.button.accessibilityValue = buttonTitle
let progress = AppManager.shared.installationProgress(for: storeApp)
cell.bannerView.button.progress = progress
if let versionDate = storeApp.latestSupportedVersion?.date, versionDate > Date()
{
cell.bannerView.button.countdownDate = versionDate
}
else
{
cell.bannerView.button.countdownDate = nil
}
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
if let progress = AppManager.shared.installationProgress(for: storeApp), progress.fractionCompleted < 1.0
{
cell.bannerView.button.progress = progress
}
else
{
cell.bannerView.button.progress = nil
}
}
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)
}
}
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppBannerCollectionViewCell
cell.bannerView.iconImageView.image = image
cell.bannerView.iconImageView.isIndicatingActivity = false
if let error
{
print("[ALTLog] Error loading source icon:", error)
}
}
return dataSource
}
func makeAboutDataSource() -> RSTDynamicCollectionViewDataSource<NSManagedObject>
{
let dataSource = RSTDynamicCollectionViewDataSource<NSManagedObject>()
dataSource.numberOfSectionsHandler = { 1 }
dataSource.numberOfItemsHandler = { _ in self.source.localizedDescription == nil ? 0 : 1 }
dataSource.cellIdentifierHandler = { _ in "AboutCell" }
dataSource.cellConfigurationHandler = { [weak self] (cell, _, indexPath) in
let cell = cell as! TextViewCollectionViewCell
cell.contentView.layoutMargins = .zero // Fixes incorrect margins if not initially on screen.
cell.textView.text = self?.source.localizedDescription
cell.textView.isCollapsed = false
}
return dataSource
}
}
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)
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)
case (.about, _):
let titleView = supplementaryView as! TitleCollectionReusableView
titleView.label.text = NSLocalizedString("About", comment: "")
}
return supplementaryView
}
}
extension SourceDetailContentViewController: ScrollableContentViewController
{
var scrollView: UIScrollView { self.collectionView }
}

View File

@@ -0,0 +1,108 @@
//
// SourceDetailViewController.swift
// AltStore
//
// Created by Riley Testut on 3/15/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
class SourceDetailViewController: HeaderContentViewController<SourceHeaderView, SourceDetailContentViewController>
{
@Managed private(set) var source: Source
private var addButton: VibrantButton!
private var previousBounds: CGRect?
init?(source: Source, coder: NSCoder)
{
self.source = source
super.init(coder: coder)
self.title = source.name
self.tintColor = source.effectiveTintColor
}
required init?(coder: NSCoder)
{
fatalError("init(coder:) has not been implemented")
}
override func viewDidLoad()
{
super.viewDidLoad()
self.addButton = VibrantButton(type: .system)
self.addButton.title = NSLocalizedString("ADD", comment: "")
self.addButton.contentInsets = PillButton.contentInsets
self.addButton.sizeToFit()
self.view.addSubview(self.addButton)
Nuke.loadImage(with: self.source.effectiveIconURL, into: self.navigationBarIconView)
Nuke.loadImage(with: self.source.effectiveHeaderImageURL, into: self.backgroundImageView)
self.update()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
self.addButton.layer.cornerRadius = self.addButton.bounds.midY
self.navigationBarIconView.layer.cornerRadius = self.navigationBarIconView.bounds.midY
var addButtonSize = self.addButton.sizeThatFits(CGSize(width: Double.infinity, height: .infinity))
addButtonSize.width = max(addButtonSize.width, PillButton.minimumSize.width)
addButtonSize.height = max(addButtonSize.height, PillButton.minimumSize.height)
self.addButton.frame.size = addButtonSize
// Place in top-right corner.
let inset = 15.0
self.addButton.center.y = self.backButton.center.y
self.addButton.frame.origin.x = self.view.bounds.width - inset - self.addButton.bounds.width
guard self.view.bounds != self.previousBounds else { return }
self.previousBounds = self.view.bounds
let headerSize = self.headerView.systemLayoutSizeFitting(CGSize(width: self.view.bounds.width - inset * 2, height: UIView.layoutFittingCompressedSize.height))
self.headerView.frame.size.height = headerSize.height
}
//MARK: Override
override func makeContentViewController() -> SourceDetailContentViewController
{
guard let storyboard = self.storyboard else { fatalError("SourceDetailViewController must be initialized via UIStoryboard.") }
let contentViewController = storyboard.instantiateViewController(identifier: "sourceDetailContentViewController") { coder in
SourceDetailContentViewController(source: self.source, coder: coder)
}
return contentViewController
}
override func makeHeaderView() -> SourceHeaderView
{
let sourceAboutView = SourceHeaderView(frame: CGRect(x: 0, y: 0, width: 375, height: 200))
sourceAboutView.configure(for: self.source)
return sourceAboutView
}
override func update()
{
super.update()
if self.source.identifier == Source.altStoreIdentifier
{
// Users can't remove default AltStore source, so hide buttons.
self.addButton.isHidden = true
self.navigationBarButton.isHidden = true
}
}
}

View File

@@ -0,0 +1,89 @@
//
// SourceDetailsComponents.swift
// AltStore
//
// Created by Riley Testut on 3/16/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import Roxas
class TitleCollectionReusableView: UICollectionReusableView
{
let label: UILabel
override init(frame: CGRect)
{
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .largeTitle).withSymbolicTraits(.traitBold)!
let font = UIFont(descriptor: fontDescriptor, size: 0.0)
self.label = UILabel(frame: .zero)
self.label.font = font
super.init(frame: frame)
self.addSubview(self.label, pinningEdgesWith: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class ButtonCollectionReusableView: UICollectionReusableView
{
let button: UIButton
override init(frame: CGRect)
{
self.button = UIButton(type: .system)
self.button.translatesAutoresizingMaskIntoConstraints = false
super.init(frame: frame)
self.addSubview(self.button, pinningEdgesWith: .zero)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
class TextViewCollectionViewCell: UICollectionViewCell
{
let textView = CollapsingTextView(frame: .zero)
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
self.textView.font = UIFont.preferredFont(forTextStyle: .body)
self.textView.isScrollEnabled = false
self.textView.isEditable = false
self.textView.isSelectable = true
self.textView.dataDetectorTypes = [.link]
self.contentView.addSubview(self.textView, pinningEdgesWith: .zero)
}
override func layoutMarginsDidChange()
{
super.layoutMarginsDidChange()
self.textView.textContainerInset.left = self.contentView.layoutMargins.left
self.textView.textContainerInset.right = self.contentView.layoutMargins.right
}
}

View File

@@ -0,0 +1,106 @@
//
// SourceHeaderView.swift
// AltStore
//
// Created by Riley Testut on 3/9/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
import AltStoreCore
import Roxas
import Nuke
class SourceHeaderView: RSTNibView
{
@IBOutlet private(set) var titleLabel: UILabel!
@IBOutlet private(set) var subtitleLabel: UILabel!
@IBOutlet private(set) var iconImageView: UIImageView!
@IBOutlet private(set) var websiteButton: UIButton!
@IBOutlet private var websiteContentView: UIView!
@IBOutlet private var websiteButtonContainerView: UIView!
@IBOutlet private var websiteImageView: UIImageView!
@IBOutlet private var widthConstraint: NSLayoutConstraint!
override init(frame: CGRect)
{
super.init(frame: frame)
self.initialize()
}
required init?(coder: NSCoder)
{
super.init(coder: coder)
self.initialize()
}
private func initialize()
{
self.clipsToBounds = true
self.layer.cornerRadius = 22
self.iconImageView.clipsToBounds = true
let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).withSymbolicTraits(.traitBold)!
let titleFont = UIFont(descriptor: fontDescriptor, size: 0.0)
self.titleLabel.font = titleFont
self.websiteButton.setTitle(nil, for: .normal)
self.websiteButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .subheadline)
let imageConfiguration = UIImage.SymbolConfiguration(scale: .medium)
let websiteImage = UIImage(systemName: "link", withConfiguration: imageConfiguration)
self.websiteImageView.image = websiteImage
self.websiteButtonContainerView.clipsToBounds = true
self.websiteButtonContainerView.layer.cornerRadius = 14 // 22 - inset (8)
}
override func layoutSubviews()
{
super.layoutSubviews()
self.iconImageView.layer.cornerRadius = self.iconImageView.bounds.midY
if let titleLabel = self.websiteButton.titleLabel, self.widthConstraint.constant == 0
{
// Left-align website button text with subtitle by increasing width by label inset.
let frame = self.websiteButton.convert(titleLabel.frame, from: titleLabel.superview)
self.widthConstraint.constant = frame.minX
}
}
}
extension SourceHeaderView
{
func configure(for source: Source)
{
self.titleLabel.text = source.name
self.subtitleLabel.text = source.subtitle
self.websiteImageView.tintColor = source.effectiveTintColor
if let websiteURL = source.websiteURL
{
self.websiteButton.setTitle(websiteURL.absoluteString, for: .normal)
self.websiteContentView.isHidden = false
self.websiteImageView.isHidden = false
}
else
{
self.websiteButton.setTitle(nil, for: .normal)
self.websiteContentView.isHidden = true
self.websiteImageView.isHidden = true
}
Nuke.loadImage(with: source.effectiveIconURL, into: self.iconImageView)
}
}

View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="SourceHeaderView" customModule="AltStore" customModuleProvider="target">
<connections>
<outlet property="iconImageView" destination="rfC-wI-8JY" id="1FW-GN-WDa"/>
<outlet property="subtitleLabel" destination="IUL-qI-QAg" id="RyD-ax-OtJ"/>
<outlet property="titleLabel" destination="FsG-Wm-6xP" id="huW-re-9G0"/>
<outlet property="websiteButton" destination="cDF-t8-8Ri" id="6YC-OT-StI"/>
<outlet property="websiteButtonContainerView" destination="kyG-Ne-9eG" id="eH9-eb-iEe"/>
<outlet property="websiteContentView" destination="lLy-42-4bf" id="sUV-gS-ykd"/>
<outlet property="websiteImageView" destination="Vd9-Y3-Vhc" id="vvT-Wx-o8o"/>
<outlet property="widthConstraint" destination="KPO-2J-5Pt" id="o0i-tJ-88g"/>
</connections>
</placeholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ed2-hy-JkU">
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VTh-Dz-qVQ" userLabel="Blur View">
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="eHD-cD-5y4">
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" distribution="equalSpacing" spacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="Ydv-2n-m56">
<rect key="frame" x="0.0" y="0.0" width="375" height="400"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="hfp-yJ-gcH" userLabel="App Info">
<rect key="frame" x="14" y="14" width="349" height="68"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="rfC-wI-8JY" userLabel="Icon Image View">
<rect key="frame" x="0.0" y="0.0" width="68" height="68"/>
<constraints>
<constraint firstAttribute="width" secondItem="rfC-wI-8JY" secondAttribute="height" multiplier="1:1" id="Pec-Vt-SX1"/>
<constraint firstAttribute="height" constant="68" id="enw-jt-m0C"/>
</constraints>
</imageView>
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="4" translatesAutoresizingMaskIntoConstraints="NO" id="a5P-5y-U64" userLabel="Labels Stack View">
<rect key="frame" x="79" y="10.000000000000004" width="270" height="48.333333333333343"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="Source Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="FsG-Wm-6xP">
<rect key="frame" x="0.0" y="0.0" width="128.66666666666666" height="26.333333333333332"/>
<accessibility key="accessibilityConfiguration" identifier="NameLabel"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleTitle2"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="7WA-03-aKF" userLabel="Vibrancy View">
<rect key="frame" x="0.0" y="30.333333333333336" width="95" height="18"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="ie4-Od-obh">
<rect key="frame" x="0.0" y="0.0" width="95" height="18"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="750" text="Subtitle" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="2" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="IUL-qI-QAg">
<rect key="frame" x="0.0" y="0.0" width="95" height="18"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstItem="IUL-qI-QAg" firstAttribute="top" secondItem="ie4-Od-obh" secondAttribute="top" id="3YV-ax-2UB"/>
<constraint firstAttribute="trailing" secondItem="IUL-qI-QAg" secondAttribute="trailing" id="PFG-Oe-76p"/>
<constraint firstAttribute="bottom" secondItem="IUL-qI-QAg" secondAttribute="bottom" id="Q0z-A4-UhM"/>
<constraint firstItem="IUL-qI-QAg" firstAttribute="leading" secondItem="ie4-Od-obh" secondAttribute="leading" id="qjX-ro-UD6"/>
</constraints>
</view>
<vibrancyEffect style="secondaryLabel">
<blurEffect style="systemThinMaterial"/>
</vibrancyEffect>
</visualEffectView>
</subviews>
</stackView>
</subviews>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="lLy-42-4bf" userLabel="Website">
<rect key="frame" x="14" y="336" width="349" height="50"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="bl1-oP-fLT" userLabel="Spacer View">
<rect key="frame" x="0.0" y="0.0" width="349" height="50"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="center" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="44" placeholderIntrinsicHeight="44" translatesAutoresizingMaskIntoConstraints="NO" id="Vd9-Y3-Vhc">
<rect key="frame" x="12" y="3" width="44" height="44"/>
</imageView>
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kyG-Ne-9eG" userLabel="Website Button Container View">
<rect key="frame" x="79" y="0.0" width="270" height="50"/>
<subviews>
<visualEffectView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c6g-CV-IeK" userLabel="Vibrancy View">
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="zhu-r0-cL8">
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="1lg-Ki-MOK">
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
<color key="backgroundColor" white="1" alpha="0.20000000000000001" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title=" "/>
<buttonConfiguration key="configuration" style="plain" title=" "/>
</button>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="1lg-Ki-MOK" secondAttribute="bottom" id="2Xf-5H-MQm"/>
<constraint firstItem="1lg-Ki-MOK" firstAttribute="top" secondItem="zhu-r0-cL8" secondAttribute="top" id="PIj-oh-hQg"/>
<constraint firstAttribute="trailing" secondItem="1lg-Ki-MOK" secondAttribute="trailing" id="f2H-jO-d5v"/>
<constraint firstItem="1lg-Ki-MOK" firstAttribute="leading" secondItem="zhu-r0-cL8" secondAttribute="leading" id="zdh-qf-8ow"/>
</constraints>
</view>
<vibrancyEffect style="secondaryFill">
<blurEffect style="systemThinMaterial"/>
</vibrancyEffect>
</visualEffectView>
<visualEffectView opaque="NO" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Sgm-uO-rMs" userLabel="Vibrancy View">
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="6e6-8c-O5P">
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="cDF-t8-8Ri">
<rect key="frame" x="0.0" y="0.0" width="270" height="50"/>
<inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/>
<state key="normal" title="https://rileytestut.com"/>
<buttonConfiguration key="configuration" style="plain" title="https://rileytestut.com"/>
</button>
</subviews>
<constraints>
<constraint firstItem="cDF-t8-8Ri" firstAttribute="leading" secondItem="6e6-8c-O5P" secondAttribute="leading" id="9pW-be-mEO"/>
<constraint firstAttribute="bottom" secondItem="cDF-t8-8Ri" secondAttribute="bottom" id="9zM-Hq-5ea"/>
<constraint firstItem="cDF-t8-8Ri" firstAttribute="top" secondItem="6e6-8c-O5P" secondAttribute="top" id="RW7-Qu-Afy"/>
<constraint firstAttribute="trailing" secondItem="cDF-t8-8Ri" secondAttribute="trailing" id="sln-aP-Czq"/>
</constraints>
</view>
<vibrancyEffect style="fill">
<blurEffect style="systemThinMaterial"/>
</vibrancyEffect>
</visualEffectView>
</subviews>
<constraints>
<constraint firstAttribute="trailing" secondItem="c6g-CV-IeK" secondAttribute="trailing" id="2Db-8d-4ta"/>
<constraint firstAttribute="bottom" secondItem="Sgm-uO-rMs" secondAttribute="bottom" id="CuL-oW-Tka"/>
<constraint firstItem="c6g-CV-IeK" firstAttribute="leading" secondItem="kyG-Ne-9eG" secondAttribute="leading" id="Wey-l1-VIN"/>
<constraint firstItem="Sgm-uO-rMs" firstAttribute="leading" secondItem="kyG-Ne-9eG" secondAttribute="leading" id="Zca-A9-Z3v"/>
<constraint firstItem="Sgm-uO-rMs" firstAttribute="top" secondItem="kyG-Ne-9eG" secondAttribute="top" id="aLc-Sh-C2g"/>
<constraint firstItem="c6g-CV-IeK" firstAttribute="top" secondItem="kyG-Ne-9eG" secondAttribute="top" id="gL7-rP-JJQ"/>
<constraint firstAttribute="bottom" secondItem="c6g-CV-IeK" secondAttribute="bottom" id="h1Q-x5-7ch"/>
<constraint firstAttribute="trailing" secondItem="Sgm-uO-rMs" secondAttribute="trailing" id="hgG-mP-CiI"/>
</constraints>
</view>
</subviews>
<constraints>
<constraint firstItem="kyG-Ne-9eG" firstAttribute="top" secondItem="bl1-oP-fLT" secondAttribute="top" id="4WU-ym-LUr"/>
<constraint firstAttribute="bottom" secondItem="kyG-Ne-9eG" secondAttribute="bottom" id="9Md-8N-jr9"/>
<constraint firstItem="Vd9-Y3-Vhc" firstAttribute="centerY" secondItem="kyG-Ne-9eG" secondAttribute="centerY" id="kd3-l3-vD7"/>
<constraint firstAttribute="trailing" secondItem="kyG-Ne-9eG" secondAttribute="trailing" id="mCi-EJ-bcd"/>
</constraints>
</view>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstAttribute="trailingMargin" secondItem="hfp-yJ-gcH" secondAttribute="trailing" id="9R7-eh-OeE"/>
<constraint firstItem="kyG-Ne-9eG" firstAttribute="width" secondItem="a5P-5y-U64" secondAttribute="width" id="KPO-2J-5Pt"/>
<constraint firstItem="Vd9-Y3-Vhc" firstAttribute="centerX" secondItem="rfC-wI-8JY" secondAttribute="centerX" id="TtS-ai-grE"/>
<constraint firstAttribute="leadingMargin" secondItem="hfp-yJ-gcH" secondAttribute="leading" id="YPF-kW-MKJ"/>
</constraints>
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
</stackView>
</subviews>
<color key="backgroundColor" name="BlurTint"/>
<constraints>
<constraint firstItem="Ydv-2n-m56" firstAttribute="leading" secondItem="eHD-cD-5y4" secondAttribute="leading" id="FdZ-7R-70D"/>
<constraint firstItem="Ydv-2n-m56" firstAttribute="top" secondItem="eHD-cD-5y4" secondAttribute="top" id="JtX-k9-9A3"/>
<constraint firstAttribute="bottom" secondItem="Ydv-2n-m56" secondAttribute="bottom" id="X2R-Ab-m7o"/>
<constraint firstAttribute="trailing" secondItem="Ydv-2n-m56" secondAttribute="trailing" id="z0g-5Q-eso"/>
</constraints>
</view>
<blurEffect style="systemThinMaterial"/>
</visualEffectView>
</subviews>
<viewLayoutGuide key="safeArea" id="jw2-Uc-PZa"/>
<constraints>
<constraint firstItem="VTh-Dz-qVQ" firstAttribute="top" secondItem="ed2-hy-JkU" secondAttribute="top" id="2oR-Up-r2e"/>
<constraint firstAttribute="bottom" secondItem="VTh-Dz-qVQ" secondAttribute="bottom" id="4bb-e5-tea"/>
<constraint firstItem="VTh-Dz-qVQ" firstAttribute="leading" secondItem="ed2-hy-JkU" secondAttribute="leading" id="aZg-21-M6I"/>
<constraint firstAttribute="trailing" secondItem="VTh-Dz-qVQ" secondAttribute="trailing" id="o4u-HT-CZ6"/>
</constraints>
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
<point key="canvasLocation" x="266" y="135"/>
</view>
</objects>
<resources>
<namedColor name="BlurTint">
<color red="1" green="1" blue="1" alpha="0.30000001192092896" colorSpace="custom" customColorSpace="sRGB"/>
</namedColor>
</resources>
</document>

View File

@@ -4,7 +4,9 @@
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -127,12 +129,73 @@
</connections>
</barButtonItem>
</navigationItem>
<connections>
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="dLj-Tf-ZjV"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="69z-hg-xF8" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="wBZ-c2-miy" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="810" y="-13"/>
</scene>
<!--Source Detail View Controller-->
<scene sceneID="xbN-Mz-TtU">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="cYc-NX-nF1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<viewController storyboardIdentifier="sourceDetailViewController" id="7XE-Wv-lf9" customClass="SourceDetailViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eIv-0H-ZIq">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<viewLayoutGuide key="safeArea" id="GND-ro-Anp"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<navigationItem key="navigationItem" id="Ocv-bj-TfG"/>
</viewController>
</objects>
<point key="canvasLocation" x="1646.5648854961833" y="-13.380281690140846"/>
</scene>
<!--Source Detail Content View Controller-->
<scene sceneID="8nl-ly-jhT">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="KEz-hK-u3f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<collectionViewController storyboardIdentifier="sourceDetailContentViewController" id="MSh-hM-32I" customClass="SourceDetailContentViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="fiF-YD-Ing">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" automaticEstimatedItemSize="YES" minimumLineSpacing="10" minimumInteritemSpacing="10" id="evc-Tb-ofk">
<size key="itemSize" width="128" height="128"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="AppCell" id="ioR-1o-Qe1" customClass="AppBannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="Bda-YQ-Gv5">
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask"/>
</collectionViewCellContentView>
</collectionViewCell>
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" reuseIdentifier="AboutCell" id="Bnj-xm-pBT" customClass="TextViewCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="265" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<collectionViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="lXN-gL-rhU">
<rect key="frame" x="0.0" y="0.0" width="128" height="128"/>
<autoresizingMask key="autoresizingMask"/>
</collectionViewCellContentView>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="MSh-hM-32I" id="iWe-66-9HQ"/>
<outlet property="delegate" destination="MSh-hM-32I" id="8SG-5v-iF2"/>
</connections>
</collectionView>
</collectionViewController>
</objects>
<point key="canvasLocation" x="2509" y="-13"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">

View File

@@ -199,6 +199,15 @@ private extension SourcesViewController
let dataSource = RSTArrayCollectionViewDataSource<Source>(items: [])
return dataSource
}
@IBSegueAction
func makeSourceDetailViewController(_ coder: NSCoder, sender: Any?) -> UIViewController?
{
guard let source = sender as? Source else { return nil }
let sourceDetailViewController = SourceDetailViewController(source: source, coder: coder)
return sourceDetailViewController
}
}
private extension SourcesViewController
@@ -229,7 +238,7 @@ private extension SourcesViewController
self.present(alertController, animated: true, completion: nil)
}
func addSource(url: URL, isTrusted: Bool = false, completionHandler: ((Result<Void, Error>) -> Void)? = nil)
func addSource(url: URL, completionHandler: ((Result<Void, Error>) -> Void)? = nil)
{
guard self.view.window != nil else { return }
@@ -262,6 +271,7 @@ private extension SourcesViewController
}
}
//TODO: Remove this now that trusted sources aren't necessary.
var dependencies: [Foundation.Operation] = []
if let fetchTrustedSourcesOperation = self.fetchTrustedSourcesOperation
{
@@ -273,34 +283,13 @@ private extension SourcesViewController
AppManager.shared.fetchSource(sourceURL: url, dependencies: dependencies) { (result) in
do
{
let source = try result.get()
let sourceName = source.name
let managedObjectContext = source.managedObjectContext
// Hide warning when adding a featured trusted source.
let message = isTrusted ? nil : NSLocalizedString("Make sure to only add sources that you trust.", comment: "")
@Managed var source = try result.get()
DispatchQueue.main.async {
let alertController = UIAlertController(title: String(format: NSLocalizedString("Would you like to add the source “%@”?", comment: ""), sourceName),
message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style) { _ in
finish(.failure(OperationError.cancelled))
})
alertController.addAction(UIAlertAction(title: NSLocalizedString("Add Source", comment: ""), style: UIAlertAction.ok.style) { _ in
managedObjectContext?.perform {
do
{
try managedObjectContext?.save()
finish(.success(()))
}
catch
{
finish(.failure(error))
}
}
})
self.present(alertController, animated: true, completion: nil)
self.showSourceDetails(for: source)
}
finish(.success(()))
}
catch
{
@@ -416,7 +405,7 @@ private extension SourcesViewController
sender.progress = completedProgress
let source = self.dataSource.item(at: indexPath)
self.addSource(url: source.sourceURL, isTrusted: true) { _ in
self.addSource(url: source.sourceURL) { _ in
//FIXME: Handle cell reuse.
sender.progress = nil
}
@@ -451,6 +440,11 @@ private extension SourcesViewController
self.present(alertController, animated: true, completion: nil)
}
func showSourceDetails(for source: Source)
{
self.performSegue(withIdentifier: "showSourceDetails", sender: source)
}
}
extension SourcesViewController
@@ -460,9 +454,7 @@ extension SourcesViewController
self.collectionView.deselectItem(at: indexPath, animated: true)
let source = self.dataSource.item(at: indexPath)
guard let error = source.error else { return }
self.present(error)
self.showSourceDetails(for: source)
}
}
@@ -613,7 +605,7 @@ extension SourcesViewController
}
let addAction = UIAction(title: String(format: NSLocalizedString("Add “%@”", comment: ""), source.name), image: UIImage(systemName: "plus")) { (action) in
self.addSource(url: source.sourceURL, isTrusted: true)
self.addSource(url: source.sourceURL)
}
var actions: [UIAction] = []