2020-03-24 13:27:44 -07:00
//
// S o u r c e s V i e w C o n t r o l l e r . s w i f t
// A l t S t o r e
//
// C r e a t e d b y R i l e y T e s t u t o n 3 / 1 7 / 2 0 .
// C o p y r i g h t © 2 0 2 0 R i l e y T e s t u t . A l l r i g h t s r e s e r v e d .
//
import UIKit
import CoreData
2020-09-03 16:39:08 -07:00
import AltStoreCore
2020-03-24 13:27:44 -07:00
import Roxas
2020-10-05 14:48:48 -07:00
struct SourceError : LocalizedError
{
enum Code
{
case unsupported
}
var code : Code
@ Managed var source : Source
var errorDescription : String ? {
switch self . code
{
case . unsupported : return String ( format : NSLocalizedString ( " The source “%@” is not supported by this version of AltStore. " , comment : " " ) , self . $ source . name )
}
}
}
2022-04-14 16:24:11 -07:00
@objc ( SourcesFooterView )
private class SourcesFooterView : TextCollectionReusableView
{
@IBOutlet var activityIndicatorView : UIActivityIndicatorView !
@IBOutlet var textView : UITextView !
}
extension SourcesViewController
{
private enum Section : Int , CaseIterable
{
case added
case trusted
}
}
2020-03-24 13:27:44 -07:00
class SourcesViewController : UICollectionViewController
{
2020-08-28 12:15:15 -07:00
var deepLinkSourceURL : URL ? {
didSet {
guard let sourceURL = self . deepLinkSourceURL else { return }
self . addSource ( url : sourceURL )
}
}
2020-03-24 13:27:44 -07:00
private lazy var dataSource = self . makeDataSource ( )
2022-04-14 16:24:11 -07:00
private lazy var addedSourcesDataSource = self . makeAddedSourcesDataSource ( )
private lazy var trustedSourcesDataSource = self . makeTrustedSourcesDataSource ( )
private var fetchTrustedSourcesOperation : FetchTrustedSourcesOperation ?
private var fetchTrustedSourcesResult : Result < Void , Error > ?
private var _fetchTrustedSourcesContext : NSManagedObjectContext ?
2020-03-24 13:27:44 -07:00
override func viewDidLoad ( )
{
super . viewDidLoad ( )
self . collectionView . dataSource = self . dataSource
2020-10-05 14:48:48 -07:00
#if ! BETA
// H i d e " A d d S o u r c e " b u t t o n f o r p u b l i c v e r s i o n w h i l e i n b e t a .
self . navigationItem . leftBarButtonItem = nil
#endif
2020-03-24 13:27:44 -07:00
}
2020-08-28 12:15:15 -07:00
override func viewWillAppear ( _ animated : Bool )
{
super . viewWillAppear ( animated )
if self . deepLinkSourceURL != nil
{
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = true
}
2022-04-14 16:24:11 -07:00
if self . fetchTrustedSourcesOperation = = nil
{
self . fetchTrustedSources ( )
}
2020-08-28 12:15:15 -07:00
}
override func viewDidAppear ( _ animated : Bool )
{
super . viewDidAppear ( animated )
if let sourceURL = self . deepLinkSourceURL
{
self . addSource ( url : sourceURL )
}
}
2020-03-24 13:27:44 -07:00
}
private extension SourcesViewController
{
2022-04-14 16:24:11 -07:00
func makeDataSource ( ) -> RSTCompositeCollectionViewDataSource < Source >
2020-03-24 13:27:44 -07:00
{
2022-04-14 16:24:11 -07:00
let dataSource = RSTCompositeCollectionViewDataSource < Source > ( dataSources : [ self . addedSourcesDataSource , self . trustedSourcesDataSource ] )
2020-03-24 13:27:44 -07:00
dataSource . proxy = self
dataSource . cellConfigurationHandler = { ( cell , source , indexPath ) in
let tintColor = UIColor . altPrimary
let cell = cell as ! BannerCollectionViewCell
cell . layoutMargins . left = self . view . layoutMargins . left
cell . layoutMargins . right = self . view . layoutMargins . right
cell . tintColor = tintColor
cell . bannerView . iconImageView . isHidden = true
cell . bannerView . buttonLabel . isHidden = true
cell . bannerView . button . isIndicatingActivity = false
2022-04-14 16:24:11 -07:00
switch Section . allCases [ indexPath . section ]
{
case . added :
cell . bannerView . button . isHidden = true
case . trusted :
// Q u i c k e r w a y t o d e t e r m i n e w h e t h e r a s o u r c e i s a l r e a d y a d d e d t h a n b y r e a d i n g f r o m d i s k .
if ( self . addedSourcesDataSource . fetchedResultsController . fetchedObjects ? ? [ ] ) . contains ( where : { $0 . identifier = = source . identifier } )
{
// S o u r c e e x i s t s i n . a d d e d s e c t i o n , s o h i d e t h e b u t t o n .
cell . bannerView . button . isHidden = true
if #available ( iOS 13.0 , * )
{
let configuation = UIImage . SymbolConfiguration ( pointSize : 24 )
let imageAttachment = NSTextAttachment ( )
imageAttachment . image = UIImage ( systemName : " checkmark.circle " , withConfiguration : configuation ) ? . withTintColor ( . altPrimary )
let attributedText = NSAttributedString ( attachment : imageAttachment )
cell . bannerView . buttonLabel . attributedText = attributedText
cell . bannerView . buttonLabel . textAlignment = . center
cell . bannerView . buttonLabel . isHidden = false
}
}
else
{
// S o u r c e d o e s n o t e x i s t i n . a d d e d s e c t i o n , s o s h o w t h e b u t t o n .
cell . bannerView . button . isHidden = false
cell . bannerView . buttonLabel . attributedText = nil
}
cell . bannerView . button . setTitle ( NSLocalizedString ( " ADD " , comment : " " ) , for : . normal )
cell . bannerView . button . addTarget ( self , action : #selector ( SourcesViewController . addTrustedSource ( _ : ) ) , for : . primaryActionTriggered )
}
2020-03-24 13:27:44 -07:00
cell . bannerView . titleLabel . text = source . name
cell . bannerView . subtitleLabel . text = source . sourceURL . absoluteString
cell . bannerView . subtitleLabel . numberOfLines = 2
2020-08-27 16:39:03 -07:00
cell . errorBadge ? . isHidden = ( source . error = = nil )
2020-08-27 15:23:21 -07:00
let attributedLabel = NSAttributedString ( string : source . name + " \n " + source . sourceURL . absoluteString , attributes : [ . accessibilitySpeechPunctuation : true ] )
cell . bannerView . accessibilityAttributedLabel = attributedLabel
cell . bannerView . accessibilityTraits . remove ( . button )
2020-03-24 13:27:44 -07:00
// M a k e s u r e r e f r e s h b u t t o n i s c o r r e c t s i z e .
cell . layoutIfNeeded ( )
}
return dataSource
}
2022-04-14 16:24:11 -07:00
func makeAddedSourcesDataSource ( ) -> RSTFetchedResultsCollectionViewDataSource < Source >
{
let fetchRequest = Source . fetchRequest ( ) as NSFetchRequest < Source >
fetchRequest . returnsObjectsAsFaults = false
fetchRequest . sortDescriptors = [ NSSortDescriptor ( keyPath : \ Source . name , ascending : true ) ,
NSSortDescriptor ( keyPath : \ Source . sourceURL , ascending : true ) ,
NSSortDescriptor ( keyPath : \ Source . identifier , ascending : true ) ]
let dataSource = RSTFetchedResultsCollectionViewDataSource < Source > ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext )
return dataSource
}
func makeTrustedSourcesDataSource ( ) -> RSTArrayCollectionViewDataSource < Source >
{
let dataSource = RSTArrayCollectionViewDataSource < Source > ( items : [ ] )
return dataSource
}
2020-03-24 13:27:44 -07:00
}
private extension SourcesViewController
{
@IBAction func addSource ( )
{
let alertController = UIAlertController ( title : NSLocalizedString ( " Add Source " , comment : " " ) , message : nil , preferredStyle : . alert )
alertController . addTextField { ( textField ) in
textField . placeholder = " https://apps.altstore.io "
textField . textContentType = . URL
}
alertController . addAction ( . cancel )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Add " , comment : " " ) , style : . default ) { ( action ) in
2020-09-27 13:56:54 -07:00
guard let text = alertController . textFields ! [ 0 ] . text else { return }
guard var sourceURL = URL ( string : text ) else { return }
if sourceURL . scheme = = nil {
guard let httpsSourceURL = URL ( string : " https:// " + text ) else { return }
sourceURL = httpsSourceURL
}
2022-04-14 16:24:11 -07:00
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = true
self . addSource ( url : sourceURL ) { _ in
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = false
}
2020-03-24 13:27:44 -07:00
} )
self . present ( alertController , animated : true , completion : nil )
}
2022-04-14 16:24:11 -07:00
func addSource ( url : URL , isTrusted : Bool = false , completionHandler : ( ( Result < Void , Error > ) -> Void ) ? = nil )
2020-08-28 12:15:15 -07:00
{
guard self . view . window != nil else { return }
2022-04-14 16:24:11 -07:00
if url = = self . deepLinkSourceURL
{
// O n l y h a n d l e d e e p l i n k o n c e .
self . deepLinkSourceURL = nil
}
2020-08-28 12:15:15 -07:00
2022-04-14 16:24:11 -07:00
func finish ( _ result : Result < Void , Error > )
2020-08-28 12:15:15 -07:00
{
DispatchQueue . main . async {
2022-04-14 16:24:11 -07:00
switch result
2020-08-28 12:15:15 -07:00
{
2022-04-14 16:24:11 -07:00
case . success : break
case . failure ( OperationError . cancelled ) : break
case . failure ( let error ) : self . present ( error )
2020-08-28 12:15:15 -07:00
}
2022-04-14 16:24:11 -07:00
self . collectionView . reloadSections ( [ Section . trusted . rawValue ] )
completionHandler ? ( result )
2020-08-28 12:15:15 -07:00
}
}
2022-04-14 16:24:11 -07:00
var dependencies : [ Foundation . Operation ] = [ ]
if let fetchTrustedSourcesOperation = self . fetchTrustedSourcesOperation
{
// M u s t f e t c h t r u s t e d s o u r c e s f i r s t t o d e t e r m i n e w h e t h e r t h i s i s a t r u s t e d s o u r c e .
// W e a s s u m e f e t c h T r u s t e d S o u r c e s ( ) h a s a l r e a d y b e e n c a l l e d b e f o r e t h i s m e t h o d .
dependencies = [ fetchTrustedSourcesOperation ]
}
AppManager . shared . fetchSource ( sourceURL : url , dependencies : dependencies ) { ( result ) in
2020-08-28 12:15:15 -07:00
do
{
let source = try result . get ( )
let sourceName = source . name
let managedObjectContext = source . managedObjectContext
2020-10-05 14:48:48 -07:00
#if ! BETA
2022-04-14 16:24:11 -07:00
guard let trustedSourceIDs = UserDefaults . shared . trustedSourceIDs , trustedSourceIDs . contains ( source . identifier ) else { throw SourceError ( code : . unsupported , source : source ) }
2020-10-05 14:48:48 -07:00
#endif
2022-04-14 16:24:11 -07:00
// H i d e w a r n i n g w h e n a d d i n g a f e a t u r e d t r u s t e d s o u r c e .
let message = isTrusted ? nil : NSLocalizedString ( " Make sure to only add sources that you trust. " , comment : " " )
2020-08-28 12:15:15 -07:00
DispatchQueue . main . async {
let alertController = UIAlertController ( title : String ( format : NSLocalizedString ( " Would you like to add the source “%@”? " , comment : " " ) , sourceName ) ,
2022-04-14 16:24:11 -07:00
message : message , preferredStyle : . alert )
2020-08-28 12:15:15 -07:00
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style ) { _ in
2022-04-14 16:24:11 -07:00
finish ( . failure ( OperationError . cancelled ) )
2020-08-28 12:15:15 -07:00
} )
2022-04-14 16:24:11 -07:00
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Add Source " , comment : " " ) , style : UIAlertAction . ok . style ) { _ in
2020-08-28 12:15:15 -07:00
managedObjectContext ? . perform {
do
{
try managedObjectContext ? . save ( )
2022-04-14 16:24:11 -07:00
finish ( . success ( ( ) ) )
2020-08-28 12:15:15 -07:00
}
catch
{
2022-04-14 16:24:11 -07:00
finish ( . failure ( error ) )
2020-08-28 12:15:15 -07:00
}
}
} )
self . present ( alertController , animated : true , completion : nil )
}
}
catch
{
2022-04-14 16:24:11 -07:00
finish ( . failure ( error ) )
2020-08-28 12:15:15 -07:00
}
}
}
2022-04-14 16:24:11 -07:00
2020-08-27 16:39:03 -07:00
func present ( _ error : Error )
{
2020-08-28 12:15:15 -07:00
if let transitionCoordinator = self . transitionCoordinator
{
transitionCoordinator . animate ( alongsideTransition : nil ) { _ in
self . present ( error )
}
return
}
2020-08-27 16:39:03 -07:00
let nsError = error as NSError
let message = nsError . userInfo [ NSDebugDescriptionErrorKey ] as ? String ? ? nsError . localizedRecoverySuggestion
let alertController = UIAlertController ( title : error . localizedDescription , message : message , preferredStyle : . alert )
alertController . addAction ( . ok )
self . present ( alertController , animated : true , completion : nil )
}
2022-04-14 16:24:11 -07:00
func fetchTrustedSources ( )
{
func finish ( _ result : Result < [ Source ] , Error > )
{
self . fetchTrustedSourcesResult = result . map { _ in ( ) }
DispatchQueue . main . async {
do
{
let sources = try result . get ( )
print ( " Fetched trusted sources: " , sources . map { $0 . identifier } )
let sectionUpdate = RSTCellContentChange ( type : . update , sectionIndex : 0 )
self . trustedSourcesDataSource . setItems ( sources , with : [ sectionUpdate ] )
}
catch
{
print ( " Error fetching trusted sources: " , error )
let sectionUpdate = RSTCellContentChange ( type : . update , sectionIndex : 0 )
self . trustedSourcesDataSource . setItems ( [ ] , with : [ sectionUpdate ] )
}
}
}
self . fetchTrustedSourcesOperation = AppManager . shared . fetchTrustedSources { result in
switch result
{
case . failure ( let error ) : finish ( . failure ( error ) )
case . success ( let trustedSources ) :
// C a c h e t r u s t e d s o u r c e I D s .
UserDefaults . shared . trustedSourceIDs = trustedSources . map { $0 . identifier }
// D o n ' t s h o w s o u r c e s w i t h o u t a s o u r c e U R L .
let featuredSourceURLs = trustedSources . compactMap { $0 . sourceURL }
// T h i s c o n t e x t i s n e v e r s a v e d , b u t k e e p s t h e m a n a g e d s o u r c e s a l i v e .
let context = DatabaseManager . shared . persistentContainer . newBackgroundSavingViewContext ( )
self . _fetchTrustedSourcesContext = context
let dispatchGroup = DispatchGroup ( )
var sourcesByURL = [ URL : Source ] ( )
var fetchError : Error ?
for sourceURL in featuredSourceURLs
{
dispatchGroup . enter ( )
AppManager . shared . fetchSource ( sourceURL : sourceURL , managedObjectContext : context ) { result in
// S e r i a l i z e a c c e s s t o s o u r c e s B y U R L .
context . performAndWait {
switch result
{
case . failure ( let error ) : fetchError = error
case . success ( let source ) : sourcesByURL [ source . sourceURL ] = source
}
dispatchGroup . leave ( )
}
}
}
dispatchGroup . notify ( queue : . main ) {
if let error = fetchError
{
finish ( . failure ( error ) )
}
else
{
let sources = featuredSourceURLs . compactMap { sourcesByURL [ $0 ] }
finish ( . success ( sources ) )
}
}
}
}
}
@IBAction func addTrustedSource ( _ sender : PillButton )
{
let point = self . collectionView . convert ( sender . center , from : sender . superview )
guard let indexPath = self . collectionView . indexPathForItem ( at : point ) else { return }
let completedProgress = Progress ( totalUnitCount : 1 )
completedProgress . completedUnitCount = 1
sender . progress = completedProgress
let source = self . dataSource . item ( at : indexPath )
self . addSource ( url : source . sourceURL , isTrusted : true ) { _ in
// FIXME: H a n d l e c e l l r e u s e .
sender . progress = nil
}
}
func remove ( _ source : Source )
{
let alertController = UIAlertController ( title : String ( format : NSLocalizedString ( " Are you sure you want to remove the source “%@”? " , comment : " " ) , source . name ) ,
message : NSLocalizedString ( " Any apps you've installed from this source will remain, but they'll no longer receive any app updates. " , comment : " " ) , preferredStyle : . alert )
alertController . addAction ( UIAlertAction ( title : UIAlertAction . cancel . title , style : UIAlertAction . cancel . style , handler : nil ) )
alertController . addAction ( UIAlertAction ( title : NSLocalizedString ( " Remove Source " , comment : " " ) , style : . destructive ) { _ in
DatabaseManager . shared . persistentContainer . performBackgroundTask { ( context ) in
let source = context . object ( with : source . objectID ) as ! Source
context . delete ( source )
do
{
try context . save ( )
DispatchQueue . main . async {
self . collectionView . reloadSections ( [ Section . trusted . rawValue ] )
}
}
catch
{
DispatchQueue . main . async {
self . present ( error )
}
}
}
} )
self . present ( alertController , animated : true , completion : nil )
}
2020-08-27 16:39:03 -07:00
}
extension SourcesViewController
{
override func collectionView ( _ collectionView : UICollectionView , didSelectItemAt indexPath : IndexPath )
{
self . collectionView . deselectItem ( at : indexPath , animated : true )
let source = self . dataSource . item ( at : indexPath )
guard let error = source . error else { return }
self . present ( error )
}
2020-03-24 13:27:44 -07:00
}
extension SourcesViewController : UICollectionViewDelegateFlowLayout
{
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , sizeForItemAt indexPath : IndexPath ) -> CGSize
{
return CGSize ( width : collectionView . bounds . width , height : 80 )
}
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , referenceSizeForHeaderInSection section : Int ) -> CGSize
{
let indexPath = IndexPath ( row : 0 , section : section )
let headerView = self . collectionView ( collectionView , viewForSupplementaryElementOfKind : UICollectionView . elementKindSectionHeader , at : indexPath )
// U s e t h i s v i e w t o c a l c u l a t e t h e o p t i m a l s i z e b a s e d o n t h e c o l l e c t i o n v i e w ' s w i d t h
let size = headerView . systemLayoutSizeFitting ( CGSize ( width : collectionView . frame . width , height : UIView . layoutFittingExpandedSize . height ) ,
withHorizontalFittingPriority : . required , // W i d t h i s f i x e d
verticalFittingPriority : . fittingSizeLevel ) // H e i g h t c a n b e a s l a r g e a s n e e d e d
return size
}
2022-04-14 16:24:11 -07:00
func collectionView ( _ collectionView : UICollectionView , layout collectionViewLayout : UICollectionViewLayout , referenceSizeForFooterInSection section : Int ) -> CGSize
{
guard Section ( rawValue : section ) = = . trusted else { return . zero }
let indexPath = IndexPath ( row : 0 , section : section )
let headerView = self . collectionView ( collectionView , viewForSupplementaryElementOfKind : UICollectionView . elementKindSectionFooter , at : indexPath )
// U s e t h i s v i e w t o c a l c u l a t e t h e o p t i m a l s i z e b a s e d o n t h e c o l l e c t i o n v i e w ' s w i d t h
let size = headerView . systemLayoutSizeFitting ( CGSize ( width : collectionView . frame . width , height : UIView . layoutFittingExpandedSize . height ) ,
withHorizontalFittingPriority : . required , // W i d t h i s f i x e d
verticalFittingPriority : . fittingSizeLevel ) // H e i g h t c a n b e a s l a r g e a s n e e d e d
return size
}
2020-03-24 13:27:44 -07:00
override func collectionView ( _ collectionView : UICollectionView , viewForSupplementaryElementOfKind kind : String , at indexPath : IndexPath ) -> UICollectionReusableView
{
2022-04-14 16:24:11 -07:00
let reuseIdentifier = ( kind = = UICollectionView . elementKindSectionHeader ) ? " Header " : " Footer "
let headerView = collectionView . dequeueReusableSupplementaryView ( ofKind : kind , withReuseIdentifier : reuseIdentifier , for : indexPath ) as ! TextCollectionReusableView
2020-03-24 13:27:44 -07:00
headerView . layoutMargins . left = self . view . layoutMargins . left
headerView . layoutMargins . right = self . view . layoutMargins . right
2022-04-14 16:24:11 -07:00
let almostRequiredPriority = UILayoutPriority ( UILayoutPriority . required . rawValue - 1 ) // C a n ' t b e r e q u i r e d o r e l s e w e c a n ' t s a t i s f y c o n s t r a i n t s w h e n h i d d e n ( s i z e = 0 ) .
headerView . leadingLayoutConstraint ? . priority = almostRequiredPriority
headerView . trailingLayoutConstraint ? . priority = almostRequiredPriority
headerView . topLayoutConstraint ? . priority = almostRequiredPriority
headerView . bottomLayoutConstraint ? . priority = almostRequiredPriority
switch kind
{
case UICollectionView . elementKindSectionHeader :
switch Section . allCases [ indexPath . section ]
{
case . added :
headerView . textLabel . text = NSLocalizedString ( " Sources control what apps are available to download through AltStore. " , comment : " " )
headerView . textLabel . font = UIFont . preferredFont ( forTextStyle : . callout )
headerView . textLabel . textAlignment = . natural
headerView . topLayoutConstraint . constant = 14
headerView . bottomLayoutConstraint . constant = 30
case . trusted :
switch self . fetchTrustedSourcesResult
{
case . failure : headerView . textLabel . text = NSLocalizedString ( " Error Loading Trusted Sources " , comment : " " )
case . success , . none : headerView . textLabel . text = NSLocalizedString ( " Trusted Sources " , comment : " " )
}
let descriptor = UIFontDescriptor . preferredFontDescriptor ( withTextStyle : . callout ) . withSymbolicTraits ( . traitBold ) !
headerView . textLabel . font = UIFont ( descriptor : descriptor , size : 0 )
headerView . textLabel . textAlignment = . center
headerView . topLayoutConstraint . constant = 54
headerView . bottomLayoutConstraint . constant = 15
}
case UICollectionView . elementKindSectionFooter :
let footerView = headerView as ! SourcesFooterView
let font = UIFont . preferredFont ( forTextStyle : . subheadline )
switch self . fetchTrustedSourcesResult
{
case . failure ( let error ) :
footerView . textView . font = font
footerView . textView . text = error . localizedDescription
footerView . activityIndicatorView . stopAnimating ( )
footerView . topLayoutConstraint . constant = 0
footerView . textView . textAlignment = . center
case . success , . none :
footerView . textView . delegate = self
let attributedText = NSMutableAttributedString (
string : NSLocalizedString ( " AltStore has reviewed these sources to make sure they meet our safety standards. \n \n Support for untrusted sources is currently in beta, but you can help test them out by " , comment : " " ) ,
attributes : [ . font : font , . foregroundColor : UIColor . gray ]
)
attributedText . mutableString . append ( " " )
let boldedFont = UIFont ( descriptor : font . fontDescriptor . withSymbolicTraits ( . traitBold ) ! , size : font . pointSize )
let openPatreonURL = URL ( string : " https://altstore.io/patreon " ) !
let joinPatreonText = NSAttributedString (
string : NSLocalizedString ( " joining our Patreon. " , comment : " " ) ,
attributes : [ . font : boldedFont , . link : openPatreonURL , . underlineColor : UIColor . clear ]
)
attributedText . append ( joinPatreonText )
footerView . textView . attributedText = attributedText
footerView . textView . textAlignment = . natural
if self . fetchTrustedSourcesResult != nil
{
footerView . activityIndicatorView . stopAnimating ( )
footerView . topLayoutConstraint . constant = 20
}
else
{
footerView . activityIndicatorView . startAnimating ( )
footerView . topLayoutConstraint . constant = 0
}
}
default : break
}
2020-03-24 13:27:44 -07:00
return headerView
}
}
@ available ( iOS 13 , * )
extension SourcesViewController
{
override func collectionView ( _ collectionView : UICollectionView , contextMenuConfigurationForItemAt indexPath : IndexPath , point : CGPoint ) -> UIContextMenuConfiguration ?
{
let source = self . dataSource . item ( at : indexPath )
return UIContextMenuConfiguration ( identifier : indexPath as NSIndexPath , previewProvider : nil ) { ( suggestedActions ) -> UIMenu ? in
2020-08-27 16:39:03 -07:00
let viewErrorAction = UIAction ( title : NSLocalizedString ( " View Error " , comment : " " ) , image : UIImage ( systemName : " exclamationmark.circle " ) ) { ( action ) in
guard let error = source . error else { return }
self . present ( error )
}
2020-03-24 13:27:44 -07:00
let deleteAction = UIAction ( title : NSLocalizedString ( " Remove " , comment : " " ) , image : UIImage ( systemName : " trash " ) , attributes : [ . destructive ] ) { ( action ) in
2022-04-14 16:24:11 -07:00
self . remove ( source )
}
let addAction = UIAction ( title : String ( format : NSLocalizedString ( " Add “%@” " , comment : " " ) , source . name ) , image : UIImage ( systemName : " plus " ) ) { ( action ) in
self . addSource ( url : source . sourceURL , isTrusted : true )
2020-03-24 13:27:44 -07:00
}
2020-08-27 16:39:03 -07:00
var actions : [ UIAction ] = [ ]
if source . error != nil
{
actions . append ( viewErrorAction )
}
2022-04-14 16:24:11 -07:00
switch Section . allCases [ indexPath . section ]
2020-08-27 16:39:03 -07:00
{
2022-04-14 16:24:11 -07:00
case . added :
if source . identifier != Source . altStoreIdentifier
{
actions . append ( deleteAction )
}
case . trusted :
if let cell = collectionView . cellForItem ( at : indexPath ) as ? BannerCollectionViewCell , ! cell . bannerView . button . isHidden
{
actions . append ( addAction )
}
}
guard ! actions . isEmpty else { return nil }
2020-08-27 16:39:03 -07:00
let menu = UIMenu ( title : " " , children : actions )
2020-03-24 13:27:44 -07:00
return menu
}
}
override func collectionView ( _ collectionView : UICollectionView , previewForHighlightingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ?
{
guard let indexPath = configuration . identifier as ? NSIndexPath else { return nil }
guard let cell = collectionView . cellForItem ( at : indexPath as IndexPath ) as ? BannerCollectionViewCell else { return nil }
let parameters = UIPreviewParameters ( )
parameters . backgroundColor = . clear
parameters . visiblePath = UIBezierPath ( roundedRect : cell . bannerView . bounds , cornerRadius : cell . bannerView . layer . cornerRadius )
let preview = UITargetedPreview ( view : cell . bannerView , parameters : parameters )
return preview
}
override func collectionView ( _ collectionView : UICollectionView , previewForDismissingContextMenuWithConfiguration configuration : UIContextMenuConfiguration ) -> UITargetedPreview ?
{
return self . collectionView ( collectionView , previewForHighlightingContextMenuWithConfiguration : configuration )
}
}
2022-04-14 16:24:11 -07:00
extension SourcesViewController : UITextViewDelegate
{
func textView ( _ textView : UITextView , shouldInteractWith URL : URL , in characterRange : NSRange , interaction : UITextItemInteraction ) -> Bool
{
return true
}
}