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 */; };
|
||||
BF1E316022A0636400370A3C /* libAltKit.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BF1E315022A0616100370A3C /* libAltKit.a */; };
|
||||
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 */; };
|
||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */ = {isa = PBXBuildFile; fileRef = BF3D648C22E79AC800E9056B /* ALTAppPermission.m */; };
|
||||
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 */; };
|
||||
BFB1169D22932DB100BB457C /* Apps-Staging.json in Resources */ = {isa = PBXBuildFile; fileRef = BFB1169C22932DB100BB457C /* Apps-Staging.json */; };
|
||||
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 */; };
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFBBE2DE22931F73002097FA /* App.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; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -386,6 +394,10 @@
|
||||
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>"; };
|
||||
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>"; };
|
||||
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>"; };
|
||||
@@ -740,6 +752,16 @@
|
||||
path = Browse;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFB6B21C2318700D0022A802 /* News */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
BFB6B21D231870160022A802 /* NewsViewController.swift */,
|
||||
BFB6B21F231870B00022A802 /* NewsCollectionViewCell.swift */,
|
||||
BFB6B22323187A3D0022A802 /* NewsCollectionViewCell.xib */,
|
||||
);
|
||||
path = News;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
BFBBE2E2229320A2002097FA /* My Apps */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -789,9 +811,10 @@
|
||||
children = (
|
||||
BF219A7E22CAC431007676A6 /* AltStore.entitlements */,
|
||||
BFD2476D2284B9A500981D42 /* AppDelegate.swift */,
|
||||
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */,
|
||||
BFD247732284B9A500981D42 /* Main.storyboard */,
|
||||
BFE338E722F10E56002E24B9 /* LaunchViewController.swift */,
|
||||
BFE6325822A83BA800F30809 /* Authentication */,
|
||||
BFB6B21C2318700D0022A802 /* News */,
|
||||
BF9ABA4322DCFF33008935CF /* Browse */,
|
||||
BF3D64A022E7FAD800E9056B /* App Detail */,
|
||||
BFBBE2E2229320A2002097FA /* My Apps */,
|
||||
@@ -845,6 +868,8 @@
|
||||
BF9ABA4C22DD16DE008935CF /* PillButton.swift */,
|
||||
BF18B0F022E25DF9005C4CF5 /* ToastView.swift */,
|
||||
BF3D649E22E7B24C00E9056B /* CollapsingTextView.swift */,
|
||||
BF2901302318F7A800D88A45 /* AppBannerView.swift */,
|
||||
BF29012E2318F6B100D88A45 /* AppBannerView.xib */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@@ -879,6 +904,7 @@
|
||||
BFBBE2DE22931F73002097FA /* App.swift */,
|
||||
BF3D648722E79A3700E9056B /* AppPermission.swift */,
|
||||
BFBBE2E022931F81002097FA /* InstalledApp.swift */,
|
||||
BFB6B21A23186D640022A802 /* NewsItem.swift */,
|
||||
BFD5D6E9230CCAE5007955AB /* PatreonAccount.swift */,
|
||||
BF02419322F2156E00129732 /* RefreshAttempt.swift */,
|
||||
BFE338DC22F0E7F3002E24B9 /* Source.swift */,
|
||||
@@ -1175,8 +1201,10 @@
|
||||
BFD2477A2284B9A700981D42 /* LaunchScreen.storyboard in Resources */,
|
||||
BF770E6922BD57DD002A40FE /* Silence.m4a in Resources */,
|
||||
BFD247772284B9A700981D42 /* Assets.xcassets in Resources */,
|
||||
BFB6B22423187A3D0022A802 /* NewsCollectionViewCell.xib in Resources */,
|
||||
BFD247752284B9A500981D42 /* Main.storyboard in Resources */,
|
||||
BFDB5B2622EFBBEA00F74113 /* BrowseCollectionViewCell.xib in Resources */,
|
||||
BF29012F2318F6B100D88A45 /* AppBannerView.xib in Resources */,
|
||||
BFE6325A22A83BEB00F30809 /* Authentication.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1336,7 +1364,9 @@
|
||||
BFBBE2E122931F81002097FA /* InstalledApp.swift in Sources */,
|
||||
BFE338DF22F0EADB002E24B9 /* FetchSourceOperation.swift in Sources */,
|
||||
BFBBE2DF22931F73002097FA /* App.swift in Sources */,
|
||||
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
|
||||
BFD5D6F4230DDB0A007955AB /* Campaign.swift in Sources */,
|
||||
BFB6B21B23186D640022A802 /* NewsItem.swift in Sources */,
|
||||
BFD5D6E8230CC961007955AB /* PatreonAPI.swift in Sources */,
|
||||
BF9ABA4522DCFF43008935CF /* BrowseViewController.swift in Sources */,
|
||||
BF43002E22A714AF0051E2BC /* Keychain.swift in Sources */,
|
||||
@@ -1375,11 +1405,13 @@
|
||||
BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */,
|
||||
BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */,
|
||||
BF0C4EBD22A1BD8B009A2DD7 /* AppManager.swift in Sources */,
|
||||
BF2901312318F7A800D88A45 /* AppBannerView.swift in Sources */,
|
||||
BF3D648D22E79AC800E9056B /* ALTAppPermission.m in Sources */,
|
||||
BFD5D6F2230DD974007955AB /* Benefit.swift in Sources */,
|
||||
BF9ABA4922DD0742008935CF /* ScreenshotCollectionViewCell.swift in Sources */,
|
||||
BF9ABA4D22DD16DE008935CF /* PillButton.swift in Sources */,
|
||||
BFE6326C22A86FF300F30809 /* AuthenticationOperation.swift in Sources */,
|
||||
BFB6B220231870B00022A802 /* NewsCollectionViewCell.swift in Sources */,
|
||||
BFDB6A0522A9AFB2007EA6D6 /* Fetchable.swift in Sources */,
|
||||
BFD2479F2284FBD000981D42 /* UIColor+AltStore.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"/>
|
||||
</tabBar>
|
||||
<connections>
|
||||
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="kCE-KJ-sWv"/>
|
||||
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="F8I-ea-yTZ"/>
|
||||
<segue destination="MGm-Zy-ffn" kind="relationship" relationship="viewControllers" id="9m0-Rb-vjU"/>
|
||||
<segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/>
|
||||
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="dXz-Tu-hW8"/>
|
||||
<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>
|
||||
</tabBarController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
|
||||
@@ -594,7 +595,7 @@ World</string>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="7Tu-x9-xBb" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3908" y="-1484"/>
|
||||
<point key="canvasLocation" x="4847.1999999999998" y="-1484.7076461769116"/>
|
||||
</scene>
|
||||
<!--Settings-->
|
||||
<scene sceneID="GaO-Ug-BdZ">
|
||||
@@ -908,6 +909,32 @@ World</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="2313" y="1193"/>
|
||||
</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-->
|
||||
<scene sceneID="VHa-uP-bFU">
|
||||
<objects>
|
||||
@@ -1108,6 +1135,25 @@ World</string>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="1517.5999999999999" y="-279.31034482758622"/>
|
||||
</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>
|
||||
<resources>
|
||||
<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>
|
||||
</uniquenessConstraints>
|
||||
</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">
|
||||
<attribute name="firstName" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="identifier" attributeType="String" syncable="YES"/>
|
||||
@@ -59,6 +78,7 @@
|
||||
<attribute name="name" attributeType="String" 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="newsItems" toMany="YES" deletionRule="Nullify" ordered="YES" destinationEntity="NewsItem" inverseName="source" inverseEntity="NewsItem" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="identifier"/>
|
||||
@@ -82,6 +102,7 @@
|
||||
<attribute name="versionDate" attributeType="Date" usesScalarValueType="NO" 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="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="source" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Source" inverseName="apps" inverseEntity="Source" syncable="YES"/>
|
||||
<uniquenessConstraints>
|
||||
@@ -106,10 +127,11 @@
|
||||
<element name="Account" positionX="-36" positionY="90" width="128" height="135"/>
|
||||
<element name="AppPermission" positionX="-45" positionY="90" width="128" height="90"/>
|
||||
<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="RefreshAttempt" positionX="-45" positionY="117" width="128" height="105"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="105"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="315"/>
|
||||
<element name="Source" positionX="-45" positionY="99" width="128" height="120"/>
|
||||
<element name="StoreApp" positionX="-63" positionY="-18" width="128" height="330"/>
|
||||
<element name="Team" positionX="-45" positionY="81" width="128" height="120"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -34,6 +34,7 @@ open class MergePolicy: RSTRelationshipPreservingMergePolicy
|
||||
guard let conflictedObject = conflict.conflictingObjects.first as? Source else { break }
|
||||
|
||||
let bundleIdentifiers = Set(conflictedObject.apps.map { $0.bundleIdentifier })
|
||||
let newsItemIdentifiers = Set(conflictedObject.newsItems.map { $0.identifier })
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 */
|
||||
@objc(apps) @NSManaged private(set) var _apps: NSOrderedSet
|
||||
@objc(newsItems) @NSManaged private(set) var _newsItems: NSOrderedSet
|
||||
|
||||
@nonobjc var apps: [StoreApp] {
|
||||
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
|
||||
{
|
||||
case name
|
||||
case identifier
|
||||
case sourceURL
|
||||
case apps
|
||||
case news
|
||||
}
|
||||
|
||||
private override init(entity: NSEntityDescription, insertInto context: NSManagedObjectContext?)
|
||||
@@ -63,10 +74,31 @@ class Source: NSManagedObject, Fetchable, Decodable
|
||||
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)
|
||||
|
||||
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.
|
||||
self._apps = NSMutableOrderedSet(array: apps)
|
||||
self._newsItems = NSMutableOrderedSet(array: newsItems)
|
||||
|
||||
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"
|
||||
]
|
||||
}
|
||||
],
|
||||
"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