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
2023-10-17 14:49:13 -05:00
import Nuke
2020-03-24 13:27:44 -07:00
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 !
}
2023-10-17 14:49:13 -05:00
private extension UIAction . Identifier
2022-04-14 16:24:11 -07:00
{
2023-10-17 14:49:13 -05:00
static let showDetails = UIAction . Identifier ( " io.altstore.showDetails " )
static let showError = UIAction . Identifier ( " io.altstore.showError " )
2022-04-14 16:24:11 -07:00
}
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 {
2023-10-17 14:49:13 -05:00
self . handleAddSourceDeepLink ( )
2020-08-28 12:15:15 -07:00
}
}
2020-03-24 13:27:44 -07:00
private lazy var dataSource = self . makeDataSource ( )
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
private var placeholderView : RSTPlaceholderView !
private var placeholderViewButton : UIButton !
private var placeholderViewCenterYConstraint : NSLayoutConstraint !
2020-03-24 13:27:44 -07:00
override func viewDidLoad ( )
{
super . viewDidLoad ( )
2023-10-17 14:49:13 -05:00
let layout = self . makeLayout ( )
self . collectionView . collectionViewLayout = layout
2023-04-04 14:37:11 -05:00
self . navigationController ? . view . tintColor = . altPrimary
2023-10-17 14:49:13 -05:00
self . collectionView . register ( AppBannerCollectionViewCell . self , forCellWithReuseIdentifier : RSTCellContentGenericCellIdentifier )
2023-04-04 14:37:11 -05:00
2020-03-24 13:27:44 -07:00
self . collectionView . dataSource = self . dataSource
2023-10-17 14:49:13 -05:00
self . collectionView . prefetchDataSource = self . dataSource
self . collectionView . allowsSelectionDuringEditing = false
2020-10-05 14:48:48 -07:00
2023-10-17 14:49:13 -05:00
let backgroundView = UIView ( frame : . zero )
backgroundView . backgroundColor = . altBackground
self . collectionView . backgroundView = backgroundView
2020-08-28 12:15:15 -07:00
2023-10-17 14:49:13 -05:00
self . placeholderView = RSTPlaceholderView ( frame : . zero )
self . placeholderView . translatesAutoresizingMaskIntoConstraints = false
self . placeholderView . textLabel . text = NSLocalizedString ( " Add More Sources! " , comment : " " )
2023-10-23 09:26:37 -05:00
self . placeholderView . detailTextLabel . text = NSLocalizedString ( " Sources determine what apps are available in AltStore. The more sources you add, the better your AltStore experience will be. \n \n Don’ t know where to start? Try adding one of our Recommended Sources! " , comment : " " )
self . placeholderView . detailTextLabel . textAlignment = . natural
2023-10-17 14:49:13 -05:00
backgroundView . addSubview ( self . placeholderView )
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
let fontDescriptor = UIFontDescriptor . preferredFontDescriptor ( withTextStyle : . title3 ) . bolded ( )
self . placeholderView . textLabel . font = UIFont ( descriptor : fontDescriptor , size : 0.0 )
self . placeholderView . detailTextLabel . font = UIFont . preferredFont ( forTextStyle : . body )
self . placeholderView . detailTextLabel . textAlignment = . natural
self . placeholderViewButton = UIButton ( type : . system , primaryAction : UIAction ( title : NSLocalizedString ( " View Recommended Sources " , comment : " " ) ) { [ weak self ] _ in
self ? . performSegue ( withIdentifier : " addSource " , sender : nil )
} )
self . placeholderViewButton . titleLabel ? . font = UIFont . preferredFont ( forTextStyle : . body )
self . placeholderView . stackView . spacing = 15
self . placeholderView . stackView . directionalLayoutMargins = NSDirectionalEdgeInsets ( top : 15 , leading : 15 , bottom : 15 , trailing : 15 )
self . placeholderView . stackView . isLayoutMarginsRelativeArrangement = true
self . placeholderView . stackView . addArrangedSubview ( self . placeholderViewButton )
self . placeholderViewCenterYConstraint = self . placeholderView . safeAreaLayoutGuide . centerYAnchor . constraint ( equalTo : backgroundView . centerYAnchor , constant : 0 )
NSLayoutConstraint . activate ( [
self . placeholderViewCenterYConstraint ,
self . placeholderView . centerXAnchor . constraint ( equalTo : backgroundView . centerXAnchor ) ,
self . placeholderView . leadingAnchor . constraint ( equalTo : backgroundView . leadingAnchor ) ,
self . placeholderView . trailingAnchor . constraint ( equalTo : backgroundView . trailingAnchor ) ,
self . placeholderView . topAnchor . constraint ( equalTo : self . placeholderView . stackView . topAnchor ) ,
self . placeholderView . bottomAnchor . constraint ( equalTo : self . placeholderView . stackView . bottomAnchor ) ,
] )
self . navigationItem . rightBarButtonItem = self . editButtonItem
self . update ( )
2020-08-28 12:15:15 -07:00
}
override func viewDidAppear ( _ animated : Bool )
{
super . viewDidAppear ( animated )
2023-10-17 14:49:13 -05:00
self . handleAddSourceDeepLink ( )
}
override func viewDidLayoutSubviews ( )
{
super . viewDidLayoutSubviews ( )
// V e r t i c a l l y c e n t e r p l a c e h o l d e r v i e w i n g a p b e l o w f i r s t i t e m .
let indexPath = IndexPath ( item : 0 , section : 0 )
guard let layoutAttributes = self . collectionView . layoutAttributesForItem ( at : indexPath ) else { return }
let maxY = layoutAttributes . frame . maxY
let constant = maxY / 2
if self . placeholderViewCenterYConstraint . constant != constant
2020-08-28 12:15:15 -07:00
{
2023-10-17 14:49:13 -05:00
self . placeholderViewCenterYConstraint . constant = constant
2020-08-28 12:15:15 -07:00
}
}
2020-03-24 13:27:44 -07:00
}
private extension SourcesViewController
{
2023-10-17 14:49:13 -05:00
func makeLayout ( ) -> UICollectionViewCompositionalLayout
2020-03-24 13:27:44 -07:00
{
2023-10-17 14:49:13 -05:00
var configuration = UICollectionLayoutListConfiguration ( appearance : . grouped )
configuration . showsSeparators = false
configuration . backgroundColor = . clear
configuration . trailingSwipeActionsConfigurationProvider = { [ weak self ] indexPath in
guard let self else { return UISwipeActionsConfiguration ( actions : [ ] ) }
2020-03-24 13:27:44 -07:00
2023-10-17 14:49:13 -05:00
let source = self . dataSource . item ( at : indexPath )
var actions : [ UIContextualAction ] = [ ]
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
if source . identifier != Source . altStoreIdentifier
2022-04-14 16:24:11 -07:00
{
2023-10-17 14:49:13 -05:00
// P r e v e n t u s e r s f r o m r e m o v i n g A l t S t o r e s o u r c e .
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
let removeAction = UIContextualAction ( style : . destructive ,
title : NSLocalizedString ( " Remove " , comment : " " ) ) { _ , _ , completion in
self . remove ( source , completionHandler : completion )
2022-04-14 16:24:11 -07:00
}
2023-10-17 14:49:13 -05:00
removeAction . image = UIImage ( systemName : " trash.fill " )
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
actions . append ( removeAction )
2022-04-14 16:24:11 -07:00
}
2020-03-24 13:27:44 -07:00
2023-10-17 14:49:13 -05:00
if let error = source . error
{
let viewErrorAction = UIContextualAction ( style : . normal ,
title : NSLocalizedString ( " View Error " , comment : " " ) ) { _ , _ , completion in
self . present ( error )
completion ( true )
}
viewErrorAction . backgroundColor = . systemYellow
viewErrorAction . image = UIImage ( systemName : " exclamationmark.circle.fill " )
actions . append ( viewErrorAction )
}
2020-08-27 16:39:03 -07:00
2023-10-17 14:49:13 -05:00
let config = UISwipeActionsConfiguration ( actions : actions )
config . performsFirstActionWithFullSwipe = false
2020-08-27 15:23:21 -07:00
2023-10-17 14:49:13 -05:00
return config
2020-03-24 13:27:44 -07:00
}
2023-10-17 14:49:13 -05:00
let layout = UICollectionViewCompositionalLayout . list ( using : configuration )
return layout
2020-03-24 13:27:44 -07:00
}
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
func makeDataSource ( ) -> RSTFetchedResultsCollectionViewPrefetchingDataSource < Source , UIImage >
2022-04-14 16:24:11 -07:00
{
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 ) ]
2023-10-17 14:49:13 -05:00
let fetchedResultsController = NSFetchedResultsController ( fetchRequest : fetchRequest , managedObjectContext : DatabaseManager . shared . viewContext , sectionNameKeyPath : nil , cacheName : nil )
fetchedResultsController . delegate = self
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource < Source , UIImage > ( fetchedResultsController : fetchedResultsController )
dataSource . proxy = self
dataSource . cellConfigurationHandler = { [ weak self ] ( cell , source , indexPath ) in
guard let self else { return }
let cell = cell as ! AppBannerCollectionViewCell
cell . layoutMargins . top = 5
cell . layoutMargins . bottom = 5
cell . layoutMargins . left = self . view . layoutMargins . left
cell . layoutMargins . right = self . view . layoutMargins . right
cell . bannerView . configure ( for : source )
cell . bannerView . iconImageView . image = nil
cell . bannerView . iconImageView . isIndicatingActivity = true
2023-11-29 18:08:42 -06:00
let numberOfApps = source . apps . filter { StoreApp . visibleAppsPredicate . evaluate ( with : $0 ) } . count
2023-10-17 14:49:13 -05:00
if let error = source . error
{
let image = UIImage ( systemName : " exclamationmark " ) ? . withTintColor ( . white , renderingMode : . alwaysOriginal )
cell . bannerView . button . setImage ( image , for : . normal )
cell . bannerView . button . setTitle ( nil , for : . normal )
cell . bannerView . button . tintColor = . systemYellow . withAlphaComponent ( 0.75 )
let action = UIAction ( identifier : . showError ) { _ in
self . present ( error )
}
cell . bannerView . button . addAction ( action , for : . primaryActionTriggered )
cell . bannerView . button . removeAction ( identifiedBy : . showDetails , for : . primaryActionTriggered )
}
else
{
cell . bannerView . button . setImage ( nil , for : . normal )
cell . bannerView . button . setTitle ( numberOfApps . description , for : . normal )
cell . bannerView . button . tintColor = . white . withAlphaComponent ( 0.2 )
let action = UIAction ( identifier : . showDetails ) { _ in
self . showSourceDetails ( for : source )
}
cell . bannerView . button . addAction ( action , for : . primaryActionTriggered )
cell . bannerView . button . removeAction ( identifiedBy : . showError , for : . primaryActionTriggered )
}
let dateText : String
if let lastUpdatedDate = source . lastUpdatedDate
{
2023-12-08 14:51:29 -06:00
dateText = Date ( ) . relativeDateString ( since : lastUpdatedDate , dateFormatter : Date . shortDateFormatter )
2023-10-17 14:49:13 -05:00
}
else
{
dateText = NSLocalizedString ( " Never " , comment : " " )
}
let text = String ( format : NSLocalizedString ( " Last Updated: %@ " , comment : " " ) , dateText )
cell . bannerView . subtitleLabel . text = text
cell . bannerView . subtitleLabel . numberOfLines = 1
let numberOfAppsText : String
if #available ( iOS 15 , * )
{
let attributedOutput = AttributedString ( localized : " ^[ \( numberOfApps ) app](inflect: true) " )
numberOfAppsText = String ( attributedOutput . characters )
}
else
{
numberOfAppsText = " "
}
let accessibilityLabel = source . name + " \n " + text + " . \n " + numberOfAppsText
cell . bannerView . accessibilityLabel = accessibilityLabel
if source . identifier != Source . altStoreIdentifier
{
cell . accessories = [ . delete ( displayed : . whenEditing ) ]
}
else
{
cell . accessories = [ ]
}
cell . bannerView . accessibilityTraits . remove ( . button )
// 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 ( )
}
dataSource . prefetchHandler = { ( source , indexPath , completionHandler ) in
guard let imageURL = source . effectiveIconURL else { return nil }
return RSTAsyncBlockOperation ( ) { ( operation ) in
ImagePipeline . shared . loadImage ( with : imageURL , progress : nil ) { result in
guard ! operation . isCancelled else { return operation . finish ( ) }
switch result
{
case . success ( let response ) : completionHandler ( response . image , nil )
case . failure ( let error ) : completionHandler ( nil , error )
}
}
}
}
dataSource . prefetchCompletionHandler = { ( cell , image , indexPath , error ) in
let cell = cell as ! AppBannerCollectionViewCell
cell . bannerView . iconImageView . isIndicatingActivity = false
cell . bannerView . iconImageView . image = image
if let error = error
{
print ( " Error loading image: " , error )
}
}
2022-04-14 16:24:11 -07:00
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
}
2023-10-18 18:56:40 -05:00
@IBAction
func unwindFromAddSource ( _ segue : UIStoryboardSegue )
{
}
2020-03-24 13:27:44 -07:00
}
private extension SourcesViewController
{
2023-10-17 14:49:13 -05:00
func handleAddSourceDeepLink ( )
2020-03-24 13:27:44 -07:00
{
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
} )
2023-10-17 14:49:13 -05:00
guard let url = self . deepLinkSourceURL , self . view . window != nil else { return }
2020-03-24 13:27:44 -07:00
2023-10-17 14:49:13 -05:00
// 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
2023-10-17 14:49:13 -05:00
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = true
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
}
2023-10-17 14:49:13 -05:00
self . navigationItem . leftBarButtonItem ? . isIndicatingActivity = false
2020-08-28 12:15:15 -07:00
}
}
2023-10-17 14:49:13 -05:00
AppManager . shared . fetchSource ( sourceURL : url ) { ( 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-10-17 14:49:13 -05:00
DispatchQueue . main . async {
self . showSourceDetails ( for : source )
2020-08-28 12:15:15 -07:00
}
2023-10-17 14:49:13 -05: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
}
}
}
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
2023-10-17 14:49:13 -05:00
func remove ( _ source : Source , completionHandler : ( ( Bool ) -> Void ) ? = nil )
2022-04-14 16:24:11 -07:00
{
2023-10-17 14:49:13 -05:00
Task < Void , Never > {
do
2022-04-14 16:24:11 -07:00
{
2023-10-17 14:49:13 -05:00
try await AppManager . shared . remove ( source , presentingViewController : self )
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
completionHandler ? ( true )
}
catch is CancellationError
{
completionHandler ? ( false )
}
catch
{
completionHandler ? ( false )
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
{
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
}
2023-10-17 14:49:13 -05:00
self . present ( error )
2022-04-14 16:24:11 -07:00
}
}
}
2023-10-17 14:49:13 -05:00
func showSourceDetails ( for source : Source )
2022-04-14 16:24:11 -07:00
{
2023-10-17 14:49:13 -05:00
self . performSegue ( withIdentifier : " showSourceDetails " , sender : source )
2022-04-14 16:24:11 -07:00
}
2023-04-04 15:41:44 -05:00
2023-10-17 14:49:13 -05:00
func update ( )
2023-04-04 15:41:44 -05:00
{
2023-10-17 14:49:13 -05:00
if self . dataSource . itemCount < 2
{
// S h o w p l a c e h o l d e r v i e w
self . placeholderView . isHidden = false
self . collectionView . alwaysBounceVertical = false
self . setEditing ( false , animated : true )
self . editButtonItem . isEnabled = false
}
else
{
self . placeholderView . isHidden = true
self . collectionView . alwaysBounceVertical = true
self . editButtonItem . isEnabled = true
}
2023-04-04 15:41:44 -05:00
}
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
}
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// o v e r r i d e f u n c c o l l e c t i o n V i e w ( _ c o l l e c t i o n V i e w : U I C o l l e c t i o n V i e w , v i e w F o r S u p p l e m e n t a r y E l e m e n t O f K i n d k i n d : S t r i n g , a t i n d e x P a t h : I n d e x P a t h ) - > U I C o l l e c t i o n R e u s a b l e V i e w
// {
// l e t h e a d e r V i e w = c o l l e c t i o n V i e w . d e q u e u e R e u s a b l e S u p p l e m e n t a r y V i e w ( o f K i n d : k i n d , w i t h R e u s e I d e n t i f i e r : k i n d , f o r : i n d e x P a t h ) a s ! U I C o l l e c t i o n V i e w L i s t C e l l
// v a r c o n f i g u a t i o n = U I L i s t C o n t e n t C o n f i g u r a t i o n . c e l l ( )
// c o n f i g u a t i o n . t e x t = N S L o c a l i z e d S t r i n g ( " S o u r c e s c o n t r o l w h a t a p p s a r e a v a i l a b l e t o d o w n l o a d t h r o u g h A l t S t o r e . " , c o m m e n t : " " )
// c o n f i g u a t i o n . t e x t P r o p e r t i e s . c o l o r = . s e c o n d a r y L a b e l
// c o n f i g u a t i o n . t e x t P r o p e r t i e s . a l i g n m e n t = . n a t u r a l
// h e a d e r V i e w . c o n t e n t C o n f i g u r a t i o n = c o n f i g u a t i o n
// s w i t c h k i n d
// {
// c a s e U I C o l l e c t i o n V i e w . e l e m e n t K i n d S e c t i o n H e a d e r :
// s w i t c h S e c t i o n . a l l C a s e s [ i n d e x P a t h . s e c t i o n ]
// {
// c a s e . a d d e d :
// h e a d e r V i e w . t e x t L a b e l . t e x t = N S L o c a l i z e d S t r i n g ( " S o u r c e s c o n t r o l w h a t a p p s a r e a v a i l a b l e t o d o w n l o a d t h r o u g h S i d e S t o r e . " , c o m m e n t : " " )
// h e a d e r V i e w . t e x t L a b e l . f o n t = U I F o n t . p r e f e r r e d F o n t ( f o r T e x t S t y l e : . c a l l o u t )
// h e a d e r V i e w . t e x t L a b e l . t e x t A l i g n m e n t = . n a t u r a l
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// 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 . c o n s t a n t = 1 4
// 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 . c o n s t a n t = 3 0
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// c a s e . t r u s t e d :
// s w i t c h s e l f . f e t c h T r u s t e d S o u r c e s R e s u l t
// {
// c a s e . f a i l u r e : h e a d e r V i e w . t e x t L a b e l . t e x t = N S L o c a l i z e d S t r i n g ( " E r r o r L o a d i n g T r u s t e d S o u r c e s " , c o m m e n t : " " )
// c a s e . s u c c e s s , . n o n e : h e a d e r V i e w . t e x t L a b e l . t e x t = N S L o c a l i z e d S t r i n g ( " T r u s t e d S o u r c e s " , c o m m e n t : " " )
// }
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// l e t d e s c r i p t o r = U I F o n t D e s c r i p t o r . p r e f e r r e d F o n t D e s c r i p t o r ( w i t h T e x t S t y l e : . c a l l o u t ) . 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 ) !
// h e a d e r V i e w . t e x t L a b e l . f o n t = U I F o n t ( d e s c r i p t o r : d e s c r i p t o r , s i z e : 0 )
// h e a d e r V i e w . t e x t L a b e l . t e x t A l i g n m e n t = . c e n t e r
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// 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 . c o n s t a n t = 5 4
// 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 . c o n s t a n t = 1 5
// }
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// c a s e U I C o l l e c t i o n V i e w . e l e m e n t K i n d S e c t i o n F o o t e r :
// l e t f o o t e r V i e w = h e a d e r V i e w a s ! S o u r c e s F o o t e r V i e w
// l e t f o n t = U I F o n t . p r e f e r r e d F o n t ( f o r T e x t S t y l e : . s u b h e a d l i n e )
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// s w i t c h s e l f . f e t c h T r u s t e d S o u r c e s R e s u l t
// {
// c a s e . f a i l u r e ( l e t e r r o r ) :
// f o o t e r V i e w . t e x t V i e w . f o n t = f o n t
// f o o t e r V i e w . t e x t V i e w . t e x t = e r r o r . l o c a l i z e d D e s c r i p t i o n
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// f o o t e r V i e w . a c t i v i t y I n d i c a t o r V i e w . s t o p A n i m a t i n g ( )
// f o o t 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 . c o n s t a n t = 0
// f o o t e r V i e w . t e x t V i e w . t e x t A l i g n m e n t = . c e n t e r
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// c a s e . s u c c e s s , . n o n e :
// f o o t e r V i e w . t e x t V i e w . d e l e g a t e = s e l f
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// l e t a t t r i b u t e d T e x t = N S M u t a b l e 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 ( " S i d e S t o r e h a s r e v i e w e d t h e s e s o u r c e s t o m a k e s u r e t h e y m e e t o u r s a f e t y s t a n d a r d s . " , c o m m e n t : " " ) ,
// a t t r i b u t e s : [ . f o n t : f o n t , . f o r e g r o u n d C o l o r : U I C o l o r . g r a y ]
// )
// / / 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
2023-10-23 09:26:37 -05: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
2023-10-23 09:26:37 -05: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
2023-10-23 09:26:37 -05:00
// f o o t e r V i e w . t e x t V i e w . a t t r i b u t e d T e x t = a t t r i b u t e d T e x t
// f o o t e r V i e w . t e x t V i e w . t e x t A l i g n m e n t = . n a t u r a l
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// i f s e l f . f e t c h T r u s t e d S o u r c e s R e s u l t ! = n i l
// {
// f o o t e r V i e w . a c t i v i t y I n d i c a t o r V i e w . s t o p A n i m a t i n g ( )
// f o o t 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 . c o n s t a n t = 2 0
// }
// e l s e
// {
// f o o t e r V i e w . a c t i v i t y I n d i c a t o r V i e w . s t a r t A n i m a t i n g ( )
// f o o t 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 . c o n s t a n t = 0
// }
// }
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// d e f a u l t : b r e a k
// }
2022-04-14 16:24:11 -07:00
2023-10-23 09:26:37 -05:00
// r e t u r n h e a d e r V i e w
// }
2020-03-24 13:27:44 -07:00
}
2023-10-17 14:49:13 -05:00
extension SourcesViewController : NSFetchedResultsControllerDelegate
2020-03-24 13:27:44 -07:00
{
2023-10-17 14:49:13 -05:00
func controllerWillChangeContent ( _ controller : NSFetchedResultsController < NSFetchRequestResult > )
2020-03-24 13:27:44 -07:00
{
2023-10-17 14:49:13 -05:00
self . dataSource . controllerWillChangeContent ( controller )
2020-03-24 13:27:44 -07:00
}
2023-10-17 14:49:13 -05:00
func controllerDidChangeContent ( _ controller : NSFetchedResultsController < NSFetchRequestResult > )
2020-03-24 13:27:44 -07:00
{
2023-10-17 14:49:13 -05:00
self . update ( )
self . dataSource . controllerDidChangeContent ( controller )
2020-03-24 13:27:44 -07:00
}
2023-10-17 14:49:13 -05:00
func controller ( _ controller : NSFetchedResultsController < NSFetchRequestResult > , didChange anObject : Any , at indexPath : IndexPath ? , for type : NSFetchedResultsChangeType , newIndexPath : IndexPath ? )
{
self . dataSource . controller ( controller , didChange : anObject , at : indexPath , for : type , newIndexPath : newIndexPath )
}
func controller ( _ controller : NSFetchedResultsController < NSFetchRequestResult > , didChange sectionInfo : NSFetchedResultsSectionInfo , atSectionIndex sectionIndex : Int , for type : NSFetchedResultsChangeType )
2020-03-24 13:27:44 -07:00
{
2023-10-17 14:49:13 -05:00
self . dataSource . controller ( controller , didChange : sectionInfo , atSectionIndex : UInt ( sectionIndex ) , for : type )
2020-03-24 13:27:44 -07:00
}
}
2022-04-14 16:24:11 -07:00
2023-10-17 14:49:13 -05:00
@ available ( iOS 17 , * )
# Preview ( traits : . portrait ) {
DatabaseManager . shared . startForPreview ( )
let storyboard = UIStoryboard ( name : " Sources " , bundle : nil )
let sourcesViewController = storyboard . instantiateInitialViewController ( ) !
let context = DatabaseManager . shared . persistentContainer . newBackgroundContext ( )
context . performAndWait {
_ = Source . make ( name : " OatmealDome's AltStore Source " ,
identifier : " me.oatmealdome.altstore " ,
sourceURL : URL ( string : " https://altstore.oatmealdome.me " ) ! ,
context : context )
_ = Source . make ( name : " UTM Repository " ,
identifier : " com.utmapp.repos.UTM " ,
sourceURL : URL ( string : " https://alt.getutm.app " ) ! ,
context : context )
_ = Source . make ( name : " Flyinghead " ,
identifier : " com.flyinghead.source " ,
sourceURL : URL ( string : " https://flyinghead.github.io/flycast-builds/altstore.json " ) ! ,
context : context )
_ = Source . make ( name : " Provenance " ,
identifier : " org.provenance-emu.AltStore " ,
sourceURL : URL ( string : " https://provenance-emu.com/apps.json " ) ! ,
context : context )
_ = Source . make ( name : " PojavLauncher Repository " ,
identifier : " dev.crystall1ne.repos.PojavLauncher " ,
sourceURL : URL ( string : " http://alt.crystall1ne.dev " ) ! ,
context : context )
try ! context . save ( )
}
AppManager . shared . fetchSources { result in
do
{
2023-12-08 14:50:42 -06:00
let ( _ , context ) = try result . get ( )
2023-10-17 14:49:13 -05:00
try context . save ( )
}
catch
{
print ( " Preview failed to fetch sources: " , error )
}
2022-04-14 16:24:11 -07:00
}
2023-10-17 14:49:13 -05:00
return sourcesViewController
2022-04-14 16:24:11 -07:00
}