mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Adds initial support for 3rd party Sources
This commit is contained in:
@@ -125,6 +125,8 @@
|
||||
BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; };
|
||||
BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; };
|
||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
|
||||
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */; };
|
||||
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */; };
|
||||
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; };
|
||||
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; };
|
||||
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD023C91BD300A89F2D /* ALTWiredConnection.m */; };
|
||||
@@ -152,7 +154,7 @@
|
||||
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
|
||||
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; };
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.swift */; };
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+Properties.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */; };
|
||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
|
||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
||||
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; };
|
||||
@@ -167,6 +169,7 @@
|
||||
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */; };
|
||||
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; };
|
||||
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */; };
|
||||
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC84A4C2421A19100853474 /* SourcesViewController.swift */; };
|
||||
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
|
||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
|
||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
|
||||
@@ -234,7 +237,6 @@
|
||||
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
|
||||
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */; };
|
||||
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFEE944023F22AA100CDA07D /* AppIDComponents.swift */; };
|
||||
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
|
||||
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
|
||||
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; };
|
||||
@@ -444,6 +446,8 @@
|
||||
BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore4ToAltStore5.xcmappingmodel; sourceTree = "<group>"; };
|
||||
BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "NSAttributedString+Markdown.m"; path = "Dependencies/MarkdownAttributedString/NSAttributedString+Markdown.m"; sourceTree = SOURCE_ROOT; };
|
||||
BF6C8FAB242935ED00125131 /* NSAttributedString+Markdown.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "NSAttributedString+Markdown.h"; path = "Dependencies/MarkdownAttributedString/NSAttributedString+Markdown.h"; sourceTree = SOURCE_ROOT; };
|
||||
BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BannerCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextCollectionReusableView.swift; sourceTree = "<group>"; };
|
||||
BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAltStoreViewController.swift; sourceTree = "<group>"; };
|
||||
BF718BC723C919CC00A89F2D /* CFNotificationName+AltStore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "CFNotificationName+AltStore.h"; sourceTree = "<group>"; };
|
||||
BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "CFNotificationName+AltStore.m"; sourceTree = "<group>"; };
|
||||
@@ -478,7 +482,7 @@
|
||||
BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchAnisetteDataOperation.swift; sourceTree = "<group>"; };
|
||||
BFA8172C23C5823E001B5953 /* InstalledExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledExtension.swift; sourceTree = "<group>"; };
|
||||
BFB11691229322E400BB457C /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = "<group>"; };
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+Properties.swift"; sourceTree = "<group>"; };
|
||||
BFB1169C22932DB100BB457C /* apps.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = apps.json; sourceTree = "<group>"; };
|
||||
BFB364592325985F00CD0EB1 /* FindServerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindServerOperation.swift; sourceTree = "<group>"; };
|
||||
BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = UpdateCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||
@@ -496,6 +500,7 @@
|
||||
BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DeactivateAppOperation.swift; sourceTree = "<group>"; };
|
||||
BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InstalledAppsCollectionHeaderView.swift; sourceTree = "<group>"; };
|
||||
BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = InstalledAppsCollectionHeaderView.xib; sourceTree = "<group>"; };
|
||||
BFC84A4C2421A19100853474 /* SourcesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourcesViewController.swift; sourceTree = "<group>"; };
|
||||
BFD2476A2284B9A500981D42 /* AltStore.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AltStore.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
BFD247742284B9A500981D42 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
@@ -567,7 +572,6 @@
|
||||
BFE6326522A857C100F30809 /* Team.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Team.swift; sourceTree = "<group>"; };
|
||||
BFE6326722A858F300F30809 /* Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Account.swift; sourceTree = "<group>"; };
|
||||
BFE6326B22A86FF300F30809 /* AuthenticationOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticationOperation.swift; sourceTree = "<group>"; };
|
||||
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppIDComponents.swift; sourceTree = "<group>"; };
|
||||
BFF0B68D23219520007A79E1 /* PatreonViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonViewController.swift; sourceTree = "<group>"; };
|
||||
BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PatreonComponents.swift; sourceTree = "<group>"; };
|
||||
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = "<group>"; };
|
||||
@@ -876,7 +880,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
|
||||
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */,
|
||||
);
|
||||
path = "App IDs";
|
||||
sourceTree = "<group>";
|
||||
@@ -982,6 +985,14 @@
|
||||
path = Server;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFC84A4B2421A13000853474 /* Sources */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFC84A4C2421A19100853474 /* SourcesViewController.swift */,
|
||||
);
|
||||
path = Sources;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFD247612284B9A500981D42 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -1025,6 +1036,7 @@
|
||||
BFD5D6E6230CC94B007955AB /* Patreon */,
|
||||
BFD2478A2284C49000981D42 /* Managing Apps */,
|
||||
BF56D2AD23DF9E170006506D /* App IDs */,
|
||||
BFC84A4B2421A13000853474 /* Sources */,
|
||||
BFC51D7922972F1F00388324 /* Server */,
|
||||
BFD247982284D7FC00981D42 /* Model */,
|
||||
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
|
||||
@@ -1078,6 +1090,8 @@
|
||||
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
||||
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
||||
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
||||
BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */,
|
||||
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -1128,7 +1142,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */,
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */,
|
||||
BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */,
|
||||
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */,
|
||||
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
|
||||
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
|
||||
@@ -1710,7 +1724,6 @@
|
||||
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
|
||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
||||
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */,
|
||||
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */,
|
||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
|
||||
@@ -1747,6 +1760,7 @@
|
||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
||||
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
|
||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
||||
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
||||
@@ -1754,7 +1768,7 @@
|
||||
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
|
||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
||||
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */,
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */,
|
||||
BFB1169B2293274D00BB457C /* JSONDecoder+Properties.swift in Sources */,
|
||||
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
|
||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
|
||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
|
||||
@@ -1771,6 +1785,7 @@
|
||||
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
|
||||
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
||||
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
|
||||
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
|
||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||
@@ -1788,6 +1803,7 @@
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
|
||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
||||
|
||||
@@ -71,7 +71,7 @@ private extension AppIDsViewController
|
||||
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
|
||||
let tintColor = UIColor.altPrimary
|
||||
|
||||
let cell = cell as! AppIDCollectionViewCell
|
||||
let cell = cell as! BannerCollectionViewCell
|
||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||
cell.tintColor = tintColor
|
||||
@@ -80,6 +80,8 @@ private extension AppIDsViewController
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
cell.bannerView.betaBadgeView.isHidden = true
|
||||
|
||||
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in…", comment: "")
|
||||
|
||||
if let expirationDate = appID.expirationDate
|
||||
{
|
||||
cell.bannerView.button.isHidden = false
|
||||
@@ -181,7 +183,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||
switch kind
|
||||
{
|
||||
case UICollectionView.elementKindSectionHeader:
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! AppIDsCollectionReusableView
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
|
||||
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
|
||||
@@ -208,7 +210,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
||||
return headerView
|
||||
|
||||
case UICollectionView.elementKindSectionFooter:
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! AppIDsCollectionReusableView
|
||||
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Footer", for: indexPath) as! TextCollectionReusableView
|
||||
|
||||
let count = self.dataSource.itemCount
|
||||
if count == 1
|
||||
|
||||
@@ -250,20 +250,20 @@ private extension AppDelegate
|
||||
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> Void)
|
||||
{
|
||||
var fetchSourceResult: Result<Source, Error>?
|
||||
var fetchSourcesResult: Result<Set<Source>, Error>?
|
||||
var serversResult: Result<Void, Error>?
|
||||
|
||||
let dispatchGroup = DispatchGroup()
|
||||
dispatchGroup.enter()
|
||||
|
||||
AppManager.shared.fetchSource() { (result) in
|
||||
fetchSourceResult = result
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
fetchSourcesResult = result
|
||||
|
||||
do
|
||||
{
|
||||
let source = try result.get()
|
||||
let sources = try result.get()
|
||||
|
||||
guard let context = source.managedObjectContext else { return }
|
||||
guard let context = sources.first?.managedObjectContext else { return }
|
||||
|
||||
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||
@@ -331,7 +331,7 @@ private extension AppDelegate
|
||||
{
|
||||
print("Error fetching apps:", error)
|
||||
|
||||
fetchSourceResult = .failure(error)
|
||||
fetchSourcesResult = .failure(error)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
@@ -424,12 +424,12 @@ private extension AppDelegate
|
||||
dispatchGroup.notify(queue: .main) {
|
||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||
{
|
||||
guard let fetchSourceResult = fetchSourceResult else {
|
||||
guard let fetchSourcesResult = fetchSourcesResult else {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
switch fetchSourceResult
|
||||
switch fetchSourcesResult
|
||||
{
|
||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||
case .success: backgroundFetchCompletionHandler(.newData)
|
||||
@@ -439,13 +439,13 @@ private extension AppDelegate
|
||||
}
|
||||
else
|
||||
{
|
||||
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
|
||||
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
|
||||
backgroundFetchCompletionHandler(.failed)
|
||||
return
|
||||
}
|
||||
|
||||
// Call completionHandler early to improve chances of refreshing in the background again.
|
||||
switch (fetchSourceResult, serversResult)
|
||||
switch (fetchSourcesResult, serversResult)
|
||||
{
|
||||
case (.success, .success): backgroundFetchCompletionHandler(.newData)
|
||||
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
||||
|
||||
@@ -64,7 +64,13 @@
|
||||
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"/>
|
||||
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr">
|
||||
<barButtonItem key="rightBarButtonItem" title="Sources" id="6Ul-JW-TMT">
|
||||
<connections>
|
||||
<segue destination="Qo4-72-Hmr" kind="presentation" id="de9-NH-aec"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
@@ -792,7 +798,7 @@ World</string>
|
||||
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="AppIDCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XWu-DU-xbh" customClass="BannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="70" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
@@ -818,7 +824,7 @@ World</string>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="th0-G6-bRt" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
@@ -839,7 +845,7 @@ World</string>
|
||||
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="AppIDsCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="xMh-lD-r6C" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
@@ -923,6 +929,109 @@ World</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2526" y="731"/>
|
||||
</scene>
|
||||
<!--Sources-->
|
||||
<scene sceneID="0S1-zn-9KZ">
|
||||
<objects>
|
||||
<collectionViewController title="Sources" id="cHC-TX-KzQ" customClass="SourcesViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" dataMode="prototypes" id="S36-hD-vu2">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="647"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" systemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="X20-b5-XEP">
|
||||
<size key="itemSize" width="375" height="80"/>
|
||||
<size key="headerReferenceSize" width="50" height="200"/>
|
||||
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
|
||||
</collectionViewFlowLayout>
|
||||
<cells>
|
||||
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="XcN-o4-9qm" customClass="BannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="210" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="LW1-CC-bWu" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="8" y="0.0" width="359" height="80"/>
|
||||
<accessibility key="accessibilityConfiguration">
|
||||
<bool key="isElement" value="YES"/>
|
||||
</accessibility>
|
||||
</view>
|
||||
</subviews>
|
||||
</view>
|
||||
<constraints>
|
||||
<constraint firstAttribute="bottom" secondItem="LW1-CC-bWu" secondAttribute="bottom" id="Pkr-zO-0wx"/>
|
||||
<constraint firstItem="LW1-CC-bWu" firstAttribute="leading" secondItem="XcN-o4-9qm" secondAttribute="leadingMargin" id="egJ-X3-yEz"/>
|
||||
<constraint firstItem="LW1-CC-bWu" firstAttribute="top" secondItem="XcN-o4-9qm" secondAttribute="top" id="glF-aM-4xQ"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="LW1-CC-bWu" secondAttribute="trailing" id="tQx-yV-LTq"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="bannerView" destination="LW1-CC-bWu" id="mwO-Ne-L1L"/>
|
||||
</connections>
|
||||
</collectionViewCell>
|
||||
</cells>
|
||||
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="8N7-JY-mcA" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="200"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Manage sources to control which apps are available to download through AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="TZv-TM-uJj">
|
||||
<rect key="frame" x="8" y="14" width="359" height="171"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
|
||||
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="TZv-TM-uJj" firstAttribute="top" secondItem="8N7-JY-mcA" secondAttribute="top" constant="14" id="2zE-UV-24S"/>
|
||||
<constraint firstAttribute="bottom" secondItem="TZv-TM-uJj" secondAttribute="bottom" constant="15" id="Aml-PC-dko"/>
|
||||
<constraint firstAttribute="trailingMargin" secondItem="TZv-TM-uJj" secondAttribute="trailing" id="V0U-al-5eb"/>
|
||||
<constraint firstAttribute="leadingMargin" secondItem="TZv-TM-uJj" secondAttribute="leading" id="aS5-6Y-rMd"/>
|
||||
</constraints>
|
||||
<connections>
|
||||
<outlet property="textLabel" destination="TZv-TM-uJj" id="kWV-Wv-5gz"/>
|
||||
</connections>
|
||||
</collectionReusableView>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="cHC-TX-KzQ" id="VHQ-ls-gde"/>
|
||||
<outlet property="delegate" destination="cHC-TX-KzQ" id="MWr-Xg-N2k"/>
|
||||
</connections>
|
||||
</collectionView>
|
||||
<navigationItem key="navigationItem" title="Sources" id="QTB-W7-6BG">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="add" id="kBB-5c-8gw">
|
||||
<connections>
|
||||
<action selector="addSource" destination="cHC-TX-KzQ" id="WiB-Jg-NzT"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="NQF-u2-PZv">
|
||||
<connections>
|
||||
<segue destination="zjS-Nr-VTw" kind="unwind" unwindAction="unwindToBrowseViewController:" id="VFy-hV-mNV"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
</collectionViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="TrV-p3-ZAt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
<exit id="zjS-Nr-VTw" userLabel="Exit" sceneMemberID="exit"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3302" y="1430"/>
|
||||
</scene>
|
||||
<!--Navigation Controller-->
|
||||
<scene sceneID="6NV-LQ-gKB">
|
||||
<objects>
|
||||
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="Qo4-72-Hmr" sceneMemberID="viewController">
|
||||
<toolbarItems/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="mcx-oR-qPe">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="108"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<nil name="viewControllers"/>
|
||||
<connections>
|
||||
<segue destination="cHC-TX-KzQ" kind="relationship" relationship="rootViewController" id="BC5-Fs-dCj"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="4mO-93-4qk" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2526" y="1445"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<inferredMetricsTieBreakers>
|
||||
<segue reference="cnd-KK-o60"/>
|
||||
|
||||
@@ -50,6 +50,11 @@ class BrowseViewController: UICollectionViewController
|
||||
self.fetchSource()
|
||||
self.updateDataSource()
|
||||
}
|
||||
|
||||
@IBAction private func unwindToBrowseViewController(_ segue: UIStoryboardSegue)
|
||||
{
|
||||
self.fetchSource()
|
||||
}
|
||||
}
|
||||
|
||||
private extension BrowseViewController
|
||||
@@ -57,17 +62,12 @@ private extension BrowseViewController
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||
{
|
||||
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true), NSSortDescriptor(keyPath: \StoreApp.name, ascending: true)]
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \StoreApp.sourceIdentifier, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \StoreApp.bundleIdentifier, ascending: true)]
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
if let source = Source.fetchAltStoreSource(in: DatabaseManager.shared.viewContext)
|
||||
{
|
||||
fetchRequest.predicate = NSPredicate(format: "%K != %@ AND %K == %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID, #keyPath(StoreApp.source), source)
|
||||
}
|
||||
else
|
||||
{
|
||||
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
}
|
||||
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||
@@ -167,11 +167,11 @@ private extension BrowseViewController
|
||||
{
|
||||
self.loadingState = .loading
|
||||
|
||||
AppManager.shared.fetchSource() { (result) in
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
do
|
||||
{
|
||||
let source = try result.get()
|
||||
try source.managedObjectContext?.save()
|
||||
let sources = try result.get()
|
||||
try sources.first?.managedObjectContext?.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.loadingState = .finished(.success(()))
|
||||
@@ -182,8 +182,8 @@ private extension BrowseViewController
|
||||
DispatchQueue.main.async {
|
||||
if self.dataSource.itemCount > 0
|
||||
{
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
let toastView = ToastView(text: NSLocalizedString("Failed to Fetch Sources", comment: ""), detailText: error.localizedDescription)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
|
||||
self.loadingState = .finished(.failure(error))
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
//
|
||||
// AppIDComponents.swift
|
||||
// BannerCollectionViewCell.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 2/10/20.
|
||||
// Created by Riley Testut on 3/23/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class AppIDCollectionViewCell: UICollectionViewCell
|
||||
class BannerCollectionViewCell: UICollectionViewCell
|
||||
{
|
||||
@IBOutlet var bannerView: AppBannerView!
|
||||
|
||||
@@ -18,13 +18,5 @@ class AppIDCollectionViewCell: UICollectionViewCell
|
||||
|
||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.contentView.preservesSuperviewLayoutMargins = true
|
||||
|
||||
self.bannerView.buttonLabel.text = NSLocalizedString("Expires in", comment: "")
|
||||
self.bannerView.buttonLabel.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
class AppIDsCollectionReusableView: UICollectionReusableView
|
||||
{
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
}
|
||||
14
AltStore/Components/TextCollectionReusableView.swift
Normal file
14
AltStore/Components/TextCollectionReusableView.swift
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// TextCollectionReusableView.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 3/23/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class TextCollectionReusableView: UICollectionReusableView
|
||||
{
|
||||
@IBOutlet var textLabel: UILabel!
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// JSONDecoder+ManagedObjectContext.swift
|
||||
// Harmony
|
||||
//
|
||||
// Created by Riley Testut on 10/3/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
private extension CodingUserInfoKey
|
||||
{
|
||||
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
|
||||
}
|
||||
|
||||
public extension JSONDecoder
|
||||
{
|
||||
var managedObjectContext: NSManagedObjectContext? {
|
||||
get {
|
||||
let managedObjectContext = self.userInfo[.managedObjectContext] as? NSManagedObjectContext
|
||||
return managedObjectContext
|
||||
}
|
||||
set {
|
||||
self.userInfo[.managedObjectContext] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension Decoder
|
||||
{
|
||||
var managedObjectContext: NSManagedObjectContext? {
|
||||
let managedObjectContext = self.userInfo[.managedObjectContext] as? NSManagedObjectContext
|
||||
return managedObjectContext
|
||||
}
|
||||
}
|
||||
64
AltStore/Extensions/JSONDecoder+Properties.swift
Normal file
64
AltStore/Extensions/JSONDecoder+Properties.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// JSONDecoder+Properties.swift
|
||||
// Harmony
|
||||
//
|
||||
// Created by Riley Testut on 10/3/18.
|
||||
// Copyright © 2018 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
extension CodingUserInfoKey
|
||||
{
|
||||
static let managedObjectContext = CodingUserInfoKey(rawValue: "managedObjectContext")!
|
||||
static let sourceURL = CodingUserInfoKey(rawValue: "sourceURL")!
|
||||
}
|
||||
|
||||
public final class JSONDecoder: Foundation.JSONDecoder
|
||||
{
|
||||
@DecoderItem(key: .managedObjectContext)
|
||||
var managedObjectContext: NSManagedObjectContext?
|
||||
|
||||
@DecoderItem(key: .sourceURL)
|
||||
var sourceURL: URL?
|
||||
}
|
||||
|
||||
extension Decoder
|
||||
{
|
||||
var managedObjectContext: NSManagedObjectContext? { self.userInfo[.managedObjectContext] as? NSManagedObjectContext }
|
||||
var sourceURL: URL? { self.userInfo[.sourceURL] as? URL }
|
||||
}
|
||||
|
||||
@propertyWrapper
|
||||
struct DecoderItem<Value>
|
||||
{
|
||||
let key: CodingUserInfoKey
|
||||
|
||||
var wrappedValue: Value? {
|
||||
get { fatalError("only works on instance properties of classes") }
|
||||
set { fatalError("only works on instance properties of classes") }
|
||||
}
|
||||
|
||||
init(key: CodingUserInfoKey)
|
||||
{
|
||||
self.key = key
|
||||
}
|
||||
|
||||
public static subscript<OuterSelf: JSONDecoder>(
|
||||
_enclosingInstance decoder: OuterSelf,
|
||||
wrapped wrappedKeyPath: ReferenceWritableKeyPath<OuterSelf, Value?>,
|
||||
storage storageKeyPath: ReferenceWritableKeyPath<OuterSelf, Self>
|
||||
) -> Value? {
|
||||
get {
|
||||
let wrapper = decoder[keyPath: storageKeyPath]
|
||||
|
||||
let value = decoder.userInfo[wrapper.key] as? Value
|
||||
return value
|
||||
}
|
||||
set {
|
||||
let wrapper = decoder[keyPath: storageKeyPath]
|
||||
decoder.userInfo[wrapper.key] = newValue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,4 +15,13 @@ extension NSError
|
||||
let localizedFailure = (self.userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedFailureErrorKey) as? String)
|
||||
return localizedFailure
|
||||
}
|
||||
|
||||
func withLocalizedFailure(_ failure: String) -> NSError
|
||||
{
|
||||
var userInfo = self.userInfo
|
||||
userInfo[NSLocalizedFailureErrorKey] = failure
|
||||
|
||||
let error = NSError(domain: self.domain, code: self.code, userInfo: userInfo)
|
||||
return error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,26 +170,68 @@ extension AppManager
|
||||
|
||||
extension AppManager
|
||||
{
|
||||
func fetchSource(completionHandler: @escaping (Result<Source, Error>) -> Void)
|
||||
func fetchSource(sourceURL: URL, completionHandler: @escaping (Result<Source, Error>) -> Void)
|
||||
{
|
||||
let fetchSourceOperation = FetchSourceOperation(sourceURL: sourceURL)
|
||||
fetchSourceOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
|
||||
case .success(let source):
|
||||
completionHandler(.success(source))
|
||||
}
|
||||
}
|
||||
|
||||
self.run([fetchSourceOperation])
|
||||
}
|
||||
|
||||
func fetchSources(completionHandler: @escaping (Result<Set<Source>, Error>) -> Void)
|
||||
{
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
guard let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) else {
|
||||
return completionHandler(.failure(OperationError.noSources))
|
||||
}
|
||||
let sources = Source.all(in: context)
|
||||
guard !sources.isEmpty else { return completionHandler(.failure(OperationError.noSources)) }
|
||||
|
||||
let fetchSourceOperation = FetchSourceOperation(sourceURL: source.sourceURL)
|
||||
fetchSourceOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let error):
|
||||
completionHandler(.failure(error))
|
||||
let dispatchGroup = DispatchGroup()
|
||||
var fetchedSources = Set<Source>()
|
||||
var error: Error?
|
||||
|
||||
case .success(let source):
|
||||
completionHandler(.success(source))
|
||||
NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
|
||||
let managedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext()
|
||||
|
||||
let operations = sources.map { (source) -> FetchSourceOperation in
|
||||
dispatchGroup.enter()
|
||||
|
||||
let fetchSourceOperation = FetchSourceOperation(sourceURL: source.sourceURL, managedObjectContext: managedObjectContext)
|
||||
fetchSourceOperation.resultHandler = { (result) in
|
||||
switch result
|
||||
{
|
||||
case .failure(let e): error = e
|
||||
case .success(let source): fetchedSources.insert(source)
|
||||
}
|
||||
|
||||
dispatchGroup.leave()
|
||||
}
|
||||
|
||||
return fetchSourceOperation
|
||||
}
|
||||
self.run([fetchSourceOperation])
|
||||
|
||||
dispatchGroup.notify(queue: .global()) {
|
||||
if let error = error
|
||||
{
|
||||
completionHandler(.failure(error))
|
||||
}
|
||||
else
|
||||
{
|
||||
managedObjectContext.perform {
|
||||
completionHandler(.success(fetchedSources))
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
|
||||
}
|
||||
|
||||
self.run(operations)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||
@@ -76,6 +77,7 @@
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
</entity>
|
||||
@@ -105,8 +107,8 @@
|
||||
<attribute name="identifier" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="sourceURL" attributeType="URI"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<relationship name="apps" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
||||
<relationship name="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
@@ -124,6 +126,7 @@
|
||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="sourceIdentifier" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||
<attribute name="version" attributeType="String"/>
|
||||
@@ -135,6 +138,7 @@
|
||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="sourceIdentifier"/>
|
||||
<constraint value="bundleIdentifier"/>
|
||||
</uniquenessConstraint>
|
||||
</uniquenessConstraints>
|
||||
@@ -159,11 +163,11 @@
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="238"/>
|
||||
<element name="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="118"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -15,8 +15,33 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
||||
{
|
||||
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
||||
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||
return try super.resolve(constraintConflicts: conflicts)
|
||||
for conflict in conflicts
|
||||
{
|
||||
switch conflict.conflictingObjects.first
|
||||
{
|
||||
case is StoreApp where conflict.conflictingObjects.count == 2:
|
||||
// Modified cached StoreApp while replacing it with new one, causing context-level conflict.
|
||||
// Most likely, we set up a relationship between the new StoreApp and a NewsItem,
|
||||
// causing cached StoreApp to delete it's NewsItem relationship, resulting in (resolvable) conflict.
|
||||
|
||||
if let previousApp = conflict.conflictingObjects.first(where: { !$0.isInserted }) as? StoreApp
|
||||
{
|
||||
// Delete previous permissions (same as below).
|
||||
for permission in previousApp.permissions
|
||||
{
|
||||
permission.managedObjectContext?.delete(permission)
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Unknown context-level conflict.
|
||||
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
||||
}
|
||||
}
|
||||
|
||||
try super.resolve(constraintConflicts: conflicts)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for conflict in conflicts
|
||||
|
||||
@@ -26,6 +26,7 @@ class NewsItem: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged var externalURL: URL?
|
||||
|
||||
@NSManaged var appID: String?
|
||||
@NSManaged var sourceIdentifier: String?
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var storeApp: StoreApp?
|
||||
|
||||
@@ -15,7 +15,7 @@ extension Source
|
||||
#if STAGING
|
||||
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
||||
#else
|
||||
static let altStoreSourceURL = URL(string: "https://cdn.altstore.io/file/altstore/apps.json")!
|
||||
static let altStoreSourceURL = URL(string: "https://apps.altstore.io/")!
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -70,55 +70,64 @@ class Source: NSManagedObject, Fetchable, Decodable
|
||||
required init(from decoder: Decoder) throws
|
||||
{
|
||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||
guard let sourceURL = decoder.sourceURL else { preconditionFailure("Decoder must have non-nil sourceURL.") }
|
||||
|
||||
super.init(entity: Source.entity(), insertInto: nil)
|
||||
|
||||
self.sourceURL = sourceURL
|
||||
|
||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||
self.name = try container.decode(String.self, forKey: .name)
|
||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||
self.sourceURL = try container.decode(URL.self, forKey: .sourceURL)
|
||||
|
||||
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||
|
||||
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
||||
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
||||
|
||||
for (index, app) in apps.enumerated()
|
||||
{
|
||||
app.sourceIdentifier = self.identifier
|
||||
app.sortIndex = Int32(index)
|
||||
}
|
||||
|
||||
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||
for (index, item) in newsItems.enumerated()
|
||||
{
|
||||
item.sourceIdentifier = self.identifier
|
||||
item.sortIndex = Int32(index)
|
||||
}
|
||||
|
||||
context.insert(self)
|
||||
|
||||
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
||||
|
||||
for newsItem in newsItems
|
||||
{
|
||||
newsItem.source = self
|
||||
|
||||
guard let appID = newsItem.appID else { continue }
|
||||
|
||||
if let storeApp = appsByID[appID]
|
||||
{
|
||||
newsItem.storeApp = storeApp
|
||||
}
|
||||
else
|
||||
{
|
||||
newsItem.storeApp = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Must assign after we're inserted into context.
|
||||
self._apps = NSMutableOrderedSet(array: apps)
|
||||
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
||||
|
||||
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
|
||||
}
|
||||
}
|
||||
|
||||
extension Source
|
||||
{
|
||||
@nonobjc class func fetchRequest() -> NSFetchRequest<Source>
|
||||
{
|
||||
return NSFetchRequest<Source>(entityName: "Source")
|
||||
}
|
||||
|
||||
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source
|
||||
{
|
||||
let source = Source(context: context)
|
||||
|
||||
@@ -46,12 +46,26 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
||||
@NSManaged private(set) var tintColor: UIColor?
|
||||
@NSManaged private(set) var isBeta: Bool
|
||||
|
||||
@NSManaged var sourceIdentifier: String?
|
||||
|
||||
@NSManaged var sortIndex: Int32
|
||||
|
||||
/* Relationships */
|
||||
@NSManaged var installedApp: InstalledApp?
|
||||
@NSManaged var source: Source?
|
||||
@objc(permissions) @NSManaged var _permissions: NSOrderedSet
|
||||
@NSManaged var newsItems: Set<NewsItem>
|
||||
|
||||
@NSManaged @objc(source) var _source: Source?
|
||||
@NSManaged @objc(permissions) var _permissions: NSOrderedSet
|
||||
|
||||
@nonobjc var source: Source? {
|
||||
set {
|
||||
self._source = newValue
|
||||
self.sourceIdentifier = newValue?.identifier
|
||||
}
|
||||
get {
|
||||
return self._source
|
||||
}
|
||||
}
|
||||
|
||||
@nonobjc var permissions: [AppPermission] {
|
||||
return self._permissions.array as! [AppPermission]
|
||||
|
||||
@@ -99,9 +99,11 @@ private extension NewsViewController
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
|
||||
{
|
||||
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: false)]
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.date, ascending: false),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: true),
|
||||
NSSortDescriptor(keyPath: \NewsItem.sourceIdentifier, ascending: true)]
|
||||
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.sortIndex), cacheName: nil)
|
||||
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.objectID), cacheName: nil)
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||
dataSource.proxy = self
|
||||
@@ -165,11 +167,11 @@ private extension NewsViewController
|
||||
{
|
||||
self.loadingState = .loading
|
||||
|
||||
AppManager.shared.fetchSource() { (result) in
|
||||
AppManager.shared.fetchSources() { (result) in
|
||||
do
|
||||
{
|
||||
let source = try result.get()
|
||||
try source.managedObjectContext?.save()
|
||||
let sources = try result.get()
|
||||
try sources.first?.managedObjectContext?.save()
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.loadingState = .finished(.success(()))
|
||||
@@ -180,7 +182,7 @@ private extension NewsViewController
|
||||
DispatchQueue.main.async {
|
||||
if self.dataSource.itemCount > 0
|
||||
{
|
||||
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||
let toastView = ToastView(text: NSLocalizedString("Failed to Fetch Sources", comment: ""), detailText: error.localizedDescription)
|
||||
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
|
||||
import Roxas
|
||||
|
||||
@objc(FetchSourceOperation)
|
||||
class FetchSourceOperation: ResultOperation<Source>
|
||||
{
|
||||
let sourceURL: URL
|
||||
let managedObjectContext: NSManagedObjectContext
|
||||
|
||||
private let session: URLSession
|
||||
|
||||
@@ -21,9 +24,10 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
return dateFormatter
|
||||
}()
|
||||
|
||||
init(sourceURL: URL)
|
||||
init(sourceURL: URL, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
|
||||
{
|
||||
self.sourceURL = sourceURL
|
||||
self.managedObjectContext = managedObjectContext
|
||||
|
||||
let configuration = URLSessionConfiguration.default
|
||||
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||
@@ -37,7 +41,7 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
super.main()
|
||||
|
||||
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
self.managedObjectContext.perform {
|
||||
do
|
||||
{
|
||||
let (data, _) = try Result((data, response), error).get()
|
||||
@@ -64,19 +68,16 @@ class FetchSourceOperation: ResultOperation<Source>
|
||||
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Date is in invalid format.")
|
||||
})
|
||||
|
||||
decoder.managedObjectContext = context
|
||||
decoder.managedObjectContext = self.managedObjectContext
|
||||
decoder.sourceURL = self.sourceURL
|
||||
|
||||
let source = try decoder.decode(Source.self, from: data)
|
||||
|
||||
if let patreonAccessToken = source.userInfo?[.patreonAccessToken]
|
||||
if source.identifier == Source.altStoreIdentifier, let patreonAccessToken = source.userInfo?[.patreonAccessToken]
|
||||
{
|
||||
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
|
||||
}
|
||||
|
||||
#if STAGING
|
||||
source.sourceURL = self.sourceURL
|
||||
#endif
|
||||
|
||||
self.finish(.success(source))
|
||||
}
|
||||
catch
|
||||
|
||||
179
AltStore/Sources/SourcesViewController.swift
Normal file
179
AltStore/Sources/SourcesViewController.swift
Normal file
@@ -0,0 +1,179 @@
|
||||
//
|
||||
// SourcesViewController.swift
|
||||
// AltStore
|
||||
//
|
||||
// Created by Riley Testut on 3/17/20.
|
||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
import Roxas
|
||||
|
||||
class SourcesViewController: UICollectionViewController
|
||||
{
|
||||
private lazy var dataSource = self.makeDataSource()
|
||||
|
||||
override func viewDidLoad()
|
||||
{
|
||||
super.viewDidLoad()
|
||||
|
||||
self.collectionView.dataSource = self.dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension SourcesViewController
|
||||
{
|
||||
func makeDataSource() -> RSTFetchedResultsCollectionViewDataSource<Source>
|
||||
{
|
||||
let fetchRequest = Source.fetchRequest() as NSFetchRequest<Source>
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \Source.name, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Source.sourceURL, ascending: true),
|
||||
NSSortDescriptor(keyPath: \Source.identifier, ascending: true)]
|
||||
fetchRequest.returnsObjectsAsFaults = false
|
||||
|
||||
let dataSource = RSTFetchedResultsCollectionViewDataSource<Source>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||
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.betaBadgeView.isHidden = true
|
||||
cell.bannerView.buttonLabel.isHidden = true
|
||||
cell.bannerView.button.isHidden = true
|
||||
cell.bannerView.button.isIndicatingActivity = false
|
||||
|
||||
cell.bannerView.titleLabel.text = source.name
|
||||
cell.bannerView.subtitleLabel.text = source.sourceURL.absoluteString
|
||||
cell.bannerView.subtitleLabel.numberOfLines = 2
|
||||
|
||||
// Make sure refresh button is correct size.
|
||||
cell.layoutIfNeeded()
|
||||
}
|
||||
|
||||
return dataSource
|
||||
}
|
||||
}
|
||||
|
||||
private extension SourcesViewController
|
||||
{
|
||||
@IBAction func addSource()
|
||||
{
|
||||
func addSource(url: URL)
|
||||
{
|
||||
AppManager.shared.fetchSource(sourceURL: url) { (result) in
|
||||
do
|
||||
{
|
||||
let source = try result.get()
|
||||
try source.managedObjectContext?.save()
|
||||
}
|
||||
catch let error as NSError
|
||||
{
|
||||
let error = error.withLocalizedFailure(NSLocalizedString("Could not add source.", comment: ""))
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let toastView = ToastView(error: error)
|
||||
toastView.show(in: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
guard let text = alertController.textFields![0].text, let sourceURL = URL(string: text) else { return }
|
||||
addSource(url: sourceURL)
|
||||
})
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
// Use this view to calculate the optimal size based on the collection view's width
|
||||
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
|
||||
withHorizontalFittingPriority: .required, // Width is fixed
|
||||
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
|
||||
return size
|
||||
}
|
||||
|
||||
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||
{
|
||||
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: "Header", for: indexPath) as! TextCollectionReusableView
|
||||
headerView.layoutMargins.left = self.view.layoutMargins.left
|
||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||
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)
|
||||
guard source.identifier != Source.altStoreIdentifier else { return nil }
|
||||
|
||||
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in
|
||||
let deleteAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
|
||||
|
||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||
let source = context.object(with: source.objectID) as! Source
|
||||
context.delete(source)
|
||||
|
||||
do
|
||||
{
|
||||
try context.save()
|
||||
}
|
||||
catch
|
||||
{
|
||||
print("Failed to save source context.", error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let menu = UIMenu(title: "", children: [deleteAction])
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user