XCode project for app, moved app project to folder

This commit is contained in:
Joe Mattiello
2023-03-01 22:07:19 -05:00
parent 365cadbb31
commit 4c9c5b1a56
371 changed files with 625 additions and 39 deletions

View File

@@ -0,0 +1,128 @@
//
// AppBannerView.swift
// AltStore
//
// Created by Riley Testut on 8/29/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import SideStoreCore
import RoxasUIKit
class AppBannerView: RSTNibView {
override var accessibilityLabel: String? {
get { self.accessibilityView?.accessibilityLabel }
set { self.accessibilityView?.accessibilityLabel = newValue }
}
override open var accessibilityAttributedLabel: NSAttributedString? {
get { self.accessibilityView?.accessibilityAttributedLabel }
set { self.accessibilityView?.accessibilityAttributedLabel = newValue }
}
override var accessibilityValue: String? {
get { self.accessibilityView?.accessibilityValue }
set { self.accessibilityView?.accessibilityValue = newValue }
}
override open var accessibilityAttributedValue: NSAttributedString? {
get { self.accessibilityView?.accessibilityAttributedValue }
set { self.accessibilityView?.accessibilityAttributedValue = newValue }
}
override open var accessibilityTraits: UIAccessibilityTraits {
get { accessibilityView?.accessibilityTraits ?? [] }
set { accessibilityView?.accessibilityTraits = newValue }
}
private var originalTintColor: UIColor?
@IBOutlet var titleLabel: UILabel!
@IBOutlet var subtitleLabel: UILabel!
@IBOutlet var iconImageView: AppIconImageView!
@IBOutlet var button: PillButton!
@IBOutlet var buttonLabel: UILabel!
@IBOutlet var betaBadgeView: UIView!
@IBOutlet var backgroundEffectView: UIVisualEffectView!
@IBOutlet private var vibrancyView: UIVisualEffectView!
@IBOutlet private var accessibilityView: UIView!
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
initialize()
}
private func initialize() {
accessibilityView.accessibilityTraits.formUnion(.button)
isAccessibilityElement = false
accessibilityElements = [accessibilityView, button].compactMap { $0 }
betaBadgeView.isHidden = true
}
override func tintColorDidChange() {
super.tintColorDidChange()
if tintAdjustmentMode != .dimmed {
originalTintColor = tintColor
}
update()
}
}
extension AppBannerView {
func configure(for app: AppProtocol) {
struct AppValues {
var name: String
var developerName: String?
var isBeta: Bool = false
init(app: AppProtocol) {
name = app.name
guard let storeApp = (app as? StoreApp) ?? (app as? InstalledApp)?.storeApp else { return }
developerName = storeApp.developerName
if storeApp.isBeta {
name = String(format: NSLocalizedString("%@ beta", comment: ""), app.name)
isBeta = true
}
}
}
let values = AppValues(app: app)
titleLabel.text = app.name // Don't use values.name since that already includes "beta".
betaBadgeView.isHidden = !values.isBeta
if let developerName = values.developerName {
subtitleLabel.text = developerName
accessibilityLabel = String(format: NSLocalizedString("%@ by %@", comment: ""), values.name, developerName)
} else {
subtitleLabel.text = NSLocalizedString("Sideloaded", comment: "")
accessibilityLabel = values.name
}
}
}
private extension AppBannerView {
func update() {
clipsToBounds = true
layer.cornerRadius = 22
subtitleLabel.textColor = originalTintColor ?? tintColor
backgroundEffectView.backgroundColor = originalTintColor ?? tintColor
}
}

View File

@@ -0,0 +1,36 @@
//
// AppIconImageView.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class AppIconImageView: UIImageView {
override func awakeFromNib() {
super.awakeFromNib()
contentMode = .scaleAspectFill
clipsToBounds = true
backgroundColor = .white
if #available(iOS 13, *) {
self.layer.cornerCurve = .continuous
} else {
if layer.responds(to: Selector(("continuousCorners"))) {
layer.setValue(true, forKey: "continuousCorners")
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
// Based off of 60pt icon having 12pt radius.
let radius = bounds.height / 5
layer.cornerRadius = radius
}
}

View File

