mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Refactors SourceViewController into dedicated tab
* Updates UI to use source icons + tint colors * Adds Edit button + swipe actions
This commit is contained in:
@@ -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 */,
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
18
AltStore/Extensions/UIFontDescriptor+Bold.swift
Normal file
18
AltStore/Extensions/UIFontDescriptor+Bold.swift
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info" : {
|
||||||
"version" : 1,
|
"author" : "xcode",
|
||||||
"author" : "xcode"
|
"version" : 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 739 B After Width: | Height: | Size: 739 B |
|
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -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>
|
||||||
|
|||||||
@@ -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) isn’t 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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user