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
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 ( )
2023-05-15 16:25:25 -05:00
private var fetchTrustedSourcesOperation : UpdateKnownSourcesOperation ?
2022-04-14 16:24:11 -07:00
private var fetchTrustedSourcesResult : Result < Void , Error > ?
private var _fetchTrustedSourcesContext : NSManagedObjectContext ?
2020-03-24 13:27:44 -07:00
override func viewDidLoad ( )
{
super . viewDidLoad ( )
2023-04-04 14:37:11 -05:00
self . view . tintColor = . altPrimary
self . navigationController ? . view . tintColor = . altPrimary
if let navigationBar = self . navigationController ? . navigationBar as ? NavigationBar
{
// D o n ' t a u t o m a t i c a l l y a d j u s t i t e m p o s i t i o n s w h e n b e i n g p r e s e n t e d n o n - f u l l s c r e e n ,
// o r e l s e t h e n a v i g a t i o n b a r c o n t e n t w o n ' t b e v e r t i c a l l y c e n t e r e d .
navigationBar . automaticallyAdjustsItemPositions = false
}
2020-03-24 13:27:44 -07:00
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
2023-05-24 19:13:40 -05:00
dataSource . cellConfigurationHandler = { [ weak self ] ( cell , source , indexPath ) in
guard let self else { return }
2020-03-24 13:27:44 -07:00
let tintColor = UIColor . altPrimary
2023-04-04 14:37:11 -05:00
let cell = cell as ! AppBannerCollectionViewCell
2020-03-24 13:27:44 -07:00
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
2023-03-02 16:53:36 -06:00
let configuation = UIImage . SymbolConfiguration ( pointSize : 24 )
let imageAttachment = NSTextAttachment ( )
imageAttachment . image = UIImage ( systemName : " checkmark.circle " , withConfiguration : configuation ) ? . withTintColor ( . altPrimary )
2022-04-14 16:24:11 -07:00
2023-03-02 16:53:36 -06:00
let attributedText = NSAttributedString ( attachment : imageAttachment )
cell . bannerView . buttonLabel . attributedText = attributedText
cell . bannerView . buttonLabel . textAlignment = . center
cell . bannerView . buttonLabel . isHidden = false
2022-04-14 16:24:11 -07:00
}
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 ) ,
2023-05-30 15:24:01 -05:00
// C a n ' t s o r t b y U R L s o r e l s e a p p w i l l c r a s h .
// N S S o r t D e s c r i p t o r ( k e y P a t h : \ S o u r c e . s o u r c e U R L , a s c e n d i n g : t r u e ) ,
2022-04-14 16:24:11 -07:00
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
}
2023-04-04 15:41:44 -05:00
@ 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
}
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
2023-04-04 15:41:44 -05:00
func addSource ( url : URL , 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
2022-11-22 13:02:19 -06:00
case . failure ( var error as SourceError ) :
let title = String ( format : NSLocalizedString ( " “%@” could not be added to AltStore. " , comment : " " ) , error . $ source . name )
error . errorTitle = title
self . present ( error )
case . failure ( let error as NSError ) :
self . present ( error . withLocalizedTitle ( NSLocalizedString ( " Unable to Add Source " , comment : " " ) ) )
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
{
2023-05-11 18:51:09 -05:00
// U s e @ M a n a g e d b e f o r e c a l l i n g p e r f o r m ( ) t o k e e p
// s t r o n g r e f e r e n c e t o s o u r c e . m a n a g e d O b j e c t C o n t e x t .
2023-04-04 15:41:44 -05:00
@ Managed var source = try result . get ( )
2022-04-14 16:24:11 -07:00
2023-05-11 18:51:09 -05:00
let backgroundContext = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
backgroundContext . perform {
do
{
let predicate = NSPredicate ( format : " %K == %@ " , # keyPath ( Source . identifier ) , $ source . identifier )
if let existingSource = Source . first ( satisfying : predicate , in : backgroundContext )
{
2023-05-16 15:39:38 -05:00
throw SourceError . duplicate ( source , existingSource : existingSource )
2023-05-11 18:51:09 -05:00
}
DispatchQueue . main . async {
self . showSourceDetails ( for : source )
}
finish ( . success ( ( ) ) )
}
catch
{
finish ( . failure ( error ) )
}
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
}
}
}
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
2022-11-22 13:02:19 -06:00
let title = nsError . localizedTitle // O K i f n i l .
let message = [ nsError . localizedDescription , nsError . localizedDebugDescription , nsError . localizedRecoverySuggestion ] . compactMap { $0 } . joined ( separator : " \n \n " )
2020-08-27 16:39:03 -07:00
2022-11-22 13:02:19 -06:00
let alertController = UIAlertController ( title : title , message : message , preferredStyle : . alert )
2020-08-27 16:39:03 -07:00
alertController . addAction ( . ok )
self . present ( alertController , animated : true , completion : nil )
}
2022-04-14 16:24:11 -07:00
func fetchTrustedSources ( )
{
2023-05-24 19:13:40 -05:00
// C l o s u r e i n s t e a d o f l o c a l f u n c t i o n s o w e c a n c a p t u r e ` s e l f ` w e a k l y .
let finish : ( Result < [ Source ] , Error > ) -> Void = { [ weak self ] result in
self ? . fetchTrustedSourcesResult = result . map { _ in ( ) }
2022-04-14 16:24:11 -07:00
DispatchQueue . main . async {
do
{
let sources = try result . get ( )
print ( " Fetched trusted sources: " , sources . map { $0 . identifier } )
let sectionUpdate = RSTCellContentChange ( type : . update , sectionIndex : 0 )
2023-05-24 19:13:40 -05:00
self ? . trustedSourcesDataSource . setItems ( sources , with : [ sectionUpdate ] )
2022-04-14 16:24:11 -07:00
}
catch
{
print ( " Error fetching trusted sources: " , error )
let sectionUpdate = RSTCellContentChange ( type : . update , sectionIndex : 0 )
2023-05-24 19:13:40 -05:00
self ? . trustedSourcesDataSource . setItems ( [ ] , with : [ sectionUpdate ] )
2022-04-14 16:24:11 -07:00
}
}
}
2023-05-24 19:13:40 -05:00
self . fetchTrustedSourcesOperation = AppManager . shared . updateKnownSources { [ weak self ] result in
2022-04-14 16:24:11 -07:00
switch result
{
case . failure ( let error ) : finish ( . failure ( error ) )
2023-05-15 16:25:25 -05:00
case . success ( ( let trustedSources , _ ) ) :
2022-04-14 16:24:11 -07:00
// 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 ( )
2023-05-24 19:13:40 -05:00
self ? . _fetchTrustedSourcesContext = context
2022-04-14 16:24:11 -07:00
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 )
2023-04-04 15:41:44 -05:00
self . addSource ( url : source . sourceURL ) { _ in
2022-04-14 16:24:11 -07:00
// 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 )
}
2023-04-04 15:41:44 -05:00
func showSourceDetails ( for source : Source )
{
self . performSegue ( withIdentifier : " showSourceDetails " , sender : source )
}
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 )
2023-04-04 15:41:44 -05:00
self . showSourceDetails ( for : source )
2020-08-27 16:39:03 -07:00
}
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
2023-02-08 13:06:44 -06:00
/* C h a n g i n g N S L a y o u t C o n s t r a i n t p r i o r i t i e s f r o m r e q u i r e d t o o p t i o n a l ( a n d v i c e v e r s a ) i s n ’ t s u p p o r t e d , a n d c r a s h e s o n i O S 1 2 . */
// l e t a l m o s t R e q u i r e d P r i o r i t y = U I L a y o u t P r i o r i t y ( U I L a y o u t P r i o r i t y . r e q u i r e d . r a w V a l u e - 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 ) .
// h e a d e r V i e w . l e a d i n g L a y o u t C o n s t r a i n t ? . p r i o r i t y = a l m o s t R e q u i r e d P r i o r i t y
// h e a d e r V i e w . t r a i l i n g L a y o u t C o n s t r a i n t ? . p r i o r i t y = a l m o s t R e q u i r e d P r i o r i t y
// h e a d e r V i e w . t o p L a y o u t C o n s t r a i n t ? . p r i o r i t y = a l m o s t R e q u i r e d P r i o r i t y
// h e a d e r V i e w . b o t t o m L a y o u t C o n s t r a i n t ? . p r i o r i t y = a l m o s t R e q u i r e d P r i o r i t y
2022-04-14 16:24:11 -07:00
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
}
}
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
2023-04-04 15:41:44 -05:00
self . addSource ( url : source . sourceURL )
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 :
2023-04-04 14:37:11 -05:00
if let cell = collectionView . cellForItem ( at : indexPath ) as ? AppBannerCollectionViewCell , ! cell . bannerView . button . isHidden
2022-04-14 16:24:11 -07:00
{
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 }
2023-04-04 14:37:11 -05:00
guard let cell = collectionView . cellForItem ( at : indexPath as IndexPath ) as ? AppBannerCollectionViewCell else { return nil }
2020-03-24 13:27:44 -07:00
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
}
}