@@ -0,0 +1,89 @@
//
// BackgroundTaskManager.swift
// AltStore
//
// Created by Riley Testut on 6/19/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import AVFoundation
public final class BackgroundTaskManager {
public static let shared = BackgroundTaskManager()
private var isPlaying = false
private let audioEngine: AVAudioEngine
private let player: AVAudioPlayerNode
private let audioFile: AVAudioFile
private let audioEngineQueue: DispatchQueue
private init() {
audioEngine = AVAudioEngine()
audioEngine.mainMixerNode.outputVolume = 0.0
player = AVAudioPlayerNode()
audioEngine.attach(player)
do {
let audioFileURL = Bundle.main.url(forResource: "Silence", withExtension: "m4a")!
audioFile = try AVAudioFile(forReading: audioFileURL)
audioEngine.connect(player, to: audioEngine.mainMixerNode, format: audioFile.processingFormat)
} catch {
fatalError("Error. \(error)")
}
audioEngineQueue = DispatchQueue(label: "com.altstore.BackgroundTaskManager")
}
}
public extension BackgroundTaskManager {
func performExtendedBackgroundTask(taskHandler: @escaping ((Result<Void, Error>, @escaping () -> Void) -> Void)) {
func finish() {
player.stop()
audioEngine.stop()
isPlaying = false
}
audioEngineQueue.sync {
do {
try AVAudioSession.sharedInstance().setCategory(.playback, options: .mixWithOthers)
try AVAudioSession.sharedInstance().setActive(true)
// Schedule audio file buffers.
self.scheduleAudioFile()
self.scheduleAudioFile()
let outputFormat = self.audioEngine.outputNode.outputFormat(forBus: 0)
self.audioEngine.connect(self.audioEngine.mainMixerNode, to: self.audioEngine.outputNode, format: outputFormat)
try self.audioEngine.start()
self.player.play()
self.isPlaying = true
taskHandler(.success(())) {
finish()
}
} catch {
taskHandler(.failure(error)) {
finish()
}
}
}
}
}
private extension BackgroundTaskManager {
func scheduleAudioFile() {
player.scheduleFile(audioFile, at: nil) {
self.audioEngineQueue.async {
guard self.isPlaying else { return }
self.scheduleAudioFile()
}
}
}
}

View File

