mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Adds News tab
This commit is contained in:
@@ -26,6 +26,8 @@
|
|||||||
BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
BF1E315F22A0635900370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||||
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||||
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF258CE222EBAE2800023032 /* AppProtocol.swift */; };
|
BF258CE322EBAE2800023032 /* AppProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF258CE222EBAE2800023032 /* AppProtocol.swift */; };
|
||||||
|
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */ = {isa = PBXBuildFile; fileRef = BF29012E2318F6B100D88A45 /* AppBannerView.xib */; };
|
||||||
|
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF2901302318F7A800D88A45 /* AppBannerView.swift */; };
|
||||||
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
|
BF3D648822E79A3700E9056B /* AppPermission.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648722E79A3700E9056B /* AppPermission.swift */; };
|
||||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; };
|
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; };
|
||||||
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; };
|
BF3D649D22E7AC1B00E9056B /* PermissionPopoverViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF3D649C22E7AC1B00E9056B /* PermissionPopoverViewController.swift */; };
|
||||||
@@ -125,6 +127,10 @@
|
|||||||
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
|
BFB1169B2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */; };
|
||||||
BFB1169D22932DB100BB457C /* Apps-Staging.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Staging.json */; };
|
BFB1169D22932DB100BB457C /* Apps-Staging.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Staging.json */; };
|
||||||
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
BFB4323F22DE852000B7F8BC /* UpdateCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB4323E22DE852000B7F8BC /* UpdateCollectionViewCell.xib */; };
|
||||||
|
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21A23186D640022A802 /* NewsItem.swift */; };
|
||||||
|
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21D231870160022A802 /* NewsViewController.swift */; };
|
||||||
|
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */; };
|
||||||
|
BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */; };
|
||||||
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
|
BFBBE2DD22931B20002097FA /* AltStore.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DB22931B20002097FA /* AltStore.xcdatamodeld */; };
|
||||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
|
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.swift */; };
|
||||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
|
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2E022931F81002097FA /* InstalledApp.swift */; };
|
||||||
@@ -279,6 +285,8 @@
|
|||||||
BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
BF1E315022A0616100370A3C /* libAltKit.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libAltKit.a; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
BF219A7E22CAC431007676A6 /* AltStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltStore.entitlements; sourceTree = "<group>"; };
|
BF219A7E22CAC431007676A6 /* AltStore.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = AltStore.entitlements; sourceTree = "<group>"; };
|
||||||
BF258CE222EBAE2800023032 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
BF258CE222EBAE2800023032 /* AppProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppProtocol.swift; sourceTree = "<group>"; };
|
||||||
|
BF29012E2318F6B100D88A45 /* AppBannerView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AppBannerView.xib; sourceTree = "<group>"; };
|
||||||
|
BF2901302318F7A800D88A45 /* AppBannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBannerView.swift; sourceTree = "<group>"; };
|
||||||
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
|
BF3D648722E79A3700E9056B /* AppPermission.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermission.swift; sourceTree = "<group>"; };
|
||||||
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
|
BF3D648B22E79AC800E9056B /* ALTAppPermission.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTAppPermission.h; sourceTree = "<group>"; };
|
||||||
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
|
BF3D648C22E79AC800E9056B /* ALTAppPermission.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ALTAppPermission.m; sourceTree = "<group>"; };
|
||||||
@@ -386,6 +394,10 @@
|
|||||||
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
|
BFB1169A2293274D00BB457C /* JSONDecoder+ManagedObjectContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "JSONDecoder+ManagedObjectContext.swift"; sourceTree = "<group>"; };
|
||||||
BFB1169C22932DB100BB457C /* Apps-Staging.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Apps-Staging.json"; sourceTree = "<group>"; };
|
BFB1169C22932DB100BB457C /* Apps-Staging.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "Apps-Staging.json"; 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>"; };
|
||||||
|
BFB6B21A23186D640022A802 /* NewsItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsItem.swift; sourceTree = "<group>"; };
|
||||||
|
BFB6B21D231870160022A802 /* NewsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewsCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NewsCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||||
BFBAC8852295C90300587369 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = "<group>"; };
|
BFBAC8852295C90300587369 /* Result+Conveniences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Result+Conveniences.swift"; sourceTree = "<group>"; };
|
||||||
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
|
BFBBE2DC22931B20002097FA /* AltStore.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = AltStore.xcdatamodel; sourceTree = "<group>"; };
|
||||||
BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
BFBBE2DE22931F73002097FA /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = "<group>"; };
|
||||||
@@ -740,6 +752,16 @@
|
|||||||
path = Browse;
|
path = Browse;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
BFB6B21C2318700D0022A802 /* News */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
BFB6B21D231870160022A802 /* NewsViewController.swift */,
|
||||||
|
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */,
|
||||||
|
BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */,
|
||||||
|
);
|
||||||
|
path = News;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
BFBBE2E2229320A2002097FA /* My Apps */ = {
|
BFBBE2E2229320A2002097FA /* My Apps */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -789,9 +811,10 @@
|
|||||||
children = (
|
children = (
|
||||||
BF219A7E22CAC431007676A6 /* AltStore.entitlements */,
|
BF219A7E22CAC431007676A6 /* AltStore.entitlements */,
|
||||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
|
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
|
||||||
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */,
|
|
||||||
BFD247732284B9A500981D42 /* Main.storyboard */,
|
BFD247732284B9A500981D42 /* Main.storyboard */,
|
||||||
|
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */,
|
||||||
BFE6325822A83BA800F30809 /* Authentication */,
|
BFE6325822A83BA800F30809 /* Authentication */,
|
||||||
|
BFB6B21C2318700D0022A802 /* News */,
|
||||||
BF9ABA4322DCFF33008935CF /* Browse */,
|
BF9ABA4322DCFF33008935CF /* Browse */,
|
||||||
BF3D64A022E7FAD800E9056B /* App Detail */,
|
BF3D64A022E7FAD800E9056B /* App Detail */,
|
||||||
BFBBE2E2229320A2002097FA /* My Apps */,
|
BFBBE2E2229320A2002097FA /* My Apps */,
|
||||||
@@ -845,6 +868,8 @@
|
|||||||
BF9ABA4C22DD16DE008935CF /* PillButton.swift */,
|
BF9ABA4C22DD16DE008935CF /* PillButton.swift */,
|
||||||
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */,
|
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */,
|
||||||
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
||||||
|
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
||||||
|
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -879,6 +904,7 @@
|
|||||||
BFBBE2DE22931F73002097FA /* App.swift */,
|
BFBBE2DE22931F73002097FA /* App.swift */,
|
||||||
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
||||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
||||||
|
BFB6B21A23186D640022A802 /* NewsItem.swift */,
|
||||||
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
|
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
|
||||||
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
|
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
|
||||||
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
|
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
|
||||||
@@ -1175,8 +1201,10 @@
|
|||||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
||||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
|
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
|
||||||
|
BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */,
|
||||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
|
BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
|
||||||
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */,
|
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */,
|
||||||
|
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */,
|
||||||
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */,
|
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -1336,7 +1364,9 @@
|
|||||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
||||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
|
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
|
||||||
|
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
||||||
|
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
|
||||||
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
|
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
|
||||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||||
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
||||||
@@ -1375,11 +1405,13 @@
|
|||||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||||
|
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
||||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
||||||
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
||||||
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 */,
|
||||||
|
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
||||||
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
|
BFD2479F2284FBD000981D42 /* UIColor+AltStore.swift in Sources */,
|
||||||
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
|
BFDB5B1622EE90D300F74113 /* Date+RelativeDate.swift in Sources */,
|
||||||
|
|||||||
@@ -39,9 +39,10 @@
|
|||||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
</tabBar>
|
</tabBar>
|
||||||
<connections>
|
<connections>
|
||||||
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="kCE-KJ-sWv"/>
|
<segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/>
|
||||||
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="F8I-ea-yTZ"/>
|
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="dXz-Tu-hW8"/>
|
||||||
<segue destination="MGm-Zy-ffn" kind="relationship" relationship="viewControllers" id="9m0-Rb-vjU"/>
|
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="zii-dF-qEt"/>
|
||||||
|
<segue destination="MGm-Zy-ffn" kind="relationship" relationship="viewControllers" id="E9G-TB-973"/>
|
||||||
</connections>
|
</connections>
|
||||||
</tabBarController>
|
</tabBarController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
|
||||||
@@ -594,7 +595,7 @@ World</string>
|
|||||||
</viewController>
|
</viewController>
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="7Tu-x9-xBb" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
<placeholder placeholderIdentifier="IBFirstResponder" id="7Tu-x9-xBb" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="3908" y="-1484"/>
|
<point key="canvasLocation" x="4847.1999999999998" y="-1484.7076461769116"/>
|
||||||
</scene>
|
</scene>
|
||||||
<!--Settings-->
|
<!--Settings-->
|
||||||
<scene sceneID="GaO-Ug-BdZ">
|
<scene sceneID="GaO-Ug-BdZ">
|
||||||
@@ -908,6 +909,32 @@ World</string>
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="2313" y="1193"/>
|
<point key="canvasLocation" x="2313" y="1193"/>
|
||||||
</scene>
|
</scene>
|
||||||
|
<!--News-->
|
||||||
|
<scene sceneID="bqw-wB-hyB">
|
||||||
|
<objects>
|
||||||
|
<collectionViewController id="3sa-FZ-PTg" customClass="NewsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
|
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="736-lq-Aef">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="63d-78-Y24">
|
||||||
|
<size key="itemSize" width="335" height="300"/>
|
||||||
|
<size key="headerReferenceSize" width="0.0" height="0.0"/>
|
||||||
|
<size key="footerReferenceSize" width="0.0" height="0.0"/>
|
||||||
|
<inset key="sectionInset" minX="20" minY="40" maxX="20" maxY="13"/>
|
||||||
|
</collectionViewFlowLayout>
|
||||||
|
<cells/>
|
||||||
|
<connections>
|
||||||
|
<outlet property="dataSource" destination="3sa-FZ-PTg" id="80N-Sr-Foq"/>
|
||||||
|
<outlet property="delegate" destination="3sa-FZ-PTg" id="9fB-sR-8Xt"/>
|
||||||
|
</connections>
|
||||||
|
</collectionView>
|
||||||
|
<navigationItem key="navigationItem" title="News" id="ZxL-Ws-lJO"/>
|
||||||
|
</collectionViewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="YS7-2X-joz" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="1517.5999999999999" y="-1749.1754122938532"/>
|
||||||
|
</scene>
|
||||||
<!--Browse-->
|
<!--Browse-->
|
||||||
<scene sceneID="VHa-uP-bFU">
|
<scene sceneID="VHa-uP-bFU">
|
||||||
<objects>
|
<objects>
|
||||||
@@ -1108,6 +1135,25 @@ World</string>
|
|||||||
</objects>
|
</objects>
|
||||||
<point key="canvasLocation" x="1517.5999999999999" y="-279.31034482758622"/>
|
<point key="canvasLocation" x="1517.5999999999999" y="-279.31034482758622"/>
|
||||||
</scene>
|
</scene>
|
||||||
|
<!--News-->
|
||||||
|
<scene sceneID="BV8-6J-nIv">
|
||||||
|
<objects>
|
||||||
|
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="kjR-gi-fgT" sceneMemberID="viewController">
|
||||||
|
<tabBarItem key="tabBarItem" title="News" id="fVN-ed-uO1"/>
|
||||||
|
<toolbarItems/>
|
||||||
|
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="525-jF-uDK" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="20" width="375" height="96"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
</navigationBar>
|
||||||
|
<nil name="viewControllers"/>
|
||||||
|
<connections>
|
||||||
|
<segue destination="3sa-FZ-PTg" kind="relationship" relationship="rootViewController" id="Dcj-St-vt5"/>
|
||||||
|
</connections>
|
||||||
|
</navigationController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iUr-Sd-9ER" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="750" y="-1749"/>
|
||||||
|
</scene>
|
||||||
</scenes>
|
</scenes>
|
||||||
<resources>
|
<resources>
|
||||||
<image name="Back" width="18" height="18"/>
|
<image name="Back" width="18" height="18"/>
|
||||||
|
|||||||
40
AltStore/Components/AppBannerView.swift
Normal file
40
AltStore/Components/AppBannerView.swift
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// AppBannerView.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
class AppBannerView: RSTNibView
|
||||||
|
{
|
||||||
|
@IBOutlet var titleLabel: UILabel!
|
||||||
|
@IBOutlet var subtitleLabel: UILabel!
|
||||||
|
@IBOutlet var iconImageView: AppIconImageView!
|
||||||
|
@IBOutlet var button: PillButton!
|
||||||
|
@IBOutlet var betaBadgeView: UIView!
|
||||||
|
|
||||||
|
override func tintColorDidChange()
|
||||||
|
{
|
||||||
|
super.tintColorDidChange()
|
||||||
|
|
||||||
|
self.update()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension AppBannerView
|
||||||
|
{
|
||||||
|
func update()
|
||||||
|
{
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.layer.cornerRadius = 22
|
||||||
|
|
||||||
|
self.subtitleLabel.textColor = self.tintColor
|
||||||
|
self.button.tintColor = self.tintColor
|
||||||
|
|
||||||
|
self.backgroundColor = self.tintColor.withAlphaComponent(0.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
87
AltStore/Components/AppBannerView.xib
Normal file
87
AltStore/Components/AppBannerView.xib
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina6_1" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="AppBannerView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<connections>
|
||||||
|
<outlet property="betaBadgeView" destination="qQl-Ez-zC5" id="6O1-Cx-7qz"/>
|
||||||
|
<outlet property="button" destination="tVx-3G-dcu" id="joa-AH-syX"/>
|
||||||
|
<outlet property="iconImageView" destination="avS-dx-4iy" id="TQs-Ej-gin"/>
|
||||||
|
<outlet property="subtitleLabel" destination="oN5-vu-Dnw" id="gA4-iJ-Tix"/>
|
||||||
|
<outlet property="titleLabel" destination="mFe-zJ-eva" id="2OH-f8-cid"/>
|
||||||
|
</connections>
|
||||||
|
</placeholder>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<view opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="FxI-Fh-ll5">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" alignment="center" spacing="11" translatesAutoresizingMaskIntoConstraints="NO" id="d1T-UD-gWG" userLabel="App Info">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="375" height="88"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="avS-dx-4iy" customClass="AppIconImageView" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="14" y="14" width="60" height="60"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="60" id="6lU-H8-nEw"/>
|
||||||
|
<constraint firstAttribute="width" secondItem="avS-dx-4iy" secondAttribute="height" multiplier="1:1" id="AYT-3g-wcV"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" verticalHuggingPriority="100" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="caL-vN-Svn">
|
||||||
|
<rect key="frame" x="85" y="24" width="195" height="40.5"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" spacing="6" translatesAutoresizingMaskIntoConstraints="NO" id="1Es-pv-zwd">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="135" height="21.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" horizontalCompressionResistancePriority="500" text="App Name" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.5" translatesAutoresizingMaskIntoConstraints="NO" id="mFe-zJ-eva">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="88" height="21.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" weight="semibold" pointSize="18"/>
|
||||||
|
<nil key="textColor"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" horizontalHuggingPriority="251" verticalHuggingPriority="251" image="BetaBadge" translatesAutoresizingMaskIntoConstraints="NO" id="qQl-Ez-zC5">
|
||||||
|
<rect key="frame" x="94" y="0.0" width="41" height="21.5"/>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="100" verticalHuggingPriority="251" text="Developer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="oN5-vu-Dnw">
|
||||||
|
<rect key="frame" x="0.0" y="23.5" width="66" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" white="0.66666666669999997" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="roundedRect" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="tVx-3G-dcu" customClass="PillButton" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="291" y="28.5" width="72" height="31"/>
|
||||||
|
<color key="backgroundColor" red="0.22352941179999999" green="0.4941176471" blue="0.39607843139999999" alpha="0.10000000000000001" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="height" constant="31" id="Zwh-yQ-GTu"/>
|
||||||
|
<constraint firstAttribute="width" constant="72" id="eGc-Dk-QbL"/>
|
||||||
|
</constraints>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="14"/>
|
||||||
|
<state key="normal" title="FREE"/>
|
||||||
|
</button>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="14" left="14" bottom="14" right="12"/>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="d1T-UD-gWG" secondAttribute="bottom" id="B9e-Mf-cy5"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="d1T-UD-gWG" secondAttribute="trailing" id="HcT-2k-z0H"/>
|
||||||
|
<constraint firstItem="d1T-UD-gWG" firstAttribute="leading" secondItem="FxI-Fh-ll5" secondAttribute="leading" id="PIM-W5-dkh"/>
|
||||||
|
<constraint firstItem="d1T-UD-gWG" firstAttribute="top" secondItem="FxI-Fh-ll5" secondAttribute="top" id="RHn-ZK-jgl"/>
|
||||||
|
</constraints>
|
||||||
|
<freeformSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
|
||||||
|
</view>
|
||||||
|
</objects>
|
||||||
|
<resources>
|
||||||
|
<image name="BetaBadge" width="41" height="17"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
@@ -32,6 +32,25 @@
|
|||||||
</uniquenessConstraint>
|
</uniquenessConstraint>
|
||||||
</uniquenessConstraints>
|
</uniquenessConstraints>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="NewsItem" representedClassName="NewsItem" syncable="YES">
|
||||||
|
<attribute name="appID" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="caption" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="date" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
|
<attribute name="externalURL" optional="YES" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
|
<attribute name="imageURL" optional="YES" attributeType="URI" syncable="YES"/>
|
||||||
|
<attribute name="isSilent" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="sortIndex" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||||
|
<attribute name="tintColor" optional="YES" attributeType="Transformable" syncable="YES"/>
|
||||||
|
<attribute name="title" attributeType="String" syncable="YES"/>
|
||||||
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="newsItems" inverseEntity="Source" syncable="YES"/>
|
||||||
|
<relationship name="storeApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="StoreApp" inverseName="newsItems" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<uniquenessConstraints>
|
||||||
|
<uniquenessConstraint>
|
||||||
|
<constraint value="identifier"/>
|
||||||
|
</uniquenessConstraint>
|
||||||
|
</uniquenessConstraints>
|
||||||
|
</entity>
|
||||||
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
<entity name="PatreonAccount" representedClassName="PatreonAccount" syncable="YES">
|
||||||
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||||
@@ -59,6 +78,7 @@
|
|||||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
|
<attribute name="sourceURL" attributeType="URI" syncable="YES"/>
|
||||||
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
|
<relationship name="apps" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="StoreApp" inverseName="source" inverseEntity="StoreApp" syncable="YES"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem" syncable="YES"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
<uniquenessConstraint>
|
<uniquenessConstraint>
|
||||||
<constraint value="identifier"/>
|
<constraint value="identifier"/>
|
||||||
@@ -82,6 +102,7 @@
|
|||||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="versionDescription" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
<relationship name="installedApp" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="InstalledApp" inverseName="storeApp" inverseEntity="InstalledApp" syncable="YES"/>
|
||||||
|
<relationship name="newsItems" toMany="YES" deletionRule="Nullify" destinationEntity="NewsItem" inverseName="storeApp" inverseEntity="NewsItem" syncable="YES"/>
|
||||||
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
<relationship name="permissions" toMany="YES" deletionRule="Cascade" ordered="YES" destinationEntity="AppPermission" inverseName="app" inverseEntity="AppPermission" syncable="YES"/>
|
||||||
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
<relationship name="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
||||||
<uniquenessConstraints>
|
<uniquenessConstraints>
|
||||||
@@ -106,10 +127,11 @@
|
|||||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||||
<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="150"/>
|
<element name="InstalledApp" positionX="-63" positionY="0" width="128" height="150"/>
|
||||||
|
<element name="NewsItem" positionX="-45" positionY="126" width="128" height="225"/>
|
||||||
<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="105"/>
|
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="315"/>
|
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
||||||
@@ -34,6 +34,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||||
|
|
||||||
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
||||||
|
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
|
||||||
|
|
||||||
for app in databaseObject.apps
|
for app in databaseObject.apps
|
||||||
{
|
{
|
||||||
@@ -44,6 +45,15 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for newsItem in databaseObject.newsItems
|
||||||
|
{
|
||||||
|
if !newsItemIdentifiers.contains(newsItem.identifier)
|
||||||
|
{
|
||||||
|
// No longer listed in Source, so remove it from database.
|
||||||
|
newsItem.managedObjectContext?.delete(newsItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
90
AltStore/Model/NewsItem.swift
Normal file
90
AltStore/Model/NewsItem.swift
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
//
|
||||||
|
// NewsItem.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CoreData
|
||||||
|
|
||||||
|
@objc(NewsItem)
|
||||||
|
class NewsItem: NSManagedObject, Decodable, Fetchable
|
||||||
|
{
|
||||||
|
/* Properties */
|
||||||
|
@NSManaged var identifier: String
|
||||||
|
@NSManaged var date: Date
|
||||||
|
|
||||||
|
@NSManaged var title: String
|
||||||
|
@NSManaged var caption: String
|
||||||
|
@NSManaged var tintColor: UIColor
|
||||||
|
@NSManaged var sortIndex: Int32
|
||||||
|
@NSManaged var isSilent: Bool
|
||||||
|
|
||||||
|
@NSManaged var imageURL: URL?
|
||||||
|
@NSManaged var externalURL: URL?
|
||||||
|
|
||||||
|
@NSManaged var appID: String?
|
||||||
|
|
||||||
|
/* Relationships */
|
||||||
|
@NSManaged var storeApp: StoreApp?
|
||||||
|
@NSManaged var source: Source?
|
||||||
|
|
||||||
|
private enum CodingKeys: String, CodingKey
|
||||||
|
{
|
||||||
|
case identifier
|
||||||
|
case date
|
||||||
|
case title
|
||||||
|
case caption
|
||||||
|
case tintColor
|
||||||
|
case imageURL
|
||||||
|
case externalURL = "url"
|
||||||
|
case appID
|
||||||
|
case notify
|
||||||
|
}
|
||||||
|
|
||||||
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
|
{
|
||||||
|
super.init(entity: entity, insertInto: context)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init(from decoder: Decoder) throws
|
||||||
|
{
|
||||||
|
guard let context = decoder.managedObjectContext else { preconditionFailure("Decoder must have non-nil NSManagedObjectContext.") }
|
||||||
|
|
||||||
|
super.init(entity: NewsItem.entity(), insertInto: context)
|
||||||
|
|
||||||
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
self.identifier = try container.decode(String.self, forKey: .identifier)
|
||||||
|
self.date = try container.decode(Date.self, forKey: .date)
|
||||||
|
|
||||||
|
self.title = try container.decode(String.self, forKey: .title)
|
||||||
|
self.caption = try container.decode(String.self, forKey: .caption)
|
||||||
|
|
||||||
|
if let tintColorHex = try container.decodeIfPresent(String.self, forKey: .tintColor)
|
||||||
|
{
|
||||||
|
guard let tintColor = UIColor(hexString: tintColorHex) else {
|
||||||
|
throw DecodingError.dataCorruptedError(forKey: .tintColor, in: container, debugDescription: "Hex code is invalid.")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.tintColor = tintColor
|
||||||
|
}
|
||||||
|
|
||||||
|
self.imageURL = try container.decodeIfPresent(URL.self, forKey: .imageURL)
|
||||||
|
self.externalURL = try container.decodeIfPresent(URL.self, forKey: .externalURL)
|
||||||
|
|
||||||
|
self.appID = try container.decodeIfPresent(String.self, forKey: .appID)
|
||||||
|
|
||||||
|
let notify = try container.decodeIfPresent(Bool.self, forKey: .notify) ?? false
|
||||||
|
self.isSilent = !notify
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsItem
|
||||||
|
{
|
||||||
|
@nonobjc class func fetchRequest() -> NSFetchRequest<NewsItem>
|
||||||
|
{
|
||||||
|
return NSFetchRequest<NewsItem>(entityName: "NewsItem")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
|
|
||||||
/* Relationships */
|
/* Relationships */
|
||||||
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
|
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
|
||||||
|
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
|
||||||
|
|
||||||
@nonobjc var apps: [StoreApp] {
|
@nonobjc var apps: [StoreApp] {
|
||||||
get {
|
get {
|
||||||
@@ -33,12 +34,22 @@ class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@nonobjc var newsItems: [NewsItem] {
|
||||||
|
get {
|
||||||
|
return self._newsItems.array as! [NewsItem]
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
self._newsItems = NSOrderedSet(array: newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private enum CodingKeys: String, CodingKey
|
private enum CodingKeys: String, CodingKey
|
||||||
{
|
{
|
||||||
case name
|
case name
|
||||||
case identifier
|
case identifier
|
||||||
case sourceURL
|
case sourceURL
|
||||||
case apps
|
case apps
|
||||||
|
case news
|
||||||
}
|
}
|
||||||
|
|
||||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||||
@@ -63,10 +74,31 @@ class Source: NSManagedObject, Fetchable, Decodable
|
|||||||
app.sortIndex = Int32(index)
|
app.sortIndex = Int32(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let newsItems = try container.decodeIfPresent([NewsItem].self, forKey: .news) ?? []
|
||||||
|
for (index, item) in newsItems.enumerated()
|
||||||
|
{
|
||||||
|
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
|
||||||
|
{
|
||||||
|
newsItem.source = self
|
||||||
|
|
||||||
|
guard let appID = newsItem.appID else { continue }
|
||||||
|
|
||||||
|
if let storeApp = appsByID[appID]
|
||||||
|
{
|
||||||
|
newsItem.storeApp = storeApp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
||||||
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
|
print("Downloaded Order:", self.apps.map { $0.bundleIdentifier })
|
||||||
}
|
}
|
||||||
|
|||||||
27
AltStore/News/NewsCollectionViewCell.swift
Normal file
27
AltStore/News/NewsCollectionViewCell.swift
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
//
|
||||||
|
// NewsCollectionViewCell.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class NewsCollectionViewCell: UICollectionViewCell
|
||||||
|
{
|
||||||
|
@IBOutlet var titleLabel: UILabel!
|
||||||
|
@IBOutlet var captionLabel: UILabel!
|
||||||
|
@IBOutlet var imageView: UIImageView!
|
||||||
|
|
||||||
|
override func awakeFromNib()
|
||||||
|
{
|
||||||
|
super.awakeFromNib()
|
||||||
|
|
||||||
|
self.contentView.layer.cornerRadius = 30
|
||||||
|
self.contentView.clipsToBounds = true
|
||||||
|
|
||||||
|
self.imageView.layer.cornerRadius = 30
|
||||||
|
self.imageView.clipsToBounds = true
|
||||||
|
}
|
||||||
|
}
|
||||||
77
AltStore/News/NewsCollectionViewCell.xib
Normal file
77
AltStore/News/NewsCollectionViewCell.xib
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
|
<device id="retina4_7" orientation="portrait">
|
||||||
|
<adaptation id="fullscreen"/>
|
||||||
|
</device>
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14490.49"/>
|
||||||
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
|
</dependencies>
|
||||||
|
<objects>
|
||||||
|
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||||
|
<collectionViewCell opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="wRF-2R-NUG" customClass="NewsCollectionViewCell" customModule="AltStore" customModuleProvider="target">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
|
||||||
|
<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="335" height="299"/>
|
||||||
|
<autoresizingMask key="autoresizingMask"/>
|
||||||
|
<subviews>
|
||||||
|
<view contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Xba-Qs-SQo">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335" height="299"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="tNk-9u-1tk">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335" height="298.5"/>
|
||||||
|
<subviews>
|
||||||
|
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" alignment="top" spacing="10" translatesAutoresizingMaskIntoConstraints="NO" id="akF-Tr-G5M">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="335" height="98.5"/>
|
||||||
|
<subviews>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="1000" text="Delta" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="AkN-BE-I1a">
|
||||||
|
<rect key="frame" x="25" y="25" width="54.5" height="26.5"/>
|
||||||
|
<fontDescription key="fontDescription" type="boldSystem" pointSize="22"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
<label opaque="NO" userInteractionEnabled="NO" alpha="0.75" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="999" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SHB-kk-YhL">
|
||||||
|
<rect key="frame" x="25" y="61.5" width="35.5" height="17"/>
|
||||||
|
<fontDescription key="fontDescription" type="system" pointSize="14"/>
|
||||||
|
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<nil key="highlightedColor"/>
|
||||||
|
</label>
|
||||||
|
</subviews>
|
||||||
|
<edgeInsets key="layoutMargins" top="25" left="25" bottom="20" right="25"/>
|
||||||
|
</stackView>
|
||||||
|
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFill" horizontalHuggingPriority="251" verticalHuggingPriority="251" placeholderIntrinsicWidth="335" placeholderIntrinsicHeight="200" translatesAutoresizingMaskIntoConstraints="NO" id="l36-Bm-De0">
|
||||||
|
<rect key="frame" x="0.0" y="98.5" width="335" height="200"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstAttribute="width" secondItem="l36-Bm-De0" secondAttribute="height" multiplier="67:40" id="QGD-YE-Hw2"/>
|
||||||
|
</constraints>
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
</stackView>
|
||||||
|
</subviews>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="tNk-9u-1tk" firstAttribute="top" secondItem="Xba-Qs-SQo" secondAttribute="top" id="Dw8-lF-Fzl"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="tNk-9u-1tk" secondAttribute="trailing" id="Zt8-Wa-oB9"/>
|
||||||
|
<constraint firstItem="tNk-9u-1tk" firstAttribute="leading" secondItem="Xba-Qs-SQo" secondAttribute="leading" id="m6p-Ee-dTh"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="tNk-9u-1tk" secondAttribute="bottom" constant="0.5" id="v9g-yC-db9"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</subviews>
|
||||||
|
</view>
|
||||||
|
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="Xba-Qs-SQo" firstAttribute="top" secondItem="wRF-2R-NUG" secondAttribute="top" id="0xe-Rt-MhF"/>
|
||||||
|
<constraint firstItem="Xba-Qs-SQo" firstAttribute="leading" secondItem="wRF-2R-NUG" secondAttribute="leading" id="5MO-c0-5rG"/>
|
||||||
|
<constraint firstAttribute="trailing" secondItem="Xba-Qs-SQo" secondAttribute="trailing" id="DNL-Jj-3By"/>
|
||||||
|
<constraint firstAttribute="bottom" secondItem="Xba-Qs-SQo" secondAttribute="bottom" id="Ecj-fN-hZv"/>
|
||||||
|
</constraints>
|
||||||
|
<connections>
|
||||||
|
<outlet property="captionLabel" destination="SHB-kk-YhL" id="zY3-qQ-9oY"/>
|
||||||
|
<outlet property="imageView" destination="l36-Bm-De0" id="3do-aQ-5r4"/>
|
||||||
|
<outlet property="titleLabel" destination="AkN-BE-I1a" id="hA2-3O-q5J"/>
|
||||||
|
</connections>
|
||||||
|
</collectionViewCell>
|
||||||
|
</objects>
|
||||||
|
</document>
|
||||||
397
AltStore/News/NewsViewController.swift
Normal file
397
AltStore/News/NewsViewController.swift
Normal file
@@ -0,0 +1,397 @@
|
|||||||
|
//
|
||||||
|
// NewsViewController.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 8/29/19.
|
||||||
|
// Copyright © 2019 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import SafariServices
|
||||||
|
|
||||||
|
import Roxas
|
||||||
|
|
||||||
|
import Nuke
|
||||||
|
|
||||||
|
private class AppBannerFooterView: UICollectionReusableView
|
||||||
|
{
|
||||||
|
let bannerView = AppBannerView(frame: .zero)
|
||||||
|
let tapGestureRecognizer = UITapGestureRecognizer(target: nil, action: nil)
|
||||||
|
|
||||||
|
override init(frame: CGRect)
|
||||||
|
{
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.bannerView, pinningEdgesWith: UIEdgeInsets(top: 0, left: 20, bottom: 0, right: 20))
|
||||||
|
self.addGestureRecognizer(self.tapGestureRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NewsViewController: UICollectionViewController
|
||||||
|
{
|
||||||
|
private lazy var dataSource = self.makeDataSource()
|
||||||
|
private var prototypeCell: NewsCollectionViewCell!
|
||||||
|
|
||||||
|
// Cache
|
||||||
|
private var cachedCellSizes = [String: CGSize]()
|
||||||
|
|
||||||
|
override func viewDidLoad()
|
||||||
|
{
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
self.prototypeCell = NewsCollectionViewCell.instantiate(with: NewsCollectionViewCell.nib!)
|
||||||
|
self.prototypeCell.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
self.prototypeCell.contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
|
||||||
|
self.collectionView.contentInset.bottom = 20
|
||||||
|
|
||||||
|
self.collectionView.dataSource = self.dataSource
|
||||||
|
self.collectionView.prefetchDataSource = self.dataSource
|
||||||
|
|
||||||
|
self.collectionView.register(NewsCollectionViewCell.nib, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
|
||||||
|
self.collectionView.register(AppBannerFooterView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner")
|
||||||
|
|
||||||
|
self.registerForPreviewing(with: self, sourceView: self.collectionView)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool)
|
||||||
|
{
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
self.fetchSource()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewsViewController
|
||||||
|
{
|
||||||
|
func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>
|
||||||
|
{
|
||||||
|
let fetchRequest = NewsItem.fetchRequest() as NSFetchRequest<NewsItem>
|
||||||
|
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \NewsItem.sortIndex, ascending: false)]
|
||||||
|
|
||||||
|
let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: #keyPath(NewsItem.date), cacheName: nil)
|
||||||
|
|
||||||
|
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<NewsItem, UIImage>(fetchedResultsController: fetchedResultsController)
|
||||||
|
dataSource.proxy = self
|
||||||
|
dataSource.cellConfigurationHandler = { (cell, newsItem, indexPath) in
|
||||||
|
let cell = cell as! NewsCollectionViewCell
|
||||||
|
cell.titleLabel.text = newsItem.title
|
||||||
|
cell.captionLabel.text = newsItem.caption
|
||||||
|
cell.contentView.backgroundColor = newsItem.tintColor
|
||||||
|
|
||||||
|
cell.imageView.image = nil
|
||||||
|
|
||||||
|
if newsItem.imageURL != nil
|
||||||
|
{
|
||||||
|
cell.imageView.isIndicatingActivity = true
|
||||||
|
cell.imageView.isHidden = false
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
cell.imageView.isIndicatingActivity = false
|
||||||
|
cell.imageView.isHidden = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchHandler = { (newsItem, indexPath, completionHandler) in
|
||||||
|
guard let imageURL = newsItem.imageURL else { return nil }
|
||||||
|
|
||||||
|
return RSTAsyncBlockOperation() { (operation) in
|
||||||
|
ImagePipeline.shared.loadImage(with: imageURL, progress: nil, completion: { (response, error) in
|
||||||
|
guard !operation.isCancelled else { return operation.finish() }
|
||||||
|
|
||||||
|
if let image = response?.image
|
||||||
|
{
|
||||||
|
completionHandler(image, nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
completionHandler(nil, error)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
|
||||||
|
let cell = cell as! NewsCollectionViewCell
|
||||||
|
cell.imageView.isIndicatingActivity = false
|
||||||
|
cell.imageView.image = image
|
||||||
|
|
||||||
|
if let error = error
|
||||||
|
{
|
||||||
|
print("Error loading image:", error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return dataSource
|
||||||
|
}
|
||||||
|
|
||||||
|
func fetchSource()
|
||||||
|
{
|
||||||
|
AppManager.shared.fetchSource() { (result) in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let source = try result.get()
|
||||||
|
try source.managedObjectContext?.save()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||||
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private extension NewsViewController
|
||||||
|
{
|
||||||
|
@objc func handleTapGesture(_ gestureRecognizer: UITapGestureRecognizer)
|
||||||
|
{
|
||||||
|
guard let footerView = gestureRecognizer.view as? UICollectionReusableView else { return }
|
||||||
|
|
||||||
|
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||||
|
|
||||||
|
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||||
|
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||||
|
return supplementaryView == footerView
|
||||||
|
}) else { return }
|
||||||
|
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
guard let storeApp = item.storeApp else { return }
|
||||||
|
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func performAppAction(_ sender: PillButton)
|
||||||
|
{
|
||||||
|
let point = self.collectionView.convert(sender.center, from: sender.superview)
|
||||||
|
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||||
|
|
||||||
|
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||||
|
let supplementaryView = self.collectionView.supplementaryView(forElementKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||||
|
return supplementaryView?.frame.contains(point) ?? false
|
||||||
|
}) else { return }
|
||||||
|
|
||||||
|
let app = self.dataSource.item(at: indexPath)
|
||||||
|
guard let storeApp = app.storeApp else { return }
|
||||||
|
|
||||||
|
if let installedApp = app.storeApp?.installedApp
|
||||||
|
{
|
||||||
|
self.open(installedApp)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.install(storeApp, at: indexPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func install(_ storeApp: StoreApp, at indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
let previousProgress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
|
guard previousProgress == nil else {
|
||||||
|
previousProgress?.cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = AppManager.shared.install(storeApp, presentingViewController: self) { (result) in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
switch result
|
||||||
|
{
|
||||||
|
case .failure(OperationError.cancelled): break // Ignore
|
||||||
|
case .failure(let error):
|
||||||
|
let toastView = ToastView(text: error.localizedDescription, detailText: nil)
|
||||||
|
toastView.show(in: self.navigationController?.view ?? self.view, duration: 2)
|
||||||
|
|
||||||
|
case .success: print("Installed app:", storeApp.bundleIdentifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UIView.performWithoutAnimation {
|
||||||
|
self.collectionView.reloadSections(IndexSet(integer: indexPath.section))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func open(_ installedApp: InstalledApp)
|
||||||
|
{
|
||||||
|
UIApplication.shared.open(installedApp.openAppURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsViewController
|
||||||
|
{
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath)
|
||||||
|
{
|
||||||
|
let newsItem = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let externalURL = newsItem.externalURL
|
||||||
|
{
|
||||||
|
let safariViewController = SFSafariViewController(url: externalURL)
|
||||||
|
safariViewController.preferredControlTintColor = newsItem.tintColor
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
else if let storeApp = newsItem.storeApp
|
||||||
|
{
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
self.navigationController?.pushViewController(appViewController, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
let footerView = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionFooter, withReuseIdentifier: "AppBanner", for: indexPath) as! AppBannerFooterView
|
||||||
|
guard let storeApp = item.storeApp else { return footerView }
|
||||||
|
|
||||||
|
footerView.bannerView.titleLabel.text = storeApp.name
|
||||||
|
footerView.bannerView.subtitleLabel.text = storeApp.developerName
|
||||||
|
footerView.bannerView.tintColor = storeApp.tintColor
|
||||||
|
footerView.bannerView.betaBadgeView.isHidden = !storeApp.isBeta
|
||||||
|
footerView.bannerView.button.addTarget(self, action: #selector(NewsViewController.performAppAction(_:)), for: .primaryActionTriggered)
|
||||||
|
footerView.tapGestureRecognizer.addTarget(self, action: #selector(NewsViewController.handleTapGesture(_:)))
|
||||||
|
|
||||||
|
footerView.bannerView.button.isIndicatingActivity = false
|
||||||
|
|
||||||
|
if storeApp.installedApp == nil
|
||||||
|
{
|
||||||
|
footerView.bannerView.button.setTitle(NSLocalizedString("FREE", comment: ""), for: .normal)
|
||||||
|
|
||||||
|
let progress = AppManager.shared.installationProgress(for: storeApp)
|
||||||
|
footerView.bannerView.button.progress = progress
|
||||||
|
footerView.bannerView.button.isInverted = false
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
footerView.bannerView.button.setTitle(NSLocalizedString("OPEN", comment: ""), for: .normal)
|
||||||
|
footerView.bannerView.button.progress = nil
|
||||||
|
footerView.bannerView.button.isInverted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Nuke.loadImage(with: storeApp.iconURL, into: footerView.bannerView.iconImageView)
|
||||||
|
|
||||||
|
return footerView
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsViewController: UICollectionViewDelegateFlowLayout
|
||||||
|
{
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
|
||||||
|
{
|
||||||
|
let padding = 40 as CGFloat
|
||||||
|
let width = collectionView.bounds.width - padding
|
||||||
|
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let previousSize = self.cachedCellSizes[item.identifier]
|
||||||
|
{
|
||||||
|
return previousSize
|
||||||
|
}
|
||||||
|
|
||||||
|
let widthConstraint = self.prototypeCell.contentView.widthAnchor.constraint(equalToConstant: width)
|
||||||
|
NSLayoutConstraint.activate([widthConstraint])
|
||||||
|
defer { NSLayoutConstraint.deactivate([widthConstraint]) }
|
||||||
|
|
||||||
|
self.dataSource.cellConfigurationHandler(self.prototypeCell, item, indexPath)
|
||||||
|
|
||||||
|
let size = self.prototypeCell.contentView.systemLayoutSizeFitting(UIView.layoutFittingCompressedSize)
|
||||||
|
self.cachedCellSizes[item.identifier] = size
|
||||||
|
return size
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
|
||||||
|
{
|
||||||
|
let item = self.dataSource.item(at: IndexPath(row: 0, section: section))
|
||||||
|
|
||||||
|
if item.storeApp != nil
|
||||||
|
{
|
||||||
|
return CGSize(width: 88, height: 88)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets
|
||||||
|
{
|
||||||
|
var insets = UIEdgeInsets(top: 30, left: 20, bottom: 13, right: 20)
|
||||||
|
|
||||||
|
if section == 0
|
||||||
|
{
|
||||||
|
insets.top = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
return insets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsViewController: UIViewControllerPreviewingDelegate
|
||||||
|
{
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, viewControllerForLocation location: CGPoint) -> UIViewController?
|
||||||
|
{
|
||||||
|
if let indexPath = self.collectionView.indexPathForItem(at: location), let cell = self.collectionView.cellForItem(at: indexPath)
|
||||||
|
{
|
||||||
|
// Previewing news item.
|
||||||
|
|
||||||
|
previewingContext.sourceRect = cell.frame
|
||||||
|
|
||||||
|
let newsItem = self.dataSource.item(at: indexPath)
|
||||||
|
|
||||||
|
if let externalURL = newsItem.externalURL
|
||||||
|
{
|
||||||
|
let safariViewController = SFSafariViewController(url: externalURL)
|
||||||
|
safariViewController.preferredControlTintColor = newsItem.tintColor
|
||||||
|
return safariViewController
|
||||||
|
}
|
||||||
|
else if let storeApp = newsItem.storeApp
|
||||||
|
{
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Previewing app banner (or nothing).
|
||||||
|
|
||||||
|
let indexPaths = self.collectionView.indexPathsForVisibleSupplementaryElements(ofKind: UICollectionView.elementKindSectionFooter)
|
||||||
|
|
||||||
|
guard let indexPath = indexPaths.first(where: { (indexPath) -> Bool in
|
||||||
|
let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath)
|
||||||
|
return layoutAttributes?.frame.contains(location) ?? false
|
||||||
|
}) else { return nil }
|
||||||
|
|
||||||
|
guard let layoutAttributes = self.collectionView.layoutAttributesForSupplementaryElement(ofKind: UICollectionView.elementKindSectionFooter, at: indexPath) else { return nil }
|
||||||
|
previewingContext.sourceRect = layoutAttributes.frame
|
||||||
|
|
||||||
|
let item = self.dataSource.item(at: indexPath)
|
||||||
|
guard let storeApp = item.storeApp else { return nil }
|
||||||
|
|
||||||
|
let appViewController = AppViewController.makeAppViewController(app: storeApp)
|
||||||
|
return appViewController
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewingContext(_ previewingContext: UIViewControllerPreviewing, commit viewControllerToCommit: UIViewController)
|
||||||
|
{
|
||||||
|
if let safariViewController = viewControllerToCommit as? SFSafariViewController
|
||||||
|
{
|
||||||
|
self.present(safariViewController, animated: true, completion: nil)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
self.navigationController?.pushViewController(viewControllerToCommit, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -97,5 +97,46 @@
|
|||||||
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png"
|
"https://user-images.githubusercontent.com/705880/63391950-34286600-c37a-11e9-965f-832efe3da507.png"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"news": [
|
||||||
|
{
|
||||||
|
"title": "Welcome to AltStore",
|
||||||
|
"identifier": "welcometoaltstore",
|
||||||
|
"caption": "Check out the FAQ for more information on how to install apps.",
|
||||||
|
"tintColor": "397E65",
|
||||||
|
"url": "http://rileytestut.com",
|
||||||
|
"appID": "com.rileytestut.AltStore",
|
||||||
|
"date": "2019-08-27"
|
||||||
|
}
|
||||||
|
,
|
||||||
|
{
|
||||||
|
"title": "Why Clip?",
|
||||||
|
"identifier": "whyclip",
|
||||||
|
"caption": "Clip lets you track your clipboard history even when in the background, something App Store apps can't do.",
|
||||||
|
"tintColor": "EC008C",
|
||||||
|
"url": "http://rileytestut.com",
|
||||||
|
"imageURL": "https://user-images.githubusercontent.com/705880/63391948-32f73900-c37a-11e9-976c-275bd8d557f0.png",
|
||||||
|
"appID": "com.rileytestut.Clip",
|
||||||
|
"date": "2019-08-28"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Delta Now Available",
|
||||||
|
"identifier": "deltaavailable",
|
||||||
|
"caption": "After almost 5 years in development, Delta is finally finished.",
|
||||||
|
"tintColor": "8A28F7",
|
||||||
|
"imageURL": "https://boygeniusreport.files.wordpress.com/2017/05/delta-emulator-game-boy-color.jpg?quality=98&strip=all&w=782",
|
||||||
|
"appID": "com.rileytestut.Delta",
|
||||||
|
"date": "2019-08-29"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Delta Gaining DS Support",
|
||||||
|
"identifier": "deltadspreview",
|
||||||
|
"caption": "Check out the upcoming DS support before everyone else when you become a Patron.",
|
||||||
|
"tintColor": "8A28F7",
|
||||||
|
"imageURL": "https://boygeniusreport.files.wordpress.com/2017/05/delta-emulator-game-boy-color.jpg?quality=98&strip=all&w=782",
|
||||||
|
"appID": "com.rileytestut.Delta",
|
||||||
|
"date": "2019-08-30",
|
||||||
|
"notify": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user