Refactors SourceViewController into dedicated tab

* Updates UI to use source icons + tint colors
* Adds Edit button + swipe actions
This commit is contained in:
Riley Testut
2023-10-17 14:49:13 -05:00
committed by Magesh K
parent f9911d285d
commit b03b7bfe68
16 changed files with 517 additions and 546 deletions

View File

@@ -433,6 +433,7 @@
D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */; }; D5F5AF7D28ECEA990067C736 /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */; };
D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; }; D5F99A1828D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */; };
D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; }; D5F99A1A28D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */; };
D5FB28EC2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */; };
D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F812A141E4900E767CD /* KnownSource.swift */; }; D5FB28EE2ADDF89800A1C337 /* KnownSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5893F812A141E4900E767CD /* KnownSource.swift */; };
D5FB7A0E2AA25A4E00EF863D /* Previews.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */; }; D5FB7A0E2AA25A4E00EF863D /* Previews.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */; };
D5FB7A212AA284ED00EF863D /* EnableJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */; }; D5FB7A212AA284ED00EF863D /* EnableJIT.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */; };
@@ -1089,6 +1090,7 @@
D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; }; D5F5AF7C28ECEA990067C736 /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = "<group>"; };
D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; }; D5F99A1728D11DB500476A16 /* AltStore10ToAltStore11.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore10ToAltStore11.xcmappingmodel; sourceTree = "<group>"; };
D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; }; D5F99A1928D12B1400476A16 /* StoreApp10ToStoreApp11Policy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StoreApp10ToStoreApp11Policy.swift; sourceTree = "<group>"; };
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFontDescriptor+Bold.swift"; sourceTree = "<group>"; };
D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Previews.xcassets; sourceTree = "<group>"; }; D5FB7A0D2AA25A4E00EF863D /* Previews.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Previews.xcassets; sourceTree = "<group>"; };
D5FB7A132AA284BE00EF863D /* altjit */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = altjit; sourceTree = BUILT_PRODUCTS_DIR; }; D5FB7A132AA284BE00EF863D /* altjit */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = altjit; sourceTree = BUILT_PRODUCTS_DIR; };
D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnableJIT.swift; path = AltJIT/Commands/EnableJIT.swift; sourceTree = SOURCE_ROOT; }; D5FB7A1A2AA284ED00EF863D /* EnableJIT.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = EnableJIT.swift; path = AltJIT/Commands/EnableJIT.swift; sourceTree = SOURCE_ROOT; };
@@ -2007,6 +2009,7 @@
0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */, 0E13E5852CC8F55900E9C0DF /* ProcessInfo+SideStore.swift */,
D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */, D5927D6529DCC89000D6898E /* UINavigationBarAppearance+TintColor.swift */,
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */, D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
); );
path = Extensions; path = Extensions;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -3151,6 +3154,7 @@
BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */, BFB6B21E231870160022A802 /* NewsViewController.swift in Sources */,
BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */, BFC57A652416C72400EB891E /* DeactivateAppOperation.swift in Sources */,
D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */, D5CD805D29CA2C1E00E591B0 /* HeaderContentViewController.swift in Sources */,
D5FB28EC2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift in Sources */,
BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */, BF3BEFC124086A1E00DE7D55 /* RefreshAppOperation.swift in Sources */,
BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */, BFE60740231AFD2A002B0E8E /* InsetGroupTableViewCell.swift in Sources */,
BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */, BF0DCA662433BDF500E3A595 /* AnalyticsManager.swift in Sources */,

View File

@@ -36,11 +36,11 @@
</tabBar> </tabBar>
<connections> <connections>
<segue destination="kjR-gi-fgT" kind="relationship" relationship="viewControllers" id="eWy-uk-nwG"/> <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="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="4Nf-rL-P4c"/>
<segue destination="bTL-bY-9Yq" kind="presentation" identifier="finishJailbreak" id="cIc-Ta-uNk"/> <segue destination="bTL-bY-9Yq" kind="presentation" identifier="finishJailbreak" id="cIc-Ta-uNk"/>
<segue destination="HCK-G6-KdY" kind="presentation" identifier="presentSources" id="SRd-VL-5nP"/> <segue destination="HCK-G6-KdY" kind="relationship" relationship="viewControllers" id="X0t-T6-JeA"/>
<segue destination="faz-B4-Sub" kind="relationship" relationship="viewControllers" id="OLu-kM-z1J"/>
<segue destination="3Ew-ox-i4n" kind="relationship" relationship="viewControllers" id="phQ-Pc-pqw"/>
<segue destination="p3d-dP-Swg" kind="relationship" relationship="viewControllers" id="cQE-Az-fdo"/>
</connections> </connections>
</tabBarController> </tabBarController>
<placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="HuB-VB-40B" sceneMemberID="firstResponder"/>
@@ -67,16 +67,7 @@
<outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/> <outlet property="delegate" destination="e3L-BF-iXp" id="Mdp-x4-hZe"/>
</connections> </connections>
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"> <navigationItem key="navigationItem" title="Browse" id="pUx-6k-rtr"/>
<barButtonItem key="rightBarButtonItem" title="Sources" id="6Ul-JW-TMT">
<connections>
<segue destination="HCK-G6-KdY" kind="presentation" id="hBK-Tt-naZ"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<outlet property="sourcesBarButtonItem" destination="6Ul-JW-TMT" id="99s-O4-OpX"/>
</connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="cMN-i4-Bxk" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
@@ -921,20 +912,20 @@ World</string>
<!--Sources--> <!--Sources-->
<scene sceneID="Vzf-tb-LIH"> <scene sceneID="Vzf-tb-LIH">
<objects> <objects>
<viewControllerPlaceholder storyboardName="Sources" id="HCK-G6-KdY" sceneMemberID="viewController"/> <viewControllerPlaceholder storyboardName="Sources" id="HCK-G6-KdY" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Item" id="Q7y-bi-ncT"/>
</viewControllerPlaceholder>
<placeholder placeholderIdentifier="IBFirstResponder" id="VTd-he-VYb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="VTd-he-VYb" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-2" y="-553"/> <point key="canvasLocation" x="-2" y="-553"/>
</scene> </scene>
</scenes> </scenes>
<inferredMetricsTieBreakers> <inferredMetricsTieBreakers>
<segue reference="hBK-Tt-naZ"/>
<segue reference="cnd-KK-o60"/> <segue reference="cnd-KK-o60"/>
</inferredMetricsTieBreakers> </inferredMetricsTieBreakers>
<color key="tintColor" name="Primary"/> <color key="tintColor" name="Primary"/>
<resources> <resources>
<image name="Back" width="18" height="18"/> <image name="Back" width="18" height="18"/>
<image name="Browse" width="20" height="20"/>
<image name="MyApps" width="20" height="20"/> <image name="MyApps" width="20" height="20"/>
<image name="News" width="19" height="20"/> <image name="News" width="19" height="20"/>
<image name="Settings" width="20" height="20"/> <image name="Settings" width="20" height="20"/>

View File

@@ -92,11 +92,6 @@ class BrowseViewController: UICollectionViewController, PeekPopPreviewing
self.update() self.update()
} }
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
{
self.fetchSource()
}
} }
private extension BrowseViewController private extension BrowseViewController

View File