@@ -0,0 +1,51 @@
//
// BannerCollectionViewCell.swift
// AltStore
//
// Created by Riley Testut on 3/23/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
final class BannerCollectionViewCell: UICollectionViewCell {
private(set) var errorBadge: UIView?
@IBOutlet private(set) var bannerView: AppBannerView!
override func awakeFromNib() {
super.awakeFromNib()
contentView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
contentView.preservesSuperviewLayoutMargins = true
if #available(iOS 13.0, *) {
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([
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

@@ -0,0 +1,57 @@
//
// Button.swift
// AltStore
//
// Created by Riley Testut on 5/9/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class Button: UIButton {
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += 20
size.height += 10
return size
}
override func awakeFromNib() {
super.awakeFromNib()
setTitleColor(.white, for: .normal)
layer.masksToBounds = true
layer.cornerRadius = 8
update()
}
override func tintColorDidChange() {
super.tintColorDidChange()
update()
}
override var isHighlighted: Bool {
didSet {
self.update()
}
}
override var isEnabled: Bool {
didSet {
update()
}
}
}
private extension Button {
func update() {
if isEnabled {
backgroundColor = tintColor
} else {
backgroundColor = .lightGray
}
}
}

View File

@@ -0,0 +1,106 @@
//
// CollapsingTextView.swift
// AltStore
//
// Created by Riley Testut on 7/23/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class CollapsingTextView: UITextView {
var isCollapsed = true {
didSet {
setNeedsLayout()
}
}
var maximumNumberOfLines = 2 {
didSet {
setNeedsLayout()
}
}
var lineSpacing: CGFloat = 2 {
didSet {
setNeedsLayout()
}
}
let moreButton = UIButton(type: .system)
override func awakeFromNib() {
super.awakeFromNib()
layoutManager.delegate = self
textContainerInset = .zero
textContainer.lineFragmentPadding = 0
textContainer.lineBreakMode = .byTruncatingTail
textContainer.heightTracksTextView = true
textContainer.widthTracksTextView = true
moreButton.setTitle(NSLocalizedString("More", comment: ""), for: .normal)
moreButton.addTarget(self, action: #selector(CollapsingTextView.toggleCollapsed(_:)), for: .primaryActionTriggered)
addSubview(moreButton)
setNeedsLayout()
}
override func layoutSubviews() {
super.layoutSubviews()
guard let font = font else { return }
let buttonFont = UIFont.systemFont(ofSize: font.pointSize, weight: .medium)
moreButton.titleLabel?.font = buttonFont
let buttonY = (font.lineHeight + lineSpacing) * CGFloat(maximumNumberOfLines - 1)
let size = moreButton.sizeThatFits(CGSize(width: 1000, height: 1000))
let moreButtonFrame = CGRect(x: bounds.width - moreButton.bounds.width,
y: buttonY,
width: size.width,
height: font.lineHeight)
moreButton.frame = moreButtonFrame
if isCollapsed {
textContainer.maximumNumberOfLines = maximumNumberOfLines
let boundingSize = attributedText.boundingRect(with: CGSize(width: textContainer.size.width, height: .infinity), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
let maximumCollapsedHeight = font.lineHeight * Double(maximumNumberOfLines)
if boundingSize.height.rounded() > maximumCollapsedHeight.rounded() {
var exclusionFrame = moreButtonFrame
exclusionFrame.origin.y += moreButton.bounds.midY
exclusionFrame.size.width = bounds.width // Extra wide to make sure it wraps to next line.
textContainer.exclusionPaths = [UIBezierPath(rect: exclusionFrame)]
moreButton.isHidden = false
} else {
textContainer.exclusionPaths = []
moreButton.isHidden = true
}
} else {
textContainer.maximumNumberOfLines = 0
textContainer.exclusionPaths = []
moreButton.isHidden = true
}
invalidateIntrinsicContentSize()
}
}
private extension CollapsingTextView {
@objc func toggleCollapsed(_: UIButton) {
isCollapsed.toggle()
}
}
extension CollapsingTextView: NSLayoutManagerDelegate {
func layoutManager(_: NSLayoutManager, lineSpacingAfterGlyphAt _: Int, withProposedLineFragmentRect _: CGRect) -> CGFloat {
lineSpacing
}
}

View File

@@ -0,0 +1,19 @@
//
// ForwardingNavigationController.swift
// AltStore
//
// Created by Riley Testut on 10/24/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class ForwardingNavigationController: UINavigationController {
override var childForStatusBarStyle: UIViewController? {
self.topViewController
}
override var childForStatusBarHidden: UIViewController? {
topViewController
}
}

View File

@@ -0,0 +1,86 @@
//
// NavigationBar.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
import RoxasUIKit
final class NavigationBar: UINavigationBar {
@IBInspectable var automaticallyAdjustsItemPositions: Bool = true
private let backgroundColorView = UIView()
override init(frame: CGRect) {
super.init(frame: frame)
initialize()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
initialize()
}
private func initialize() {
if #available(iOS 13, *) {
let standardAppearance = UINavigationBarAppearance()
standardAppearance.configureWithDefaultBackground()
standardAppearance.shadowColor = nil
let edgeAppearance = UINavigationBarAppearance()
edgeAppearance.configureWithOpaqueBackground()
edgeAppearance.backgroundColor = self.barTintColor
edgeAppearance.shadowColor = nil
if let tintColor = self.barTintColor {
let textAttributes = [NSAttributedString.Key.foregroundColor: UIColor.white]
standardAppearance.backgroundColor = tintColor
standardAppearance.titleTextAttributes = textAttributes
standardAppearance.largeTitleTextAttributes = textAttributes
edgeAppearance.titleTextAttributes = textAttributes
edgeAppearance.largeTitleTextAttributes = textAttributes
} else {
standardAppearance.backgroundColor = nil
}
self.scrollEdgeAppearance = edgeAppearance
self.standardAppearance = standardAppearance
} else {
shadowImage = UIImage()
if let tintColor = barTintColor {
backgroundColorView.backgroundColor = tintColor
// Top = -50 to cover status bar area above navigation bar on any device.
// Bottom = -1 to prevent a flickering gray line from appearing.
addSubview(backgroundColorView, pinningEdgesWith: UIEdgeInsets(top: -50, left: 0, bottom: -1, right: 0))
} else {
barTintColor = .white
}
}
}
override func layoutSubviews() {
super.layoutSubviews()
if backgroundColorView.superview != nil {
insertSubview(backgroundColorView, at: 1)
}
if automaticallyAdjustsItemPositions {
// We can't easily shift just the back button up, so we shift the entire content view slightly.
for contentView in subviews {
guard NSStringFromClass(type(of: contentView)).contains("ContentView") else { continue }
contentView.center.y -= 2
}
}
}
}

View File

@@ -0,0 +1,172 @@
//
// PillButton.swift
// AltStore
//
// Created by Riley Testut on 7/15/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import UIKit
final class PillButton: UIButton {
override var accessibilityValue: String? {
get {
guard progress != nil else { return super.accessibilityValue }
return progressView.accessibilityValue
}
set { super.accessibilityValue = newValue }
}
var progress: Progress? {
didSet {
progressView.progress = Float(progress?.fractionCompleted ?? 0)
progressView.observedProgress = progress
let isUserInteractionEnabled = self.isUserInteractionEnabled
isIndicatingActivity = (progress != nil)
self.isUserInteractionEnabled = isUserInteractionEnabled
update()
}
}
var progressTintColor: UIColor? {
get {
progressView.progressTintColor
}
set {
progressView.progressTintColor = newValue
}
}
var countdownDate: Date? {
didSet {
isEnabled = (countdownDate == nil)
displayLink.isPaused = (countdownDate == nil)
if countdownDate == nil {
setTitle(nil, for: .disabled)
}
}
}
private let progressView = UIProgressView(progressViewStyle: .default)
private lazy var displayLink: CADisplayLink = {
let displayLink = CADisplayLink(target: self, selector: #selector(PillButton.updateCountdown))
displayLink.preferredFramesPerSecond = 15
displayLink.isPaused = true
displayLink.add(to: .main, forMode: .common)
return displayLink
}()
private let dateComponentsFormatter: DateComponentsFormatter = {
let dateComponentsFormatter = DateComponentsFormatter()
dateComponentsFormatter.zeroFormattingBehavior = [.pad]
dateComponentsFormatter.collapsesLargestUnit = false
return dateComponentsFormatter
}()
override var intrinsicContentSize: CGSize {
var size = super.intrinsicContentSize
size.width += 26
size.height += 3
return size
}
deinit {
self.displayLink.remove(from: .main, forMode: RunLoop.Mode.default)
}
override func awakeFromNib() {
super.awakeFromNib()
layer.masksToBounds = true
accessibilityTraits.formUnion([.updatesFrequently, .button])
activityIndicatorView.style = .medium
activityIndicatorView.isUserInteractionEnabled = false
progressView.progress = 0
progressView.trackImage = UIImage()
progressView.isUserInteractionEnabled = false
addSubview(progressView)
update()
}
override func layoutSubviews() {
super.layoutSubviews()
progressView.bounds.size.width = bounds.width
let scale = bounds.height / progressView.bounds.height
progressView.transform = CGAffineTransform.identity.scaledBy(x: 1, y: scale)
progressView.center = CGPoint(x: bounds.midX, y: bounds.midY)
layer.cornerRadius = bounds.midY
}
override func tintColorDidChange() {
super.tintColorDidChange()
update()
}
}
private extension PillButton {
func update() {
if progress == nil {
setTitleColor(.white, for: .normal)
backgroundColor = tintColor
} else {
setTitleColor(tintColor, for: .normal)
backgroundColor = tintColor.withAlphaComponent(0.15)
}
progressView.progressTintColor = tintColor
}
@objc func updateCountdown() {
guard let endDate = countdownDate else { return }
let startDate = Date()
let interval = endDate.timeIntervalSince(startDate)
guard interval > 0 else {
isEnabled = true
return
}
let text: String?
if interval < (1 * 60 * 60) {
dateComponentsFormatter.unitsStyle = .positional
dateComponentsFormatter.allowedUnits = [.minute, .second]
text = dateComponentsFormatter.string(from: startDate, to: endDate)
} else if interval < (2 * 24 * 60 * 60) {
dateComponentsFormatter.unitsStyle = .positional
dateComponentsFormatter.allowedUnits = [.hour, .minute, .second]
text = dateComponentsFormatter.string(from: startDate, to: endDate)
} else {
dateComponentsFormatter.unitsStyle = .full
dateComponentsFormatter.allowedUnits = [.day]
let numberOfDays = endDate.numberOfCalendarDays(since: startDate)
text = String(format: NSLocalizedString("%@ DAYS", comment: ""), NSNumber(value: numberOfDays))
}
if let text = text {
UIView.performWithoutAnimation {
self.isEnabled = false
self.setTitle(text, for: .disabled)
self.layoutIfNeeded()
}
} else {
isEnabled = true
}
}
}

View File

@@ -0,0 +1,18 @@
//
// TextCollectionReusableView.swift
// AltStore
//
// Created by Riley Testut on 3/23/20.
// Copyright © 2020 Riley Testut. All rights reserved.
//
import UIKit
class TextCollectionReusableView: UICollectionReusableView {
@IBOutlet var textLabel: UILabel!
@IBOutlet var topLayoutConstraint: NSLayoutConstraint!
@IBOutlet var bottomLayoutConstraint: NSLayoutConstraint!
@IBOutlet var leadingLayoutConstraint: NSLayoutConstraint!
@IBOutlet var trailingLayoutConstraint: NSLayoutConstraint!
}

View File

@@ -0,0 +1,115 @@
//
// ToastView.swift
// AltStore
//
// Created by Riley Testut on 7/19/19.
// Copyright © 2019 Riley Testut. All rights reserved.
//
import RoxasUIKit
import Shared
import SideStoreCore
import SideKit
import AltSign
extension TimeInterval {
static let shortToastViewDuration = 4.0
static let longToastViewDuration = 8.0
}
final class ToastView: RSTToastView {
var preferredDuration: TimeInterval
override init(text: String, detailText detailedText: String?) {
if detailedText == nil {
preferredDuration = .shortToastViewDuration
} else {
preferredDuration = .longToastViewDuration
}
super.init(text: text, detailText: detailedText)
isAccessibilityElement = true
layoutMargins = UIEdgeInsets(top: 8, left: 16, bottom: 10, right: 16)
setNeedsLayout()
if let stackView = textLabel.superview as? UIStackView {
// RSTToastView does not expose stack view containing labels,
// so we access it indirectly as the labels' superview.
stackView.spacing = (detailedText != nil) ? 4.0 : 0.0
}
}
convenience init(error: Error) {
var error = error as NSError
var underlyingError = error.underlyingError
var preferredDuration: TimeInterval?
if
let unwrappedUnderlyingError = underlyingError,
error.domain == AltServerErrorDomain && error.code == -1 //ALTServerError.underlyingError().rawValue
{
// Treat underlyingError as the primary error.
error = unwrappedUnderlyingError as NSError
underlyingError = nil
preferredDuration = .longToastViewDuration
}
let text: String
let detailText: String?
if let failure = error.localizedFailure {
text = failure
detailText = error.localizedFailureReason ?? error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription ?? error.localizedDescription
} else if let reason = error.localizedFailureReason {
text = reason
detailText = error.localizedRecoverySuggestion ?? underlyingError?.localizedDescription
} else {
text = error.localizedDescription
detailText = underlyingError?.localizedDescription ?? error.localizedRecoverySuggestion
}
self.init(text: text, detailText: detailText)
if let preferredDuration = preferredDuration {
self.preferredDuration = preferredDuration
}
}
@available(*, unavailable)
required init(coder _: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func layoutSubviews() {
super.layoutSubviews()
// Rough calculation to determine height of ToastView with one-line textLabel.
let minimumHeight = textLabel.font.lineHeight.rounded() + 18
layer.cornerRadius = minimumHeight / 2
}
func show(in viewController: UIViewController) {
show(in: viewController.navigationController?.view ?? viewController.view, duration: preferredDuration)
}
override func show(in view: UIView, duration: TimeInterval) {
super.show(in: view, duration: duration)
let announcement = (textLabel.text ?? "") + ". " + (detailTextLabel.text ?? "")
accessibilityLabel = announcement
// Minimum 0.75 delay to prevent announcement being cut off by VoiceOver.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.75) {
UIAccessibility.post(notification: .announcement, argument: announcement)
}
}
override func show(in view: UIView) {
show(in: view, duration: preferredDuration)
}
}