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
2024-08-06 10:43:52 +09:00
struct SourceError : ALTLocalizedError
2020-10-05 14:48:48 -07:00
{
2024-08-06 10:43:52 +09:00
enum Code : Int , ALTErrorCode
2020-10-05 14:48:48 -07:00
{
2024-08-06 10:43:52 +09:00
typealias Error = SourceError
2020-10-05 14:48:48 -07:00
case unsupported
}
var code : Code
2024-08-06 10:43:52 +09:00
var errorTitle : String ?
var errorFailure : String ?
2020-10-05 14:48:48 -07:00
@ Managed var source : Source
2024-08-06 10:43:52 +09:00
var errorFailureReason : String {
2020-10-05 14:48:48 -07:00
switch self . code
{
2022-11-05 23:50:07 -07:00
case . unsupported : return String ( format : NSLocalizedString ( " The source “%@” is not supported by this version of SideStore. " , comment : " " ) , self . $ source . name )
2020-10-05 14:48:48 -07:00
}
}
}
2022-04-14 16:24:11 -07:00
@objc ( SourcesFooterView )
2023-01-04 09:52:12 -05:00
private final class SourcesFooterView : TextCollectionReusableView
2022-04-14 16:24:11 -07:00
{
@IBOutlet var activityIndicatorView : UIActivityIndicatorView !
@IBOutlet var textView : UITextView !
}
extension SourcesViewController
{
private enum Section : Int , CaseIterable
{
case added
case trusted
}
}
2023-01-04 09:52:12 -05:00
final class SourcesViewController : UICollectionViewController
2020-03-24 13:27:44 -07:00
{
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 ( )
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
dataSource . cellConfigurationHandler = { ( cell , source , indexPath ) in
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 ) ,
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
}
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
2024-03-14 01:39:40 -07:00
textField . placeholder = " https://apps.sidestore.io "
2020-03-24 13:27:44 -07:00
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
}
}
2023-04-04 15:41:44 -05:00
// TODO: R e m o v e t h i s n o w t h a t t r u s t e d s o u r c e s a r e n ' t n e c e s s a r y .
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 )
{
throw SourceError . duplicate ( source , previousSourceName : existingSource . name )
}
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 ( )
{
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
{
2023-02-18 20:26:32 -08:00
print ( error )
// 1 e r r o r d o e s n ' t m e a n a l l t r u s t e d s o u r c e s f a i l e d t o l o a d ! R i l e y , w h y d i d y o u d o t h i s ? ? ? ? ? ? ?
// f i n i s h ( . f a i l u r e ( e r r o r ) )
2022-04-14 16:24:11 -07:00
}
2023-02-18 20:26:32 -08:00
let sources = featuredSourceURLs . compactMap { sourcesByURL [ $0 ] }
finish ( . success ( sources ) )
2022-04-14 16:24:11 -07:00
}
}
}
}
@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 :
2022-11-05 23:50:07 -07:00
headerView . textLabel . text = NSLocalizedString ( " Sources control what apps are available to download through SideStore. " , comment : " " )
2022-04-14 16:24:11 -07:00
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 (
2024-03-14 01:39:40 -07:00
string : NSLocalizedString ( " SideStore has reviewed these sources to make sure they meet our safety standards. " , comment : " " ) ,
2022-04-14 16:24:11 -07:00
attributes : [ . font : font , . foregroundColor : UIColor . gray ]
)
2024-03-14 01:39:40 -07:00
// a t t r i b u t e d T e x t . m u t a b l e S t r i n g . a p p e n d ( " " )
2022-04-14 16:24:11 -07:00
2024-03-14 01:39:40 -07:00
// l e t b o l d e d F o n t = U I F o n t ( d e s c r i p t o r : f o n t . f o n t D e s c r i p t o r . w i t h S y m b o l i c T r a i t s ( . t r a i t B o l d ) ! , s i z e : f o n t . p o i n t S i z e )
// l e t o p e n P a t r e o n U R L = U R L ( s t r i n g : " h t t p s : / / S i d e S t o r e . i o / " ) !
2022-04-14 16:24:11 -07:00
2024-03-14 01:39:40 -07:00
// l e t j o i n P a t r e o n T e x t = N S A t t r i b u t e d S t r i n g (
// s t r i n g : N S L o c a l i z e d S t r i n g ( " " , c o m m e n t : " " ) ,
// a t t r i b u t e s : [ . f o n t : b o l d e d F o n t , . l i n k : o p e n P a t r e o n U R L , . u n d e r l i n e C o l o r : U I C o l o r . c l e a r ]
// )
// a t t r i b u t e d T e x t . a p p e n d ( j o i n P a t r e o n T e x t )
2022-04-14 16:24:11 -07:00
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
}
}