@@ -8,12 +8,10 @@
import UIKit import UIKit
final class BannerCollectionViewCell: UICollectionViewCell class AppBannerCollectionViewCell: UICollectionViewListCell
{ {
let bannerView = AppBannerView(frame: .zero) let bannerView = AppBannerView(frame: .zero)
private(set) var errorBadge: UIView!
override init(frame: CGRect) override init(frame: CGRect)
{ {
super.init(frame: frame) super.init(frame: frame)
@@ -30,43 +28,24 @@ final class BannerCollectionViewCell: UICollectionViewCell
private func initialize() private func initialize()
{ {
// Prevent content "squishing" when scrolling offscreen.
self.insetsLayoutMarginsFromSafeArea = false
self.contentView.insetsLayoutMarginsFromSafeArea = false
self.bannerView.insetsLayoutMarginsFromSafeArea = false
self.selectedBackgroundView = UIView() // Disable selection highlighting.
self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight] self.contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
self.contentView.preservesSuperviewLayoutMargins = true self.contentView.preservesSuperviewLayoutMargins = true
self.bannerView.translatesAutoresizingMaskIntoConstraints = false self.bannerView.translatesAutoresizingMaskIntoConstraints = false
self.contentView.addSubview(self.bannerView) self.contentView.addSubview(self.bannerView)
let errorBadge = UIView()
errorBadge.translatesAutoresizingMaskIntoConstraints = false
errorBadge.isHidden = true
self.addSubview(errorBadge)
// Solid background to make the X opaque white.
let backgroundView = UIView()
backgroundView.translatesAutoresizingMaskIntoConstraints = false
backgroundView.backgroundColor = .white
errorBadge.addSubview(backgroundView)
let badgeView = UIImageView(image: UIImage(systemName: "exclamationmark.circle.fill"))
badgeView.preferredSymbolConfiguration = UIImage.SymbolConfiguration(scale: .large)
badgeView.tintColor = .systemRed
errorBadge.addSubview(badgeView, pinningEdgesWith: .zero)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
self.bannerView.topAnchor.constraint(equalTo: self.contentView.topAnchor), self.bannerView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor),
self.bannerView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), self.bannerView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor),
self.bannerView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor), self.bannerView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor),
self.bannerView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor), self.bannerView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor),
errorBadge.centerXAnchor.constraint(equalTo: self.bannerView.trailingAnchor, constant: -5),
errorBadge.centerYAnchor.constraint(equalTo: self.bannerView.topAnchor, constant: 5),
backgroundView.centerXAnchor.constraint(equalTo: badgeView.centerXAnchor),
backgroundView.centerYAnchor.constraint(equalTo: badgeView.centerYAnchor),
backgroundView.widthAnchor.constraint(equalTo: badgeView.widthAnchor, multiplier: 0.5),
backgroundView.heightAnchor.constraint(equalTo: badgeView.heightAnchor, multiplier: 0.5)
]) ])
self.errorBadge = errorBadge
} }
} }

View File

@@ -131,6 +131,8 @@ extension AppBannerView
} }
} }
} }
self.style = .app
let values = AppValues(app: app) let values = AppValues(app: app)
self.titleLabel.text = app.name // Don't use values.name since that already includes "beta". self.titleLabel.text = app.name // Don't use values.name since that already includes "beta".
@@ -147,6 +149,22 @@ extension AppBannerView
self.accessibilityLabel = values.name self.accessibilityLabel = values.name
} }
} }
func configure(for source: Source)
{
self.style = .source
self.titleLabel.text = source.name
let subtitle = source.subtitle ?? source.sourceURL.absoluteString
self.subtitleLabel.text = subtitle
let tintColor = source.effectiveTintColor ?? .altPrimary
self.tintColor = tintColor
let accessibilityLabel = source.name + "\n" + subtitle
self.accessibilityLabel = accessibilityLabel
}
} }
private extension AppBannerView private extension AppBannerView
@@ -162,20 +180,28 @@ private extension AppBannerView
switch self.style switch self.style
{ {
case .app: case .app:
self.directionalLayoutMargins.trailing = self.stackView.directionalLayoutMargins.trailing
self.iconImageViewHeightConstraint.constant = 60 self.iconImageViewHeightConstraint.constant = 60
self.iconImageView.style = .icon self.iconImageView.style = .icon
self.titleLabel.textColor = .label self.titleLabel.textColor = .label
self.button.style = .pill
self.backgroundEffectView.contentView.backgroundColor = UIColor(resource: .blurTint) self.backgroundEffectView.contentView.backgroundColor = UIColor(resource: .blurTint)
self.backgroundEffectView.backgroundColor = tintColor self.backgroundEffectView.backgroundColor = tintColor
case .source: case .source:
self.directionalLayoutMargins.trailing = 20
self.iconImageViewHeightConstraint.constant = 44 self.iconImageViewHeightConstraint.constant = 44
self.iconImageView.style = .circular self.iconImageView.style = .circular
self.titleLabel.textColor = .white self.titleLabel.textColor = .white
self.button.style = .custom
self.backgroundEffectView.contentView.backgroundColor = tintColor?.adjustedForDisplay self.backgroundEffectView.contentView.backgroundColor = tintColor?.adjustedForDisplay
self.backgroundEffectView.backgroundColor = nil self.backgroundEffectView.backgroundColor = nil

View File

@@ -0,0 +1,18 @@
//
// UIFontDescriptor+Bold.swift
// AltStore
//
// Created by Riley Testut on 10/16/23.
// Copyright © 2023 Riley Testut. All rights reserved.
//
import UIKit
extension UIFontDescriptor
{
func bolded() -> UIFontDescriptor
{
guard let descriptor = self.withSymbolicTraits(.traitBold) else { return self }
return descriptor
}
}

View File

@@ -133,11 +133,6 @@ class NewsViewController: UICollectionViewController, PeekPopPreviewing
self.collectionView.contentInset.bottom = 20 self.collectionView.contentInset.bottom = 20
} }
} }
@IBAction private func unwindFromSourcesViewController(_ segue: UIStoryboardSegue)
{
self.fetchSource()
}
} }
private extension NewsViewController private extension NewsViewController

View File

@@ -1,6 +1,6 @@
{ {
"info" : { "info" : {
"version" : 1, "author" : "xcode",
"author" : "xcode" "version" : 1
} }
} }

View File

