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 */; };
|
BF6C336224197D700034FD24 /* NSError+LocalizedFailure.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6C336124197D700034FD24 /* NSError+LocalizedFailure.swift */; };
|
||||||
BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; };
|
BF6C33652419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = BF6C33642419AE310034FD24 /* AltStore4ToAltStore5.xcmappingmodel */; };
|
||||||
BF6C8FAC242935ED00125131 /* NSAttributedString+Markdown.m in Sources */ = {isa = PBXBuildFile; fileRef = BF6C8FAA242935ED00125131 /* NSAttributedString+Markdown.m */; };
|
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 */; };
|
BF6F439223644C6E00A0B879 /* RefreshAltStoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF6F439123644C6E00A0B879 /* RefreshAltStoreViewController.swift */; };
|
||||||
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; };
|
BF718BC923C919E300A89F2D /* CFNotificationName+AltStore.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BC823C919E300A89F2D /* CFNotificationName+AltStore.m */; };
|
||||||
BF718BD123C91BD300A89F2D /* ALTWiredConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = BF718BD023C91BD300A89F2D /* ALTWiredConnection.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 */; };
|
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172A23C5633D001B5953 /* FetchAnisetteDataOperation.swift */; };
|
||||||
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; };
|
BFA8172D23C5823E001B5953 /* InstalledExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFA8172C23C5823E001B5953 /* InstalledExtension.swift */; };
|
||||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB11691229322E400BB457C /* DatabaseManager.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 */; };
|
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB364592325985F00CD0EB1 /* FindServerOperation.swift */; };
|
||||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
||||||
BFB49AAA23834CF900D542D9 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; };
|
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 */; };
|
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A642416C72400EB891E /* DeactivateAppOperation.swift */; };
|
||||||
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; };
|
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFC57A6D2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift */; };
|
||||||
BFC57A702416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFC57A6F2416FC7600EB891E /* InstalledAppsCollectionHeaderView.xib */; };
|
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 */; };
|
BFD2476E2284B9A500981D42 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFD2476D2284B9A500981D42 /* AppDelegate.swift */; };
|
||||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
|
BFD247752284B9A500981D42 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = BFD247732284B9A500981D42 /* Main.storyboard */; };
|
||||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = BFD247762284B9A700981D42 /* Assets.xcassets */; };
|
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 */; };
|
BFE6326622A857C200F30809 /* Team.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326522A857C100F30809 /* Team.swift */; };
|
||||||
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
|
BFE6326822A858F300F30809 /* Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326722A858F300F30809 /* Account.swift */; };
|
||||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE6326B22A86FF300F30809 /* AuthenticationOperation.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 */; };
|
BFF0B68E23219520007A79E1 /* PatreonViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68D23219520007A79E1 /* PatreonViewController.swift */; };
|
||||||
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
|
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFF0B68F23219C6D007A79E1 /* PatreonComponents.swift */; };
|
||||||
BFF0B6922321A305007A79E1 /* AboutPatreonHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */; };
|
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>"; };
|
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; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
BFF0B6912321A305007A79E1 /* AboutPatreonHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AboutPatreonHeaderView.xib; sourceTree = "<group>"; };
|
||||||
@@ -876,7 +880,6 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
|
BF56D2AE23DF9E310006506D /* AppIDsViewController.swift */,
|
||||||
BFEE944023F22AA100CDA07D /* AppIDComponents.swift */,
|
|
||||||
);
|
);
|
||||||
path = "App IDs";
|
path = "App IDs";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -982,6 +985,14 @@
|
|||||||
path = Server;
|
path = Server;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
BFC84A4B2421A13000853474 /* Sources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BFC84A4C2421A19100853474 /* SourcesViewController.swift */,
|
||||||
|
);
|
||||||
|
path = Sources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
BFD247612284B9A500981D42 = {
|
BFD247612284B9A500981D42 = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -1025,6 +1036,7 @@
|
|||||||
BFD5D6E6230CC94B007955AB /* Patreon */,
|
BFD5D6E6230CC94B007955AB /* Patreon */,
|
||||||
BFD2478A2284C49000981D42 /* Managing Apps */,
|
BFD2478A2284C49000981D42 /* Managing Apps */,
|
||||||
BF56D2AD23DF9E170006506D /* App IDs */,
|
BF56D2AD23DF9E170006506D /* App IDs */,
|
||||||
|
BFC84A4B2421A13000853474 /* Sources */,
|
||||||
BFC51D7922972F1F00388324 /* Server */,
|
BFC51D7922972F1F00388324 /* Server */,
|
||||||
BFD247982284D7FC00981D42 /* Model */,
|
BFD247982284D7FC00981D42 /* Model */,
|
||||||
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
|
BFDB6A0922AAEDA1007EA6D6 /* Operations */,
|
||||||
@@ -1078,6 +1090,8 @@
|
|||||||
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
||||||
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
||||||
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
||||||
|
BF6C8FAD2429597900125131 /* BannerCollectionViewCell.swift */,
|
||||||
|
BF6C8FAF2429599900125131 /* TextCollectionReusableView.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1128,7 +1142,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */,
|
BFD2479E2284FBD000981D42 /* UIColor+AltStore.swift */,
|
||||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */,
|
BFB1169A2293274D00BB457C /* JSONDecoder+Properties.swift */,
|
||||||
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */,
|
BF43002F22A71C960051E2BC /* UserDefaults+AltStore.swift */,
|
||||||
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
|
BF9ABA4E22DD41A9008935CF /* UIColor+Hex.swift */,
|
||||||
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
|
BFDB5B1522EE90D300F74113 /* Date+RelativeDate.swift */,
|
||||||
@@ -1710,7 +1724,6 @@
|
|||||||
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
|
BFD5D6F6230DDB12007955AB /* Tier.swift in Sources */,
|
||||||
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
BFB11692229322E400BB457C /* DatabaseManager.swift in Sources */,
|
||||||
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
BF56D2AC23DF8E170006506D /* FetchAppIDsOperation.swift in Sources */,
|
||||||
BFEE944123F22AA100CDA07D /* AppIDComponents.swift in Sources */,
|
|
||||||
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */,
|
BF26A0E12370C5D400F53F9F /* ALTSourceUserInfoKey.m in Sources */,
|
||||||
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
BFC1F38D22AEE3A4003AC21A /* DownloadAppOperation.swift in Sources */,
|
||||||
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
|
BF54E8212315EF0D000AE0D8 /* ALTPatreonBenefitType.m in Sources */,
|
||||||
@@ -1747,6 +1760,7 @@
|
|||||||
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
BF100C50232D7CD1006A8926 /* AltStoreToAltStore2.xcmappingmodel in Sources */,
|
||||||
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
BF3D64B022E8D4B800E9056B /* AppContentViewControllerCells.swift in Sources */,
|
||||||
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
BFC57A6E2416FC5D00EB891E /* InstalledAppsCollectionHeaderView.swift in Sources */,
|
||||||
|
BF6C8FAE2429597900125131 /* BannerCollectionViewCell.swift in Sources */,
|
||||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */,
|
||||||
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
BF56D2AA23DF88310006506D /* AppID.swift in Sources */,
|
||||||
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
BF02419422F2156E00129732 /* RefreshAttempt.swift in Sources */,
|
||||||
@@ -1754,7 +1768,7 @@
|
|||||||
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
|
BFE60742231B07E6002B0E8E /* SettingsHeaderFooterView.swift in Sources */,
|
||||||
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
BFE338E822F10E56002E24B9 /* LaunchViewController.swift in Sources */,
|
||||||
BFA8172B23C5633D001B5953 /* FetchAnisetteDataOperation.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 */,
|
BF9ABA4722DD0638008935CF /* BrowseCollectionViewCell.swift in Sources */,
|
||||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
|
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */,
|
||||||
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
|
BFD6B03322DFF20800B86064 /* MyAppsComponents.swift in Sources */,
|
||||||
@@ -1771,6 +1785,7 @@
|
|||||||
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
|
BF0F5FC723F394AD0080DB64 /* AltStore3ToAltStore4.xcmappingmodel in Sources */,
|
||||||
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
BF02419622F2199300129732 /* RefreshAttemptsViewController.swift in Sources */,
|
||||||
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
BF08858322DE795100DE9F1E /* MyAppsViewController.swift in Sources */,
|
||||||
|
BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */,
|
||||||
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
|
BF9ABA4F22DD41A9008935CF /* UIColor+Hex.swift in Sources */,
|
||||||
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */,
|
||||||
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
BFD52BD422A0800A000B7ED1 /* ServerManager.swift in Sources */,
|
||||||
@@ -1788,6 +1803,7 @@
|
|||||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||||
|
BF6C8FB02429599900125131 /* TextCollectionReusableView.swift in Sources */,
|
||||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
||||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ private extension AppIDsViewController
|
|||||||
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
|
dataSource.cellConfigurationHandler = { (cell, appID, indexPath) in
|
||||||
let tintColor = UIColor.altPrimary
|
let tintColor = UIColor.altPrimary
|
||||||
|
|
||||||
let cell = cell as! AppIDCollectionViewCell
|
let cell = cell as! BannerCollectionViewCell
|
||||||
cell.layoutMargins.left = self.view.layoutMargins.left
|
cell.layoutMargins.left = self.view.layoutMargins.left
|
||||||
cell.layoutMargins.right = self.view.layoutMargins.right
|
cell.layoutMargins.right = self.view.layoutMargins.right
|
||||||
cell.tintColor = tintColor
|
cell.tintColor = tintColor
|
||||||
@@ -80,6 +80,8 @@ private extension AppIDsViewController
|
|||||||
cell.bannerView.button.isIndicatingActivity = false
|
cell.bannerView.button.isIndicatingActivity = false
|
||||||
cell.bannerView.betaBadgeView.isHidden = true
|
cell.bannerView.betaBadgeView.isHidden = true
|
||||||
|
|
||||||
|
cell.bannerView.buttonLabel.text = NSLocalizedString("Expires in…", comment: "")
|
||||||
|
|
||||||
if let expirationDate = appID.expirationDate
|
if let expirationDate = appID.expirationDate
|
||||||
{
|
{
|
||||||
cell.bannerView.button.isHidden = false
|
cell.bannerView.button.isHidden = false
|
||||||
@@ -181,7 +183,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
switch kind
|
switch kind
|
||||||
{
|
{
|
||||||
case UICollectionView.elementKindSectionHeader:
|
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.left = self.view.layoutMargins.left
|
||||||
headerView.layoutMargins.right = self.view.layoutMargins.right
|
headerView.layoutMargins.right = self.view.layoutMargins.right
|
||||||
|
|
||||||
@@ -208,7 +210,7 @@ extension AppIDsViewController: UICollectionViewDelegateFlowLayout
|
|||||||
return headerView
|
return headerView
|
||||||
|
|
||||||
case UICollectionView.elementKindSectionFooter:
|
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
|
let count = self.dataSource.itemCount
|
||||||
if count == 1
|
if count == 1
|
||||||
|
|||||||
@@ -250,20 +250,20 @@ private extension AppDelegate
|
|||||||
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
backgroundFetchCompletionHandler: @escaping (UIBackgroundFetchResult) -> Void,
|
||||||
completionHandler: @escaping (Result<[String: Result<InstalledApp, Error>], Error>) -> 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>?
|
var serversResult: Result<Void, Error>?
|
||||||
|
|
||||||
let dispatchGroup = DispatchGroup()
|
let dispatchGroup = DispatchGroup()
|
||||||
dispatchGroup.enter()
|
dispatchGroup.enter()
|
||||||
|
|
||||||
AppManager.shared.fetchSource() { (result) in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
fetchSourceResult = result
|
fetchSourcesResult = result
|
||||||
|
|
||||||
do
|
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>
|
let previousUpdatesFetchRequest = InstalledApp.updatesFetchRequest() as! NSFetchRequest<NSFetchRequestResult>
|
||||||
previousUpdatesFetchRequest.includesPendingChanges = false
|
previousUpdatesFetchRequest.includesPendingChanges = false
|
||||||
@@ -331,7 +331,7 @@ private extension AppDelegate
|
|||||||
{
|
{
|
||||||
print("Error fetching apps:", error)
|
print("Error fetching apps:", error)
|
||||||
|
|
||||||
fetchSourceResult = .failure(error)
|
fetchSourcesResult = .failure(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatchGroup.leave()
|
dispatchGroup.leave()
|
||||||
@@ -424,12 +424,12 @@ private extension AppDelegate
|
|||||||
dispatchGroup.notify(queue: .main) {
|
dispatchGroup.notify(queue: .main) {
|
||||||
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
if !UserDefaults.standard.isBackgroundRefreshEnabled
|
||||||
{
|
{
|
||||||
guard let fetchSourceResult = fetchSourceResult else {
|
guard let fetchSourcesResult = fetchSourcesResult else {
|
||||||
backgroundFetchCompletionHandler(.failed)
|
backgroundFetchCompletionHandler(.failed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch fetchSourceResult
|
switch fetchSourcesResult
|
||||||
{
|
{
|
||||||
case .failure: backgroundFetchCompletionHandler(.failed)
|
case .failure: backgroundFetchCompletionHandler(.failed)
|
||||||
case .success: backgroundFetchCompletionHandler(.newData)
|
case .success: backgroundFetchCompletionHandler(.newData)
|
||||||
@@ -439,13 +439,13 @@ private extension AppDelegate
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
guard let fetchSourceResult = fetchSourceResult, let serversResult = serversResult else {
|
guard let fetchSourcesResult = fetchSourcesResult, let serversResult = serversResult else {
|
||||||
backgroundFetchCompletionHandler(.failed)
|
backgroundFetchCompletionHandler(.failed)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call completionHandler early to improve chances of refreshing in the background again.
|
// 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, .success): backgroundFetchCompletionHandler(.newData)
|
||||||
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
case (.success, .failure(ConnectionError.serverNotFound)): backgroundFetchCompletionHandler(.newData)
|
||||||
|
|||||||
@@ -64,7 +64,13 @@
|
|||||||
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
|
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
|
||||||
</connections>
|
</connections>
|
||||||
</collectionView>
|
</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>
|
</collectionViewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
@@ -792,7 +798,7 @@ World</string>
|
|||||||
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
|
<inset key="sectionInset" minX="0.0" minY="10" maxX="0.0" maxY="20"/>
|
||||||
</collectionViewFlowLayout>
|
</collectionViewFlowLayout>
|
||||||
<cells>
|
<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"/>
|
<rect key="frame" x="0.0" y="70" width="375" height="80"/>
|
||||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
|
||||||
@@ -818,7 +824,7 @@ World</string>
|
|||||||
</connections>
|
</connections>
|
||||||
</collectionViewCell>
|
</collectionViewCell>
|
||||||
</cells>
|
</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"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="60"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -839,7 +845,7 @@ World</string>
|
|||||||
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
<outlet property="textLabel" destination="83Z-Ih-nOW" id="xxM-HD-iJS"/>
|
||||||
</connections>
|
</connections>
|
||||||
</collectionReusableView>
|
</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"/>
|
<rect key="frame" x="0.0" y="170" width="375" height="50"/>
|
||||||
<autoresizingMask key="autoresizingMask"/>
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
@@ -923,6 +929,109 @@ World</string>
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="2526" y="731"/>
|
<point key="canvasLocation" x="2526" y="731"/>
|
||||||
</scene>
|
</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>
|
</scenes>
|
||||||
<inferredMetricsTieBreakers>
|
<inferredMetricsTieBreakers>
|
||||||
<segue reference="cnd-KK-o60"/>
|
<segue reference="cnd-KK-o60"/>
|
||||||
|
|||||||
@@ -50,6 +50,11 @@ class BrowseViewController: UICollectionViewController
|
|||||||
self.fetchSource()
|
self.fetchSource()
|
||||||
self.updateDataSource()
|
self.updateDataSource()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@IBAction private func unwindToBrowseViewController(_ segue: UIStoryboardSegue)
|
||||||
|
{
|
||||||
|
self.fetchSource()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension BrowseViewController
|
private extension BrowseViewController
|
||||||
@@ -57,17 +62,12 @@ private extension BrowseViewController
|
|||||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>
|
||||||
{
|
{
|
||||||
let fetchRequest = StoreApp.fetchRequest() as NSFetchRequest<StoreApp>
|
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
|
fetchRequest.returnsObjectsAsFaults = false
|
||||||
|
fetchRequest.predicate = NSPredicate(format: "%K != %@", #keyPath(StoreApp.bundleIdentifier), StoreApp.altstoreAppID)
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<StoreApp, UIImage>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext)
|
||||||
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
dataSource.cellConfigurationHandler = { (cell, app, indexPath) in
|
||||||
@@ -167,11 +167,11 @@ private extension BrowseViewController
|
|||||||
{
|
{
|
||||||
self.loadingState = .loading
|
self.loadingState = .loading
|
||||||
|
|
||||||
AppManager.shared.fetchSource() { (result) in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let source = try result.get()
|
let sources = try result.get()
|
||||||
try source.managedObjectContext?.save()
|
try sources.first?.managedObjectContext?.save()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingState = .finished(.success(()))
|
self.loadingState = .finished(.success(()))
|
||||||
@@ -182,8 +182,8 @@ private extension BrowseViewController
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.dataSource.itemCount > 0
|
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)
|
toastView.show(in: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.loadingState = .finished(.failure(error))
|
self.loadingState = .finished(.failure(error))
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
//
|
//
|
||||||
// AppIDComponents.swift
|
// BannerCollectionViewCell.swift
|
||||||
// AltStore
|
// AltStore
|
||||||
//
|
//
|
||||||
// Created by Riley Testut on 2/10/20.
|
// Created by Riley Testut on 3/23/20.
|
||||||
// Copyright © 2020 Riley Testut. All rights reserved.
|
// Copyright © 2020 Riley Testut. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
class AppIDCollectionViewCell: UICollectionViewCell
|
class BannerCollectionViewCell: UICollectionViewCell
|
||||||
{
|
{
|
||||||
@IBOutlet var bannerView: AppBannerView!
|
@IBOutlet var bannerView: AppBannerView!
|
||||||
|
|
||||||
@@ -18,13 +18,5 @@ class AppIDCollectionViewCell: UICollectionViewCell
|
|||||||
|
|
||||||
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||||
self.contentView.preservesSuperviewLayoutMargins = true
|
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)
|
let localizedFailure = (self.userInfo[NSLocalizedFailureErrorKey] as? String) ?? (NSError.userInfoValueProvider(forDomain: self.domain)?(self, NSLocalizedFailureErrorKey) as? String)
|
||||||
return localizedFailure
|
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
|
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
|
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
||||||
guard let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) else {
|
let sources = Source.all(in: context)
|
||||||
return completionHandler(.failure(OperationError.noSources))
|
guard !sources.isEmpty else { return completionHandler(.failure(OperationError.noSources)) }
|
||||||
|
|
||||||
|
let dispatchGroup = DispatchGroup()
|
||||||
|
var fetchedSources = Set<Source>()
|
||||||
|
var error: Error?
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
let fetchSourceOperation = FetchSourceOperation(sourceURL: source.sourceURL)
|
dispatchGroup.notify(queue: .global()) {
|
||||||
fetchSourceOperation.resultHandler = { (result) in
|
if let error = error
|
||||||
switch result
|
|
||||||
{
|
{
|
||||||
case .failure(let error):
|
|
||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
|
|
||||||
case .success(let source):
|
|
||||||
completionHandler(.success(source))
|
|
||||||
NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
|
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
managedObjectContext.perform {
|
||||||
|
completionHandler(.success(fetchedSources))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.post(name: AppManager.didFetchSourceNotification, object: self)
|
||||||
}
|
}
|
||||||
self.run([fetchSourceOperation])
|
|
||||||
|
self.run(operations)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
<attribute name="imageURL" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||||
<attribute name="sortIndex" 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="tintColor" optional="YES" attributeType="Transformable"/>
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
<attribute name="title" attributeType="String"/>
|
<attribute name="title" attributeType="String"/>
|
||||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source"/>
|
||||||
@@ -76,6 +77,7 @@
|
|||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
<constraint value="identifier"/>
|
<constraint value="identifier"/>
|
||||||
|
<constraint value="sourceIdentifier"/>
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
@@ -105,8 +107,8 @@
|
|||||||
<attribute name="identifier" attributeType="String"/>
|
<attribute name="identifier" attributeType="String"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
<attribute name="sourceURL" attributeType="URI"/>
|
<attribute name="sourceURL" attributeType="URI"/>
|
||||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp"/>
|
<relationship name="apps" toMany="YES" deletionRule="Cascade" 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="newsItems" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
<constraint value="identifier"/>
|
<constraint value="identifier"/>
|
||||||
@@ -124,6 +126,7 @@
|
|||||||
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
<attribute name="screenshotURLs" attributeType="Transformable"/>
|
||||||
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
<attribute name="size" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
<attribute name="sortIndex" 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="subtitle" optional="YES" attributeType="String"/>
|
||||||
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
<attribute name="tintColor" optional="YES" attributeType="Transformable"/>
|
||||||
<attribute name="version" attributeType="String"/>
|
<attribute name="version" attributeType="String"/>
|
||||||
@@ -135,6 +138,7 @@
|
|||||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
|
<constraint value="sourceIdentifier"/>
|
||||||
<constraint value="bundleIdentifier"/>
|
<constraint value="bundleIdentifier"/>
|
||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
@@ -159,11 +163,11 @@
|
|||||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||||
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="223"/>
|
||||||
<element name="InstalledExtension" positionX="-45" positionY="135" width="128" height="163"/>
|
<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="PatreonAccount" positionX="-45" positionY="117" width="128" height="105"/>
|
||||||
<element name="RefreshAttempt" 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="Source" positionX="-45" positionY="99" width="128" height="118"/>
|
||||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="343"/>
|
||||||
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
<element name="Team" positionX="-45" positionY="81" width="128" height="148"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
@@ -15,8 +15,33 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
open override func resolve(constraintConflicts conflicts: [NSConstraintConflict]) throws
|
||||||
{
|
{
|
||||||
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
guard conflicts.allSatisfy({ $0.databaseObject != nil }) else {
|
||||||
assertionFailure("MergePolicy is only intended to work with database-level conflicts.")
|
for conflict in conflicts
|
||||||
return try super.resolve(constraintConflicts: 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
|
for conflict in conflicts
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class NewsItem: NSManagedObject, Decodable, Fetchable
|
|||||||
@NSManaged var externalURL: URL?
|
@NSManaged var externalURL: URL?
|
||||||
|
|
||||||
@NSManaged var appID: String?
|
@NSManaged var appID: String?
|
||||||
|
@NSManaged var sourceIdentifier: String?
|
||||||
|
|
||||||
/* Relationships */
|
/* Relationships */
|
||||||
@NSManaged var storeApp: StoreApp?
|
@NSManaged var storeApp: StoreApp?
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ extension Source
|
|||||||
#if STAGING
|
#if STAGING
|
||||||
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
static let altStoreSourceURL = URL(string: "https://f000.backblazeb2.com/file/altstore-staging/apps-staging.json")!
|
||||||
#else
|
#else
|
||||||
static let altStoreSourceURL = URL(string: "https://cdn.altstore.io/file/altstore/apps.json")!
|
static let altStoreSourceURL = URL(string: "https://apps.altstore.io/")!
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,55 +70,64 @@ class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
required init(from decoder: Decoder) throws
|
required init(from decoder: Decoder) throws
|
||||||
{
|
{
|
||||||
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
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)
|
super.init(entity: Source.entity(), insertInto: nil)
|
||||||
|
|
||||||
|
self.sourceURL = sourceURL
|
||||||
|
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
self.name = try container.decode(String.self, forKey: .name)
|
self.name = try container.decode(String.self, forKey: .name)
|
||||||
self.identifier = try container.decode(String.self, forKey: .identifier)
|
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)
|
let userInfo = try container.decodeIfPresent([String: String].self, forKey: .userInfo)
|
||||||
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
self.userInfo = userInfo?.reduce(into: [:]) { $0[ALTSourceUserInfoKey($1.key)] = $1.value }
|
||||||
|
|
||||||
let apps = try container.decodeIfPresent([StoreApp].self, forKey: .apps) ?? []
|
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()
|
for (index, app) in apps.enumerated()
|
||||||
{
|
{
|
||||||
|
app.sourceIdentifier = self.identifier
|
||||||
app.sortIndex = Int32(index)
|
app.sortIndex = Int32(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||||
for (index, item) in newsItems.enumerated()
|
for (index, item) in newsItems.enumerated()
|
||||||
{
|
{
|
||||||
|
item.sourceIdentifier = self.identifier
|
||||||
item.sortIndex = Int32(index)
|
item.sortIndex = Int32(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
context.insert(self)
|
context.insert(self)
|
||||||
|
|
||||||
let appsByID = Dictionary(apps.map { ($0.bundleIdentifier, $0) }, uniquingKeysWith: { (a, b) in return a })
|
|
||||||
|
|
||||||
for newsItem in newsItems
|
for newsItem in newsItems
|
||||||
{
|
{
|
||||||
newsItem.source = self
|
|
||||||
|
|
||||||
guard let appID = newsItem.appID else { continue }
|
guard let appID = newsItem.appID else { continue }
|
||||||
|
|
||||||
if let storeApp = appsByID[appID]
|
if let storeApp = appsByID[appID]
|
||||||
{
|
{
|
||||||
newsItem.storeApp = storeApp
|
newsItem.storeApp = storeApp
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
newsItem.storeApp = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Must assign after we're inserted into context.
|
// Must assign after we're inserted into context.
|
||||||
self._apps = NSMutableOrderedSet(array: apps)
|
self._apps = NSMutableOrderedSet(array: apps)
|
||||||
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
||||||
|
|
||||||
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Source
|
extension Source
|
||||||
{
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<Source>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<Source>(entityName: "Source")
|
||||||
|
}
|
||||||
|
|
||||||
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source
|
class func makeAltStoreSource(in context: NSManagedObjectContext) -> Source
|
||||||
{
|
{
|
||||||
let source = Source(context: context)
|
let source = Source(context: context)
|
||||||
|
|||||||
@@ -46,12 +46,26 @@ class StoreApp: NSManagedObject, Decodable, Fetchable
|
|||||||
@NSManaged private(set) var tintColor: UIColor?
|
@NSManaged private(set) var tintColor: UIColor?
|
||||||
@NSManaged private(set) var isBeta: Bool
|
@NSManaged private(set) var isBeta: Bool
|
||||||
|
|
||||||
|
@NSManaged var sourceIdentifier: String?
|
||||||
|
|
||||||
@NSManaged var sortIndex: Int32
|
@NSManaged var sortIndex: Int32
|
||||||
|
|
||||||
/* Relationships */
|
/* Relationships */
|
||||||
@NSManaged var installedApp: InstalledApp?
|
@NSManaged var installedApp: InstalledApp?
|
||||||
@NSManaged var source: Source?
|
@NSManaged var newsItems: Set<NewsItem>
|
||||||
@objc(permissions) @NSManaged var _permissions: NSOrderedSet
|
|
||||||
|
@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] {
|
@nonobjc var permissions: [AppPermission] {
|
||||||
return self._permissions.array as! [AppPermission]
|
return self._permissions.array as! [AppPermission]
|
||||||
|
|||||||
@@ -99,9 +99,11 @@ private extension NewsViewController
|
|||||||
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
|
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
|
||||||
{
|
{
|
||||||
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
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)
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||||
dataSource.proxy = self
|
dataSource.proxy = self
|
||||||
@@ -165,11 +167,11 @@ private extension NewsViewController
|
|||||||
{
|
{
|
||||||
self.loadingState = .loading
|
self.loadingState = .loading
|
||||||
|
|
||||||
AppManager.shared.fetchSource() { (result) in
|
AppManager.shared.fetchSources() { (result) in
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let source = try result.get()
|
let sources = try result.get()
|
||||||
try source.managedObjectContext?.save()
|
try sources.first?.managedObjectContext?.save()
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.loadingState = .finished(.success(()))
|
self.loadingState = .finished(.success(()))
|
||||||
@@ -180,7 +182,7 @@ private extension NewsViewController
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if self.dataSource.itemCount > 0
|
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)
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,15 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreData
|
||||||
|
|
||||||
import Roxas
|
import Roxas
|
||||||
|
|
||||||
@objc(FetchSourceOperation)
|
@objc(FetchSourceOperation)
|
||||||
class FetchSourceOperation: ResultOperation<Source>
|
class FetchSourceOperation: ResultOperation<Source>
|
||||||
{
|
{
|
||||||
let sourceURL: URL
|
let sourceURL: URL
|
||||||
|
let managedObjectContext: NSManagedObjectContext
|
||||||
|
|
||||||
private let session: URLSession
|
private let session: URLSession
|
||||||
|
|
||||||
@@ -21,9 +24,10 @@ class FetchSourceOperation: ResultOperation<Source>
|
|||||||
return dateFormatter
|
return dateFormatter
|
||||||
}()
|
}()
|
||||||
|
|
||||||
init(sourceURL: URL)
|
init(sourceURL: URL, managedObjectContext: NSManagedObjectContext = DatabaseManager.shared.persistentContainer.newBackgroundContext())
|
||||||
{
|
{
|
||||||
self.sourceURL = sourceURL
|
self.sourceURL = sourceURL
|
||||||
|
self.managedObjectContext = managedObjectContext
|
||||||
|
|
||||||
let configuration = URLSessionConfiguration.default
|
let configuration = URLSessionConfiguration.default
|
||||||
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
configuration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||||
@@ -37,7 +41,7 @@ class FetchSourceOperation: ResultOperation<Source>
|
|||||||
super.main()
|
super.main()
|
||||||
|
|
||||||
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
|
let dataTask = self.session.dataTask(with: self.sourceURL) { (data, response, error) in
|
||||||
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
|
self.managedObjectContext.perform {
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let (data, _) = try Result((data, response), error).get()
|
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.")
|
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)
|
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
|
Keychain.shared.patreonCreatorAccessToken = patreonAccessToken
|
||||||
}
|
}
|
||||||
|
|
||||||
#if STAGING
|
|
||||||
source.sourceURL = self.sourceURL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
self.finish(.success(source))
|
self.finish(.success(source))
|
||||||
}
|
}
|
||||||
catch
|
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