@@ -1,150 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv"> <document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22154" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="7We-99-yEv">
<device id="retina6_12" orientation="portrait" appearance="light"/> <device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies> <dependencies>
<deployment identifier="iOS"/> <deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/> <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22130"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/> <capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/> <capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="collection view cell content view" minToolsVersion="11.0"/> <capability name="collection view cell content view" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies> </dependencies>
<scenes> <scenes>
<!--Forwarding Navigation Controller--> <!--Sources-->
<scene sceneID="QxB-Dd-1xC"> <scene sceneID="QxB-Dd-1xC">
<objects> <objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" id="7We-99-yEv" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController"> <navigationController automaticallyAdjustsScrollViewInsets="NO" id="7We-99-yEv" customClass="ForwardingNavigationController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" title="Sources" image="Sources" id="xPv-dc-X4v"/>
<toolbarItems/> <toolbarItems/>
<simulatedTabBarMetrics key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xWh-1U-u0q" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target"> <navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" largeTitles="YES" id="xWh-1U-u0q" customClass="NavigationBar" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="59" width="393" height="96"/> <rect key="frame" x="0.0" y="59" width="393" height="96"/>
<autoresizingMask key="autoresizingMask"/> <autoresizingMask key="autoresizingMask"/>
</navigationBar> </navigationBar>
<nil name="viewControllers"/> <nil name="viewControllers"/>
<connections> <connections>
<segue destination="5vR-Su-j54" kind="relationship" relationship="rootViewController" id="Kt6-Nl-WsS"/> <segue destination="Wm7-1O-FkD" kind="relationship" relationship="rootViewController" id="B9x-Lz-fg2"/>
</connections> </connections>
</navigationController> </navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ULL-gB-Cpt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/> <placeholder placeholderIdentifier="IBFirstResponder" id="ULL-gB-Cpt" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="-18" y="-13"/> <point key="canvasLocation" x="-18" y="-13"/>
</scene> </scene>
<!--Sources-->
<scene sceneID="hR0-Xj-lcc">
<objects>
<collectionViewController title="Sources" id="5vR-Su-j54" customClass="SourcesViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="M2N-lD-Q3M">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="ldV-TM-Jx1">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="50" height="200"/>
<size key="footerReferenceSize" width="50" height="50"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="mkn-CU-TaQ" customClass="AppBannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="9" y="200" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</collectionViewCell>
</cells>
<collectionReusableView key="sectionHeaderView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Header" id="w5e-xs-D45" customClass="TextCollectionReusableView" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="393" height="200"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Manage sources to control which apps are available to download through AltStore." textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="KCm-fD-Jy0">
<rect key="frame" x="8" y="14" width="377" height="171"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleCallout"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<constraints>
<constraint firstAttribute="bottom" secondItem="KCm-fD-Jy0" secondAttribute="bottom" priority="999" constant="15" id="1M7-ad-U2f"/>
<constraint firstItem="KCm-fD-Jy0" firstAttribute="top" secondItem="w5e-xs-D45" secondAttribute="top" priority="999" constant="14" id="5h0-b0-UWE"/>
<constraint firstAttribute="trailingMargin" secondItem="KCm-fD-Jy0" secondAttribute="trailing" priority="999" id="K04-Ud-iGz"/>
<constraint firstAttribute="leadingMargin" secondItem="KCm-fD-Jy0" secondAttribute="leading" priority="999" id="MN6-lr-3tF"/>
</constraints>
<connections>
<outlet property="bottomLayoutConstraint" destination="1M7-ad-U2f" id="tej-O7-Lyh"/>
<outlet property="leadingLayoutConstraint" destination="MN6-lr-3tF" id="Deq-Tk-7Lc"/>
<outlet property="textLabel" destination="KCm-fD-Jy0" id="alm-sb-NAa"/>
<outlet property="topLayoutConstraint" destination="5h0-b0-UWE" id="kHq-up-pCk"/>
<outlet property="trailingLayoutConstraint" destination="K04-Ud-iGz" id="rS5-c5-EkL"/>
</connections>
</collectionReusableView>
<collectionReusableView key="sectionFooterView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" reuseIdentifier="Footer" id="H5R-Ci-5aX" customClass="SourcesFooterView">
<rect key="frame" x="0.0" y="280" width="393" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="15" translatesAutoresizingMaskIntoConstraints="NO" id="oeg-V0-knE">
<rect key="frame" x="8" y="0.0" width="377" height="50"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="hnb-2l-24w">
<rect key="frame" x="0.0" y="0.0" width="377" height="0.0"/>
</activityIndicatorView>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" verticalCompressionResistancePriority="800" scrollEnabled="NO" contentInsetAdjustmentBehavior="never" editable="NO" text="Test" textAlignment="natural" translatesAutoresizingMaskIntoConstraints="NO" id="4Mf-Ge-exp">
<rect key="frame" x="0.0" y="15" width="377" height="35"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<color key="textColor" white="0.5" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
</stackView>
</subviews>
<constraints>
<constraint firstItem="oeg-V0-knE" firstAttribute="top" secondItem="H5R-Ci-5aX" secondAttribute="top" priority="999" id="KK7-2G-rL0" propertyAccessControl="none"/>
<constraint firstAttribute="trailingMargin" secondItem="oeg-V0-knE" secondAttribute="trailing" priority="999" id="aG4-4x-ACP"/>
<constraint firstAttribute="bottom" secondItem="oeg-V0-knE" secondAttribute="bottom" priority="999" id="ueD-zU-eSQ"/>
<constraint firstItem="oeg-V0-knE" firstAttribute="leading" secondItem="H5R-Ci-5aX" secondAttribute="leadingMargin" priority="999" id="w2y-e8-rJG"/>
</constraints>
<connections>
<outlet property="activityIndicatorView" destination="hnb-2l-24w" id="6Dp-gp-nlC"/>
<outlet property="bottomLayoutConstraint" destination="ueD-zU-eSQ" id="ChX-d2-sRT"/>
<outlet property="leadingLayoutConstraint" destination="w2y-e8-rJG" id="9j3-ao-bfA"/>
<outlet property="textView" destination="4Mf-Ge-exp" id="XqS-CB-3ek"/>
<outlet property="topLayoutConstraint" destination="KK7-2G-rL0" id="oFe-fV-BgO"/>
<outlet property="trailingLayoutConstraint" destination="aG4-4x-ACP" id="YOc-HZ-1pw"/>
</connections>
</collectionReusableView>
<connections>
<outlet property="dataSource" destination="5vR-Su-j54" id="lGW-bH-xYZ"/>
<outlet property="delegate" destination="5vR-Su-j54" id="bj3-kR-erl"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Sources" id="UQX-GH-OMC">
<barButtonItem key="leftBarButtonItem" systemItem="add" id="ox5-Bu-RLr">
<connections>
<action selector="addSource" destination="5vR-Su-j54" id="4s7-KQ-ume"/>
</connections>
</barButtonItem>
<barButtonItem key="rightBarButtonItem" style="done" systemItem="done" id="QYt-Dn-SKf">
<connections>
<segue destination="wBZ-c2-miy" kind="unwind" unwindAction="unwindFromSourcesViewController:" id="Dc6-tD-Z2I"/>
</connections>
</barButtonItem>
</navigationItem>
<connections>
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="dLj-Tf-ZjV"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="69z-hg-xF8" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<exit id="wBZ-c2-miy" userLabel="Exit" sceneMemberID="exit"/>
</objects>
<point key="canvasLocation" x="810" y="-13"/>
</scene>
<!--All Apps--> <!--All Apps-->
<scene sceneID="d8a-U8-CPc"> <scene sceneID="d8a-U8-CPc">
<objects> <objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="ih5-9R-QX7" userLabel="First Responder" sceneMemberID="firstResponder"/>
<collectionViewController storyboardIdentifier="browseViewController" id="Nhf-Gw-Ukx" customClass="BrowseViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController storyboardIdentifier="browseViewController" id="Nhf-Gw-Ukx" customClass="BrowseViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="oBI-6P-Lm3"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="oBI-6P-Lm3">
<rect key="frame" x="0.0" y="0.0" width="393" height="783"/> <rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="10" id="pSh-Xl-aNg"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="50" minimumInteritemSpacing="10" id="pSh-Xl-aNg">
@@ -161,13 +52,13 @@
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="All Apps" id="rUb-ON-AON"/> <navigationItem key="navigationItem" title="All Apps" id="rUb-ON-AON"/>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="ih5-9R-QX7" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="3404" y="-13"/> <point key="canvasLocation" x="3404" y="-13"/>
</scene> </scene>
<!--Source Detail View Controller--> <!--Source Detail View Controller-->
<scene sceneID="xbN-Mz-TtU"> <scene sceneID="xbN-Mz-TtU">
<objects> <objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="cYc-NX-nF1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<viewController storyboardIdentifier="sourceDetailViewController" id="7XE-Wv-lf9" customClass="SourceDetailViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController"> <viewController storyboardIdentifier="sourceDetailViewController" id="7XE-Wv-lf9" customClass="SourceDetailViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="eIv-0H-ZIq"> <view key="view" contentMode="scaleToFill" id="eIv-0H-ZIq">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
@@ -177,13 +68,13 @@
</view> </view>
<navigationItem key="navigationItem" id="Ocv-bj-TfG"/> <navigationItem key="navigationItem" id="Ocv-bj-TfG"/>
</viewController> </viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cYc-NX-nF1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="1646.5648854961833" y="-13.380281690140846"/> <point key="canvasLocation" x="1646.5648854961833" y="-13.380281690140846"/>
</scene> </scene>
<!--Source Detail Content View Controller--> <!--Source Detail Content View Controller-->
<scene sceneID="8nl-ly-jhT"> <scene sceneID="8nl-ly-jhT">
<objects> <objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="KEz-hK-u3f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<collectionViewController storyboardIdentifier="sourceDetailContentViewController" id="MSh-hM-32I" customClass="SourceDetailContentViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController storyboardIdentifier="sourceDetailContentViewController" id="MSh-hM-32I" customClass="SourceDetailContentViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="fiF-YD-Ing"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="fiF-YD-Ing">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/> <rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
@@ -223,16 +114,16 @@
<segue destination="Nhf-Gw-Ukx" kind="show" identifier="showAllApps" destinationCreationSelector="makeBrowseViewController:" id="On0-GP-kaE"/> <segue destination="Nhf-Gw-Ukx" kind="show" identifier="showAllApps" destinationCreationSelector="makeBrowseViewController:" id="On0-GP-kaE"/>
</connections> </connections>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="KEz-hK-u3f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="2509" y="-13"/> <point key="canvasLocation" x="2509" y="-13"/>
</scene> </scene>
<!--All News--> <!--All News-->
<scene sceneID="avV-5f-uNE"> <scene sceneID="avV-5f-uNE">
<objects> <objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="7f5-vn-JrS" userLabel="First Responder" sceneMemberID="firstResponder"/>
<collectionViewController storyboardIdentifier="newsViewController" id="MVH-oB-c8m" customClass="NewsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController"> <collectionViewController storyboardIdentifier="newsViewController" id="MVH-oB-c8m" customClass="NewsViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="p9p-rr-fbF"> <collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" id="p9p-rr-fbF">
<rect key="frame" x="0.0" y="0.0" width="393" height="783"/> <rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/> <color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="3cD-ax-3h6"> <collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="40" minimumInteritemSpacing="40" id="3cD-ax-3h6">
@@ -249,11 +140,53 @@
</collectionView> </collectionView>
<navigationItem key="navigationItem" title="All News" id="FGB-cd-Vkd"/> <navigationItem key="navigationItem" title="All News" id="FGB-cd-Vkd"/>
</collectionViewController> </collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="7f5-vn-JrS" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects> </objects>
<point key="canvasLocation" x="3404" y="-711"/> <point key="canvasLocation" x="3404" y="-711"/>
</scene> </scene>
<!--Sources-->
<scene sceneID="w2v-ek-6dY">
<objects>
<collectionViewController storyboardIdentifier="sourcesViewController" title="Sources" id="Wm7-1O-FkD" customClass="SourcesViewController" customModule="AltStore" customModuleProvider="target" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" alwaysBounceVertical="YES" dataMode="prototypes" id="VJr-nx-7Vh">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="10" id="Rlv-wJ-9Ef">
<size key="itemSize" width="375" height="80"/>
<size key="headerReferenceSize" width="0.0" height="0.0"/>
<size key="footerReferenceSize" width="0.0" height="0.0"/>
<inset key="sectionInset" minX="0.0" minY="0.0" maxX="0.0" maxY="0.0"/>
</collectionViewFlowLayout>
<cells>
<collectionViewCell opaque="NO" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" reuseIdentifier="Cell" id="1Rm-Gf-VDt" customClass="AppBannerCollectionViewCell" customModule="AltStore" customModuleProvider="target">
<rect key="frame" x="9" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO">
<rect key="frame" x="0.0" y="0.0" width="375" height="80"/>
<autoresizingMask key="autoresizingMask"/>
</view>
</collectionViewCell>
</cells>
<connections>
<outlet property="dataSource" destination="Wm7-1O-FkD" id="239-Tq-YUw"/>
<outlet property="delegate" destination="Wm7-1O-FkD" id="UvE-ta-8ir"/>
</connections>
</collectionView>
<navigationItem key="navigationItem" title="Sources" largeTitleDisplayMode="always" id="Noh-fc-wch">
<barButtonItem key="leftBarButtonItem" systemItem="add" id="y96-Ve-1gW"/>
</navigationItem>
<connections>
<segue destination="7XE-Wv-lf9" kind="show" identifier="showSourceDetails" destinationCreationSelector="makeSourceDetailViewController:sender:" id="eVl-NI-lj3"/>
</connections>
</collectionViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cR1-aE-0KX" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="810" y="-13"/>
</scene>
</scenes> </scenes>
<resources> <resources>
<image name="Sources" width="20" height="20"/>
<systemColor name="systemBackgroundColor"> <systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor> </systemColor>

View File

@@ -11,6 +11,7 @@ import CoreData
import AltStoreCore import AltStoreCore
import Roxas import Roxas
import Nuke
struct SourceError: ALTLocalizedError struct SourceError: ALTLocalizedError
{ {
@@ -40,151 +41,170 @@ private final class SourcesFooterView: TextCollectionReusableView
@IBOutlet var textView: UITextView! @IBOutlet var textView: UITextView!
} }
extension SourcesViewController private extension UIAction.Identifier
{ {
private enum Section: Int, CaseIterable static let showDetails = UIAction.Identifier("io.altstore.showDetails")
{ static let showError = UIAction.Identifier("io.altstore.showError")
case added
case trusted
}
} }
final class SourcesViewController: UICollectionViewController final class SourcesViewController: UICollectionViewController
{ {
var deepLinkSourceURL: URL? { var deepLinkSourceURL: URL? {
didSet { didSet {
guard let sourceURL = self.deepLinkSourceURL else { return } self.handleAddSourceDeepLink()
self.addSource(url: sourceURL)
} }
} }
private lazy var dataSource = self.makeDataSource() private lazy var dataSource = self.makeDataSource()
private lazy var addedSourcesDataSource = self.makeAddedSourcesDataSource()
private lazy var trustedSourcesDataSource = self.makeTrustedSourcesDataSource()
private var fetchTrustedSourcesOperation: UpdateKnownSourcesOperation? private lazy var dateFormatter: DateFormatter = {
private var fetchTrustedSourcesResult: Result<Void, Error>? let dateFormatter = DateFormatter()
private var _fetchTrustedSourcesContext: NSManagedObjectContext? dateFormatter.dateStyle = .short
dateFormatter.timeStyle = .none
return dateFormatter
}()
private var placeholderView: RSTPlaceholderView!
private var placeholderViewButton: UIButton!
private var placeholderViewCenterYConstraint: NSLayoutConstraint!
override func viewDidLoad() override func viewDidLoad()
{ {
super.viewDidLoad() super.viewDidLoad()
self.view.tintColor = .altPrimary let layout = self.makeLayout()
self.collectionView.collectionViewLayout = layout
self.navigationController?.view.tintColor = .altPrimary self.navigationController?.view.tintColor = .altPrimary
if let navigationBar = self.navigationController?.navigationBar as? NavigationBar self.collectionView.register(AppBannerCollectionViewCell.self, forCellWithReuseIdentifier: RSTCellContentGenericCellIdentifier)
{ self.collectionView.register(UICollectionViewListCell.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: UICollectionView.elementKindSectionHeader)
// Don't automatically adjust item positions when being presented non-full screen,
// or else the navigation bar content won't be vertically centered.
navigationBar.automaticallyAdjustsItemPositions = false
}
self.collectionView.dataSource = self.dataSource self.collectionView.dataSource = self.dataSource
self.collectionView.prefetchDataSource = self.dataSource
self.collectionView.allowsSelectionDuringEditing = false
#if !BETA let backgroundView = UIView(frame: .zero)
// Hide "Add Source" button for public version while in beta. backgroundView.backgroundColor = .altBackground
self.navigationItem.leftBarButtonItem = nil self.collectionView.backgroundView = backgroundView
#endif
}
override func viewWillAppear(_ animated: Bool)
{
super.viewWillAppear(animated)
if self.deepLinkSourceURL != nil self.placeholderView = RSTPlaceholderView(frame: .zero)
{ self.placeholderView.translatesAutoresizingMaskIntoConstraints = false
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true self.placeholderView.textLabel.text = NSLocalizedString("Add More Sources!", comment: "")
} self.placeholderView.detailTextLabel.text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis massa tortor, tempor vel est vitae, consequat luctus arcu."
backgroundView.addSubview(self.placeholderView)
if self.fetchTrustedSourcesOperation == nil let fontDescriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title3).bolded()
{ self.placeholderView.textLabel.font = UIFont(descriptor: fontDescriptor, size: 0.0)
self.fetchTrustedSources() self.placeholderView.detailTextLabel.font = UIFont.preferredFont(forTextStyle: .body)
} self.placeholderView.detailTextLabel.textAlignment = .natural
self.placeholderViewButton = UIButton(type: .system, primaryAction: UIAction(title: NSLocalizedString("View Recommended Sources", comment: "")) { [weak self] _ in
self?.performSegue(withIdentifier: "addSource", sender: nil)
})
self.placeholderViewButton.titleLabel?.font = UIFont.preferredFont(forTextStyle: .body)
self.placeholderView.stackView.spacing = 15
self.placeholderView.stackView.directionalLayoutMargins = NSDirectionalEdgeInsets(top: 15, leading: 15, bottom: 15, trailing: 15)
self.placeholderView.stackView.isLayoutMarginsRelativeArrangement = true
self.placeholderView.stackView.addArrangedSubview(self.placeholderViewButton)
self.placeholderViewCenterYConstraint = self.placeholderView.safeAreaLayoutGuide.centerYAnchor.constraint(equalTo: backgroundView.centerYAnchor, constant: 0)
NSLayoutConstraint.activate([
self.placeholderViewCenterYConstraint,
self.placeholderView.centerXAnchor.constraint(equalTo: backgroundView.centerXAnchor),
self.placeholderView.leadingAnchor.constraint(equalTo: backgroundView.leadingAnchor),
self.placeholderView.trailingAnchor.constraint(equalTo: backgroundView.trailingAnchor),
self.placeholderView.leadingAnchor.constraint(equalTo: self.placeholderView.stackView.leadingAnchor),
self.placeholderView.trailingAnchor.constraint(equalTo: self.placeholderView.stackView.trailingAnchor),
self.placeholderView.topAnchor.constraint(equalTo: self.placeholderView.stackView.topAnchor),
self.placeholderView.bottomAnchor.constraint(equalTo: self.placeholderView.stackView.bottomAnchor),
])
self.navigationItem.rightBarButtonItem = self.editButtonItem
self.update()
} }
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
{ {
super.viewDidAppear(animated) super.viewDidAppear(animated)
if let sourceURL = self.deepLinkSourceURL self.handleAddSourceDeepLink()
}
override func viewDidLayoutSubviews()
{
super.viewDidLayoutSubviews()
// Vertically center placeholder view in gap below first item.
let indexPath = IndexPath(item: 0, section: 0)
guard let layoutAttributes = self.collectionView.layoutAttributesForItem(at: indexPath) else { return }
let maxY = layoutAttributes.frame.maxY
let constant = maxY / 2
if self.placeholderViewCenterYConstraint.constant != constant
{ {
self.addSource(url: sourceURL) self.placeholderViewCenterYConstraint.constant = constant
} }
} }
} }
private extension SourcesViewController private extension SourcesViewController
{ {
func makeDataSource() -> RSTCompositeCollectionViewDataSource<Source> func makeLayout() -> UICollectionViewCompositionalLayout
{ {
let dataSource = RSTCompositeCollectionViewDataSource<Source>(dataSources: [self.addedSourcesDataSource, self.trustedSourcesDataSource]) var configuration = UICollectionLayoutListConfiguration(appearance: .grouped)
dataSource.proxy = self configuration.headerMode = .supplementary
dataSource.cellConfigurationHandler = { [weak self] (cell, source, indexPath) in configuration.showsSeparators = false
guard let self else { return } configuration.backgroundColor = .clear
configuration.trailingSwipeActionsConfigurationProvider = { [weak self] indexPath in
guard let self else { return UISwipeActionsConfiguration(actions: []) }
let tintColor = UIColor.altPrimary let source = self.dataSource.item(at: indexPath)
var actions: [UIContextualAction] = []
let cell = cell as! AppBannerCollectionViewCell if source.identifier != Source.altStoreIdentifier
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.tintColor = tintColor
cell.bannerView.iconImageView.isHidden = true
cell.bannerView.buttonLabel.isHidden = true
cell.bannerView.button.isIndicatingActivity = false
switch Section.allCases[indexPath.section]
{ {
case .added: // Prevent users from removing AltStore source.
cell.bannerView.button.isHidden = true
case .trusted: let removeAction = UIContextualAction(style: .destructive,
// Quicker way to determine whether a source is already added than by reading from disk. title: NSLocalizedString("Remove", comment: "")) { _, _, completion in
if (self.addedSourcesDataSource.fetchedResultsController.fetchedObjects ?? []).contains(where: { $0.identifier == source.identifier }) self.remove(source, completionHandler: completion)
{
// Source exists in .added section, so hide the button.
cell.bannerView.button.isHidden = true
let configuation = UIImage.SymbolConfiguration(pointSize: 24)
let imageAttachment = NSTextAttachment()
imageAttachment.image = UIImage(systemName: "checkmark.circle", withConfiguration: configuation)?.withTintColor(.altPrimary)
let attributedText = NSAttributedString(attachment: imageAttachment)
cell.bannerView.buttonLabel.attributedText = attributedText
cell.bannerView.buttonLabel.textAlignment = .center
cell.bannerView.buttonLabel.isHidden = false
}
else
{
// Source does not exist in .added section, so show the button.
cell.bannerView.button.isHidden = false
cell.bannerView.buttonLabel.attributedText = nil
} }
removeAction.image = UIImage(systemName: "trash.fill")
cell.bannerView.button.setTitle(NSLocalizedString("ADD", comment: ""), for: .normal) actions.append(removeAction)
cell.bannerView.button.addTarget(self, action: #selector(SourcesViewController.addTrustedSource(_:)), for: .primaryActionTriggered)
} }
cell.bannerView.titleLabel.text = source.name
cell.bannerView.subtitleLabel.text = source.sourceURL.absoluteString
cell.bannerView.subtitleLabel.numberOfLines = 2
cell.errorBadge?.isHidden = (source.error == nil) if let error = source.error
{
let viewErrorAction = UIContextualAction(style: .normal,
title: NSLocalizedString("View Error", comment: "")) { _, _, completion in
self.present(error)
completion(true)
}
viewErrorAction.backgroundColor = .systemYellow
viewErrorAction.image = UIImage(systemName: "exclamationmark.circle.fill")
actions.append(viewErrorAction)
}
let attributedLabel = NSAttributedString(string: source.name + "\n" + source.sourceURL.absoluteString, attributes: [.accessibilitySpeechPunctuation: true]) let config = UISwipeActionsConfiguration(actions: actions)
cell.bannerView.accessibilityAttributedLabel = attributedLabel config.performsFirstActionWithFullSwipe = false
cell.bannerView.accessibilityTraits.remove(.button)
// Make sure refresh button is correct size. return config
cell.layoutIfNeeded()
} }
return dataSource let layout = UICollectionViewCompositionalLayout.list(using: configuration)
return layout
} }
func makeAddedSourcesDataSource() -> RSTFetchedResultsCollectionViewDataSource<Source> func makeDataSource() -> RSTFetchedResultsCollectionViewPrefetchingDataSource<Source, UIImage>
{ {
let fetchRequest = Source.fetchRequest() as NSFetchRequest<Source> let fetchRequest = Source.fetchRequest() as NSFetchRequest<Source>
fetchRequest.returnsObjectsAsFaults = false fetchRequest.returnsObjectsAsFaults = false
@@ -195,13 +215,129 @@ private extension SourcesViewController
NSSortDescriptor(keyPath: \Source.identifier, ascending: true)] NSSortDescriptor(keyPath: \Source.identifier, ascending: true)]
let dataSource = RSTFetchedResultsCollectionViewDataSource<Source>(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext) let fetchedResultsController = NSFetchedResultsController(fetchRequest: fetchRequest, managedObjectContext: DatabaseManager.shared.viewContext, sectionNameKeyPath: nil, cacheName: nil)
return dataSource fetchedResultsController.delegate = self
}
let dataSource = RSTFetchedResultsCollectionViewPrefetchingDataSource<Source, UIImage>(fetchedResultsController: fetchedResultsController)
func makeTrustedSourcesDataSource() -> RSTArrayCollectionViewDataSource<Source> dataSource.proxy = self
{ dataSource.cellConfigurationHandler = { [weak self] (cell, source, indexPath) in
let dataSource = RSTArrayCollectionViewDataSource<Source>(items: []) guard let self else { return }
let cell = cell as! AppBannerCollectionViewCell
cell.layoutMargins.top = 5
cell.layoutMargins.bottom = 5
cell.layoutMargins.left = self.view.layoutMargins.left
cell.layoutMargins.right = self.view.layoutMargins.right
cell.bannerView.configure(for: source)
cell.bannerView.iconImageView.image = nil
cell.bannerView.iconImageView.isIndicatingActivity = true
let numberOfApps: Int
if let patreonAccount = DatabaseManager.shared.patreonAccount(), patreonAccount.isPatron, PatreonAPI.shared.isAuthenticated
{
numberOfApps = source.apps.count
}
else
{
numberOfApps = source.apps.filter { !$0.isBeta }.count
}
if let error = source.error
{
let image = UIImage(systemName: "exclamationmark")?.withTintColor(.white, renderingMode: .alwaysOriginal)
cell.bannerView.button.setImage(image, for: .normal)
cell.bannerView.button.setTitle(nil, for: .normal)
cell.bannerView.button.tintColor = .systemYellow.withAlphaComponent(0.75)
let action = UIAction(identifier: .showError) { _ in
self.present(error)
}
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
cell.bannerView.button.removeAction(identifiedBy: .showDetails, for: .primaryActionTriggered)
}
else
{
cell.bannerView.button.setImage(nil, for: .normal)
cell.bannerView.button.setTitle(numberOfApps.description, for: .normal)
cell.bannerView.button.tintColor = .white.withAlphaComponent(0.2)
let action = UIAction(identifier: .showDetails) { _ in
self.showSourceDetails(for: source)
}
cell.bannerView.button.addAction(action, for: .primaryActionTriggered)
cell.bannerView.button.removeAction(identifiedBy: .showError, for: .primaryActionTriggered)
}
let dateText: String
if let lastUpdatedDate = source.lastUpdatedDate
{
dateText = Date().relativeDateString(since: lastUpdatedDate, dateFormatter: self.dateFormatter)
}
else
{
dateText = NSLocalizedString("Never", comment: "")
}
let text = String(format: NSLocalizedString("Last Updated: %@", comment: ""), dateText)
cell.bannerView.subtitleLabel.text = text
cell.bannerView.subtitleLabel.numberOfLines = 1
let numberOfAppsText: String
if #available(iOS 15, *)
{
let attributedOutput = AttributedString(localized: "^[\(numberOfApps) app](inflect: true)")
numberOfAppsText = String(attributedOutput.characters)
}
else
{
numberOfAppsText = ""
}
let accessibilityLabel = source.name + "\n" + text + ".\n" + numberOfAppsText
cell.bannerView.accessibilityLabel = accessibilityLabel
if source.identifier != Source.altStoreIdentifier
{
cell.accessories = [.delete(displayed: .whenEditing)]
}
else
{
cell.accessories = []
}
cell.bannerView.accessibilityTraits.remove(.button)
// Make sure refresh button is correct size.
cell.layoutIfNeeded()
}
dataSource.prefetchHandler = { (source, indexPath, completionHandler) in
guard let imageURL = source.effectiveIconURL else { return nil }
return RSTAsyncBlockOperation() { (operation) in
ImagePipeline.shared.loadImage(with: imageURL, progress: nil) { result in
guard !operation.isCancelled else { return operation.finish() }
switch result
{
case .success(let response): completionHandler(response.image, nil)
case .failure(let error): completionHandler(nil, error)
}
}
}
}
dataSource.prefetchCompletionHandler = { (cell, image, indexPath, error) in
let cell = cell as! AppBannerCollectionViewCell
cell.bannerView.iconImageView.isIndicatingActivity = false
cell.bannerView.iconImageView.image = image
if let error = error
{
print("Error loading image:", error)
}
}
return dataSource return dataSource
} }
@@ -217,7 +353,7 @@ private extension SourcesViewController
private extension SourcesViewController private extension SourcesViewController
{ {
@IBAction func addSource() func handleAddSourceDeepLink()
{ {
let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert) let alertController = UIAlertController(title: NSLocalizedString("Add Source", comment: ""), message: nil, preferredStyle: .alert)
alertController.addTextField { (textField) in alertController.addTextField { (textField) in
@@ -239,19 +375,12 @@ private extension SourcesViewController
self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
} }
}) })
guard let url = self.deepLinkSourceURL, self.view.window != nil else { return }
self.present(alertController, animated: true, completion: nil) // Only handle deep link once.
} self.deepLinkSourceURL = nil
func addSource(url: URL, completionHandler: ((Result<Void, Error>) -> Void)? = nil)
{
guard self.view.window != nil else { return }
if url == self.deepLinkSourceURL self.navigationItem.leftBarButtonItem?.isIndicatingActivity = true
{
// Only handle deep link once.
self.deepLinkSourceURL = nil
}
func finish(_ result: Result<Void, Error>) func finish(_ result: Result<Void, Error>)
{ {
@@ -270,48 +399,22 @@ private extension SourcesViewController
self.present(error.withLocalizedTitle(NSLocalizedString("Unable to Add Source", comment: ""))) self.present(error.withLocalizedTitle(NSLocalizedString("Unable to Add Source", comment: "")))
} }
self.collectionView.reloadSections([Section.trusted.rawValue]) self.navigationItem.leftBarButtonItem?.isIndicatingActivity = false
completionHandler?(result)
} }
} }
var dependencies: [Foundation.Operation] = [] AppManager.shared.fetchSource(sourceURL: url) { (result) in
if let fetchTrustedSourcesOperation = self.fetchTrustedSourcesOperation
{
// Must fetch trusted sources first to determine whether this is a trusted source.
// We assume fetchTrustedSources() has already been called before this method.
dependencies = [fetchTrustedSourcesOperation]
}
AppManager.shared.fetchSource(sourceURL: url, dependencies: dependencies) { (result) in
do do
{ {
// Use @Managed before calling perform() to keep // Use @Managed before calling perform() to keep
// strong reference to source.managedObjectContext. // strong reference to source.managedObjectContext.
@Managed var source = try result.get() @Managed var source = try result.get()
let backgroundContext = DatabaseManager.shared.persistentContainer.newBackgroundContext() DispatchQueue.main.async {
backgroundContext.perform { self.showSourceDetails(for: source)
do
{
let predicate = NSPredicate(format: "%K == %@", #keyPath(Source.identifier), $source.identifier)
if let existingSource = Source.first(satisfying: predicate, in: backgroundContext)
{
throw SourceError.duplicate(source, existingSource: existingSource)
}
DispatchQueue.main.async {
self.showSourceDetails(for: source)
}
finish(.success(()))
}
catch
{
finish(.failure(error))
}
} }
finish(.success(()))
} }
catch catch
{ {
@@ -340,42 +443,22 @@ private extension SourcesViewController
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }
func fetchTrustedSources() func remove(_ source: Source, completionHandler: ((Bool) -> Void)? = nil)
{ {
// Closure instead of local function so we can capture `self` weakly. Task<Void, Never> {
let finish: (Result<[Source], Error>) -> Void = { [weak self] result in do
self?.fetchTrustedSourcesResult = result.map { _ in () }
DispatchQueue.main.async {
do
{
let sources = try result.get()
print("Fetched trusted sources:", sources.map { $0.identifier })
let sectionUpdate = RSTCellContentChange(type: .update, sectionIndex: 0)
self?.trustedSourcesDataSource.setItems(sources, with: [sectionUpdate])
}
catch
{
print("Error fetching trusted sources:", error)
let sectionUpdate = RSTCellContentChange(type: .update, sectionIndex: 0)
self?.trustedSourcesDataSource.setItems([], with: [sectionUpdate])
}
}
}
self.fetchTrustedSourcesOperation = AppManager.shared.updateKnownSources { [weak self] result in
switch result
{ {
case .failure(let error): finish(.failure(error)) try await AppManager.shared.remove(source, presentingViewController: self)
case .success((let trustedSources, _)):
// Don't show sources without a sourceURL.
let featuredSourceURLs = trustedSources.compactMap { $0.sourceURL }
// This context is never saved, but keeps the managed sources alive. completionHandler?(true)
let context = DatabaseManager.shared.persistentContainer.newBackgroundSavingViewContext() }
self?._fetchTrustedSourcesContext = context catch is CancellationError
{
completionHandler?(false)
}
catch
{
completionHandler?(false)
let dispatchGroup = DispatchGroup() let dispatchGroup = DispatchGroup()
@@ -410,60 +493,36 @@ private extension SourcesViewController
let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] } let sources = featuredSourceURLs.compactMap { sourcesByURL[$0] }
finish(.success(sources)) finish(.success(sources))
} }
self.present(error)
} }
} }
} }
@IBAction func addTrustedSource(_ sender: PillButton)
{
let point = self.collectionView.convert(sender.center, from: sender.superview)
guard let indexPath = self.collectionView.indexPathForItem(at: point) else { return }
let completedProgress = Progress(totalUnitCount: 1)
completedProgress.completedUnitCount = 1
sender.progress = completedProgress
let source = self.dataSource.item(at: indexPath)
self.addSource(url: source.sourceURL) { _ in
//FIXME: Handle cell reuse.
sender.progress = nil
}
}
func remove(_ source: Source)
{
let alertController = UIAlertController(title: String(format: NSLocalizedString("Are you sure you want to remove the source “%@”?", comment: ""), source.name),
message: NSLocalizedString("Any apps you've installed from this source will remain, but they'll no longer receive any app updates.", comment: ""), preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: UIAlertAction.cancel.title, style: UIAlertAction.cancel.style, handler: nil))
alertController.addAction(UIAlertAction(title: NSLocalizedString("Remove Source", comment: ""), style: .destructive) { _ in
DatabaseManager.shared.persistentContainer.performBackgroundTask { (context) in
let source = context.object(with: source.objectID) as! Source
context.delete(source)
do
{
try context.save()
DispatchQueue.main.async {
self.collectionView.reloadSections([Section.trusted.rawValue])
}
}
catch
{
DispatchQueue.main.async {
self.present(error)
}
}
}
})
self.present(alertController, animated: true, completion: nil)
}
func showSourceDetails(for source: Source) func showSourceDetails(for source: Source)
{ {
self.performSegue(withIdentifier: "showSourceDetails", sender: source) self.performSegue(withIdentifier: "showSourceDetails", sender: source)
} }
func update()
{
if self.dataSource.itemCount < 2
{
// Show placeholder view
self.placeholderView.isHidden = false
self.collectionView.alwaysBounceVertical = false
self.setEditing(false, animated: true)
self.editButtonItem.isEnabled = false
}
else
{
self.placeholderView.isHidden = true
self.collectionView.alwaysBounceVertical = true
self.editButtonItem.isEnabled = true
}
}
} }
extension SourcesViewController extension SourcesViewController
@@ -475,55 +534,17 @@ extension SourcesViewController
let source = self.dataSource.item(at: indexPath) let source = self.dataSource.item(at: indexPath)
self.showSourceDetails(for: source) self.showSourceDetails(for: source)
} }
}
extension SourcesViewController: UICollectionViewDelegateFlowLayout
{
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize
{
return CGSize(width: collectionView.bounds.width, height: 80)
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize
{
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionHeader, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize
{
guard Section(rawValue: section) == .trusted else { return .zero }
let indexPath = IndexPath(row: 0, section: section)
let headerView = self.collectionView(collectionView, viewForSupplementaryElementOfKind: UICollectionView.elementKindSectionFooter, at: indexPath)
// Use this view to calculate the optimal size based on the collection view's width
let size = headerView.systemLayoutSizeFitting(CGSize(width: collectionView.frame.width, height: UIView.layoutFittingExpandedSize.height),
withHorizontalFittingPriority: .required, // Width is fixed
verticalFittingPriority: .fittingSizeLevel) // Height can be as large as needed
return size
}
override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView
{ {
let reuseIdentifier = (kind == UICollectionView.elementKindSectionHeader) ? "Header" : "Footer" let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: kind, for: indexPath) as! UICollectionViewListCell
let headerView = collectionView.dequeueReusableSupplementaryView(ofKind: kind, withReuseIdentifier: reuseIdentifier, for: indexPath) as! TextCollectionReusableView var configuation = UIListContentConfiguration.cell()
headerView.layoutMargins.left = self.view.layoutMargins.left configuation.text = NSLocalizedString("Sources control what apps are available to download through AltStore.", comment: "")
headerView.layoutMargins.right = self.view.layoutMargins.right configuation.textProperties.color = .secondaryLabel
configuation.textProperties.alignment = .natural
/* Changing NSLayoutConstraint priorities from required to optional (and vice versa) isnt supported, and crashes on iOS 12. */ headerView.contentConfiguration = configuation
// let almostRequiredPriority = UILayoutPriority(UILayoutPriority.required.rawValue - 1) // Can't be required or else we can't satisfy constraints when hidden (size = 0).
// headerView.leadingLayoutConstraint?.priority = almostRequiredPriority
// headerView.trailingLayoutConstraint?.priority = almostRequiredPriority
// headerView.topLayoutConstraint?.priority = almostRequiredPriority
// headerView.bottomLayoutConstraint?.priority = almostRequiredPriority
switch kind switch kind
{ {
@@ -607,78 +628,79 @@ extension SourcesViewController: UICollectionViewDelegateFlowLayout
} }
} }
extension SourcesViewController extension SourcesViewController: NSFetchedResultsControllerDelegate
{ {
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? func controllerWillChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{ {
let source = self.dataSource.item(at: indexPath) self.dataSource.controllerWillChangeContent(controller)
}
func controllerDidChangeContent(_ controller: NSFetchedResultsController<NSFetchRequestResult>)
{
self.update()
self.dataSource.controllerDidChangeContent(controller)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange anObject: Any, at indexPath: IndexPath?, for type: NSFetchedResultsChangeType, newIndexPath: IndexPath?)
{
self.dataSource.controller(controller, didChange: anObject, at: indexPath, for: type, newIndexPath: newIndexPath)
}
func controller(_ controller: NSFetchedResultsController<NSFetchRequestResult>, didChange sectionInfo: NSFetchedResultsSectionInfo, atSectionIndex sectionIndex: Int, for type: NSFetchedResultsChangeType)
{
self.dataSource.controller(controller, didChange: sectionInfo, atSectionIndex: UInt(sectionIndex), for: type)
}
}
return UIContextMenuConfiguration(identifier: indexPath as NSIndexPath, previewProvider: nil) { (suggestedActions) -> UIMenu? in @available(iOS 17, *)
let viewErrorAction = UIAction(title: NSLocalizedString("View Error", comment: ""), image: UIImage(systemName: "exclamationmark.circle")) { (action) in #Preview(traits: .portrait) {
guard let error = source.error else { return } DatabaseManager.shared.startForPreview()
self.present(error)
} let storyboard = UIStoryboard(name: "Sources", bundle: nil)
let sourcesViewController = storyboard.instantiateInitialViewController()!
let deleteAction = UIAction(title: NSLocalizedString("Remove", comment: ""), image: UIImage(systemName: "trash"), attributes: [.destructive]) { (action) in
self.remove(source) let context = DatabaseManager.shared.persistentContainer.newBackgroundContext()
} context.performAndWait {
_ = Source.make(name: "OatmealDome's AltStore Source",
let addAction = UIAction(title: String(format: NSLocalizedString("Add “%@”", comment: ""), source.name), image: UIImage(systemName: "plus")) { (action) in identifier: "me.oatmealdome.altstore",
self.addSource(url: source.sourceURL) sourceURL: URL(string: "https://altstore.oatmealdome.me")!,
} context: context)
var actions: [UIAction] = [] _ = Source.make(name: "UTM Repository",
identifier: "com.utmapp.repos.UTM",
if source.error != nil sourceURL: URL(string: "https://alt.getutm.app")!,
{ context: context)
actions.append(viewErrorAction)
} _ = Source.make(name: "Flyinghead",
identifier: "com.flyinghead.source",
switch Section.allCases[indexPath.section] sourceURL: URL(string: "https://flyinghead.github.io/flycast-builds/altstore.json")!,
{ context: context)
case .added:
if source.identifier != Source.altStoreIdentifier _ = Source.make(name: "Provenance",
{ identifier: "org.provenance-emu.AltStore",
actions.append(deleteAction) sourceURL: URL(string: "https://provenance-emu.com/apps.json")!,
} context: context)
case .trusted: _ = Source.make(name: "PojavLauncher Repository",
if let cell = collectionView.cellForItem(at: indexPath) as? AppBannerCollectionViewCell, !cell.bannerView.button.isHidden identifier: "dev.crystall1ne.repos.PojavLauncher",
{ sourceURL: URL(string: "http://alt.crystall1ne.dev")!,
actions.append(addAction) context: context)
}
} try! context.save()
}
guard !actions.isEmpty else { return nil }
AppManager.shared.fetchSources { result in
let menu = UIMenu(title: "", children: actions) do
return menu {
let (sources, context) = try result.get()
try context.save()
}
catch
{
print("Preview failed to fetch sources:", error)
} }
} }
override func collectionView(_ collectionView: UICollectionView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? return sourcesViewController
{
guard let indexPath = configuration.identifier as? NSIndexPath else { return nil }
guard let cell = collectionView.cellForItem(at: indexPath as IndexPath) as? AppBannerCollectionViewCell else { return nil }
let parameters = UIPreviewParameters()
parameters.backgroundColor = .clear
parameters.visiblePath = UIBezierPath(roundedRect: cell.bannerView.bounds, cornerRadius: cell.bannerView.layer.cornerRadius)
let preview = UITargetedPreview(view: cell.bannerView, parameters: parameters)
return preview
}
override func collectionView(_ collectionView: UICollectionView, previewForDismissingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview?
{
return self.collectionView(collectionView, previewForHighlightingContextMenuWithConfiguration: configuration)
}
}
extension SourcesViewController: UITextViewDelegate
{
func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool
{
return true
}
} }

View File

@@ -14,6 +14,7 @@ extension TabBarController
private enum Tab: Int, CaseIterable private enum Tab: Int, CaseIterable
{ {
case news case news
case sources
case browse case browse
case myApps case myApps
case settings case settings
@@ -26,6 +27,8 @@ final class TabBarController: UITabBarController
private var _viewDidAppear = false private var _viewDidAppear = false
private var sourcesViewController: SourcesViewController!
required init?(coder aDecoder: NSCoder) required init?(coder aDecoder: NSCoder)
{ {
super.init(coder: aDecoder) super.init(coder: aDecoder)
@@ -36,6 +39,14 @@ final class TabBarController: UITabBarController
NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(TabBarController.openErrorLog(_:)), name: ToastView.openErrorLogNotification, object: nil)
} }
override func viewDidLoad()
{
super.viewDidLoad()
let sourcesNavigationController = self.viewControllers![Tab.sources.rawValue] as! UINavigationController
self.sourcesViewController = sourcesNavigationController.viewControllers.first as? SourcesViewController
}
override func viewDidAppear(_ animated: Bool) override func viewDidAppear(_ animated: Bool)
{ {
super.viewDidAppear(animated) super.viewDidAppear(animated)
@@ -63,15 +74,6 @@ final class TabBarController: UITabBarController
switch identifier switch identifier
{ {
case "presentSources":
guard let notification = sender as? Notification,
let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
else { return }
let navigationController = segue.destination as! UINavigationController
let sourcesViewController = navigationController.viewControllers.first as! SourcesViewController
sourcesViewController.deepLinkSourceURL = sourceURL
case "finishJailbreak": case "finishJailbreak":
guard let installedApp = sender as? InstalledApp else { return } guard let installedApp = sender as? InstalledApp else { return }
@@ -104,30 +106,19 @@ extension TabBarController
{ {
if let presentedViewController = self.presentedViewController if let presentedViewController = self.presentedViewController
{ {
if let navigationController = presentedViewController as? UINavigationController, presentedViewController.dismiss(animated: true) {
let sourcesViewController = navigationController.viewControllers.first as? SourcesViewController self.presentSources(sender)
{
if let notification = (sender as? Notification),
let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
{
sourcesViewController.deepLinkSourceURL = sourceURL
}
else
{
// Don't dismiss SourcesViewController if it's already presented.
}
}
else
{
presentedViewController.dismiss(animated: true) {
self.presentSources(sender)
}
} }
return return
} }
if let notification = (sender as? Notification), let sourceURL = notification.userInfo?[AppDelegate.addSourceDeepLinkURLKey] as? URL
{
self.sourcesViewController?.deepLinkSourceURL = sourceURL
}
self.performSegue(withIdentifier: "presentSources", sender: sender) self.selectedIndex = Tab.sources.rawValue
} }
} }

View File

@@ -386,6 +386,13 @@ public extension Source
} }
return isRecommended return isRecommended
} }
var lastUpdatedDate: Date? {
let allDates = self.apps.compactMap { $0.latestAvailableVersion?.date } + self.newsItems.map { $0.date }
let lastUpdatedDate = allDates.sorted().last
return lastUpdatedDate
}
} }
internal extension Source internal extension Source
@@ -433,4 +440,14 @@ public extension Source
let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context) let source = Source.first(satisfying: NSPredicate(format: "%K == %@", #keyPath(Source.identifier), Source.altStoreIdentifier), in: context)
return source return source
} }
class func make(name: String, identifier: String, sourceURL: URL, context: NSManagedObjectContext) -> Source
{
let source = Source(context: context)
source.name = name
source.identifier = identifier
source.sourceURL = sourceURL
return source
}
} }