Compare commits
3 Commits
4bb01becd2
...
cf71fc3f98
Author | SHA1 | Date |
---|---|---|
Shadowfacts | cf71fc3f98 | |
Shadowfacts | be977dbea9 | |
Shadowfacts | f327cfd197 |
|
@ -1,7 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<Workspace
|
|
||||||
version = "1.0">
|
|
||||||
<FileRef
|
|
||||||
location = "self:">
|
|
||||||
</FileRef>
|
|
||||||
</Workspace>
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/configuration/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
|
@ -0,0 +1,26 @@
|
||||||
|
// swift-tools-version: 5.10
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "GalleryVC",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v15),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||||
|
.library(
|
||||||
|
name: "GalleryVC",
|
||||||
|
targets: ["GalleryVC"]),
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||||
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
|
.target(
|
||||||
|
name: "GalleryVC"),
|
||||||
|
.testTarget(
|
||||||
|
name: "GalleryVCTests",
|
||||||
|
dependencies: ["GalleryVC"]),
|
||||||
|
]
|
||||||
|
)
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// GalleryContentViewController.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/17/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public protocol GalleryContentViewController: UIViewController {
|
||||||
|
var container: GalleryContentViewControllerContainer? { get set }
|
||||||
|
var contentSize: CGSize { get }
|
||||||
|
var activityItemsForSharing: [Any] { get }
|
||||||
|
var caption: String? { get }
|
||||||
|
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||||
|
var canAnimateFromSourceView: Bool { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension GalleryContentViewController {
|
||||||
|
var bottomControlsAccessoryViewController: UIViewController? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var canAnimateFromSourceView: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
//
|
||||||
|
// GalleryContentViewControllerContainer.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/28/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public protocol GalleryContentViewControllerContainer {
|
||||||
|
func setGalleryContentLoading(_ loading: Bool)
|
||||||
|
func galleryContentChanged()
|
||||||
|
func disableGalleryScrollAndZoom()
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
//
|
||||||
|
// GalleryDataSource.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/28/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public protocol GalleryDataSource {
|
||||||
|
func galleryItemsCount() -> Int
|
||||||
|
func galleryContentViewController(forItemAt index: Int) -> GalleryContentViewController
|
||||||
|
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView?
|
||||||
|
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]?
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension GalleryDataSource {
|
||||||
|
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,127 @@
|
||||||
|
//
|
||||||
|
// GalleryDismissAnimationController.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/1/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
private let sourceView: UIView
|
||||||
|
private let interactiveTranslation: CGPoint?
|
||||||
|
private let interactiveVelocity: CGPoint?
|
||||||
|
|
||||||
|
init(sourceView: UIView, interactiveTranslation: CGPoint?, interactiveVelocity: CGPoint?) {
|
||||||
|
self.sourceView = sourceView
|
||||||
|
self.interactiveTranslation = interactiveTranslation
|
||||||
|
self.interactiveVelocity = interactiveVelocity
|
||||||
|
}
|
||||||
|
|
||||||
|
func transitionDuration(using transitionContext: (any UIViewControllerContextTransitioning)?) -> TimeInterval {
|
||||||
|
return 0.3
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: any UIViewControllerContextTransitioning) {
|
||||||
|
guard let to = transitionContext.viewController(forKey: .to),
|
||||||
|
let from = transitionContext.viewController(forKey: .from) as? GalleryViewController else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemViewController = from.currentItemViewController
|
||||||
|
|
||||||
|
if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
|
||||||
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = transitionContext.containerView
|
||||||
|
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
||||||
|
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
||||||
|
|
||||||
|
let origSourceTransform = sourceView.transform
|
||||||
|
let appliedSourceToDestTransform: Bool
|
||||||
|
if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 {
|
||||||
|
appliedSourceToDestTransform = true
|
||||||
|
let sourceToDestTransform = origSourceTransform
|
||||||
|
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
||||||
|
.scaledBy(x: destFrameInContainer.width / sourceFrameInContainer.width, y: destFrameInContainer.height / sourceFrameInContainer.height)
|
||||||
|
sourceView.transform = sourceToDestTransform
|
||||||
|
} else {
|
||||||
|
appliedSourceToDestTransform = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = itemViewController.takeContent()
|
||||||
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
content.view.layer.masksToBounds = true
|
||||||
|
|
||||||
|
container.addSubview(to.view)
|
||||||
|
container.addSubview(from.view)
|
||||||
|
container.addSubview(content.view)
|
||||||
|
|
||||||
|
content.view.frame = destFrameInContainer
|
||||||
|
content.view.layer.opacity = 1
|
||||||
|
|
||||||
|
container.layoutIfNeeded()
|
||||||
|
|
||||||
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
|
var initialVelocity: CGVector
|
||||||
|
if let interactiveVelocity,
|
||||||
|
let interactiveTranslation,
|
||||||
|
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the springs initial undershoot
|
||||||
|
sqrt(pow(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
|
||||||
|
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
|
||||||
|
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
|
||||||
|
let yDistance = sourceFrameInContainer.midY - destFrameInContainer.midY
|
||||||
|
initialVelocity = CGVector(
|
||||||
|
dx: xDistance == 0 ? 0 : interactiveVelocity.x / xDistance,
|
||||||
|
dy: yDistance == 0 ? 0 : interactiveVelocity.y / yDistance
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
initialVelocity = .zero
|
||||||
|
}
|
||||||
|
initialVelocity.dx = max(-10, min(10, initialVelocity.dx))
|
||||||
|
initialVelocity.dy = max(-10, min(10, initialVelocity.dy))
|
||||||
|
// no bounce for the dismiss animation
|
||||||
|
let spring = UISpringTimingParameters(mass: 1, stiffness: 439, damping: 42, initialVelocity: initialVelocity)
|
||||||
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
||||||
|
|
||||||
|
animator.addAnimations {
|
||||||
|
from.view.layer.opacity = 0
|
||||||
|
|
||||||
|
if appliedSourceToDestTransform {
|
||||||
|
self.sourceView.transform = origSourceTransform
|
||||||
|
}
|
||||||
|
content.view.frame = sourceFrameInContainer
|
||||||
|
content.view.layer.opacity = 0
|
||||||
|
|
||||||
|
itemViewController.setControlsVisible(false, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.addCompletion { _ in
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
guard let fromVC = transitionContext.viewController(forKey: .from),
|
||||||
|
let toVC = transitionContext.viewController(forKey: .to) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionContext.containerView.addSubview(toVC.view)
|
||||||
|
transitionContext.containerView.addSubview(fromVC.view)
|
||||||
|
|
||||||
|
let duration = transitionDuration(using: transitionContext)
|
||||||
|
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
|
||||||
|
animator.addAnimations {
|
||||||
|
fromVC.view.alpha = 0
|
||||||
|
}
|
||||||
|
animator.addCompletion { _ in
|
||||||
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,82 @@
|
||||||
|
//
|
||||||
|
// GalleryDismissInteraction.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/1/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class GalleryDismissInteraction: NSObject {
|
||||||
|
|
||||||
|
private let viewController: GalleryViewController
|
||||||
|
|
||||||
|
private var content: GalleryContentViewController?
|
||||||
|
private var origContentFrameInGallery: CGRect?
|
||||||
|
private var origControlsVisible: Bool?
|
||||||
|
|
||||||
|
private(set) var isActive = false
|
||||||
|
private(set) var dismissVelocity: CGPoint?
|
||||||
|
private(set) var dismissTranslation: CGPoint?
|
||||||
|
|
||||||
|
init(viewController: GalleryViewController) {
|
||||||
|
self.viewController = viewController
|
||||||
|
super.init()
|
||||||
|
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(panRecognized))
|
||||||
|
panRecognizer.delegate = self
|
||||||
|
viewController.view.addGestureRecognizer(panRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func panRecognized(_ recognizer: UIPanGestureRecognizer) {
|
||||||
|
switch recognizer.state {
|
||||||
|
case .began:
|
||||||
|
isActive = true
|
||||||
|
|
||||||
|
origContentFrameInGallery = viewController.view.convert(viewController.currentItemViewController.content.view.bounds, from: viewController.currentItemViewController.content.view)
|
||||||
|
content = viewController.currentItemViewController.takeContent()
|
||||||
|
content!.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
content!.view.frame = origContentFrameInGallery!
|
||||||
|
viewController.view.addSubview(content!.view)
|
||||||
|
|
||||||
|
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
||||||
|
if origControlsVisible! {
|
||||||
|
viewController.currentItemViewController.setControlsVisible(false, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .changed:
|
||||||
|
let translation = recognizer.translation(in: viewController.view)
|
||||||
|
content!.view.frame = origContentFrameInGallery!.offsetBy(dx: translation.x, dy: translation.y)
|
||||||
|
|
||||||
|
case .ended:
|
||||||
|
let translation = recognizer.translation(in: viewController.view)
|
||||||
|
let velocity = recognizer.velocity(in: viewController.view)
|
||||||
|
|
||||||
|
dismissVelocity = velocity
|
||||||
|
dismissTranslation = translation
|
||||||
|
viewController.dismiss(animated: true)
|
||||||
|
|
||||||
|
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it
|
||||||
|
isActive = false
|
||||||
|
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryDismissInteraction: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
let itemVC = viewController.currentItemViewController
|
||||||
|
if viewController.galleryDataSource.galleryContentTransitionSourceView(forItemAt: itemVC.itemIndex) == nil {
|
||||||
|
return false
|
||||||
|
} else if itemVC.scrollView.zoomScale > itemVC.scrollView.minimumZoomScale {
|
||||||
|
return false
|
||||||
|
} else if !itemVC.scrollAndZoomEnabled {
|
||||||
|
return false
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,467 @@
|
||||||
|
//
|
||||||
|
// GalleryItemViewController.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/28/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
protocol GalleryItemViewControllerDelegate: AnyObject {
|
||||||
|
func isGalleryBeingPresented() -> Bool
|
||||||
|
func addPresentationAnimationCompletion(_ block: @escaping () -> Void)
|
||||||
|
func galleryItemClose(_ item: GalleryItemViewController)
|
||||||
|
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]?
|
||||||
|
}
|
||||||
|
|
||||||
|
class GalleryItemViewController: UIViewController {
|
||||||
|
private weak var delegate: GalleryItemViewControllerDelegate?
|
||||||
|
|
||||||
|
let itemIndex: Int
|
||||||
|
let content: GalleryContentViewController
|
||||||
|
|
||||||
|
private var activityIndicator: UIActivityIndicatorView?
|
||||||
|
private(set) var scrollView: UIScrollView!
|
||||||
|
private var topControlsView: UIView!
|
||||||
|
private var shareButton: UIButton!
|
||||||
|
private var shareButtonLeadingConstraint: NSLayoutConstraint!
|
||||||
|
private var shareButtonTopConstraint: NSLayoutConstraint!
|
||||||
|
private var closeButtonTrailingConstraint: NSLayoutConstraint!
|
||||||
|
private var closeButtonTopConstraint: NSLayoutConstraint!
|
||||||
|
private var bottomControlsView: UIStackView!
|
||||||
|
private(set) var captionTextView: UITextView!
|
||||||
|
|
||||||
|
private var contentViewLeadingConstraint: NSLayoutConstraint?
|
||||||
|
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
private(set) var controlsVisible: Bool = true
|
||||||
|
private(set) var scrollAndZoomEnabled = true
|
||||||
|
|
||||||
|
override var prefersHomeIndicatorAutoHidden: Bool {
|
||||||
|
return !controlsVisible
|
||||||
|
}
|
||||||
|
|
||||||
|
init(delegate: GalleryItemViewControllerDelegate, itemIndex: Int, content: GalleryContentViewController) {
|
||||||
|
self.delegate = delegate
|
||||||
|
self.itemIndex = itemIndex
|
||||||
|
self.content = content
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
content.container = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let scrollView = UIScrollView()
|
||||||
|
self.scrollView = scrollView
|
||||||
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
scrollView.delegate = self
|
||||||
|
|
||||||
|
view.addSubview(scrollView)
|
||||||
|
|
||||||
|
addContent()
|
||||||
|
centerContent()
|
||||||
|
|
||||||
|
topControlsView = UIView()
|
||||||
|
topControlsView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(topControlsView)
|
||||||
|
|
||||||
|
var shareConfig = UIButton.Configuration.plain()
|
||||||
|
shareConfig.baseForegroundColor = .white
|
||||||
|
shareConfig.image = UIImage(systemName: "square.and.arrow.up")
|
||||||
|
shareButton = UIButton(configuration: shareConfig)
|
||||||
|
shareButton.addTarget(self, action: #selector(shareButtonPressed), for: .touchUpInside)
|
||||||
|
shareButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
updateShareButton()
|
||||||
|
topControlsView.addSubview(shareButton)
|
||||||
|
|
||||||
|
var closeConfig = UIButton.Configuration.plain()
|
||||||
|
closeConfig.baseForegroundColor = .white
|
||||||
|
closeConfig.image = UIImage(systemName: "xmark")
|
||||||
|
let closeButton = UIButton(configuration: closeConfig)
|
||||||
|
closeButton.addTarget(self, action: #selector(closeButtonPressed), for: .touchUpInside)
|
||||||
|
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
topControlsView.addSubview(closeButton)
|
||||||
|
|
||||||
|
bottomControlsView = UIStackView()
|
||||||
|
bottomControlsView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
bottomControlsView.axis = .vertical
|
||||||
|
bottomControlsView.alignment = .fill
|
||||||
|
bottomControlsView.backgroundColor = .black.withAlphaComponent(0.5)
|
||||||
|
view.addSubview(bottomControlsView)
|
||||||
|
|
||||||
|
if let controlsAccessory = content.bottomControlsAccessoryViewController {
|
||||||
|
addChild(controlsAccessory)
|
||||||
|
bottomControlsView.addArrangedSubview(controlsAccessory.view)
|
||||||
|
controlsAccessory.didMove(toParent: self)
|
||||||
|
|
||||||
|
// Make sure the controls accessory is within the safe area.
|
||||||
|
let spacer = UIView()
|
||||||
|
bottomControlsView.addArrangedSubview(spacer)
|
||||||
|
let spacerTopConstraint = spacer.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
|
||||||
|
spacerTopConstraint.priority = .init(999)
|
||||||
|
spacerTopConstraint.isActive = true
|
||||||
|
}
|
||||||
|
|
||||||
|
captionTextView = UITextView()
|
||||||
|
captionTextView.backgroundColor = .clear
|
||||||
|
captionTextView.textColor = .white
|
||||||
|
captionTextView.isEditable = false
|
||||||
|
captionTextView.isSelectable = true
|
||||||
|
captionTextView.font = .preferredFont(forTextStyle: .body)
|
||||||
|
captionTextView.adjustsFontForContentSizeCategory = true
|
||||||
|
updateCaptionTextView()
|
||||||
|
bottomControlsView.addArrangedSubview(captionTextView)
|
||||||
|
|
||||||
|
closeButtonTrailingConstraint = topControlsView.trailingAnchor.constraint(equalTo: closeButton.trailingAnchor)
|
||||||
|
closeButtonTopConstraint = closeButton.topAnchor.constraint(equalTo: topControlsView.topAnchor)
|
||||||
|
shareButtonLeadingConstraint = shareButton.leadingAnchor.constraint(equalTo: topControlsView.leadingAnchor)
|
||||||
|
shareButtonTopConstraint = shareButton.topAnchor.constraint(equalTo: topControlsView.topAnchor)
|
||||||
|
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
scrollView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
|
||||||
|
topControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
topControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
topControlsView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
|
||||||
|
shareButtonLeadingConstraint,
|
||||||
|
shareButtonTopConstraint,
|
||||||
|
shareButton.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
|
||||||
|
|
||||||
|
closeButtonTrailingConstraint,
|
||||||
|
closeButtonTopConstraint,
|
||||||
|
closeButton.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
|
||||||
|
|
||||||
|
bottomControlsView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
bottomControlsView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
bottomControlsView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
|
||||||
|
captionTextView.heightAnchor.constraint(equalToConstant: 150),
|
||||||
|
])
|
||||||
|
|
||||||
|
let singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
|
||||||
|
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
|
||||||
|
doubleTap.numberOfTapsRequired = 2
|
||||||
|
// this requirement is needed to make sure the double tap is ever recognized
|
||||||
|
singleTap.require(toFail: doubleTap)
|
||||||
|
view.addGestureRecognizer(singleTap)
|
||||||
|
view.addGestureRecognizer(doubleTap)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewSafeAreaInsetsDidChange() {
|
||||||
|
super.viewSafeAreaInsetsDidChange()
|
||||||
|
|
||||||
|
updateZoomScale(resetZoom: false)
|
||||||
|
// Ensure the transform is correct if the controls are hidden
|
||||||
|
setControlsVisible(controlsVisible, animated: false)
|
||||||
|
|
||||||
|
updateTopControlsInsets()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLayoutSubviews() {
|
||||||
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
centerContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
|
super.viewDidAppear(animated)
|
||||||
|
|
||||||
|
if controlsVisible && !captionTextView.isHidden {
|
||||||
|
captionTextView.flashScrollIndicators()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func takeContent() -> GalleryContentViewController {
|
||||||
|
content.willMove(toParent: nil)
|
||||||
|
content.removeFromParent()
|
||||||
|
content.view.removeFromSuperview()
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
func addContent() {
|
||||||
|
content.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
if content.parent != self {
|
||||||
|
addChild(content)
|
||||||
|
content.didMove(toParent: self)
|
||||||
|
}
|
||||||
|
if scrollAndZoomEnabled {
|
||||||
|
scrollView.addSubview(content.view)
|
||||||
|
contentViewLeadingConstraint = content.view.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
||||||
|
contentViewLeadingConstraint!.isActive = true
|
||||||
|
contentViewTopConstraint = content.view.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
||||||
|
contentViewTopConstraint!.isActive = true
|
||||||
|
updateZoomScale(resetZoom: true)
|
||||||
|
} else {
|
||||||
|
// If the content was previously added, deactivate the old constraints.
|
||||||
|
contentViewLeadingConstraint?.isActive = false
|
||||||
|
contentViewTopConstraint?.isActive = false
|
||||||
|
|
||||||
|
view.addSubview(content.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
content.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
content.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
content.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
content.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
|
controlsVisible = visible
|
||||||
|
guard let topControlsView,
|
||||||
|
let bottomControlsView else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
func updateControlsViews() {
|
||||||
|
topControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : -topControlsView.bounds.height)
|
||||||
|
bottomControlsView.transform = CGAffineTransform(translationX: 0, y: visible ? 0 : bottomControlsView.bounds.height)
|
||||||
|
}
|
||||||
|
if animated {
|
||||||
|
let animator = UIViewPropertyAnimator(duration: 0.2, timingParameters: UISpringTimingParameters())
|
||||||
|
animator.addAnimations(updateControlsViews)
|
||||||
|
animator.startAnimation()
|
||||||
|
} else {
|
||||||
|
updateControlsViews()
|
||||||
|
}
|
||||||
|
|
||||||
|
setNeedsUpdateOfHomeIndicatorAutoHidden()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateZoomScale(resetZoom: Bool) {
|
||||||
|
guard scrollAndZoomEnabled else {
|
||||||
|
scrollView.maximumZoomScale = 1
|
||||||
|
scrollView.minimumZoomScale = 1
|
||||||
|
scrollView.zoomScale = 1
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard content.contentSize.width > 0 && content.contentSize.height > 0 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let heightScale = view.safeAreaLayoutGuide.layoutFrame.height / content.contentSize.height
|
||||||
|
let widthScale = view.safeAreaLayoutGuide.layoutFrame.width / content.contentSize.width
|
||||||
|
let minScale = min(widthScale, heightScale)
|
||||||
|
let maxScale = minScale >= 1 ? minScale + 2 : 2
|
||||||
|
|
||||||
|
scrollView.minimumZoomScale = minScale
|
||||||
|
scrollView.maximumZoomScale = maxScale
|
||||||
|
if resetZoom {
|
||||||
|
scrollView.zoomScale = minScale
|
||||||
|
} else {
|
||||||
|
scrollView.zoomScale = max(minScale, min(maxScale, scrollView.zoomScale))
|
||||||
|
}
|
||||||
|
|
||||||
|
centerContent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func centerContent() {
|
||||||
|
guard scrollAndZoomEnabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: use frame for the content.view, because that's in the coordinate space of the scroll view
|
||||||
|
// which means it's already been scaled by the zoom factor.
|
||||||
|
let yOffset = max(0, (view.bounds.height - content.view.frame.height) / 2)
|
||||||
|
contentViewTopConstraint!.constant = yOffset
|
||||||
|
|
||||||
|
let xOffset = max(0, (view.bounds.width - content.view.frame.width) / 2)
|
||||||
|
contentViewLeadingConstraint!.constant = xOffset
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateShareButton() {
|
||||||
|
shareButton.isEnabled = !content.activityItemsForSharing.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCaptionTextView() {
|
||||||
|
guard let caption = content.caption,
|
||||||
|
!caption.isEmpty else {
|
||||||
|
captionTextView.isHidden = true
|
||||||
|
return
|
||||||
|
}
|
||||||
|
captionTextView.text = caption
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTopControlsInsets() {
|
||||||
|
let notchedDeviceTopInsets: [CGFloat] = [
|
||||||
|
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
||||||
|
48, // iPhone XR, 11
|
||||||
|
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
||||||
|
50, // iPhone 12 mini, 13 mini
|
||||||
|
]
|
||||||
|
let islandDeviceTopInsets: [CGFloat] = [
|
||||||
|
59, // iPhone 14 Pro, 14 Pro Max, 15 Pro, 15 Pro Max
|
||||||
|
]
|
||||||
|
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||||
|
// the notch width is not the same for the iPhones 13,
|
||||||
|
// but what we actually want is the same offset from the edges
|
||||||
|
// since the corner radius didn't change
|
||||||
|
let notchWidth: CGFloat = 210
|
||||||
|
let earWidth = (view.bounds.width - notchWidth) / 2
|
||||||
|
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
|
||||||
|
shareButtonLeadingConstraint.constant = offset
|
||||||
|
closeButtonTrailingConstraint.constant = offset
|
||||||
|
} else if islandDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||||
|
shareButtonLeadingConstraint.constant = 24
|
||||||
|
shareButtonTopConstraint.constant = 24
|
||||||
|
closeButtonTrailingConstraint.constant = 24
|
||||||
|
closeButtonTopConstraint.constant = 24
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
|
||||||
|
var zoomRect = CGRect.zero
|
||||||
|
zoomRect.size.width = content.view.frame.width / scale
|
||||||
|
zoomRect.size.height = content.view.frame.height / scale
|
||||||
|
let newCenter = scrollView.convert(center, to: content.view)
|
||||||
|
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
|
||||||
|
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
|
||||||
|
return zoomRect
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateZoomOut() {
|
||||||
|
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
|
||||||
|
animator.addAnimations {
|
||||||
|
self.scrollView.zoomScale = self.scrollView.minimumZoomScale
|
||||||
|
self.scrollView.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: Interaction
|
||||||
|
|
||||||
|
@objc private func viewPressed() {
|
||||||
|
if scrollAndZoomEnabled,
|
||||||
|
scrollView.zoomScale > scrollView.minimumZoomScale {
|
||||||
|
animateZoomOut()
|
||||||
|
} else {
|
||||||
|
setControlsVisible(!controlsVisible, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func viewDoublePressed(_ recognizer: UITapGestureRecognizer) {
|
||||||
|
guard scrollAndZoomEnabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
||||||
|
let point = recognizer.location(in: recognizer.view)
|
||||||
|
let scale = min(
|
||||||
|
max(
|
||||||
|
scrollView.bounds.width / content.contentSize.width,
|
||||||
|
scrollView.bounds.height / content.contentSize.height,
|
||||||
|
scrollView.zoomScale + 0.75
|
||||||
|
),
|
||||||
|
scrollView.maximumZoomScale
|
||||||
|
)
|
||||||
|
let rect = zoomRectFor(scale: scale, center: point)
|
||||||
|
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: UISpringTimingParameters())
|
||||||
|
animator.addAnimations {
|
||||||
|
self.scrollView.zoom(to: rect, animated: false)
|
||||||
|
self.view.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
} else {
|
||||||
|
animateZoomOut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func closeButtonPressed() {
|
||||||
|
delegate?.galleryItemClose(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func shareButtonPressed() {
|
||||||
|
let items = content.activityItemsForSharing
|
||||||
|
guard !items.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let activityVC = UIActivityViewController(activityItems: items, applicationActivities: delegate?.galleryItemApplicationActivities(self))
|
||||||
|
activityVC.popoverPresentationController?.sourceView = shareButton
|
||||||
|
present(activityVC, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
||||||
|
func setGalleryContentLoading(_ loading: Bool) {
|
||||||
|
if loading {
|
||||||
|
if activityIndicator == nil {
|
||||||
|
let activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
|
self.activityIndicator = activityIndicator
|
||||||
|
activityIndicator.startAnimating()
|
||||||
|
activityIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(activityIndicator)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let activityIndicator {
|
||||||
|
// If we're in the middle of the presentation animation,
|
||||||
|
// wait until it finishes to hide the loading indicator.
|
||||||
|
// Since the updated content frame won't affect the animation,
|
||||||
|
// make sure the loading indicator remains visible.
|
||||||
|
if let delegate,
|
||||||
|
delegate.isGalleryBeingPresented() {
|
||||||
|
delegate.addPresentationAnimationCompletion { [unowned self] in
|
||||||
|
self.setGalleryContentLoading(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
activityIndicator.removeFromSuperview()
|
||||||
|
self.activityIndicator = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryContentChanged() {
|
||||||
|
updateZoomScale(resetZoom: true)
|
||||||
|
updateShareButton()
|
||||||
|
updateCaptionTextView()
|
||||||
|
}
|
||||||
|
|
||||||
|
func disableGalleryScrollAndZoom() {
|
||||||
|
scrollAndZoomEnabled = false
|
||||||
|
updateZoomScale(resetZoom: true)
|
||||||
|
scrollView.isScrollEnabled = false
|
||||||
|
// Make sure the content is re-added with the correct constraints
|
||||||
|
if content.parent == self {
|
||||||
|
addContent()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryItemViewController: UIScrollViewDelegate {
|
||||||
|
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||||
|
if scrollAndZoomEnabled {
|
||||||
|
return content.view
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||||
|
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
||||||
|
setControlsVisible(true, animated: true)
|
||||||
|
} else {
|
||||||
|
setControlsVisible(false, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
centerContent()
|
||||||
|
scrollView.layoutIfNeeded()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
//
|
||||||
|
// GalleryPresentationAnimationController.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/28/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
|
private let sourceView: UIView
|
||||||
|
private var completionHandlers: [() -> Void] = []
|
||||||
|
|
||||||
|
init(sourceView: UIView) {
|
||||||
|
self.sourceView = sourceView
|
||||||
|
}
|
||||||
|
|
||||||
|
func addCompletionHandler(_ block: @escaping () -> Void) {
|
||||||
|
completionHandlers.append(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
return 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
|
||||||
|
fatalError()
|
||||||
|
}
|
||||||
|
|
||||||
|
to.presentationAnimationController = self
|
||||||
|
|
||||||
|
let itemViewController = to.currentItemViewController
|
||||||
|
|
||||||
|
if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions {
|
||||||
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let container = transitionContext.containerView
|
||||||
|
itemViewController.view.layoutIfNeeded()
|
||||||
|
|
||||||
|
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
||||||
|
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
||||||
|
|
||||||
|
// Use a transformation to make the actual source view appear to move into the destination frame.
|
||||||
|
// Doing this while having the content view fade-in papers over the z-index change when
|
||||||
|
// there was something overlapping the source view.
|
||||||
|
let origSourceTransform = sourceView.transform
|
||||||
|
let sourceToDestTransform: CGAffineTransform?
|
||||||
|
if destFrameInContainer.width > 0 && destFrameInContainer.height > 0 {
|
||||||
|
sourceToDestTransform = origSourceTransform
|
||||||
|
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
||||||
|
.scaledBy(x: destFrameInContainer.width / sourceFrameInContainer.width, y: destFrameInContainer.height / sourceFrameInContainer.height)
|
||||||
|
} else {
|
||||||
|
sourceToDestTransform = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = itemViewController.takeContent()
|
||||||
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
|
|
||||||
|
container.addSubview(to.view)
|
||||||
|
container.addSubview(content.view)
|
||||||
|
|
||||||
|
to.view.layer.opacity = 0
|
||||||
|
content.view.frame = sourceFrameInContainer
|
||||||
|
content.view.layer.opacity = 0
|
||||||
|
|
||||||
|
container.layoutIfNeeded()
|
||||||
|
|
||||||
|
// This needs to take place after the layout, so that the transform is correct.
|
||||||
|
itemViewController.setControlsVisible(false, animated: false)
|
||||||
|
|
||||||
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
|
// rougly equivalent to duration: 0.4, bounce: 0.3
|
||||||
|
let spring = UISpringTimingParameters(mass: 1, stiffness: 247, damping: 22, initialVelocity: .zero)
|
||||||
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
||||||
|
|
||||||
|
animator.addAnimations {
|
||||||
|
to.view.layer.opacity = 1
|
||||||
|
|
||||||
|
content.view.frame = destFrameInContainer
|
||||||
|
content.view.layer.opacity = 1
|
||||||
|
|
||||||
|
itemViewController.setControlsVisible(true, animated: false)
|
||||||
|
|
||||||
|
if let sourceToDestTransform {
|
||||||
|
self.sourceView.transform = sourceToDestTransform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.addCompletion { _ in
|
||||||
|
if sourceToDestTransform != nil {
|
||||||
|
self.sourceView.transform = origSourceTransform
|
||||||
|
}
|
||||||
|
|
||||||
|
itemViewController.addContent()
|
||||||
|
|
||||||
|
transitionContext.completeTransition(true)
|
||||||
|
|
||||||
|
for block in self.completionHandlers {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
to.presentationAnimationController = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||||
|
guard let to = transitionContext.viewController(forKey: .to) as? GalleryViewController else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
transitionContext.containerView.addSubview(to.view)
|
||||||
|
to.view.alpha = 0
|
||||||
|
|
||||||
|
let duration = transitionDuration(using: transitionContext)
|
||||||
|
let animator = UIViewPropertyAnimator(duration: duration, curve: .easeInOut)
|
||||||
|
animator.addAnimations {
|
||||||
|
to.view.alpha = 1
|
||||||
|
}
|
||||||
|
animator.addCompletion { _ in
|
||||||
|
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
||||||
|
|
||||||
|
for block in self.completionHandlers {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
to.presentationAnimationController = nil
|
||||||
|
}
|
||||||
|
animator.startAnimation()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
//
|
||||||
|
// GalleryViewController.swift
|
||||||
|
// GalleryVC
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 12/28/23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public class GalleryViewController: UIPageViewController {
|
||||||
|
|
||||||
|
let galleryDataSource: GalleryDataSource
|
||||||
|
let initialItemIndex: Int
|
||||||
|
private let _itemsCount: Int
|
||||||
|
private var itemsCount: Int {
|
||||||
|
get {
|
||||||
|
precondition(_itemsCount == galleryDataSource.galleryItemsCount(), "GalleryDataSource item count cannot change")
|
||||||
|
return _itemsCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentItemViewController: GalleryItemViewController {
|
||||||
|
viewControllers![0] as! GalleryItemViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dismissInteraction: GalleryDismissInteraction!
|
||||||
|
var presentationAnimationController: GalleryPresentationAnimationController?
|
||||||
|
|
||||||
|
override public var prefersStatusBarHidden: Bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
override public var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||||
|
.none
|
||||||
|
}
|
||||||
|
override public var childForHomeIndicatorAutoHidden: UIViewController? {
|
||||||
|
currentItemViewController
|
||||||
|
}
|
||||||
|
|
||||||
|
public init(dataSource: GalleryDataSource, initialItemIndex: Int) {
|
||||||
|
self.galleryDataSource = dataSource
|
||||||
|
self.initialItemIndex = initialItemIndex
|
||||||
|
self._itemsCount = dataSource.galleryItemsCount()
|
||||||
|
precondition(initialItemIndex >= 0 && initialItemIndex < _itemsCount, "initialItemIndex is out of bounds")
|
||||||
|
|
||||||
|
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [
|
||||||
|
.interPageSpacing: 50
|
||||||
|
])
|
||||||
|
|
||||||
|
modalPresentationStyle = .fullScreen
|
||||||
|
transitioningDelegate = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override public func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
dismissInteraction = GalleryDismissInteraction(viewController: self)
|
||||||
|
|
||||||
|
view.backgroundColor = .black
|
||||||
|
overrideUserInterfaceStyle = .dark
|
||||||
|
|
||||||
|
dataSource = self
|
||||||
|
|
||||||
|
setViewControllers([makeItemVC(index: initialItemIndex)], direction: .forward, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func makeItemVC(index: Int) -> GalleryItemViewController {
|
||||||
|
let content = galleryDataSource.galleryContentViewController(forItemAt: index)
|
||||||
|
return GalleryItemViewController(delegate: self, itemIndex: index, content: content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryViewController: UIPageViewControllerDataSource {
|
||||||
|
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
||||||
|
guard let viewController = viewController as? GalleryItemViewController else {
|
||||||
|
preconditionFailure("VC must be GalleryItemViewController")
|
||||||
|
}
|
||||||
|
guard viewController.itemIndex > 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return makeItemVC(index: viewController.itemIndex - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
||||||
|
guard let viewController = viewController as? GalleryItemViewController else {
|
||||||
|
preconditionFailure("VC must be GalleryItemViewController")
|
||||||
|
}
|
||||||
|
guard viewController.itemIndex < itemsCount - 1 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return makeItemVC(index: viewController.itemIndex + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryViewController: GalleryItemViewControllerDelegate {
|
||||||
|
func isGalleryBeingPresented() -> Bool {
|
||||||
|
isBeingPresented
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPresentationAnimationCompletion(_ block: @escaping () -> Void) {
|
||||||
|
presentationAnimationController?.addCompletionHandler(block)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryItemClose(_ item: GalleryItemViewController) {
|
||||||
|
dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]? {
|
||||||
|
galleryDataSource.galleryApplicationActivities(forItemAt: item.itemIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GalleryViewController: UIViewControllerTransitioningDelegate {
|
||||||
|
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
|
if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: initialItemIndex) {
|
||||||
|
return GalleryPresentationAnimationController(sourceView: sourceView)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
|
if let sourceView = galleryDataSource.galleryContentTransitionSourceView(forItemAt: currentItemViewController.itemIndex) {
|
||||||
|
let translation: CGPoint?
|
||||||
|
let velocity: CGPoint?
|
||||||
|
if let dismissInteraction,
|
||||||
|
dismissInteraction.isActive {
|
||||||
|
translation = dismissInteraction.dismissTranslation
|
||||||
|
velocity = dismissInteraction.dismissVelocity
|
||||||
|
} else {
|
||||||
|
translation = nil
|
||||||
|
velocity = nil
|
||||||
|
}
|
||||||
|
return GalleryDismissAnimationController(sourceView: sourceView, interactiveTranslation: translation, interactiveVelocity: velocity)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import XCTest
|
||||||
|
@testable import GalleryVC
|
||||||
|
|
||||||
|
final class GalleryVCTests: XCTestCase {
|
||||||
|
func testExample() throws {
|
||||||
|
// XCTest Documentation
|
||||||
|
// https://developer.apple.com/documentation/xctest
|
||||||
|
|
||||||
|
// Defining Test Cases and Test Methods
|
||||||
|
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,13 +7,11 @@
|
||||||
objects = {
|
objects = {
|
||||||
|
|
||||||
/* Begin PBXBuildFile section */
|
/* Begin PBXBuildFile section */
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */; };
|
|
||||||
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
0427033822B30F5F000D31B6 /* BehaviorPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */; };
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0427033922B31269000D31B6 /* AdvancedPrefsView.swift */; };
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0450531E22B0097E00100BA2 /* Timline+UI.swift */; };
|
||||||
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
04586B4122B2FFB10021BD04 /* PreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4022B2FFB10021BD04 /* PreferencesView.swift */; };
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04586B4222B301470021BD04 /* AppearancePrefsView.swift */; };
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04D14BAE22B34A2800642648 /* GalleryViewController.swift */; };
|
|
||||||
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
04DACE8C212CB14B009840C4 /* MainTabBarViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */; };
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04DACE8D212CC7CC009840C4 /* ImageCache.swift */; };
|
||||||
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 04ED00B021481ED800567C53 /* SteppedProgressView.swift */; };
|
||||||
|
@ -87,7 +85,6 @@
|
||||||
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; };
|
D627943223A5466600D38C68 /* SelectableTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627943123A5466600D38C68 /* SelectableTableViewCell.swift */; };
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; };
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */; };
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; };
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */; };
|
||||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */; };
|
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2421217AA7E1005076CC /* UserActivityManager.swift */; };
|
||||||
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
D62D2424217ABF3F005076CC /* NSUserActivity+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */; };
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = D62D2425217ABF63005076CC /* UserActivityType.swift */; };
|
||||||
|
@ -113,9 +110,6 @@
|
||||||
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
D6412B0D24B0D4CF00F5412E /* ProfileHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */; };
|
||||||
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
D641C77F213DC78A004B4513 /* InlineTextAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */; };
|
||||||
D642E83A2BA75F4C004BFD6A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */; };
|
D642E83A2BA75F4C004BFD6A /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */; };
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */; };
|
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */; };
|
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */; };
|
|
||||||
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */; };
|
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */; };
|
||||||
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */; };
|
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */; };
|
||||||
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */; };
|
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */; };
|
||||||
|
@ -124,15 +118,13 @@
|
||||||
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */; };
|
||||||
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */; };
|
||||||
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
|
D646DCDC2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */; };
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */; };
|
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9026C80DC600FC57FB /* ToastView.swift */; };
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */; };
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */; };
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */; };
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */; };
|
||||||
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */; };
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */; };
|
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DED246B81C9000F9538 /* GifvPlayerView.swift */; };
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */; };
|
|
||||||
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */; };
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; };
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */ = {isa = PBXBuildFile; platformFilters = (ios, maccatalyst, ); productRef = D6552366289870790048A653 /* ScreenCorners */; };
|
||||||
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
|
D659F35E2953A212002D944A /* TTTKit in Frameworks */ = {isa = PBXBuildFile; productRef = D659F35D2953A212002D944A /* TTTKit */; };
|
||||||
|
@ -154,7 +146,6 @@
|
||||||
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
D663626C21361C6700C9CBA2 /* Account+Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = D663626B21361C6700C9CBA2 /* Account+Preferences.swift */; };
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */; };
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */ = {isa = PBXBuildFile; productRef = D6676CA427A8D0020052936B /* WebURLFoundationExtras */; };
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */; };
|
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */; };
|
||||||
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
D66A77BB233838DC0058F1EC /* UIFont+Traits.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */; };
|
||||||
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
D66C900B28DAB7FD00217BF2 /* TimelineViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */; };
|
||||||
|
@ -166,7 +157,6 @@
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */ = {isa = PBXBuildFile; fileRef = D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */; };
|
||||||
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
D68015402401A6BA00D6103B /* ComposingPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */; };
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68015412401A74600D6103B /* MediaPrefsView.swift */; };
|
||||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681A299249AD62D0085E54E /* LargeImageContentView.swift */; };
|
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D6246E32290053414F /* StatusActivityItemSource.swift */; };
|
||||||
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
D681E4D9246E346E0053414F /* AccountActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */; };
|
||||||
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
D68232F72464F4FD00325FB8 /* ComposeDrawingViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */; };
|
||||||
|
@ -195,6 +185,17 @@
|
||||||
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
|
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */; };
|
||||||
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
|
D691771929A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */; };
|
||||||
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
|
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */; };
|
||||||
|
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
|
||||||
|
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
|
||||||
|
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
|
||||||
|
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */; };
|
||||||
|
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; };
|
||||||
|
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
|
||||||
|
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
|
||||||
|
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */; };
|
||||||
|
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
|
||||||
|
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
|
||||||
|
D6934F402BAA19EC002B1C8D /* GifvActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* GifvActivityItemSource.swift */; };
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */; };
|
||||||
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
D693A72F25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */; };
|
||||||
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
|
D693A73025CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */; };
|
||||||
|
@ -278,10 +279,8 @@
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */; };
|
||||||
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
D6C7D27D22B6EBF800071952 /* AttachmentsContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */; };
|
||||||
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */; };
|
D6C82B4125C5BB7E0017F1E6 /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */; };
|
||||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D862139E62700CB5196 /* LargeImageViewController.swift */; };
|
|
||||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6C94D882139E6EC00CB5196 /* AttachmentView.swift */; };
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */; };
|
||||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */; };
|
|
||||||
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D6CA6ED129EF6091003EC5DF /* TuskerPreferences */; };
|
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */ = {isa = PBXBuildFile; productRef = D6CA6ED129EF6091003EC5DF /* TuskerPreferences */; };
|
||||||
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
D6CA8CDA2962231F0050C433 /* ArrayUniqueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */; };
|
||||||
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
|
D6CA8CDE296387310050C433 /* SaveToPhotosActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */; };
|
||||||
|
@ -308,7 +307,6 @@
|
||||||
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
|
D6D79F532A0FFE3200AB2315 /* ToggleableButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */; };
|
||||||
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; };
|
D6D79F572A1160B800AB2315 /* AccountDisplayNameLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */; };
|
||||||
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; };
|
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F582A13293200AB2315 /* BackgroundManager.swift */; };
|
||||||
D6D79F5B2A13D22B00AB2315 /* GalleryFallbackViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D79F5A2A13D22B00AB2315 /* GalleryFallbackViewController.swift */; };
|
|
||||||
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
D6D94955298963A900C59229 /* Colors.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D94954298963A900C59229 /* Colors.swift */; };
|
||||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */; };
|
||||||
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
D6D9498F298EB79400C59229 /* CopyableLable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6D9498E298EB79400C59229 /* CopyableLable.swift */; };
|
||||||
|
@ -409,13 +407,11 @@
|
||||||
/* End PBXCopyFilesBuildPhase section */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingLargeImageViewController.swift; sourceTree = "<group>"; };
|
|
||||||
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
|
0427033722B30F5F000D31B6 /* BehaviorPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BehaviorPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
0427033922B31269000D31B6 /* AdvancedPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPrefsView.swift; sourceTree = "<group>"; };
|
||||||
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
0450531E22B0097E00100BA2 /* Timline+UI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timline+UI.swift"; sourceTree = "<group>"; };
|
||||||
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
04586B4022B2FFB10021BD04 /* PreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesView.swift; sourceTree = "<group>"; };
|
||||||
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
04586B4222B301470021BD04 /* AppearancePrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearancePrefsView.swift; sourceTree = "<group>"; };
|
||||||
04D14BAE22B34A2800642648 /* GalleryViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryViewController.swift; sourceTree = "<group>"; };
|
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTabBarViewController.swift; sourceTree = "<group>"; };
|
||||||
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
04DACE8D212CC7CC009840C4 /* ImageCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageCache.swift; sourceTree = "<group>"; };
|
||||||
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
04ED00B021481ED800567C53 /* SteppedProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SteppedProgressView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -488,7 +484,6 @@
|
||||||
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
|
D627943123A5466600D38C68 /* SelectableTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectableTableViewCell.swift; sourceTree = "<group>"; };
|
||||||
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
|
D627944C23A9A03D00D38C68 /* ListTimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListTimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
|
D627944E23A9C99800D38C68 /* EditListAccountsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListAccountsViewController.swift; sourceTree = "<group>"; };
|
||||||
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LargeImageViewController.xib; sourceTree = "<group>"; };
|
|
||||||
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
D62D2421217AA7E1005076CC /* UserActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityManager.swift; sourceTree = "<group>"; };
|
||||||
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
D62D2423217ABF3F005076CC /* NSUserActivity+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSUserActivity+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
D62D2425217ABF63005076CC /* UserActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityType.swift; sourceTree = "<group>"; };
|
||||||
|
@ -513,9 +508,7 @@
|
||||||
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
D6412B0C24B0D4CF00F5412E /* ProfileHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderView.swift; sourceTree = "<group>"; };
|
||||||
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
D641C77E213DC78A004B4513 /* InlineTextAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTextAttachment.swift; sourceTree = "<group>"; };
|
||||||
D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
D642E8392BA75F4C004BFD6A /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = "<group>"; };
|
||||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageExpandAnimationController.swift; sourceTree = "<group>"; };
|
D642E83D2BA7AD0F004BFD6A /* GalleryVC */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = GalleryVC; sourceTree = "<group>"; };
|
||||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageShrinkAnimationController.swift; sourceTree = "<group>"; };
|
|
||||||
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageInteractionController.swift; sourceTree = "<group>"; };
|
|
||||||
D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldValueView.swift; sourceTree = "<group>"; };
|
D646DCAB2A06C8840059ECEB /* ProfileFieldValueView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldValueView.swift; sourceTree = "<group>"; };
|
||||||
D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldVerificationView.swift; sourceTree = "<group>"; };
|
D646DCAD2A06C8C90059ECEB /* ProfileFieldVerificationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldVerificationView.swift; sourceTree = "<group>"; };
|
||||||
D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsCollectionViewController.swift; sourceTree = "<group>"; };
|
D646DCD12A06F2510059ECEB /* NotificationsCollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsCollectionViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -524,15 +517,13 @@
|
||||||
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
D646DCD72A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowRequestNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
D646DCD92A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PollFinishedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
D646DCDB2A081CF10059ECEB /* StatusUpdatedNotificationCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusUpdatedNotificationCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentPreviewViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
D64AAE9026C80DC600FC57FB /* ToastView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastView.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
D64AAE9426C88C5000FC57FB /* ToastableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastableViewController.swift; sourceTree = "<group>"; };
|
||||||
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
D64AAE9626C88DC400FC57FB /* ToastConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToastConfiguration.swift; sourceTree = "<group>"; };
|
||||||
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
D64D0AB02128D9AE005A6F37 /* OnboardingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OnboardingViewController.swift; sourceTree = "<group>"; };
|
||||||
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
D64D8CA82463B494006B0BAA /* MultiThreadDictionary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultiThreadDictionary.swift; sourceTree = "<group>"; };
|
||||||
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
D651C5B32915B00400236EF6 /* ProfileFieldsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileFieldsView.swift; sourceTree = "<group>"; };
|
||||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentView.swift; sourceTree = "<group>"; };
|
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvPlayerView.swift; sourceTree = "<group>"; };
|
||||||
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvAttachmentViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
D6538944214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TableViewSwipeActionProvider.swift; sourceTree = "<group>"; };
|
||||||
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
|
D659F36129541065002D944A /* TTTView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TTTView.swift; sourceTree = "<group>"; };
|
||||||
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
|
D65B4B532971F71D00DABDFB /* EditedReport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditedReport.swift; sourceTree = "<group>"; };
|
||||||
|
@ -555,12 +546,11 @@
|
||||||
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
D6620ACD2511A0ED00312CA0 /* StatusStateResolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusStateResolver.swift; sourceTree = "<group>"; };
|
||||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Account+Preferences.swift"; sourceTree = "<group>"; };
|
||||||
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
D6674AE923341F7600E8DF94 /* AppShortcutItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppShortcutItems.swift; sourceTree = "<group>"; };
|
||||||
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Delegates.swift"; sourceTree = "<group>"; };
|
|
||||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Mastodon+Equatable.swift"; sourceTree = "<group>"; };
|
||||||
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
D66A77BA233838DC0058F1EC /* UIFont+Traits.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIFont+Traits.swift"; sourceTree = "<group>"; };
|
||||||
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
D66C900A28DAB7FD00217BF2 /* TimelineViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineViewController.swift; sourceTree = "<group>"; };
|
||||||
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
|
D671A6BE299DA96100A81FEA /* Tusker-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Tusker-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Pachyderm; path = Packages/Pachyderm; sourceTree = "<group>"; };
|
D674A50727F910F300BA03AC /* Pachyderm */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Pachyderm; sourceTree = "<group>"; };
|
||||||
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
D67895BF246870DE00D4CD9E /* LocalAccountAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalAccountAvatarView.swift; sourceTree = "<group>"; };
|
||||||
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
D67B506C250B291200FAECFB /* BlurHashDecode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlurHashDecode.swift; sourceTree = "<group>"; };
|
||||||
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
|
D67C1794266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FastAccountSwitcherIndicatorView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -568,7 +558,6 @@
|
||||||
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
D67C57AE21E28EAD00C3118B /* Array+Uniques.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array+Uniques.swift"; sourceTree = "<group>"; };
|
||||||
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
D680153F2401A6BA00D6103B /* ComposingPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposingPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
D68015412401A74600D6103B /* MediaPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D681A299249AD62D0085E54E /* LargeImageContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageContentView.swift; sourceTree = "<group>"; };
|
|
||||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
D68232F62464F4FD00325FB8 /* ComposeDrawingViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeDrawingViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -584,7 +573,7 @@
|
||||||
D68A76EB295369A8001DA1B3 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
D68A76EB295369A8001DA1B3 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||||
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = "<group>"; };
|
D68A76ED295369C7001DA1B3 /* AcknowledgementsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AcknowledgementsView.swift; sourceTree = "<group>"; };
|
||||||
D68A76F029539116001DA1B3 /* FlipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipView.swift; sourceTree = "<group>"; };
|
D68A76F029539116001DA1B3 /* FlipView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FlipView.swift; sourceTree = "<group>"; };
|
||||||
D68A76F22953915C001DA1B3 /* TTTKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TTTKit; path = Packages/TTTKit; sourceTree = "<group>"; };
|
D68A76F22953915C001DA1B3 /* TTTKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TTTKit; sourceTree = "<group>"; };
|
||||||
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
D68C2AE225869BAB00548EFF /* AuxiliarySceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuxiliarySceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TuskerRootViewController.swift; sourceTree = "<group>"; };
|
||||||
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrendsViewController.swift; sourceTree = "<group>"; };
|
D68E525C24A3E8F00054355A /* InlineTrendsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InlineTrendsViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -598,6 +587,16 @@
|
||||||
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
D691771629A710520054D7EF /* ProfileNoContentCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileNoContentCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
|
D691771829A7B8820054D7EF /* ProfileHeaderMovedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileHeaderMovedOverlayView.swift; sourceTree = "<group>"; };
|
||||||
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
|
D691772D29AA5D420054D7EF /* UserActivityHandlingContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserActivityHandlingContext.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGalleryDataSource.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FallbackGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F3F2BAA19EC002B1C8D /* GifvActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
D693A72725CF282E003A14E2 /* TrendingHashtagsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingHashtagsViewController.swift; sourceTree = "<group>"; };
|
||||||
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
D693A72D25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeaturedProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
|
D693A72E25CF91C6003A14E2 /* FeaturedProfileCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FeaturedProfileCollectionViewCell.xib; sourceTree = "<group>"; };
|
||||||
|
@ -634,7 +633,7 @@
|
||||||
D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; };
|
D6A6C10E25B62D2400298D0F /* DiskCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DiskCache.swift; sourceTree = "<group>"; };
|
||||||
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
D6A6C11425B62E9700298D0F /* CacheExpiry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CacheExpiry.swift; sourceTree = "<group>"; };
|
||||||
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
D6A6C11A25B63CEE00298D0F /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
|
||||||
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = MatchedGeometryPresentation; path = Packages/MatchedGeometryPresentation; sourceTree = "<group>"; };
|
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = MatchedGeometryPresentation; sourceTree = "<group>"; };
|
||||||
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
|
D6AC956623C4347E008C9946 /* MainSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSceneDelegate.swift; sourceTree = "<group>"; };
|
||||||
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6ADB6E728E8C878009924AB /* PublicTimelineDescriptionCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicTimelineDescriptionCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6ADB6E928E91C30009924AB /* TimelineStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
|
@ -643,7 +642,7 @@
|
||||||
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedImageView.swift; sourceTree = "<group>"; };
|
D6ADB6EF28ED1F25009924AB /* CachedImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CachedImageView.swift; sourceTree = "<group>"; };
|
||||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIActivity+Types.swift"; sourceTree = "<group>"; };
|
||||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = "<group>"; };
|
||||||
D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = UserAccounts; path = Packages/UserAccounts; sourceTree = "<group>"; };
|
D6B0026C29B5245400C70BE2 /* UserAccounts */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = UserAccounts; sourceTree = "<group>"; };
|
||||||
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
D6B17254254F88B800128392 /* OppositeCollapseKeywordsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OppositeCollapseKeywordsView.swift; sourceTree = "<group>"; };
|
||||||
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
|
D6B22A0E2560D52D004D82EF /* TabbedPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabbedPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
D6B30E08254BAF63009CAEE5 /* ImageGrayscalifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageGrayscalifier.swift; sourceTree = "<group>"; };
|
||||||
|
@ -663,10 +662,10 @@
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
D6BC9DD6232D7811002CA326 /* TimelinesPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelinesPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
D6BC9DD9232D8BE5002CA326 /* SearchResultsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResultsViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BD395729B6441F005FFD2B /* ComposeUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ComposeUI; path = Packages/ComposeUI; sourceTree = "<group>"; };
|
D6BD395729B6441F005FFD2B /* ComposeUI */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ComposeUI; sourceTree = "<group>"; };
|
||||||
D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeHostingController.swift; sourceTree = "<group>"; };
|
D6BD395A29B64441005FFD2B /* ComposeHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeHostingController.swift; sourceTree = "<group>"; };
|
||||||
D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerComponents; path = Packages/TuskerComponents; sourceTree = "<group>"; };
|
D6BD395C29B789D5005FFD2B /* TuskerComponents */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TuskerComponents; sourceTree = "<group>"; };
|
||||||
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Duckable; path = Packages/Duckable; sourceTree = "<group>"; };
|
D6BEA243291A0C83002F4D01 /* Duckable */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Duckable; sourceTree = "<group>"; };
|
||||||
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; };
|
D6BEA246291A0F2D002F4D01 /* Duckable+Root.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Duckable+Root.swift"; sourceTree = "<group>"; };
|
||||||
D6C041C32AED77730094D32D /* EditListSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSettingsService.swift; sourceTree = "<group>"; };
|
D6C041C32AED77730094D32D /* EditListSettingsService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditListSettingsService.swift; sourceTree = "<group>"; };
|
||||||
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
D6C1B2072545D1EC00DAAA66 /* StatusCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusCardView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -681,11 +680,9 @@
|
||||||
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
D6C693FD2162FEEA007D6A6D /* UIViewController+Children.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController+Children.swift"; sourceTree = "<group>"; };
|
||||||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentsContainerView.swift; sourceTree = "<group>"; };
|
||||||
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
D6C82B4025C5BB7E0017F1E6 /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = "<group>"; };
|
||||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LargeImageViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = "<group>"; };
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioSessionHelper.swift; sourceTree = "<group>"; };
|
||||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryPlayerViewController.swift; sourceTree = "<group>"; };
|
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = TuskerPreferences; sourceTree = "<group>"; };
|
||||||
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = TuskerPreferences; path = Packages/TuskerPreferences; sourceTree = "<group>"; };
|
|
||||||
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
D6CA8CD92962231F0050C433 /* ArrayUniqueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArrayUniqueTests.swift; sourceTree = "<group>"; };
|
||||||
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
|
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SaveToPhotosActivity.swift; sourceTree = "<group>"; };
|
||||||
D6CF5B822AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSection+Readable.swift"; sourceTree = "<group>"; };
|
D6CF5B822AC65DDF00F15D83 /* NSCollectionLayoutSection+Readable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSCollectionLayoutSection+Readable.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -718,7 +715,6 @@
|
||||||
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; };
|
D6D79F522A0FFE3200AB2315 /* ToggleableButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToggleableButton.swift; sourceTree = "<group>"; };
|
||||||
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
D6D79F562A1160B800AB2315 /* AccountDisplayNameLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDisplayNameLabel.swift; sourceTree = "<group>"; };
|
||||||
D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
|
D6D79F582A13293200AB2315 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = "<group>"; };
|
||||||
D6D79F5A2A13D22B00AB2315 /* GalleryFallbackViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GalleryFallbackViewController.swift; sourceTree = "<group>"; };
|
|
||||||
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
D6D94954298963A900C59229 /* Colors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Colors.swift; sourceTree = "<group>"; };
|
||||||
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
D6D9498C298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrendingStatusCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
D6D9498E298EB79400C59229 /* CopyableLable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CopyableLable.swift; sourceTree = "<group>"; };
|
||||||
|
@ -761,7 +757,7 @@
|
||||||
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = "<group>"; };
|
D6F6A553291F0D9600F496A8 /* DeleteListService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteListService.swift; sourceTree = "<group>"; };
|
||||||
D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteAccountView.swift; sourceTree = "<group>"; };
|
D6F6A556291F4F1600F496A8 /* MuteAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MuteAccountView.swift; sourceTree = "<group>"; };
|
||||||
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
D6F953EF21251A2900CF0F2B /* MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonController.swift; sourceTree = "<group>"; };
|
||||||
D6FA94DF29B52891006AAC51 /* InstanceFeatures */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = InstanceFeatures; path = Packages/InstanceFeatures; sourceTree = "<group>"; };
|
D6FA94DF29B52891006AAC51 /* InstanceFeatures */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = InstanceFeatures; sourceTree = "<group>"; };
|
||||||
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
D6FF985F255C717400845181 /* AccountSwitchingContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountSwitchingContainerViewController.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
@ -790,6 +786,7 @@
|
||||||
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
|
D6B0026E29B5248800C70BE2 /* UserAccounts in Frameworks */,
|
||||||
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
|
D60088EF2980D8B5005B4D00 /* StoreKit.framework in Frameworks */,
|
||||||
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */,
|
D6CA6ED229EF6091003EC5DF /* TuskerPreferences in Frameworks */,
|
||||||
|
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */,
|
||||||
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
D6552367289870790048A653 /* ScreenCorners in Frameworks */,
|
||||||
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
D60BB3942B30076F00DAEA65 /* HTMLStreamer in Frameworks */,
|
||||||
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
D6676CA527A8D0020052936B /* WebURLFoundationExtras in Frameworks */,
|
||||||
|
@ -823,15 +820,18 @@
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */ = {
|
0411610522B457290030A9B7 /* Gallery */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
04D14BAE22B34A2800642648 /* GalleryViewController.swift */,
|
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
|
||||||
D647D92724257BEB0005044F /* AttachmentPreviewViewController.swift */,
|
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
|
||||||
D6531DEF246B867E000F9538 /* GifvAttachmentViewController.swift */,
|
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */,
|
||||||
D6CA6A93249FADE700AD45C1 /* GalleryPlayerViewController.swift */,
|
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
|
||||||
|
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
||||||
|
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
|
||||||
|
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
|
||||||
);
|
);
|
||||||
path = "Attachment Gallery";
|
path = Gallery;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D60089172981FEA4005B4D00 /* Tip Jar */ = {
|
D60089172981FEA4005B4D00 /* Tip Jar */ = {
|
||||||
|
@ -991,14 +991,13 @@
|
||||||
children = (
|
children = (
|
||||||
D65B4B89297879DE00DABDFB /* Account Follows */,
|
D65B4B89297879DE00DABDFB /* Account Follows */,
|
||||||
D6A3BC822321F69400FD64D5 /* Account List */,
|
D6A3BC822321F69400FD64D5 /* Account List */,
|
||||||
0411610522B457290030A9B7 /* Attachment Gallery */,
|
|
||||||
D641C787213DD862004B4513 /* Compose */,
|
D641C787213DD862004B4513 /* Compose */,
|
||||||
D641C785213DD83B004B4513 /* Conversation */,
|
D641C785213DD83B004B4513 /* Conversation */,
|
||||||
D6F2E960249E772F005846BB /* Crash Reporter */,
|
D6F2E960249E772F005846BB /* Crash Reporter */,
|
||||||
D61F759729384D4200C0B37F /* Customize Timelines */,
|
D61F759729384D4200C0B37F /* Customize Timelines */,
|
||||||
D627943C23A5635D00D38C68 /* Explore */,
|
D627943C23A5635D00D38C68 /* Explore */,
|
||||||
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
D6A4DCC92553666600D9DE31 /* Fast Account Switcher */,
|
||||||
D641C788213DD86D004B4513 /* Large Image */,
|
0411610522B457290030A9B7 /* Gallery */,
|
||||||
D627944B23A9A02400D38C68 /* Lists */,
|
D627944B23A9A02400D38C68 /* Lists */,
|
||||||
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
|
D627944823A6AD5100D38C68 /* Local Predicate Statuses List */,
|
||||||
D641C782213DD7F0004B4513 /* Main */,
|
D641C782213DD7F0004B4513 /* Main */,
|
||||||
|
@ -1101,19 +1100,6 @@
|
||||||
path = Compose;
|
path = Compose;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D641C788213DD86D004B4513 /* Large Image */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
D646C954213B364600269FB5 /* Transitions */,
|
|
||||||
D6289E83217B795D0003D1D7 /* LargeImageViewController.xib */,
|
|
||||||
D6C94D862139E62700CB5196 /* LargeImageViewController.swift */,
|
|
||||||
D681A299249AD62D0085E54E /* LargeImageContentView.swift */,
|
|
||||||
041160FE22B442870030A9B7 /* LoadingLargeImageViewController.swift */,
|
|
||||||
D6D79F5A2A13D22B00AB2315 /* GalleryFallbackViewController.swift */,
|
|
||||||
);
|
|
||||||
path = "Large Image";
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
D641C789213DD87E004B4513 /* Preferences */ = {
|
D641C789213DD87E004B4513 /* Preferences */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1165,14 +1151,21 @@
|
||||||
path = "Profile Header";
|
path = "Profile Header";
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D646C954213B364600269FB5 /* Transitions */ = {
|
D642E83C2BA7ACC3004BFD6A /* Packages */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D646C955213B365700269FB5 /* LargeImageExpandAnimationController.swift */,
|
D674A50727F910F300BA03AC /* Pachyderm */,
|
||||||
D646C957213B367000269FB5 /* LargeImageShrinkAnimationController.swift */,
|
D6BEA243291A0C83002F4D01 /* Duckable */,
|
||||||
D646C959213B5D0500269FB5 /* LargeImageInteractionController.swift */,
|
D68A76F22953915C001DA1B3 /* TTTKit */,
|
||||||
|
D6B0026C29B5245400C70BE2 /* UserAccounts */,
|
||||||
|
D6FA94DF29B52891006AAC51 /* InstanceFeatures */,
|
||||||
|
D6BD395C29B789D5005FFD2B /* TuskerComponents */,
|
||||||
|
D6BD395729B6441F005FFD2B /* ComposeUI */,
|
||||||
|
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */,
|
||||||
|
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */,
|
||||||
|
D642E83D2BA7AD0F004BFD6A /* GalleryVC */,
|
||||||
);
|
);
|
||||||
path = Transitions;
|
path = Packages;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
D64AAE8F26C80DB600FC57FB /* Toast */ = {
|
D64AAE8F26C80DB600FC57FB /* Toast */ = {
|
||||||
|
@ -1229,7 +1222,6 @@
|
||||||
D667E5F62135C2ED0057A976 /* Extensions */ = {
|
D667E5F62135C2ED0057A976 /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D667E5F02134D5050057A976 /* UIViewController+Delegates.swift */,
|
|
||||||
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
|
D667E5F72135C3040057A976 /* Mastodon+Equatable.swift */,
|
||||||
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
|
D663626B21361C6700C9CBA2 /* Account+Preferences.swift */,
|
||||||
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
|
D6F4D79329ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift */,
|
||||||
|
@ -1355,6 +1347,8 @@
|
||||||
children = (
|
children = (
|
||||||
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */,
|
D681E4D6246E32290053414F /* StatusActivityItemSource.swift */,
|
||||||
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
|
D681E4D8246E346E0053414F /* AccountActivityItemSource.swift */,
|
||||||
|
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */,
|
||||||
|
D6934F3F2BAA19EC002B1C8D /* GifvActivityItemSource.swift */,
|
||||||
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
D6AEBB3D2321638100E5038B /* UIActivity+Types.swift */,
|
||||||
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
D6AEBB422321685E00E5038B /* OpenInSafariActivity.swift */,
|
||||||
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */,
|
D6CA8CDD296387310050C433 /* SaveToPhotosActivity.swift */,
|
||||||
|
@ -1445,7 +1439,8 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
|
D6C94D882139E6EC00CB5196 /* AttachmentView.swift */,
|
||||||
D6531DED246B81C9000F9538 /* GifvAttachmentView.swift */,
|
D6531DED246B81C9000F9538 /* GifvPlayerView.swift */,
|
||||||
|
D6934F372BA8E2B7002B1C8D /* GifvController.swift */,
|
||||||
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */,
|
D6C7D27C22B6EBF800071952 /* AttachmentsContainerView.swift */,
|
||||||
);
|
);
|
||||||
path = Attachments;
|
path = Attachments;
|
||||||
|
@ -1457,15 +1452,7 @@
|
||||||
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
D63CC703290EC472000E19DE /* Dist.xcconfig */,
|
||||||
D6D706A829498C82000827ED /* Tusker.xcconfig */,
|
D6D706A829498C82000827ED /* Tusker.xcconfig */,
|
||||||
D6B5F3BC2ACA586C00309734 /* Version.xcconfig */,
|
D6B5F3BC2ACA586C00309734 /* Version.xcconfig */,
|
||||||
D674A50727F910F300BA03AC /* Pachyderm */,
|
D642E83C2BA7ACC3004BFD6A /* Packages */,
|
||||||
D6BEA243291A0C83002F4D01 /* Duckable */,
|
|
||||||
D68A76F22953915C001DA1B3 /* TTTKit */,
|
|
||||||
D6B0026C29B5245400C70BE2 /* UserAccounts */,
|
|
||||||
D6FA94DF29B52891006AAC51 /* InstanceFeatures */,
|
|
||||||
D6BD395C29B789D5005FFD2B /* TuskerComponents */,
|
|
||||||
D6BD395729B6441F005FFD2B /* ComposeUI */,
|
|
||||||
D6CA6ED029EF6060003EC5DF /* TuskerPreferences */,
|
|
||||||
D6A9E04F29F8917500BEDC7E /* MatchedGeometryPresentation */,
|
|
||||||
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
D6D4DDCE212518A000E1C4BB /* Tusker */,
|
||||||
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
D6D4DDE3212518A200E1C4BB /* TuskerTests */,
|
||||||
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
D6D4DDEE212518A200E1C4BB /* TuskerUITests */,
|
||||||
|
@ -1714,6 +1701,7 @@
|
||||||
D6BD395829B64426005FFD2B /* ComposeUI */,
|
D6BD395829B64426005FFD2B /* ComposeUI */,
|
||||||
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
D6CA6ED129EF6091003EC5DF /* TuskerPreferences */,
|
||||||
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
|
D60BB3932B30076F00DAEA65 /* HTMLStreamer */,
|
||||||
|
D6934F2B2BA7AD32002B1C8D /* GalleryVC */,
|
||||||
);
|
);
|
||||||
productName = Tusker;
|
productName = Tusker;
|
||||||
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
productReference = D6D4DDCC212518A000E1C4BB /* Tusker.app */;
|
||||||
|
@ -1865,7 +1853,6 @@
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */,
|
||||||
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
D6D4DDD7212518A200E1C4BB /* Assets.xcassets in Resources */,
|
||||||
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
D640D76922BAF5E6004FBE69 /* DomainBlocks.plist in Resources */,
|
||||||
D6289E84217B795D0003D1D7 /* LargeImageViewController.xib in Resources */,
|
|
||||||
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */,
|
D601FA84297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.xib in Resources */,
|
||||||
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */,
|
D6F2E966249E8BFD005846BB /* IssueReporterViewController.xib in Resources */,
|
||||||
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
D6A4DCCD2553667800D9DE31 /* FastAccountSwitcherViewController.xib in Resources */,
|
||||||
|
@ -1959,7 +1946,6 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
D6531DF0246B867E000F9538 /* GifvAttachmentViewController.swift in Sources */,
|
|
||||||
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
0427033A22B31269000D31B6 /* AdvancedPrefsView.swift in Sources */,
|
||||||
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
D6C3F4F5298ED0890009FCFF /* LocalPredicateStatusesViewController.swift in Sources */,
|
||||||
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
|
D691771729A710520054D7EF /* ProfileNoContentCollectionViewCell.swift in Sources */,
|
||||||
|
@ -1988,6 +1974,7 @@
|
||||||
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */,
|
D65B4B682977769E00DABDFB /* StatusActionAccountListViewController.swift in Sources */,
|
||||||
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
D61F759B29384F9C00C0B37F /* FilterMO.swift in Sources */,
|
||||||
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
D68FEC4F232C5BC300C84F23 /* SegmentedPageViewController.swift in Sources */,
|
||||||
|
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */,
|
||||||
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
D64AAE9126C80DC600FC57FB /* ToastView.swift in Sources */,
|
||||||
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
|
D6BC74862AFC4772000DD603 /* SuggestedProfileCardView.swift in Sources */,
|
||||||
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
0450531F22B0097E00100BA2 /* Timline+UI.swift in Sources */,
|
||||||
|
@ -1998,15 +1985,16 @@
|
||||||
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
D6B22A0F2560D52D004D82EF /* TabbedPageViewController.swift in Sources */,
|
||||||
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */,
|
D6895DC228D65274006341DA /* CustomAlertController.swift in Sources */,
|
||||||
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
D6F953F021251A2900CF0F2B /* MastodonController.swift in Sources */,
|
||||||
0411610022B442870030A9B7 /* LoadingLargeImageViewController.swift in Sources */,
|
|
||||||
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
D6A4DCE525537C7A00D9DE31 /* FastSwitchingAccountView.swift in Sources */,
|
||||||
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
D6A4DCCC2553667800D9DE31 /* FastAccountSwitcherViewController.swift in Sources */,
|
||||||
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
D627944F23A9C99800D38C68 /* EditListAccountsViewController.swift in Sources */,
|
||||||
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
D6945C3423AC6431005C403C /* AddSavedHashtagViewController.swift in Sources */,
|
||||||
|
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */,
|
||||||
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
D6F6A552291F098700F496A8 /* RenameListService.swift in Sources */,
|
||||||
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
D62D2426217ABF63005076CC /* UserActivityType.swift in Sources */,
|
||||||
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
D6AC956723C4347E008C9946 /* MainSceneDelegate.swift in Sources */,
|
||||||
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
D6311C5025B3765B00B27539 /* ImageDataCache.swift in Sources */,
|
||||||
|
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */,
|
||||||
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */,
|
D6D79F2B2A0D5D5C00AB2315 /* StatusEditCollectionViewCell.swift in Sources */,
|
||||||
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
D6BC9DD7232D7811002CA326 /* TimelinesPageViewController.swift in Sources */,
|
||||||
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
|
D6D9498D298CBB4000C59229 /* TrendingStatusCollectionViewCell.swift in Sources */,
|
||||||
|
@ -2015,12 +2003,12 @@
|
||||||
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
|
D651C5B42915B00400236EF6 /* ProfileFieldsView.swift in Sources */,
|
||||||
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */,
|
D646DCD82A07F3500059ECEB /* FollowRequestNotificationCollectionViewCell.swift in Sources */,
|
||||||
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */,
|
D6F2E965249E8BFD005846BB /* IssueReporterViewController.swift in Sources */,
|
||||||
D6CA6A94249FADE700AD45C1 /* GalleryPlayerViewController.swift in Sources */,
|
|
||||||
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
D627944D23A9A03D00D38C68 /* ListTimelineViewController.swift in Sources */,
|
||||||
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
|
D6D12B2F2925D66500D528E1 /* TimelineGapCollectionViewCell.swift in Sources */,
|
||||||
D6D94955298963A900C59229 /* Colors.swift in Sources */,
|
D6D94955298963A900C59229 /* Colors.swift in Sources */,
|
||||||
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
D6945C3823AC739F005C403C /* InstanceTimelineViewController.swift in Sources */,
|
||||||
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
|
D625E4822588262A0074BB2B /* DraggableTableViewCell.swift in Sources */,
|
||||||
|
D6934F322BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift in Sources */,
|
||||||
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
D68E525B24A3D77E0054355A /* TuskerRootViewController.swift in Sources */,
|
||||||
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
D62D2422217AA7E1005076CC /* UserActivityManager.swift in Sources */,
|
||||||
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
D6CA6A92249FAD8900AD45C1 /* AudioSessionHelper.swift in Sources */,
|
||||||
|
@ -2038,23 +2026,23 @@
|
||||||
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
D6A6C10F25B62D2400298D0F /* DiskCache.swift in Sources */,
|
||||||
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
D6C3F4F9298EDBF20009FCFF /* ConversationTree.swift in Sources */,
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */,
|
||||||
D646C958213B367000269FB5 /* LargeImageShrinkAnimationController.swift in Sources */,
|
|
||||||
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
D6674AEA23341F7600E8DF94 /* AppShortcutItems.swift in Sources */,
|
||||||
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
D6DD8FFF2984D327002AD3FD /* BookmarksViewController.swift in Sources */,
|
||||||
D646C956213B365700269FB5 /* LargeImageExpandAnimationController.swift in Sources */,
|
|
||||||
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
D646DCD22A06F2510059ECEB /* NotificationsCollectionViewController.swift in Sources */,
|
||||||
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
D667E5F82135C3040057A976 /* Mastodon+Equatable.swift in Sources */,
|
||||||
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
D6B4A4FF2506B81A000C81C1 /* AccountDisplayNameView.swift in Sources */,
|
||||||
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
D63D8DF42850FE7A008D95E1 /* ViewTags.swift in Sources */,
|
||||||
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
D61DC84B28F4FD2000B82C6E /* ProfileHeaderCollectionViewCell.swift in Sources */,
|
||||||
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
D61A45E828DF477D002BE511 /* LoadingCollectionViewCell.swift in Sources */,
|
||||||
|
D6934F3A2BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift in Sources */,
|
||||||
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
D67B506D250B291200FAECFB /* BlurHashDecode.swift in Sources */,
|
||||||
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
D68A76EC295369A8001DA1B3 /* AboutView.swift in Sources */,
|
||||||
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
04DACE8E212CC7CC009840C4 /* ImageCache.swift in Sources */,
|
||||||
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */,
|
D646DCAE2A06C8C90059ECEB /* ProfileFieldVerificationView.swift in Sources */,
|
||||||
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
|
D61F75AD293AF39000C0B37F /* Filter+Helpers.swift in Sources */,
|
||||||
D6531DEE246B81C9000F9538 /* GifvAttachmentView.swift in Sources */,
|
D6531DEE246B81C9000F9538 /* GifvPlayerView.swift in Sources */,
|
||||||
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
D6370B9C24421FF30092A7FF /* Tusker.xcdatamodeld in Sources */,
|
||||||
|
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */,
|
||||||
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
|
D601FA83297EEC3F00A8E8B5 /* SuggestedProfileCardCollectionViewCell.swift in Sources */,
|
||||||
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */,
|
D646DCDA2A081A2C0059ECEB /* PollFinishedNotificationCollectionViewCell.swift in Sources */,
|
||||||
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
|
D691772E29AA5D420054D7EF /* UserActivityHandlingContext.swift in Sources */,
|
||||||
|
@ -2119,7 +2107,6 @@
|
||||||
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
|
D6945C2F23AC47C3005C403C /* SavedDataManager.swift in Sources */,
|
||||||
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
|
D6C94D892139E6EC00CB5196 /* AttachmentView.swift in Sources */,
|
||||||
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
|
D6C693EF216192C2007D6A6D /* TuskerNavigationDelegate.swift in Sources */,
|
||||||
D6C94D872139E62700CB5196 /* LargeImageViewController.swift in Sources */,
|
|
||||||
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */,
|
D6D79F592A13293200AB2315 /* BackgroundManager.swift in Sources */,
|
||||||
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */,
|
D6E4269D2532A3E100C02E1C /* FuzzyMatcher.swift in Sources */,
|
||||||
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
D6969EA0240C8384002843CE /* EmojiLabel.swift in Sources */,
|
||||||
|
@ -2147,7 +2134,6 @@
|
||||||
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
|
D646DCAC2A06C8840059ECEB /* ProfileFieldValueView.swift in Sources */,
|
||||||
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
D6F4D79429ECB0AF00351B87 /* UIBackgroundConfiguration+AppColors.swift in Sources */,
|
||||||
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
D6333B372137838300CE884A /* AttributedString+Helpers.swift in Sources */,
|
||||||
D681A29A249AD62D0085E54E /* LargeImageContentView.swift in Sources */,
|
|
||||||
D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */,
|
D6C041C42AED77730094D32D /* EditListSettingsService.swift in Sources */,
|
||||||
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
D6DFC69E242C490400ACC392 /* TrackpadScrollGestureRecognizer.swift in Sources */,
|
||||||
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
D61AC1D8232EA42D00C54D2D /* InstanceTableViewCell.swift in Sources */,
|
||||||
|
@ -2166,6 +2152,8 @@
|
||||||
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
D6C693FE2162FEEA007D6A6D /* UIViewController+Children.swift in Sources */,
|
||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
|
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
|
||||||
|
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */,
|
||||||
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
D67C1795266D57D10070F250 /* FastAccountSwitcherIndicatorView.swift in Sources */,
|
||||||
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
D61F75962937037800C0B37F /* ToggleFollowHashtagService.swift in Sources */,
|
||||||
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
|
D60089192981FEBA005B4D00 /* ConfettiView.swift in Sources */,
|
||||||
|
@ -2177,7 +2165,6 @@
|
||||||
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
D6E77D0B286D426E00D8B732 /* TrendingLinkCardCollectionViewCell.swift in Sources */,
|
||||||
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
D6114E1127F899B30080E273 /* TrendingLinksViewController.swift in Sources */,
|
||||||
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
D65B4B58297203A700DABDFB /* ReportSelectRulesView.swift in Sources */,
|
||||||
D6D79F5B2A13D22B00AB2315 /* GalleryFallbackViewController.swift in Sources */,
|
|
||||||
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */,
|
D6C3F4FB299035650009FCFF /* TrendsViewController.swift in Sources */,
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */,
|
||||||
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
D691771129A2B76A0054D7EF /* MainActor+Unsafe.swift in Sources */,
|
||||||
|
@ -2187,14 +2174,12 @@
|
||||||
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
D67C57AF21E28EAD00C3118B /* Array+Uniques.swift in Sources */,
|
||||||
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
D68A76F129539116001DA1B3 /* FlipView.swift in Sources */,
|
||||||
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
D6945C3223AC4D36005C403C /* HashtagTimelineViewController.swift in Sources */,
|
||||||
D647D92824257BEB0005044F /* AttachmentPreviewViewController.swift in Sources */,
|
|
||||||
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
|
D61F75B5293BD97400C0B37F /* DeleteFilterService.swift in Sources */,
|
||||||
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
|
D6DFC6A0242C4CCC00ACC392 /* Weak.swift in Sources */,
|
||||||
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
D6C693FC2162FE6F007D6A6D /* LoadingViewController.swift in Sources */,
|
||||||
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */,
|
D61ABEF828EFC3F900B29151 /* ProfileStatusesViewController.swift in Sources */,
|
||||||
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
|
D6ADB6EA28E91C30009924AB /* TimelineStatusCollectionViewCell.swift in Sources */,
|
||||||
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
D681E4D7246E32290053414F /* StatusActivityItemSource.swift in Sources */,
|
||||||
D646C95A213B5D0500269FB5 /* LargeImageInteractionController.swift in Sources */,
|
|
||||||
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
D693A72825CF282E003A14E2 /* TrendingHashtagsViewController.swift in Sources */,
|
||||||
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
D621733328F1D5ED004C7DB1 /* ReblogService.swift in Sources */,
|
||||||
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
D62275A824F1CA2800B82A16 /* ComposeReplyContentView.swift in Sources */,
|
||||||
|
@ -2210,7 +2195,6 @@
|
||||||
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
D6895DC428D65342006341DA /* ConfirmReblogStatusPreviewView.swift in Sources */,
|
||||||
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
D62E9987279D094F00C26176 /* StatusMetaIndicatorsView.swift in Sources */,
|
||||||
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
D6BEA247291A0F2D002F4D01 /* Duckable+Root.swift in Sources */,
|
||||||
04D14BB022B34A2800642648 /* GalleryViewController.swift in Sources */,
|
|
||||||
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
D6C1B2082545D1EC00DAAA66 /* StatusCardView.swift in Sources */,
|
||||||
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
D64D8CA92463B494006B0BAA /* MultiThreadDictionary.swift in Sources */,
|
||||||
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
D6F6A554291F0D9600F496A8 /* DeleteListService.swift in Sources */,
|
||||||
|
@ -2223,11 +2207,11 @@
|
||||||
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
D68015422401A74600D6103B /* MediaPrefsView.swift in Sources */,
|
||||||
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
D6093F9B25BDD4B9004811E6 /* HashtagSearchResultsViewController.swift in Sources */,
|
||||||
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
D6E77D09286D25FA00D8B732 /* TrendingHashtagCollectionViewCell.swift in Sources */,
|
||||||
D667E5F12134D5050057A976 /* UIViewController+Delegates.swift in Sources */,
|
|
||||||
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
D6D79F292A0D596B00AB2315 /* StatusEditHistoryViewController.swift in Sources */,
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */,
|
||||||
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
D6D4DDD0212518A000E1C4BB /* AppDelegate.swift in Sources */,
|
||||||
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
D61F75B3293BD89C00C0B37F /* UpdateFilterService.swift in Sources */,
|
||||||
|
D6934F402BAA19EC002B1C8D /* GifvActivityItemSource.swift in Sources */,
|
||||||
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
D6C3F5172991C1A00009FCFF /* View+AppListStyle.swift in Sources */,
|
||||||
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
D65B4B6A297777D900DABDFB /* StatusNotFoundView.swift in Sources */,
|
||||||
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
D6412B0924B0291E00F5412E /* MyProfileViewController.swift in Sources */,
|
||||||
|
@ -2239,6 +2223,7 @@
|
||||||
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */,
|
D6A3A380295515550036B6EF /* ProfileHeaderButton.swift in Sources */,
|
||||||
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */,
|
D6958F3D2AA383D90062FE52 /* WidescreenNavigationPrefsView.swift in Sources */,
|
||||||
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
D65B4B562971F98300DABDFB /* ReportView.swift in Sources */,
|
||||||
|
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */,
|
||||||
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
D623A543263634100095BD04 /* PollOptionCheckboxView.swift in Sources */,
|
||||||
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
D68A76E829527884001DA1B3 /* PinnedTimelinesView.swift in Sources */,
|
||||||
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
D690797324A4EF9700023A34 /* UIBezierPath+Helpers.swift in Sources */,
|
||||||
|
@ -3042,6 +3027,10 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Pachyderm;
|
productName = Pachyderm;
|
||||||
};
|
};
|
||||||
|
D6934F2B2BA7AD32002B1C8D /* GalleryVC */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = GalleryVC;
|
||||||
|
};
|
||||||
D6A4532329EF665200032932 /* ComposeUI */ = {
|
D6A4532329EF665200032932 /* ComposeUI */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = ComposeUI;
|
productName = ComposeUI;
|
||||||
|
|
|
@ -0,0 +1,55 @@
|
||||||
|
//
|
||||||
|
// GifvActivityItemSource.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/19/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class GifvActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
|
let asset: AVAsset
|
||||||
|
let url: URL
|
||||||
|
|
||||||
|
init(asset: AVAsset, url: URL) {
|
||||||
|
self.asset = asset
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
||||||
|
#if os(visionOS)
|
||||||
|
#warning("Use async AVAssetImageGenerator.image(at:)")
|
||||||
|
return nil
|
||||||
|
#else
|
||||||
|
let generator = AVAssetImageGenerator(asset: self.asset)
|
||||||
|
generator.appliesPreferredTrackTransform = true
|
||||||
|
if let image = try? generator.copyCGImage(at: .zero, actualTime: nil) {
|
||||||
|
return UIImage(cgImage: image)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||||
|
do {
|
||||||
|
let data = try Data(contentsOf: url)
|
||||||
|
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
try data.write(to: tempURL)
|
||||||
|
return tempURL
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||||
|
return (UTType(filenameExtension: url.pathExtension) ?? .video).identifier
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
//
|
||||||
|
// ImageActivityItemSource.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/19/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
class ImageActivityItemSource: NSObject, UIActivityItemSource {
|
||||||
|
let data: Data
|
||||||
|
let url: URL
|
||||||
|
let image: UIImage?
|
||||||
|
|
||||||
|
init(data: Data, url: URL, image: UIImage?) {
|
||||||
|
self.data = data
|
||||||
|
self.url = url
|
||||||
|
self.image = image
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
||||||
|
return image
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||||
|
do {
|
||||||
|
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
||||||
|
try data.write(to: tempURL)
|
||||||
|
return tempURL
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||||
|
return (UTType(filenameExtension: url.pathExtension) ?? .image).identifier
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +0,0 @@
|
||||||
//
|
|
||||||
// UIViewController+Delegate.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 8/27/18.
|
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
#if !os(visionOS)
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension UIViewController: UIViewControllerTransitioningDelegate {
|
|
||||||
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
||||||
if let presented = presented as? LargeImageAnimatableViewController,
|
|
||||||
presented.animationImage != nil {
|
|
||||||
return LargeImageExpandAnimationController()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
||||||
if let dismissed = dismissed as? LargeImageAnimatableViewController,
|
|
||||||
dismissed.animationImage != nil {
|
|
||||||
return LargeImageShrinkAnimationController(interactionController: dismissed.dismissInteractionController)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
public func interactionControllerForDismissal(using animator: UIViewControllerAnimatedTransitioning) -> UIViewControllerInteractiveTransitioning? {
|
|
||||||
if let animator = animator as? LargeImageShrinkAnimationController,
|
|
||||||
let interactionController = animator.interactionController,
|
|
||||||
interactionController.inProgress {
|
|
||||||
return interactionController
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
|
@ -1,50 +0,0 @@
|
||||||
//
|
|
||||||
// AttachmentPreviewViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 3/20/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
import TuskerComponents
|
|
||||||
|
|
||||||
class AttachmentPreviewViewController: UIViewController {
|
|
||||||
|
|
||||||
private let attachment: Attachment
|
|
||||||
private let sourceView: AttachmentView
|
|
||||||
|
|
||||||
init(sourceView: AttachmentView) {
|
|
||||||
self.attachment = sourceView.attachment
|
|
||||||
self.sourceView = sourceView
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadView() {
|
|
||||||
if let data = ImageCache.attachments.getData(attachment.url),
|
|
||||||
let image = UIImage(data: data) {
|
|
||||||
let imageView: UIImageView
|
|
||||||
if attachment.url.pathExtension == "gif" {
|
|
||||||
let gifView = GIFImageView(image: image)
|
|
||||||
let controller = sourceView.gifController ?? GIFController(gifData: data)
|
|
||||||
controller.attach(to: gifView)
|
|
||||||
controller.startAnimating()
|
|
||||||
imageView = gifView
|
|
||||||
} else {
|
|
||||||
imageView = UIImageView(image: image)
|
|
||||||
}
|
|
||||||
imageView.contentMode = .scaleAspectFit
|
|
||||||
imageView.backgroundColor = .black
|
|
||||||
view = imageView
|
|
||||||
preferredContentSize = image.size
|
|
||||||
} else {
|
|
||||||
view = UIActivityIndicatorView(style: .large)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
//
|
|
||||||
// GalleryPlayerViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/21/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import AVKit
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class GalleryPlayerViewController: UIViewController {
|
|
||||||
|
|
||||||
let playerVC = AVPlayerViewController()
|
|
||||||
|
|
||||||
var attachment: Attachment!
|
|
||||||
|
|
||||||
private var isFirstAppearance = true
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
view.backgroundColor = .black
|
|
||||||
|
|
||||||
playerVC.allowsPictureInPicturePlayback = true
|
|
||||||
|
|
||||||
playerVC.view.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addChild(playerVC)
|
|
||||||
playerVC.didMove(toParent: self)
|
|
||||||
view.addSubview(playerVC.view)
|
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
playerVC.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
|
|
||||||
playerVC.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
|
|
||||||
playerVC.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
|
||||||
playerVC.view.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
// starting while audio is already playing from another app often takes nearly a second,
|
|
||||||
// so do it on a background thread as to not block the UI
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
AudioSessionHelper.enable()
|
|
||||||
AudioSessionHelper.setVideoPlayback()
|
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
if self.isFirstAppearance {
|
|
||||||
self.isFirstAppearance = false
|
|
||||||
self.playerVC.player?.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillDisappear(_ animated: Bool) {
|
|
||||||
super.viewWillDisappear(animated)
|
|
||||||
|
|
||||||
// starting often takes around half a second,
|
|
||||||
// so do it on a background thread as to not block the UI
|
|
||||||
DispatchQueue.global(qos: .userInitiated).async {
|
|
||||||
AudioSessionHelper.setDefault()
|
|
||||||
AudioSessionHelper.disable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,179 +0,0 @@
|
||||||
// GalleryViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/13/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
import AVFoundation
|
|
||||||
import AVKit
|
|
||||||
|
|
||||||
class GalleryViewController: UIPageViewController, UIPageViewControllerDataSource, UIPageViewControllerDelegate, LargeImageAnimatableViewController {
|
|
||||||
|
|
||||||
weak var avPlayerViewControllerDelegate: AVPlayerViewControllerDelegate?
|
|
||||||
|
|
||||||
let attachments: [Attachment]
|
|
||||||
let sourceViews: WeakArray<UIImageView>
|
|
||||||
let startIndex: Int
|
|
||||||
|
|
||||||
var pages: [UIViewController]!
|
|
||||||
|
|
||||||
var currentIndex: Int {
|
|
||||||
guard let vc = viewControllers?.first,
|
|
||||||
let index = pages.firstIndex(of: vc) else {
|
|
||||||
fatalError()
|
|
||||||
}
|
|
||||||
return index
|
|
||||||
}
|
|
||||||
|
|
||||||
var animationSourceView: UIImageView? { sourceViews[currentIndex] }
|
|
||||||
var largeImageController: LargeImageViewController? {
|
|
||||||
// use protocol because page controllers may be loading or non-loading VCs
|
|
||||||
(pages[currentIndex] as? LargeImageAnimatableViewController)?.largeImageController
|
|
||||||
}
|
|
||||||
var animationImage: UIImage? {
|
|
||||||
if let page = pages[currentIndex] as? LargeImageAnimatableViewController,
|
|
||||||
let image = page.animationImage {
|
|
||||||
return image
|
|
||||||
} else {
|
|
||||||
return animationSourceView?.image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
|
||||||
|
|
||||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
|
||||||
didSet {
|
|
||||||
#if !os(visionOS)
|
|
||||||
setNeedsStatusBarAppearanceUpdate()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
|
||||||
return !isInteractivelyAnimatingDismissal
|
|
||||||
}
|
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
override var childForHomeIndicatorAutoHidden: UIViewController? {
|
|
||||||
return viewControllers?.first
|
|
||||||
}
|
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
|
||||||
return .allButUpsideDown
|
|
||||||
} else {
|
|
||||||
return .all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) {
|
|
||||||
self.attachments = attachments
|
|
||||||
self.sourceViews = WeakArray(sourceViews)
|
|
||||||
self.startIndex = startIndex
|
|
||||||
|
|
||||||
super.init(transitionStyle: .scroll, navigationOrientation: .horizontal)
|
|
||||||
|
|
||||||
modalPresentationStyle = .fullScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
self.pages = attachments.enumerated().map { (index, attachment) in
|
|
||||||
switch attachment.kind {
|
|
||||||
case .image:
|
|
||||||
let vc = LoadingLargeImageViewController(attachment: attachment)
|
|
||||||
vc.shrinkGestureEnabled = false
|
|
||||||
vc.animationSourceView = sourceViews[index]
|
|
||||||
return vc
|
|
||||||
case .video, .audio:
|
|
||||||
let vc = GalleryPlayerViewController()
|
|
||||||
vc.playerVC.player = AVPlayer(url: attachment.url)
|
|
||||||
vc.playerVC.delegate = avPlayerViewControllerDelegate
|
|
||||||
vc.attachment = attachment
|
|
||||||
return vc
|
|
||||||
case .gifv:
|
|
||||||
// Passing the source view to the LargeImageGifvContentView is a crappy workaround for not
|
|
||||||
// having the current frame to use as the animationImage. This will break when there
|
|
||||||
// are more than 4 attachments and there is a gifv at index >= 3 (the More... button will show
|
|
||||||
// in place of the fourth attachment, so there aren't source views for the attachments at index >= 3).
|
|
||||||
// This isn't a priority as only Mastodon converts gifs to gifvs, and Mastodon (in its default configuration,
|
|
||||||
// I don't know about forks) doesn't allow more than four attachments, meaning there will always be a source view.
|
|
||||||
let gifvContentView = LargeImageGifvContentView(attachment: attachment, source: sourceViews[index]!)
|
|
||||||
let vc = LargeImageViewController(contentView: gifvContentView, description: attachment.description, sourceView: nil)
|
|
||||||
vc.shrinkGestureEnabled = false
|
|
||||||
return vc
|
|
||||||
default:
|
|
||||||
return UINavigationController(rootViewController: GalleryFallbackViewController(attachment: attachment))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setViewControllers([pages[startIndex]], direction: .forward, animated: false)
|
|
||||||
|
|
||||||
self.dataSource = self
|
|
||||||
self.delegate = self
|
|
||||||
|
|
||||||
overrideUserInterfaceStyle = .dark
|
|
||||||
|
|
||||||
dismissInteractionController = LargeImageInteractionController(viewController: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
if let vc = pages[currentIndex] as? AVPlayerViewController {
|
|
||||||
// when the gallery is first shown, after the transition finishes, the controls for the player controller appear semi-transparent
|
|
||||||
// hiding the controls and then immediately reshowing them makes sure they're visible when the gallery is presented
|
|
||||||
vc.showsPlaybackControls = false
|
|
||||||
vc.showsPlaybackControls = true
|
|
||||||
|
|
||||||
// begin playing the video as soon as we appear
|
|
||||||
vc.player?.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func accessibilityPerformEscape() -> Bool {
|
|
||||||
dismiss(animated: true)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Page View Controller Data Source
|
|
||||||
|
|
||||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? {
|
|
||||||
guard let index = pages.firstIndex(of: viewController),
|
|
||||||
index > 0 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return pages[index - 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? {
|
|
||||||
guard let index = pages.firstIndex(of: viewController),
|
|
||||||
index < pages.count - 1 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return pages[index + 1]
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Page View Controller Delegate
|
|
||||||
func pageViewController(_ pageViewController: UIPageViewController, willTransitionTo pendingViewControllers: [UIViewController]) {
|
|
||||||
if let pending = pendingViewControllers.first as? LoadingLargeImageViewController,
|
|
||||||
let current = viewControllers!.first as? LoadingLargeImageViewController {
|
|
||||||
pending.controlsVisible = current.controlsVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
if let pending = pendingViewControllers.first as? AVPlayerViewController {
|
|
||||||
// show controls and begin playing when the player page becomes visible
|
|
||||||
pending.showsPlaybackControls = true
|
|
||||||
pending.player?.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,33 +0,0 @@
|
||||||
//
|
|
||||||
// GifvAttachmentViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/12/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
class GifvAttachmentViewController: UIViewController {
|
|
||||||
|
|
||||||
private let attachment: Attachment
|
|
||||||
|
|
||||||
init(attachment: Attachment) {
|
|
||||||
precondition(attachment.kind == .gifv)
|
|
||||||
self.attachment = attachment
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func loadView() {
|
|
||||||
let asset = AVURLAsset(url: attachment.url)
|
|
||||||
self.view = GifvAttachmentView(asset: asset, gravity: .resizeAspect)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
//
|
||||||
|
// FallbackGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/18/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
import QuickLook
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
private class FallbackGalleryContentViewController: QLPreviewController {
|
||||||
|
private let previewItem = GalleryPreviewItem()
|
||||||
|
|
||||||
|
init(url: URL) {
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
self.previewItem.previewItemURL = url
|
||||||
|
|
||||||
|
dataSource = self
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
overrideUserInterfaceStyle = .dark
|
||||||
|
|
||||||
|
navigationItem.rightBarButtonItems = [
|
||||||
|
UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(closePressed))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func closePressed() {
|
||||||
|
self.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension FallbackGalleryContentViewController: QLPreviewControllerDataSource {
|
||||||
|
func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> any QLPreviewItem {
|
||||||
|
previewItem
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController {
|
||||||
|
init(url: URL) {
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
self.viewControllers = [FallbackGalleryContentViewController(url: url)]
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
container?.disableGalleryScrollAndZoom()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder aDecoder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
|
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
|
var contentSize: CGSize {
|
||||||
|
.zero
|
||||||
|
}
|
||||||
|
|
||||||
|
var activityItemsForSharing: [Any] {
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
var caption: String? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var canAnimateFromSourceView: Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class GalleryPreviewItem: NSObject, QLPreviewItem {
|
||||||
|
var previewItemURL: URL? = nil
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
//
|
||||||
|
// GifvGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/18/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class GifvGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
|
let controller: GifvController
|
||||||
|
let caption: String?
|
||||||
|
|
||||||
|
private var presentationSizeCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
init(controller: GifvController, caption: String?) {
|
||||||
|
self.controller = controller
|
||||||
|
self.caption = caption
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let playerView = GifvPlayerView(controller: controller, gravity: .resizeAspect)
|
||||||
|
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(playerView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
playerView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
presentationSizeCancellable = controller.presentationSizeSubject
|
||||||
|
.sink { [unowned self] size in
|
||||||
|
self.preferredContentSize = size
|
||||||
|
self.container?.galleryContentChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
preferredContentSize = controller.item.presentationSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
controller.play()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
|
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
|
var contentSize: CGSize {
|
||||||
|
controller.item.presentationSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var activityItemsForSharing: [Any] {
|
||||||
|
// TODO: share gifv
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
//
|
||||||
|
// ImageGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
import Pachyderm
|
||||||
|
import TuskerComponents
|
||||||
|
|
||||||
|
class ImageGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
|
let url: URL
|
||||||
|
let caption: String?
|
||||||
|
let originalData: Data?
|
||||||
|
let image: UIImage
|
||||||
|
let gifController: GIFController?
|
||||||
|
|
||||||
|
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
|
||||||
|
self.url = url
|
||||||
|
self.caption = caption
|
||||||
|
self.originalData = originalData
|
||||||
|
self.image = image
|
||||||
|
self.gifController = gifController
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
|
||||||
|
preferredContentSize = image.size
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
let imageView = GIFImageView(image: image)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
view.addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
imageView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
imageView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
if let gifController {
|
||||||
|
gifController.attach(to: imageView)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if let gifController {
|
||||||
|
gifController.startAnimating()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
|
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
|
var contentSize: CGSize {
|
||||||
|
image.size
|
||||||
|
}
|
||||||
|
|
||||||
|
var activityItemsForSharing: [Any] {
|
||||||
|
if let data = originalData ?? image.pngData() {
|
||||||
|
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
//
|
||||||
|
// ImageGalleryDataSource.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/18/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GalleryVC
|
||||||
|
import TuskerComponents
|
||||||
|
|
||||||
|
class ImageGalleryDataSource: GalleryDataSource {
|
||||||
|
let url: URL
|
||||||
|
let cache: ImageCache
|
||||||
|
let sourceView: UIView
|
||||||
|
|
||||||
|
init(url: URL, cache: ImageCache, sourceView: UIView) {
|
||||||
|
self.url = url
|
||||||
|
self.cache = cache
|
||||||
|
self.sourceView = sourceView
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryItemsCount() -> Int {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryContentViewController(forItemAt index: Int) -> any GalleryVC.GalleryContentViewController {
|
||||||
|
if let entry = cache.get(url, loadOriginal: true) {
|
||||||
|
let gifController: GIFController? =
|
||||||
|
if url.pathExtension == "gif",
|
||||||
|
let data = entry.data {
|
||||||
|
GIFController(gifData: data)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
return ImageGalleryContentViewController(
|
||||||
|
url: url,
|
||||||
|
caption: nil,
|
||||||
|
originalData: entry.data,
|
||||||
|
image: entry.image,
|
||||||
|
gifController: gifController
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return LoadingGalleryContentViewController {
|
||||||
|
let (data, image) = await self.cache.get(self.url, loadOriginal: true)
|
||||||
|
if let image {
|
||||||
|
let gifController: GIFController? =
|
||||||
|
if self.url.pathExtension == "gif",
|
||||||
|
let data {
|
||||||
|
GIFController(gifData: data)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
return ImageGalleryContentViewController(
|
||||||
|
url: self.url,
|
||||||
|
caption: nil,
|
||||||
|
originalData: data,
|
||||||
|
image: image,
|
||||||
|
gifController: gifController
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
|
||||||
|
sourceView
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
||||||
|
[SaveToPhotosActivity()]
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,102 @@
|
||||||
|
//
|
||||||
|
// LoadingGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
|
class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
|
private let provider: () async -> (any GalleryContentViewController)?
|
||||||
|
private var wrapped: (any GalleryContentViewController)!
|
||||||
|
|
||||||
|
var container: GalleryContentViewControllerContainer?
|
||||||
|
|
||||||
|
var contentSize: CGSize {
|
||||||
|
wrapped?.contentSize ?? .zero
|
||||||
|
}
|
||||||
|
|
||||||
|
var activityItemsForSharing: [Any] {
|
||||||
|
wrapped?.activityItemsForSharing ?? []
|
||||||
|
}
|
||||||
|
|
||||||
|
var caption: String? {
|
||||||
|
wrapped?.caption
|
||||||
|
}
|
||||||
|
|
||||||
|
var canAnimateFromSourceView: Bool {
|
||||||
|
wrapped?.canAnimateFromSourceView ?? true
|
||||||
|
}
|
||||||
|
|
||||||
|
init(provider: @escaping () async -> (any GalleryContentViewController)?) {
|
||||||
|
self.provider = provider
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
container?.setGalleryContentLoading(true)
|
||||||
|
|
||||||
|
Task {
|
||||||
|
if let wrapped = await provider() {
|
||||||
|
self.wrapped = wrapped
|
||||||
|
wrapped.container = container
|
||||||
|
|
||||||
|
addChild(wrapped)
|
||||||
|
wrapped.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(wrapped.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
wrapped.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
wrapped.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
wrapped.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
wrapped.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
wrapped.didMove(toParent: self)
|
||||||
|
|
||||||
|
container?.galleryContentChanged()
|
||||||
|
} else {
|
||||||
|
showErrorView()
|
||||||
|
}
|
||||||
|
|
||||||
|
container?.setGalleryContentLoading(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func showErrorView() {
|
||||||
|
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
||||||
|
image.tintColor = .secondaryLabel
|
||||||
|
image.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
|
let label = UILabel()
|
||||||
|
label.text = "Error Loading"
|
||||||
|
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
|
label.textColor = .secondaryLabel
|
||||||
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
|
let stackView = UIStackView(arrangedSubviews: [
|
||||||
|
image,
|
||||||
|
label,
|
||||||
|
])
|
||||||
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.alignment = .center
|
||||||
|
stackView.spacing = 8
|
||||||
|
view.addSubview(stackView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
image.widthAnchor.constraint(equalToConstant: 64),
|
||||||
|
image.heightAnchor.constraint(equalToConstant: 64),
|
||||||
|
stackView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
stackView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
//
|
||||||
|
// StatusAttachmentsGalleryDataSource.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/17/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GalleryVC
|
||||||
|
import Pachyderm
|
||||||
|
import TuskerComponents
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
||||||
|
let attachments: [Attachment]
|
||||||
|
let sourceViews: NSHashTable<AttachmentView>
|
||||||
|
|
||||||
|
init(attachments: [Attachment], sourceViews: NSHashTable<AttachmentView>) {
|
||||||
|
self.attachments = attachments
|
||||||
|
self.sourceViews = sourceViews
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryItemsCount() -> Int {
|
||||||
|
attachments.count
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryContentViewController(forItemAt index: Int) -> any GalleryContentViewController {
|
||||||
|
let attachment = attachments[index]
|
||||||
|
switch attachment.kind {
|
||||||
|
case .image:
|
||||||
|
if let view = attachmentView(for: attachment),
|
||||||
|
let image = view.image {
|
||||||
|
return ImageGalleryContentViewController(
|
||||||
|
url: attachment.url,
|
||||||
|
caption: attachment.description,
|
||||||
|
originalData: view.originalData,
|
||||||
|
image: image,
|
||||||
|
// TODO: if automatically play gifs is off, this will start the source view playing too
|
||||||
|
gifController: view.gifController
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return LoadingGalleryContentViewController {
|
||||||
|
let (data, image) = await ImageCache.attachments.get(attachment.url, loadOriginal: true)
|
||||||
|
if let image {
|
||||||
|
let gifController: GIFController? =
|
||||||
|
if attachment.url.pathExtension == "gif",
|
||||||
|
let data {
|
||||||
|
GIFController(gifData: data)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
return ImageGalleryContentViewController(
|
||||||
|
url: attachment.url,
|
||||||
|
caption: attachment.description,
|
||||||
|
originalData: data,
|
||||||
|
image: image,
|
||||||
|
gifController: gifController
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case .gifv:
|
||||||
|
let controller: GifvController =
|
||||||
|
if Preferences.shared.automaticallyPlayGifs,
|
||||||
|
let view = attachmentView(for: attachment),
|
||||||
|
let controller = view.gifvView?.controller {
|
||||||
|
controller
|
||||||
|
} else {
|
||||||
|
GifvController(asset: AVAsset(url: attachment.url))
|
||||||
|
}
|
||||||
|
return GifvGalleryContentViewController(controller: controller, caption: attachment.description)
|
||||||
|
case .video:
|
||||||
|
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
|
case .audio:
|
||||||
|
// TODO: use separate content VC with audio visualization?
|
||||||
|
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
|
case .unknown:
|
||||||
|
return LoadingGalleryContentViewController {
|
||||||
|
do {
|
||||||
|
let (data, _) = try await URLSession.shared.data(from: attachment.url)
|
||||||
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
||||||
|
try data.write(to: url)
|
||||||
|
return FallbackGalleryNavigationController(url: url)
|
||||||
|
} catch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryContentTransitionSourceView(forItemAt index: Int) -> UIView? {
|
||||||
|
let attachment = attachments[index]
|
||||||
|
return attachmentView(for: attachment)
|
||||||
|
}
|
||||||
|
|
||||||
|
func galleryApplicationActivities(forItemAt index: Int) -> [UIActivity]? {
|
||||||
|
[SaveToPhotosActivity()]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func attachmentView(for attachment: Attachment) -> AttachmentView? {
|
||||||
|
return sourceViews.allObjects.first(where: { $0.attachment?.id == attachment.id })
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
//
|
||||||
|
// VideoGalleryContentViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/19/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
|
private let url: URL
|
||||||
|
let caption: String?
|
||||||
|
private let item: AVPlayerItem
|
||||||
|
let player: AVPlayer
|
||||||
|
|
||||||
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
|
private var statusObservation: NSKeyValueObservation?
|
||||||
|
private var isFirstAppearance = true
|
||||||
|
|
||||||
|
init(url: URL, caption: String?) {
|
||||||
|
self.url = url
|
||||||
|
self.caption = caption
|
||||||
|
|
||||||
|
let asset = AVAsset(url: url)
|
||||||
|
self.item = AVPlayerItem(asset: asset)
|
||||||
|
self.player = AVPlayer(playerItem: item)
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
container?.setGalleryContentLoading(true)
|
||||||
|
|
||||||
|
let playerView = PlayerView(item: item, player: player)
|
||||||
|
playerView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(playerView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
playerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
playerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
playerView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
playerView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
||||||
|
MainActor.runUnsafely {
|
||||||
|
self.preferredContentSize = item.presentationSize
|
||||||
|
self.container?.galleryContentChanged()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
|
||||||
|
MainActor.runUnsafely {
|
||||||
|
if item.status == .readyToPlay {
|
||||||
|
self.container?.setGalleryContentLoading(false)
|
||||||
|
statusObservation = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
preferredContentSize = item.presentationSize
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
|
if isFirstAppearance {
|
||||||
|
isFirstAppearance = false
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewWillDisappear(_ animated: Bool) {
|
||||||
|
super.viewWillDisappear(animated)
|
||||||
|
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
|
var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
|
var contentSize: CGSize {
|
||||||
|
item.presentationSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var activityItemsForSharing: [Any] {
|
||||||
|
// TODO: share videos
|
||||||
|
[]
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class PlayerView: UIView {
|
||||||
|
override class var layerClass: AnyClass {
|
||||||
|
AVPlayerLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playerLayer: AVPlayerLayer {
|
||||||
|
layer as! AVPlayerLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
private let player: AVPlayer
|
||||||
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
player.currentItem?.presentationSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(item: AVPlayerItem, player: AVPlayer) {
|
||||||
|
self.player = player
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
playerLayer.player = player
|
||||||
|
playerLayer.videoGravity = .resizeAspect
|
||||||
|
|
||||||
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in
|
||||||
|
MainActor.runUnsafely {
|
||||||
|
self.invalidateIntrinsicContentSize()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,73 +0,0 @@
|
||||||
//
|
|
||||||
// GalleryFallbackViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 5/16/23.
|
|
||||||
// Copyright © 2023 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import QuickLook
|
|
||||||
import Pachyderm
|
|
||||||
|
|
||||||
class GalleryFallbackViewController: QLPreviewController {
|
|
||||||
|
|
||||||
private let attachment: Attachment
|
|
||||||
private let previewItem: GalleryPreviewItem
|
|
||||||
|
|
||||||
private var loadAttachmentTask: Task<Void, Never>?
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
loadAttachmentTask?.cancel()
|
|
||||||
}
|
|
||||||
|
|
||||||
init(attachment: Attachment) {
|
|
||||||
self.attachment = attachment
|
|
||||||
self.previewItem = GalleryPreviewItem()
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
|
|
||||||
dataSource = self
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
|
||||||
super.viewWillAppear(animated)
|
|
||||||
|
|
||||||
if previewItem.previewItemURL == nil,
|
|
||||||
loadAttachmentTask == nil {
|
|
||||||
loadAttachmentTask = Task {
|
|
||||||
do {
|
|
||||||
let (data, _) = try await URLSession.shared.data(from: attachment.url)
|
|
||||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
|
||||||
try data.write(to: url)
|
|
||||||
try Task.checkCancellation()
|
|
||||||
previewItem.previewItemURL = url
|
|
||||||
refreshCurrentPreviewItem()
|
|
||||||
} catch {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GalleryFallbackViewController: QLPreviewControllerDataSource {
|
|
||||||
nonisolated func numberOfPreviewItems(in controller: QLPreviewController) -> Int {
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
nonisolated func previewController(_ controller: QLPreviewController, previewItemAt index: Int) -> QLPreviewItem {
|
|
||||||
return MainActor.runUnsafely {
|
|
||||||
previewItem
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private class GalleryPreviewItem: NSObject, QLPreviewItem {
|
|
||||||
var previewItemURL: URL? = nil
|
|
||||||
}
|
|
|
@ -1,276 +0,0 @@
|
||||||
//
|
|
||||||
// LargeImageContentView.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/17/20.
|
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
@preconcurrency import AVFoundation
|
|
||||||
@preconcurrency import VisionKit
|
|
||||||
import TuskerComponents
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol LargeImageContentView: UIView {
|
|
||||||
var animationImage: UIImage? { get }
|
|
||||||
var activityItemsForSharing: [Any] { get }
|
|
||||||
var owner: LargeImageViewController? { get set }
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool)
|
|
||||||
func grayscaleStateChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
class LargeImageImageContentView: UIImageView, LargeImageContentView {
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private static let analyzer = ImageAnalyzer()
|
|
||||||
private var _analysisInteraction: AnyObject?
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
|
|
||||||
#endif
|
|
||||||
|
|
||||||
var animationImage: UIImage? { image! }
|
|
||||||
var activityItemsForSharing: [Any] {
|
|
||||||
guard let data else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
|
||||||
}
|
|
||||||
weak var owner: LargeImageViewController?
|
|
||||||
|
|
||||||
private let url: URL
|
|
||||||
private let data: Data?
|
|
||||||
|
|
||||||
init(url: URL, data: Data?, image: UIImage) {
|
|
||||||
self.url = url
|
|
||||||
self.data = data
|
|
||||||
|
|
||||||
super.init(image: image)
|
|
||||||
|
|
||||||
contentMode = .scaleAspectFit
|
|
||||||
isUserInteractionEnabled = true
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
ImageAnalyzer.isSupported {
|
|
||||||
let interaction = ImageAnalysisInteraction()
|
|
||||||
self._analysisInteraction = interaction
|
|
||||||
interaction.delegate = self
|
|
||||||
interaction.preferredInteractionTypes = .automatic
|
|
||||||
addInteraction(interaction)
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
let result = try await LargeImageImageContentView.analyzer.analyze(image, configuration: ImageAnalyzer.Configuration([.text, .machineReadableCode]))
|
|
||||||
interaction.analysis = result
|
|
||||||
} catch {
|
|
||||||
// if analysis fails, we just don't show anything
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
if #available(iOS 16.0, *),
|
|
||||||
let analysisInteraction {
|
|
||||||
analysisInteraction.setSupplementaryInterfaceHidden(!controlsVisible, animated: animated)
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
|
||||||
guard let data else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let image: UIImage?
|
|
||||||
if Preferences.shared.grayscaleImages {
|
|
||||||
image = ImageGrayscalifier.convert(url: nil, data: data)
|
|
||||||
} else {
|
|
||||||
image = UIImage(data: data)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let image = image {
|
|
||||||
self.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !targetEnvironment(macCatalyst)
|
|
||||||
@available(iOS 16.0, *)
|
|
||||||
extension LargeImageImageContentView: ImageAnalysisInteractionDelegate {
|
|
||||||
func presentingViewController(for interaction: ImageAnalysisInteraction) -> UIViewController? {
|
|
||||||
return owner
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class LargeImageGifContentView: GIFImageView, LargeImageContentView {
|
|
||||||
var animationImage: UIImage? { image }
|
|
||||||
var activityItemsForSharing: [Any] {
|
|
||||||
[ImageActivityItemSource(data: gifController!.gifData, url: url, image: image)]
|
|
||||||
}
|
|
||||||
weak var owner: LargeImageViewController?
|
|
||||||
|
|
||||||
private let url: URL
|
|
||||||
|
|
||||||
init(url: URL, gifController: GIFController) {
|
|
||||||
self.url = url
|
|
||||||
|
|
||||||
super.init(image: gifController.lastFrame?.image)
|
|
||||||
|
|
||||||
contentMode = .scaleAspectFit
|
|
||||||
|
|
||||||
gifController.attach(to: self)
|
|
||||||
// todo: doing this in the init feels wrong
|
|
||||||
gifController.startAnimating()
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
|
||||||
// todo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LargeImageGifvContentView: GifvAttachmentView, LargeImageContentView {
|
|
||||||
private(set) var animationImage: UIImage?
|
|
||||||
var activityItemsForSharing: [Any] {
|
|
||||||
[GifvActivityItemSource(asset: asset, attachment: attachment)]
|
|
||||||
}
|
|
||||||
weak var owner: LargeImageViewController?
|
|
||||||
|
|
||||||
private let attachment: Attachment
|
|
||||||
private let asset: AVURLAsset
|
|
||||||
|
|
||||||
private var videoSize: CGSize?
|
|
||||||
override var intrinsicContentSize: CGSize {
|
|
||||||
videoSize ?? CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
|
||||||
}
|
|
||||||
|
|
||||||
init(attachment: Attachment, source: UIImageView) {
|
|
||||||
precondition(attachment.kind == .gifv)
|
|
||||||
|
|
||||||
self.attachment = attachment
|
|
||||||
self.asset = AVURLAsset(url: attachment.url)
|
|
||||||
|
|
||||||
super.init(asset: asset, gravity: .resizeAspect)
|
|
||||||
|
|
||||||
self.animationImage = source.image
|
|
||||||
|
|
||||||
self.player.play()
|
|
||||||
|
|
||||||
Task {
|
|
||||||
do {
|
|
||||||
if let track = try await asset.loadTracks(withMediaType: .video).first {
|
|
||||||
let (size, transform) = try await track.load(.naturalSize, .preferredTransform)
|
|
||||||
self.videoSize = size.applying(transform)
|
|
||||||
self.invalidateIntrinsicContentSize()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
|
||||||
}
|
|
||||||
|
|
||||||
func grayscaleStateChanged() {
|
|
||||||
// no-op, GifvAttachmentView observes the grayscale state itself
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
class ImageActivityItemSource: NSObject, UIActivityItemSource {
|
|
||||||
let data: Data
|
|
||||||
let url: URL
|
|
||||||
let image: UIImage?
|
|
||||||
|
|
||||||
init(data: Data, url: URL, image: UIImage?) {
|
|
||||||
self.data = data
|
|
||||||
self.url = url
|
|
||||||
self.image = image
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
|
||||||
return image
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
|
||||||
do {
|
|
||||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(url.lastPathComponent)
|
|
||||||
try data.write(to: tempURL)
|
|
||||||
return tempURL
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
|
||||||
return (UTType(filenameExtension: url.pathExtension) ?? .image).identifier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class GifvActivityItemSource: NSObject, UIActivityItemSource {
|
|
||||||
let asset: AVAsset
|
|
||||||
let attachment: Attachment
|
|
||||||
|
|
||||||
init(asset: AVAsset, attachment: Attachment) {
|
|
||||||
self.asset = asset
|
|
||||||
self.attachment = attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any {
|
|
||||||
return attachment.url
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, thumbnailImageForActivityType activityType: UIActivity.ActivityType?, suggestedSize size: CGSize) -> UIImage? {
|
|
||||||
#if os(visionOS)
|
|
||||||
#warning("Use async AVAssetImageGenerator.image(at:)")
|
|
||||||
return nil
|
|
||||||
#else
|
|
||||||
let generator = AVAssetImageGenerator(asset: self.asset)
|
|
||||||
generator.appliesPreferredTrackTransform = true
|
|
||||||
if let image = try? generator.copyCGImage(at: .zero, actualTime: nil) {
|
|
||||||
return UIImage(cgImage: image)
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
|
||||||
do {
|
|
||||||
let data = try Data(contentsOf: attachment.url)
|
|
||||||
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(attachment.url.lastPathComponent)
|
|
||||||
try data.write(to: tempURL)
|
|
||||||
return tempURL
|
|
||||||
} catch {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func activityViewController(_ activityViewController: UIActivityViewController, dataTypeIdentifierForActivityType activityType: UIActivity.ActivityType?) -> String {
|
|
||||||
return (UTType(filenameExtension: attachment.url.pathExtension) ?? .video).identifier
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,397 +0,0 @@
|
||||||
//
|
|
||||||
// LargeImageViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 8/31/18.
|
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
|
|
||||||
|
|
||||||
weak var animationSourceView: UIImageView?
|
|
||||||
var largeImageController: LargeImageViewController? { self }
|
|
||||||
var animationImage: UIImage? { contentView.animationImage }
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
|
||||||
|
|
||||||
@IBOutlet weak var scrollView: UIScrollView!
|
|
||||||
@IBOutlet weak var topControlsView: UIView!
|
|
||||||
@IBOutlet weak var descriptionTextView: UITextView!
|
|
||||||
|
|
||||||
private var shareContainer: UIView!
|
|
||||||
private var closeContainer: UIView!
|
|
||||||
private var shareImage: UIImageView!
|
|
||||||
private var shareButtonTopConstraint: NSLayoutConstraint!
|
|
||||||
private var shareButtonLeadingConstraint: NSLayoutConstraint!
|
|
||||||
private var closeButtonTopConstraint: NSLayoutConstraint!
|
|
||||||
private var closeButtonTrailingConstraint: NSLayoutConstraint!
|
|
||||||
|
|
||||||
var contentView: LargeImageContentView {
|
|
||||||
didSet {
|
|
||||||
oldValue.removeFromSuperview()
|
|
||||||
setupContentView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var contentViewLeadingConstraint: NSLayoutConstraint!
|
|
||||||
var contentViewTopConstraint: NSLayoutConstraint!
|
|
||||||
|
|
||||||
var imageDescription: String?
|
|
||||||
|
|
||||||
var initialControlsVisible = true
|
|
||||||
private(set) var controlsVisible = true {
|
|
||||||
didSet {
|
|
||||||
setNeedsUpdateOfHomeIndicatorAutoHidden()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var shrinkGestureEnabled = true
|
|
||||||
|
|
||||||
private var isInitialAppearance = true
|
|
||||||
private var skipUpdatingControlsWhileZooming = false
|
|
||||||
private var prevZoomScale: CGFloat?
|
|
||||||
private var isGrayscale = false
|
|
||||||
private var contentViewSizeObservation: NSKeyValueObservation?
|
|
||||||
|
|
||||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
|
||||||
didSet {
|
|
||||||
#if !os(visionOS)
|
|
||||||
setNeedsStatusBarAppearanceUpdate()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
|
||||||
return !isInteractivelyAnimatingDismissal
|
|
||||||
}
|
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
override var prefersHomeIndicatorAutoHidden: Bool {
|
|
||||||
return !controlsVisible
|
|
||||||
}
|
|
||||||
|
|
||||||
init(contentView: LargeImageContentView, description: String?, sourceView: UIImageView?) {
|
|
||||||
self.imageDescription = description
|
|
||||||
self.animationSourceView = sourceView
|
|
||||||
|
|
||||||
self.contentView = contentView
|
|
||||||
|
|
||||||
super.init(nibName: "LargeImageViewController", bundle: nil)
|
|
||||||
|
|
||||||
modalPresentationStyle = .fullScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder aDecoder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
setupContentView()
|
|
||||||
setupControls()
|
|
||||||
|
|
||||||
setControlsVisible(initialControlsVisible, animated: false)
|
|
||||||
if contentView.activityItemsForSharing.isEmpty {
|
|
||||||
shareContainer.isUserInteractionEnabled = false
|
|
||||||
shareImage.tintColor = .systemGray
|
|
||||||
}
|
|
||||||
|
|
||||||
scrollView.delegate = self
|
|
||||||
|
|
||||||
if let imageDescription = imageDescription,
|
|
||||||
!imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
descriptionTextView.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
||||||
descriptionTextView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
|
|
||||||
// i'm not sure why .automatic doesn't work for this
|
|
||||||
descriptionTextView.contentInsetAdjustmentBehavior = .always
|
|
||||||
let height = min(150, descriptionTextView.contentSize.height)
|
|
||||||
descriptionTextView.topAnchor.constraint(equalTo: descriptionTextView.safeAreaLayoutGuide.bottomAnchor, constant: -(height + 16)).isActive = true
|
|
||||||
} else {
|
|
||||||
descriptionTextView.isHidden = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if shrinkGestureEnabled {
|
|
||||||
dismissInteractionController = LargeImageInteractionController(viewController: self)
|
|
||||||
}
|
|
||||||
|
|
||||||
let singleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewPressed(_:)))
|
|
||||||
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
|
|
||||||
doubleTap.numberOfTapsRequired = 2
|
|
||||||
// this requirement is needed to make sure the double tap is ever recognized
|
|
||||||
singleTap.require(toFail: doubleTap)
|
|
||||||
view.addGestureRecognizer(singleTap)
|
|
||||||
view.addGestureRecognizer(doubleTap)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
|
|
||||||
accessibilityElements = [
|
|
||||||
topControlsView!,
|
|
||||||
contentView,
|
|
||||||
descriptionTextView!,
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupContentView() {
|
|
||||||
contentView.owner = self
|
|
||||||
contentView.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
scrollView.addSubview(contentView)
|
|
||||||
contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
|
|
||||||
contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
contentViewLeadingConstraint,
|
|
||||||
contentViewTopConstraint,
|
|
||||||
])
|
|
||||||
contentViewSizeObservation = (contentView as UIView).observe(\.bounds, changeHandler: { [unowned self] _, _ in
|
|
||||||
MainActor.runUnsafely {
|
|
||||||
self.centerImage()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setupControls() {
|
|
||||||
shareContainer = UIView()
|
|
||||||
shareContainer.isAccessibilityElement = true
|
|
||||||
shareContainer.accessibilityTraits = .button
|
|
||||||
shareContainer.accessibilityLabel = "Share"
|
|
||||||
shareContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(sharePressed)))
|
|
||||||
shareContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
topControlsView.addSubview(shareContainer)
|
|
||||||
shareImage = UIImageView(image: UIImage(systemName: "square.and.arrow.up"))
|
|
||||||
shareImage.tintColor = .white
|
|
||||||
shareImage.contentMode = .scaleAspectFit
|
|
||||||
shareImage.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
shareContainer.addSubview(shareImage)
|
|
||||||
shareButtonTopConstraint = shareImage.topAnchor.constraint(greaterThanOrEqualTo: shareContainer.topAnchor)
|
|
||||||
shareButtonLeadingConstraint = shareImage.leadingAnchor.constraint(greaterThanOrEqualTo: shareContainer.leadingAnchor)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
shareContainer.topAnchor.constraint(equalTo: topControlsView.topAnchor),
|
|
||||||
shareContainer.leadingAnchor.constraint(equalTo: topControlsView.leadingAnchor),
|
|
||||||
shareContainer.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
|
|
||||||
shareContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
|
||||||
shareContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
|
||||||
|
|
||||||
shareImage.centerXAnchor.constraint(equalTo: shareContainer.centerXAnchor),
|
|
||||||
shareImage.centerYAnchor.constraint(equalTo: shareContainer.centerYAnchor),
|
|
||||||
shareButtonTopConstraint,
|
|
||||||
shareButtonLeadingConstraint,
|
|
||||||
|
|
||||||
shareImage.widthAnchor.constraint(equalToConstant: 24),
|
|
||||||
shareImage.heightAnchor.constraint(equalToConstant: 24),
|
|
||||||
])
|
|
||||||
|
|
||||||
closeContainer = UIView()
|
|
||||||
closeContainer.isAccessibilityElement = true
|
|
||||||
closeContainer.accessibilityTraits = .button
|
|
||||||
closeContainer.accessibilityLabel = "Close"
|
|
||||||
closeContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(closeButtonPressed)))
|
|
||||||
closeContainer.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
topControlsView.addSubview(closeContainer)
|
|
||||||
let closeImage = UIImageView(image: UIImage(systemName: "xmark"))
|
|
||||||
closeImage.tintColor = .white
|
|
||||||
closeImage.contentMode = .scaleAspectFit
|
|
||||||
closeImage.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
closeContainer.addSubview(closeImage)
|
|
||||||
closeButtonTopConstraint = closeImage.topAnchor.constraint(greaterThanOrEqualTo: closeContainer.topAnchor)
|
|
||||||
closeButtonTrailingConstraint = closeContainer.trailingAnchor.constraint(greaterThanOrEqualTo: closeImage.trailingAnchor)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
closeContainer.topAnchor.constraint(equalTo: topControlsView.topAnchor),
|
|
||||||
closeContainer.trailingAnchor.constraint(equalTo: topControlsView.trailingAnchor),
|
|
||||||
closeContainer.bottomAnchor.constraint(equalTo: closeContainer.bottomAnchor),
|
|
||||||
closeContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
|
||||||
closeContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
|
|
||||||
|
|
||||||
closeImage.centerXAnchor.constraint(equalTo: closeContainer.centerXAnchor),
|
|
||||||
closeImage.centerYAnchor.constraint(equalTo: closeContainer.centerYAnchor),
|
|
||||||
closeButtonTopConstraint,
|
|
||||||
closeButtonTrailingConstraint,
|
|
||||||
|
|
||||||
closeImage.widthAnchor.constraint(equalToConstant: 24),
|
|
||||||
closeImage.heightAnchor.constraint(equalToConstant: 24),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLayoutSubviews() {
|
|
||||||
super.viewDidLayoutSubviews()
|
|
||||||
|
|
||||||
// limit the image height to the safe area height, so the image doesn't overlap the top controls
|
|
||||||
// while zoomed all the way out
|
|
||||||
let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
|
|
||||||
let heightScale = maxHeight / contentView.intrinsicContentSize.height
|
|
||||||
let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
|
|
||||||
let minScale = min(widthScale, heightScale)
|
|
||||||
skipUpdatingControlsWhileZooming = true
|
|
||||||
scrollView.minimumZoomScale = minScale
|
|
||||||
scrollView.zoomScale = minScale
|
|
||||||
scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
|
|
||||||
skipUpdatingControlsWhileZooming = false
|
|
||||||
|
|
||||||
centerImage()
|
|
||||||
|
|
||||||
let notchedDeviceTopInsets: [CGFloat] = [
|
|
||||||
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
|
||||||
48, // iPhone XR, 11
|
|
||||||
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
|
||||||
50, // iPhone 12 mini, 13 mini
|
|
||||||
]
|
|
||||||
let pillDeviceTopInsets: [CGFloat] = [
|
|
||||||
59, // iPhone 14 Pro, 14 Pro Max
|
|
||||||
]
|
|
||||||
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
|
||||||
// the notch width is not the same for the iPhones 13,
|
|
||||||
// but what we actually want is the same offset from the edges
|
|
||||||
// since the corner radius didn't change
|
|
||||||
let notchWidth: CGFloat = 210
|
|
||||||
let earWidth = (view.bounds.width - notchWidth) / 2
|
|
||||||
let offset = (earWidth - shareImage.bounds.width) / 2
|
|
||||||
shareButtonLeadingConstraint.constant = offset
|
|
||||||
closeButtonTrailingConstraint.constant = offset
|
|
||||||
} else if pillDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
|
||||||
shareButtonLeadingConstraint.constant = 24
|
|
||||||
shareButtonTopConstraint.constant = 24
|
|
||||||
closeButtonTrailingConstraint.constant = 24
|
|
||||||
closeButtonTopConstraint.constant = 24
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewSafeAreaInsetsDidChange() {
|
|
||||||
super.viewSafeAreaInsetsDidChange()
|
|
||||||
// the controls view transforms take the safe area insets into account, so they need to be updated
|
|
||||||
updateControlsView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
|
||||||
super.viewDidAppear(animated)
|
|
||||||
|
|
||||||
// on the first appearance, the text view flashes its own scroll indicators automatically
|
|
||||||
// so we only need to do it on subsequent appearances
|
|
||||||
if isInitialAppearance {
|
|
||||||
isInitialAppearance = false
|
|
||||||
} else {
|
|
||||||
if animated && controlsVisible && !descriptionTextView.isHidden {
|
|
||||||
descriptionTextView.flashScrollIndicators()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
contentView.grayscaleStateChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
|
|
||||||
self.controlsVisible = controlsVisible
|
|
||||||
if animated {
|
|
||||||
UIView.animate(withDuration: 0.2) {
|
|
||||||
// note: the value of animated: is passed to ImageAnalysisInteractino.setSupplementaryInterfaceHidden which (as of iOS 17.0.2 (21A350)):
|
|
||||||
// - does not animate with animated:false when wrapped in a UIView.animate block
|
|
||||||
// - does not animate with animated:true unless also wrapped in a UIView.animate block
|
|
||||||
self.contentView.setControlsVisible(controlsVisible, animated: true)
|
|
||||||
self.updateControlsView()
|
|
||||||
}
|
|
||||||
if controlsVisible && !descriptionTextView.isHidden {
|
|
||||||
descriptionTextView.flashScrollIndicators()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
contentView.setControlsVisible(controlsVisible, animated: false)
|
|
||||||
updateControlsView()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateControlsView() {
|
|
||||||
let topOffset = self.controlsVisible ? 0 : -(self.topControlsView.bounds.height + self.view.safeAreaInsets.top)
|
|
||||||
self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset)
|
|
||||||
if self.imageDescription != nil {
|
|
||||||
let bottomOffset = self.controlsVisible ? 0 : self.descriptionTextView.bounds.height + self.view.safeAreaInsets.bottom
|
|
||||||
self.descriptionTextView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
|
||||||
return contentView
|
|
||||||
}
|
|
||||||
|
|
||||||
func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
|
||||||
let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale
|
|
||||||
if !skipUpdatingControlsWhileZooming {
|
|
||||||
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
|
||||||
setControlsVisible(true, animated: true)
|
|
||||||
} else if scrollView.zoomScale > prevZoomScale {
|
|
||||||
setControlsVisible(false, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.prevZoomScale = scrollView.zoomScale
|
|
||||||
}
|
|
||||||
|
|
||||||
func centerImage() {
|
|
||||||
let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2)
|
|
||||||
contentViewTopConstraint.constant = yOffset
|
|
||||||
|
|
||||||
let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2)
|
|
||||||
contentViewLeadingConstraint.constant = xOffset
|
|
||||||
}
|
|
||||||
|
|
||||||
func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
|
|
||||||
var zoomRect = CGRect.zero
|
|
||||||
zoomRect.size.width = contentView.frame.width / scale
|
|
||||||
zoomRect.size.height = contentView.frame.height / scale
|
|
||||||
let newCenter = scrollView.convert(center, to: contentView)
|
|
||||||
zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
|
|
||||||
zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
|
|
||||||
return zoomRect
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Interaction
|
|
||||||
|
|
||||||
func animateZoomOut() {
|
|
||||||
UIView.animate(withDuration: 0.3, animations: {
|
|
||||||
self.scrollView.zoomScale = self.scrollView.minimumZoomScale
|
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func scrollViewPressed(_ sender: UITapGestureRecognizer) {
|
|
||||||
if scrollView.zoomScale > scrollView.minimumZoomScale {
|
|
||||||
animateZoomOut()
|
|
||||||
} else {
|
|
||||||
setControlsVisible(!controlsVisible, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func scrollViewDoubleTapped(_ recognizer: UITapGestureRecognizer) {
|
|
||||||
if scrollView.zoomScale <= scrollView.minimumZoomScale {
|
|
||||||
let point = recognizer.location(in: recognizer.view)
|
|
||||||
let scale: CGFloat
|
|
||||||
if scrollView.minimumZoomScale < 1 {
|
|
||||||
if 1 - scrollView.zoomScale <= 0.5 {
|
|
||||||
scale = scrollView.zoomScale + 1
|
|
||||||
} else {
|
|
||||||
scale = 1
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
scale = scrollView.maximumZoomScale
|
|
||||||
}
|
|
||||||
let rect = zoomRectFor(scale: scale, center: point)
|
|
||||||
UIView.animate(withDuration: 0.3) {
|
|
||||||
self.scrollView.zoom(to: rect, animated: false)
|
|
||||||
self.view.layoutIfNeeded()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
animateZoomOut()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func closeButtonPressed(_ sender: Any) {
|
|
||||||
dismiss(animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
@IBAction func sharePressed(_ sender: Any) {
|
|
||||||
let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: [SaveToPhotosActivity()])
|
|
||||||
activityVC.popoverPresentationController?.sourceView = shareImage
|
|
||||||
present(activityVC, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,61 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
|
||||||
<dependencies>
|
|
||||||
<deployment identifier="iOS"/>
|
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
|
||||||
</dependencies>
|
|
||||||
<objects>
|
|
||||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="LargeImageViewController" customModule="Tusker" customModuleProvider="target">
|
|
||||||
<connections>
|
|
||||||
<outlet property="descriptionTextView" destination="JZk-BO-2Vh" id="cby-Hc-ezg"/>
|
|
||||||
<outlet property="scrollView" destination="Skj-xq-AgQ" id="TFb-zF-m1b"/>
|
|
||||||
<outlet property="topControlsView" destination="kHo-B9-R7a" id="8sJ-xQ-7ix"/>
|
|
||||||
<outlet property="view" destination="BJw-5C-9nT" id="1C2-VA-mNf"/>
|
|
||||||
</connections>
|
|
||||||
</placeholder>
|
|
||||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
|
||||||
<view contentMode="scaleToFill" id="BJw-5C-9nT">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
|
||||||
<subviews>
|
|
||||||
<scrollView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" minimumZoomScale="0.25" maximumZoomScale="2" translatesAutoresizingMaskIntoConstraints="NO" id="Skj-xq-AgQ">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
|
||||||
<gestureRecognizers/>
|
|
||||||
</scrollView>
|
|
||||||
<view contentMode="scaleToFill" ambiguous="YES" translatesAutoresizingMaskIntoConstraints="NO" id="kHo-B9-R7a">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="36"/>
|
|
||||||
</view>
|
|
||||||
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" editable="NO" textAlignment="natural" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="JZk-BO-2Vh">
|
|
||||||
<rect key="frame" x="0.0" y="517" width="375" height="150"/>
|
|
||||||
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.5" colorSpace="custom" customColorSpace="displayP3"/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstAttribute="height" constant="150" placeholder="YES" id="YfV-kQ-0RT"/>
|
|
||||||
</constraints>
|
|
||||||
<string key="text">Lorem ipsum dolor sit er elit lamet, consectetaur cillium adipisicing pecu, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Nam liber te conscient to factor tum poen legum odioque civiuda.</string>
|
|
||||||
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
|
||||||
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
|
|
||||||
</textView>
|
|
||||||
</subviews>
|
|
||||||
<viewLayoutGuide key="safeArea" id="w1g-VC-Ll9"/>
|
|
||||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
<gestureRecognizers/>
|
|
||||||
<constraints>
|
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerY" secondItem="BJw-5C-9nT" secondAttribute="centerY" id="0Xb-ib-2hg"/>
|
|
||||||
<constraint firstAttribute="bottom" secondItem="JZk-BO-2Vh" secondAttribute="bottom" id="7Z2-gW-sPj"/>
|
|
||||||
<constraint firstItem="kHo-B9-R7a" firstAttribute="leading" secondItem="w1g-VC-Ll9" secondAttribute="leading" id="IvH-gU-Kie"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="JZk-BO-2Vh" secondAttribute="trailing" id="JgV-jy-qjS"/>
|
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="centerX" secondItem="BJw-5C-9nT" secondAttribute="centerX" id="KMe-Zc-NZq"/>
|
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="width" secondItem="BJw-5C-9nT" secondAttribute="width" id="Onj-l9-fBu"/>
|
|
||||||
<constraint firstItem="w1g-VC-Ll9" firstAttribute="trailing" secondItem="kHo-B9-R7a" secondAttribute="trailing" id="Uh0-ub-R9V"/>
|
|
||||||
<constraint firstItem="Skj-xq-AgQ" firstAttribute="height" secondItem="BJw-5C-9nT" secondAttribute="height" id="jvz-QW-n9c"/>
|
|
||||||
<constraint firstItem="JZk-BO-2Vh" firstAttribute="leading" secondItem="BJw-5C-9nT" secondAttribute="leading" id="kkj-O9-1rE"/>
|
|
||||||
<constraint firstItem="kHo-B9-R7a" firstAttribute="top" secondItem="BJw-5C-9nT" secondAttribute="top" id="n1O-C3-yQR"/>
|
|
||||||
</constraints>
|
|
||||||
<point key="canvasLocation" x="-164" y="475.41229385307349"/>
|
|
||||||
</view>
|
|
||||||
</objects>
|
|
||||||
</document>
|
|
|
@ -1,176 +0,0 @@
|
||||||
// LoadingLargeImageViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 6/14/19.
|
|
||||||
// Copyright © 2019 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import Pachyderm
|
|
||||||
import TuskerComponents
|
|
||||||
|
|
||||||
class LoadingLargeImageViewController: UIViewController, LargeImageAnimatableViewController {
|
|
||||||
|
|
||||||
private var attachment: Attachment?
|
|
||||||
let url: URL
|
|
||||||
let cache: ImageCache
|
|
||||||
let imageDescription: String?
|
|
||||||
|
|
||||||
private(set) var loaded = false
|
|
||||||
private(set) var largeImageVC: LargeImageViewController?
|
|
||||||
private var loadingVC: LoadingViewController?
|
|
||||||
|
|
||||||
private var imageRequest: ImageCache.Request?
|
|
||||||
|
|
||||||
private var initialControlsVisible: Bool = true
|
|
||||||
var controlsVisible: Bool {
|
|
||||||
get {
|
|
||||||
return largeImageVC?.controlsVisible ?? initialControlsVisible
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if let largeImageVC = largeImageVC {
|
|
||||||
largeImageVC.setControlsVisible(newValue, animated: false)
|
|
||||||
} else {
|
|
||||||
initialControlsVisible = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var shrinkGestureEnabled = true
|
|
||||||
|
|
||||||
weak var animationSourceView: UIImageView?
|
|
||||||
var largeImageController: LargeImageViewController? { largeImageVC }
|
|
||||||
var animationImage: UIImage? { largeImageVC?.animationImage ?? animationSourceView?.image }
|
|
||||||
var dismissInteractionController: LargeImageInteractionController?
|
|
||||||
|
|
||||||
var isInteractivelyAnimatingDismissal: Bool = false {
|
|
||||||
didSet {
|
|
||||||
#if !os(visionOS)
|
|
||||||
setNeedsStatusBarAppearanceUpdate()
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var prefersStatusBarHidden: Bool {
|
|
||||||
return !isInteractivelyAnimatingDismissal
|
|
||||||
}
|
|
||||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
override var childForHomeIndicatorAutoHidden: UIViewController? {
|
|
||||||
return largeImageVC
|
|
||||||
}
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
|
||||||
return .allButUpsideDown
|
|
||||||
} else {
|
|
||||||
return .all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(url: URL, cache: ImageCache, imageDescription: String?) {
|
|
||||||
self.url = url
|
|
||||||
self.cache = cache
|
|
||||||
self.imageDescription = imageDescription
|
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
|
||||||
|
|
||||||
modalPresentationStyle = .fullScreen
|
|
||||||
}
|
|
||||||
|
|
||||||
convenience init(attachment: Attachment) {
|
|
||||||
self.init(url: attachment.url, cache: .attachments, imageDescription: attachment.description)
|
|
||||||
self.attachment = attachment
|
|
||||||
}
|
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
|
||||||
|
|
||||||
overrideUserInterfaceStyle = .dark
|
|
||||||
view.backgroundColor = .black
|
|
||||||
|
|
||||||
// always load full resolution from disk for large image, in case the cache is scaled
|
|
||||||
if let entry = cache.get(url, loadOriginal: true) {
|
|
||||||
// todo: if load original is true, is there any way entry.data could be nil?
|
|
||||||
// feels like the data param of createLargeImage shouldn't be optional
|
|
||||||
createLargeImage(data: entry.data, image: entry.image, url: url)
|
|
||||||
} else {
|
|
||||||
createPreview()
|
|
||||||
|
|
||||||
loadingVC = LoadingViewController()
|
|
||||||
embedChild(loadingVC!)
|
|
||||||
imageRequest = cache.get(url, loadOriginal: true) { [weak self] (data, image) in
|
|
||||||
guard let self = self, let image = image else { return }
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.imageRequest = nil
|
|
||||||
self.loadingVC?.removeViewAndController()
|
|
||||||
self.createLargeImage(data: data, image: image, url: self.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if shrinkGestureEnabled {
|
|
||||||
dismissInteractionController = LargeImageInteractionController(viewController: self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func didMove(toParent parent: UIViewController?) {
|
|
||||||
super.didMove(toParent: parent)
|
|
||||||
|
|
||||||
if parent == nil {
|
|
||||||
imageRequest?.cancel()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createLargeImage(data: Data?, image: UIImage, url: URL) {
|
|
||||||
guard !loaded else { return }
|
|
||||||
loaded = true
|
|
||||||
|
|
||||||
let content: LargeImageContentView
|
|
||||||
|
|
||||||
// todo: p sure grayscaling gifs has never worked
|
|
||||||
if url.pathExtension == "gif", let data = data {
|
|
||||||
// todo: pulling the gif controller out of the source view feels icky
|
|
||||||
// is it possible for the source view's gif controller to have different data than we just got?
|
|
||||||
// should this be a property set by the animation controller instead?
|
|
||||||
let gifController = (animationSourceView as? GIFImageView)?.gifController ?? GIFController(gifData: data)
|
|
||||||
content = LargeImageGifContentView(url: url, gifController: gifController)
|
|
||||||
} else {
|
|
||||||
if let transformedImage = ImageGrayscalifier.convertIfNecessary(url: url, image: image) {
|
|
||||||
content = LargeImageImageContentView(url: url, data: data, image: transformedImage)
|
|
||||||
} else {
|
|
||||||
content = LargeImageImageContentView(url: url, data: data, image: image)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setContent(content)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func setContent(_ content: LargeImageContentView) {
|
|
||||||
if let existing = largeImageVC {
|
|
||||||
existing.contentView = content
|
|
||||||
} else {
|
|
||||||
largeImageVC = LargeImageViewController(contentView: content, description: imageDescription, sourceView: animationSourceView)
|
|
||||||
largeImageVC!.initialControlsVisible = initialControlsVisible
|
|
||||||
largeImageVC!.shrinkGestureEnabled = false
|
|
||||||
embedChild(largeImageVC!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func createPreview() {
|
|
||||||
guard !self.loaded,
|
|
||||||
var image = animationSourceView?.image else { return }
|
|
||||||
|
|
||||||
if Preferences.shared.grayscaleImages,
|
|
||||||
let source = image.cgImage,
|
|
||||||
let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: source) {
|
|
||||||
image = grayscale
|
|
||||||
}
|
|
||||||
setContent(LargeImageImageContentView(url: url, data: nil, image: image))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,141 +0,0 @@
|
||||||
//
|
|
||||||
// LargeImageExpandAnimationController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/1/18.
|
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import TuskerComponents
|
|
||||||
|
|
||||||
@MainActor
|
|
||||||
protocol LargeImageAnimatableViewController: UIViewController {
|
|
||||||
var animationSourceView: UIImageView? { get }
|
|
||||||
var largeImageController: LargeImageViewController? { get }
|
|
||||||
var animationImage: UIImage? { get }
|
|
||||||
var dismissInteractionController: LargeImageInteractionController? { get }
|
|
||||||
var isInteractivelyAnimatingDismissal: Bool { get set }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension LargeImageAnimatableViewController {
|
|
||||||
func sourceViewFrame(in coordinateSpace: UIView) -> CGRect? {
|
|
||||||
guard let sourceView = animationSourceView else { return nil }
|
|
||||||
|
|
||||||
var sourceFrame = sourceView.convert(sourceView.bounds, to: coordinateSpace)
|
|
||||||
if let scrollView = coordinateSpace as? UIScrollView {
|
|
||||||
let scale = scrollView.zoomScale
|
|
||||||
let width = sourceFrame.width * scale
|
|
||||||
let height = sourceFrame.height * scale
|
|
||||||
let x = sourceFrame.minX * scale - scrollView.contentOffset.x + scrollView.frame.minX
|
|
||||||
let y = sourceFrame.minY * scale - scrollView.contentOffset.y + scrollView.frame.minY
|
|
||||||
sourceFrame = CGRect(x: x, y: y, width: width, height: height)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sourceFrame
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class LargeImageExpandAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitions {
|
|
||||||
return 0.2
|
|
||||||
} else {
|
|
||||||
return 0.4
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from),
|
|
||||||
let toVC = transitionContext.viewController(forKey: .to) as? LargeImageAnimatableViewController else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitions {
|
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let containerView = transitionContext.containerView
|
|
||||||
containerView.addSubview(toVC.view)
|
|
||||||
|
|
||||||
let finalVCFrame = transitionContext.finalFrame(for: toVC)
|
|
||||||
guard let sourceView = toVC.animationSourceView,
|
|
||||||
let sourceFrame = toVC.sourceViewFrame(in: fromVC.view),
|
|
||||||
let image = toVC.animationImage else {
|
|
||||||
toVC.view.frame = finalVCFrame
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// use alpha, because isHidden makes stack views re-layout
|
|
||||||
sourceView.alpha = 0
|
|
||||||
toVC.view.alpha = 0
|
|
||||||
toVC.largeImageController?.contentView.isHidden = true
|
|
||||||
toVC.largeImageController?.setControlsVisible(false, animated: false)
|
|
||||||
|
|
||||||
var finalFrameSize = finalVCFrame.inset(by: toVC.view.safeAreaInsets).size
|
|
||||||
let newWidth = finalFrameSize.width / image.size.width
|
|
||||||
let newHeight = finalFrameSize.height / image.size.height
|
|
||||||
if newHeight < newWidth {
|
|
||||||
finalFrameSize.width = newHeight * image.size.width
|
|
||||||
} else {
|
|
||||||
finalFrameSize.height = newWidth * image.size.height
|
|
||||||
}
|
|
||||||
let finalFrame = CGRect(origin: CGPoint(x: finalVCFrame.midX - finalFrameSize.width / 2, y: finalVCFrame.midY - finalFrameSize.height / 2), size: finalFrameSize)
|
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: sourceFrame)
|
|
||||||
imageView.image = image
|
|
||||||
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
|
||||||
gifController.attach(to: imageView)
|
|
||||||
}
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.layer.cornerRadius = sourceView.layer.cornerRadius
|
|
||||||
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
|
|
||||||
containerView.addSubview(imageView)
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
let velocity = 1 / CGFloat(duration)
|
|
||||||
UIView.animate(withDuration: duration, delay: 0, usingSpringWithDamping: 0.75, initialSpringVelocity: velocity, options: []) {
|
|
||||||
imageView.frame = finalFrame
|
|
||||||
imageView.layer.cornerRadius = 0
|
|
||||||
toVC.view.alpha = 1
|
|
||||||
toVC.largeImageController?.setControlsVisible(true, animated: false)
|
|
||||||
} completion: { (_) in
|
|
||||||
// This shouldn't be necessary. I believe it's a workaround for using a XIB
|
|
||||||
// for the large image VC. Without this, the final frame of the large image VC
|
|
||||||
// is not set to the propper rect (it uses the frame of the preview device
|
|
||||||
// in the XIB). When using a storyboard, the final frame is automatically set
|
|
||||||
// (or UIKit does layout differently when loading the view) and this is not necessary.
|
|
||||||
toVC.view.frame = finalVCFrame
|
|
||||||
|
|
||||||
toVC.largeImageController?.contentView.isHidden = false
|
|
||||||
fromVC.view.isHidden = false
|
|
||||||
imageView.removeFromSuperview()
|
|
||||||
|
|
||||||
sourceView.alpha = 1
|
|
||||||
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let toVC = transitionContext.viewController(forKey: .to) as? LargeImageAnimatableViewController else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
transitionContext.containerView.addSubview(toVC.view)
|
|
||||||
toVC.view.alpha = 0
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
UIView.animate(withDuration: duration) {
|
|
||||||
toVC.view.alpha = 1
|
|
||||||
} completion: { (_) in
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,76 +0,0 @@
|
||||||
//
|
|
||||||
// LargeImageInteractionController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/1/18.
|
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
class LargeImageInteractionController: UIPercentDrivenInteractiveTransition {
|
|
||||||
|
|
||||||
var inProgress = false
|
|
||||||
var direction: CGFloat?
|
|
||||||
|
|
||||||
var shouldCompleteTransition = false
|
|
||||||
private weak var viewController: LargeImageAnimatableViewController!
|
|
||||||
|
|
||||||
init(viewController: LargeImageAnimatableViewController) {
|
|
||||||
super.init()
|
|
||||||
self.viewController = viewController
|
|
||||||
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(handleGesture(_:)))
|
|
||||||
panRecognizer.allowedScrollTypesMask = .continuous
|
|
||||||
viewController.view.addGestureRecognizer(panRecognizer)
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func handleGesture(_ recognizer: UIPanGestureRecognizer) {
|
|
||||||
guard let recognizerSuperview = recognizer.view?.superview else {
|
|
||||||
// Assume the gesture has ended b/c we don't have a view/superview anymore.
|
|
||||||
inProgress = false
|
|
||||||
direction = nil
|
|
||||||
cancel()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let translation = recognizer.translation(in: recognizerSuperview)
|
|
||||||
var progress = translation.y / 200
|
|
||||||
if let direction = direction {
|
|
||||||
progress *= direction
|
|
||||||
} else if abs(progress) > 0.01 {
|
|
||||||
// if the progress is less than +/- 1%, don't set the direction because the translation may be random jitter in the user's finger
|
|
||||||
direction = progress > 0 ? 1 : -1
|
|
||||||
progress = abs(progress)
|
|
||||||
}
|
|
||||||
progress = min(max(progress, 0), 1)
|
|
||||||
let velocity = abs(recognizer.velocity(in: recognizer.view!.superview!).y)
|
|
||||||
|
|
||||||
switch recognizer.state {
|
|
||||||
case .began:
|
|
||||||
inProgress = true
|
|
||||||
viewController.dismiss(animated: true)
|
|
||||||
case .changed:
|
|
||||||
shouldCompleteTransition = progress > 0.5 || velocity > 1000
|
|
||||||
viewController.isInteractivelyAnimatingDismissal = progress > 0.1
|
|
||||||
update(progress)
|
|
||||||
case .cancelled:
|
|
||||||
inProgress = false
|
|
||||||
cancel()
|
|
||||||
case .ended:
|
|
||||||
inProgress = false
|
|
||||||
direction = nil
|
|
||||||
if shouldCompleteTransition {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
cancel()
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func cancel() {
|
|
||||||
super.cancel()
|
|
||||||
viewController?.isInteractivelyAnimatingDismissal = false
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -1,122 +0,0 @@
|
||||||
//
|
|
||||||
// LargeImageShrinkAnimationController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 9/1/18.
|
|
||||||
// Copyright © 2018 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import TuskerComponents
|
|
||||||
|
|
||||||
class LargeImageShrinkAnimationController: NSObject, UIViewControllerAnimatedTransitioning {
|
|
||||||
|
|
||||||
let interactionController: LargeImageInteractionController?
|
|
||||||
|
|
||||||
init(interactionController: LargeImageInteractionController?) {
|
|
||||||
self.interactionController = interactionController
|
|
||||||
}
|
|
||||||
|
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
|
||||||
return 0.2
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageAnimatableViewController,
|
|
||||||
let toVC = transitionContext.viewController(forKey: .to) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if UIAccessibility.prefersCrossFadeTransitions && !transitionContext.isInteractive {
|
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let sourceView = fromVC.animationSourceView,
|
|
||||||
let sourceFrame = fromVC.sourceViewFrame(in: toVC.view),
|
|
||||||
let image = fromVC.animationImage else {
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// use alpha, becaus isHidden makes stack views re-layout
|
|
||||||
sourceView.alpha = 0
|
|
||||||
|
|
||||||
let containerView = transitionContext.containerView
|
|
||||||
|
|
||||||
let originalVCFrame = fromVC.view.frame
|
|
||||||
var originalFrameSize = originalVCFrame.inset(by: fromVC.view.safeAreaInsets).size
|
|
||||||
let newWidth = originalFrameSize.width / image.size.width
|
|
||||||
let newHeight = originalFrameSize.height / image.size.height
|
|
||||||
if newHeight < newWidth {
|
|
||||||
originalFrameSize.width = newHeight * image.size.width
|
|
||||||
} else {
|
|
||||||
originalFrameSize.height = newWidth * image.size.height
|
|
||||||
}
|
|
||||||
let originalFrame = CGRect(origin: CGPoint(x: originalVCFrame.midX - originalFrameSize.width / 2, y: originalVCFrame.midY - originalFrameSize.height / 2), size: originalFrameSize)
|
|
||||||
|
|
||||||
let imageView = GIFImageView(frame: originalFrame)
|
|
||||||
imageView.image = image
|
|
||||||
if let gifController = (sourceView as? GIFImageView)?.gifController {
|
|
||||||
gifController.attach(to: imageView)
|
|
||||||
// todo: this might not be necessary, the large image content view should have started it
|
|
||||||
gifController.startAnimating()
|
|
||||||
}
|
|
||||||
imageView.contentMode = .scaleAspectFill
|
|
||||||
imageView.layer.cornerRadius = 0
|
|
||||||
imageView.layer.masksToBounds = true
|
|
||||||
|
|
||||||
let blackView = UIView(frame: originalVCFrame)
|
|
||||||
blackView.backgroundColor = .black
|
|
||||||
blackView.alpha = 1
|
|
||||||
|
|
||||||
// Save the old superview of toVC so we can move it back after the animation.
|
|
||||||
// It would be better to just not move toVC and instead remove from fromVC from the
|
|
||||||
// containerView so that toVC is visible behind it, but that doens't work for some reason
|
|
||||||
// (the entire screen just goes black, not even the contents of the containerView appear).
|
|
||||||
let oldSuperview = toVC.view.superview
|
|
||||||
|
|
||||||
containerView.addSubview(toVC.view)
|
|
||||||
containerView.addSubview(blackView)
|
|
||||||
containerView.addSubview(imageView)
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
UIView.animate(withDuration: duration, animations: {
|
|
||||||
imageView.frame = sourceFrame
|
|
||||||
imageView.layer.cornerRadius = sourceView.layer.cornerRadius
|
|
||||||
imageView.layer.maskedCorners = sourceView.layer.maskedCorners
|
|
||||||
blackView.alpha = 0
|
|
||||||
}, completion: { _ in
|
|
||||||
blackView.removeFromSuperview()
|
|
||||||
imageView.removeFromSuperview()
|
|
||||||
if transitionContext.transitionWasCancelled {
|
|
||||||
toVC.view.removeFromSuperview()
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceView.alpha = 1
|
|
||||||
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
|
|
||||||
// move the toVC back to the view that it was in before
|
|
||||||
oldSuperview?.addSubview(toVC.view)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func animateCrossFadeTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
|
||||||
guard let fromVC = transitionContext.viewController(forKey: .from) as? LargeImageAnimatableViewController,
|
|
||||||
let toVC = transitionContext.viewController(forKey: .to) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
transitionContext.containerView.addSubview(toVC.view)
|
|
||||||
transitionContext.containerView.addSubview(fromVC.view)
|
|
||||||
|
|
||||||
let duration = transitionDuration(using: transitionContext)
|
|
||||||
UIView.animate(withDuration: duration) {
|
|
||||||
fromVC.view.alpha = 0
|
|
||||||
} completion: { (_) in
|
|
||||||
transitionContext.completeTransition(!transitionContext.transitionWasCancelled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol StatusEditCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate {
|
protocol StatusEditCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate {
|
||||||
|
@ -234,16 +235,10 @@ class StatusEditCollectionViewCell: UICollectionViewListCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusEditCollectionViewCell: AttachmentViewDelegate {
|
extension StatusEditCollectionViewCell: AttachmentViewDelegate {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
|
func attachmentViewGallery(startingAt index: Int) -> UIViewController? {
|
||||||
guard let delegate else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let attachments = attachmentsView.attachments!
|
let attachments = attachmentsView.attachments!
|
||||||
let sourceViews = attachments.map {
|
let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView>
|
||||||
attachmentsView.getAttachmentView(for: $0)
|
return GalleryVC.GalleryViewController(dataSource: StatusAttachmentsGalleryDataSource(attachments: attachments, sourceViews: sourceViews), initialItemIndex: index)
|
||||||
}
|
|
||||||
let gallery = delegate.gallery(attachments: attachments, sourceViews: sourceViews, startIndex: index)
|
|
||||||
return gallery
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||||
|
|
|
@ -474,7 +474,6 @@ class CustomAlertActionButton: UIControl {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
|
||||||
extension CustomAlertController: UIViewControllerTransitioningDelegate {
|
extension CustomAlertController: UIViewControllerTransitioningDelegate {
|
||||||
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||||
return CustomAlertPresentationAnimation()
|
return CustomAlertPresentationAnimation()
|
||||||
|
@ -484,17 +483,6 @@ extension CustomAlertController: UIViewControllerTransitioningDelegate {
|
||||||
return CustomAlertDismissAnimation()
|
return CustomAlertDismissAnimation()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#else
|
|
||||||
extension CustomAlertController {
|
|
||||||
override func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
||||||
return CustomAlertPresentationAnimation()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
|
||||||
return CustomAlertDismissAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
class CustomAlertPresentationAnimation: NSObject, UIViewControllerAnimatedTransitioning {
|
class CustomAlertPresentationAnimation: NSObject, UIViewControllerAnimatedTransitioning {
|
||||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||||
|
|
|
@ -651,18 +651,6 @@ struct MenuPreviewHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension LargeImageViewController: CustomPreviewPresenting {
|
|
||||||
func presentFromPreview(presenter: UIViewController) {
|
|
||||||
presenter.present(self, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension GalleryViewController: CustomPreviewPresenting {
|
|
||||||
func presentFromPreview(presenter: UIViewController) {
|
|
||||||
presenter.present(self, animated: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SFSafariViewController: CustomPreviewPresenting {
|
extension SFSafariViewController: CustomPreviewPresenting {
|
||||||
func presentFromPreview(presenter: UIViewController) {
|
func presentFromPreview(presenter: UIViewController) {
|
||||||
presenter.present(self, animated: true)
|
presenter.present(self, animated: true)
|
||||||
|
|
|
@ -10,6 +10,7 @@ import UIKit
|
||||||
import SafariServices
|
import SafariServices
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import ComposeUI
|
import ComposeUI
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
||||||
|
@ -19,7 +20,7 @@ protocol TuskerNavigationDelegate: UIViewController, ToastableViewController {
|
||||||
extension TuskerNavigationDelegate {
|
extension TuskerNavigationDelegate {
|
||||||
|
|
||||||
func show(_ vc: UIViewController) {
|
func show(_ vc: UIViewController) {
|
||||||
if vc is LargeImageViewController || vc is GalleryViewController || vc is SFSafariViewController {
|
if vc is SFSafariViewController {
|
||||||
present(vc, animated: true)
|
present(vc, animated: true)
|
||||||
} else {
|
} else {
|
||||||
show(vc, sender: self)
|
show(vc, sender: self)
|
||||||
|
@ -126,27 +127,6 @@ extension TuskerNavigationDelegate {
|
||||||
compose(editing: draft, animated: animated)
|
compose(editing: draft, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showLoadingLargeImage(url: URL, cache: ImageCache, description: String?, animatingFrom sourceView: UIImageView) {
|
|
||||||
let vc = LoadingLargeImageViewController(url: url, cache: cache, imageDescription: description)
|
|
||||||
vc.animationSourceView = sourceView
|
|
||||||
#if !os(visionOS)
|
|
||||||
vc.transitioningDelegate = self
|
|
||||||
#endif
|
|
||||||
present(vc, animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func gallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) -> GalleryViewController {
|
|
||||||
let vc = GalleryViewController(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex)
|
|
||||||
#if !os(visionOS)
|
|
||||||
vc.transitioningDelegate = self
|
|
||||||
#endif
|
|
||||||
return vc
|
|
||||||
}
|
|
||||||
|
|
||||||
func showGallery(attachments: [Attachment], sourceViews: [UIImageView?], startIndex: Int) {
|
|
||||||
present(gallery(attachments: attachments, sourceViews: sourceViews, startIndex: startIndex), animated: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func moreOptions(forURL url: URL) -> UIActivityViewController {
|
private func moreOptions(forURL url: URL) -> UIActivityViewController {
|
||||||
let customActivites: [UIActivity] = [
|
let customActivites: [UIActivity] = [
|
||||||
OpenInSafariActivity()
|
OpenInSafariActivity()
|
||||||
|
|
|
@ -13,7 +13,7 @@ import TuskerComponents
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol AttachmentViewDelegate: AnyObject {
|
protocol AttachmentViewDelegate: AnyObject {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
|
func attachmentViewGallery(startingAt index: Int) -> UIViewController?
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,8 +23,8 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
weak var delegate: AttachmentViewDelegate?
|
weak var delegate: AttachmentViewDelegate?
|
||||||
|
|
||||||
var playImageView: UIImageView?
|
private var playImageView: UIImageView?
|
||||||
var gifvView: GifvAttachmentView?
|
private(set) var gifvView: GifvPlayerView?
|
||||||
private var badgeContainer: UIStackView?
|
private var badgeContainer: UIStackView?
|
||||||
|
|
||||||
var attachment: Attachment!
|
var attachment: Attachment!
|
||||||
|
@ -32,6 +32,16 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
private var loadAttachmentTask: Task<Void, Never>?
|
private var loadAttachmentTask: Task<Void, Never>?
|
||||||
private var source: Source?
|
private var source: Source?
|
||||||
|
var originalData: Data? {
|
||||||
|
switch source {
|
||||||
|
case .image(_, let data, _):
|
||||||
|
return data
|
||||||
|
case .gifData(_, let data, _):
|
||||||
|
return data
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var autoplayGifs: Bool {
|
private var autoplayGifs: Bool {
|
||||||
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
|
||||||
|
@ -103,9 +113,9 @@ class AttachmentView: GIFImageView {
|
||||||
} else if self.attachment.kind == .gifv,
|
} else if self.attachment.kind == .gifv,
|
||||||
let gifvView = self.gifvView {
|
let gifvView = self.gifvView {
|
||||||
if self.autoplayGifs {
|
if self.autoplayGifs {
|
||||||
gifvView.player.play()
|
gifvView.controller.play()
|
||||||
} else {
|
} else {
|
||||||
gifvView.player.pause()
|
gifvView.controller.pause()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -274,11 +284,12 @@ class AttachmentView: GIFImageView {
|
||||||
|
|
||||||
private func loadGifv() {
|
private func loadGifv() {
|
||||||
let asset = AVURLAsset(url: attachment.url)
|
let asset = AVURLAsset(url: attachment.url)
|
||||||
let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
|
let controller = GifvController(asset: asset)
|
||||||
|
let gifvView = GifvPlayerView(controller: controller, gravity: .resizeAspectFill)
|
||||||
self.gifvView = gifvView
|
self.gifvView = gifvView
|
||||||
gifvView.translatesAutoresizingMaskIntoConstraints = false
|
gifvView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
if autoplayGifs {
|
if autoplayGifs {
|
||||||
gifvView.player.play()
|
controller.play()
|
||||||
}
|
}
|
||||||
addSubview(gifvView)
|
addSubview(gifvView)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
@ -419,7 +430,6 @@ fileprivate extension AttachmentView {
|
||||||
enum Source {
|
enum Source {
|
||||||
case image(URL, Data?, UIImage)
|
case image(URL, Data?, UIImage)
|
||||||
case gifData(URL, Data, UIImage?)
|
case gifData(URL, Data, UIImage?)
|
||||||
// case cgImage(URL, CGImage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Badges: OptionSet {
|
struct Badges: OptionSet {
|
||||||
|
@ -433,11 +443,15 @@ fileprivate extension AttachmentView {
|
||||||
extension AttachmentView: UIContextMenuInteractionDelegate {
|
extension AttachmentView: UIContextMenuInteractionDelegate {
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in
|
return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in
|
||||||
if self.attachment.kind == .image {
|
if self.attachment.kind == .image,
|
||||||
return AttachmentPreviewViewController(sourceView: self)
|
let image {
|
||||||
} else if self.attachment.kind == .gifv {
|
return ImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController)
|
||||||
let vc = GifvAttachmentViewController(attachment: self.attachment)
|
} else if self.attachment.kind == .gifv,
|
||||||
vc.preferredContentSize = self.image?.size ?? .zero
|
let gifvView {
|
||||||
|
return GifvGalleryContentViewController(controller: gifvView.controller, caption: nil)
|
||||||
|
} else if self.attachment.kind == .video || self.attachment.kind == .audio {
|
||||||
|
let vc = VideoGalleryContentViewController(url: self.attachment.url, caption: nil)
|
||||||
|
vc.player.isMuted = true
|
||||||
return vc
|
return vc
|
||||||
} else {
|
} else {
|
||||||
return self.delegate?.attachmentViewGallery(startingAt: self.index)
|
return self.delegate?.attachmentViewGallery(startingAt: self.index)
|
||||||
|
@ -465,7 +479,7 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
|
||||||
itemData = Task { data }
|
itemData = Task { data }
|
||||||
}
|
}
|
||||||
} else if self.attachment.kind == .gifv {
|
} else if self.attachment.kind == .gifv {
|
||||||
itemSource = GifvActivityItemSource(asset: AVAsset(url: self.attachment.url), attachment: self.attachment)
|
itemSource = GifvActivityItemSource(asset: AVAsset(url: self.attachment.url), url: self.attachment.url)
|
||||||
itemData = Task {
|
itemData = Task {
|
||||||
try? await URLSession.shared.data(from: self.attachment.url).0
|
try? await URLSession.shared.data(from: self.attachment.url).0
|
||||||
}
|
}
|
||||||
|
@ -499,11 +513,7 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||||
animator.addCompletion {
|
animator.addCompletion {
|
||||||
animator.preferredCommitStyle = .pop
|
animator.preferredCommitStyle = .pop
|
||||||
if let gallery = animator.previewViewController as? GalleryViewController {
|
self.showGallery()
|
||||||
self.delegate?.attachmentViewPresent(gallery, animated: true)
|
|
||||||
} else {
|
|
||||||
self.showGallery()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,72 @@
|
||||||
//
|
//
|
||||||
// GifvAttachmentView.swift
|
// GifvController.swift
|
||||||
// Tusker
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 5/12/20.
|
// Created by Shadowfacts on 3/18/24.
|
||||||
// Copyright © 2020 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import Foundation
|
||||||
import AVFoundation
|
import AVKit
|
||||||
|
import Combine
|
||||||
|
|
||||||
class GifvAttachmentView: UIView {
|
@MainActor
|
||||||
|
class GifvController {
|
||||||
override class var layerClass: AnyClass {
|
private let asset: AVAsset
|
||||||
return AVPlayerLayer.self
|
|
||||||
}
|
|
||||||
|
|
||||||
private var playerLayer: AVPlayerLayer {
|
|
||||||
layer as! AVPlayerLayer
|
|
||||||
}
|
|
||||||
|
|
||||||
private var asset: AVAsset
|
|
||||||
private(set) var item: AVPlayerItem
|
private(set) var item: AVPlayerItem
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
private var isGrayscale = false
|
private var isGrayscale = false
|
||||||
|
|
||||||
init(asset: AVAsset, gravity: AVLayerVideoGravity) {
|
let presentationSizeSubject = PassthroughSubject<CGSize, Never>()
|
||||||
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
init(asset: AVAsset) {
|
||||||
self.asset = asset
|
self.asset = asset
|
||||||
item = GifvAttachmentView.createItem(asset: asset)
|
self.item = AVPlayerItem(asset: asset)
|
||||||
player = AVPlayer(playerItem: item)
|
self.player = AVPlayer(playerItem: item)
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
super.init(frame: .zero)
|
|
||||||
|
|
||||||
playerLayer.player = player
|
|
||||||
playerLayer.videoGravity = gravity
|
|
||||||
player.isMuted = true
|
player.isMuted = true
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
NotificationCenter.default.addObserver(self, selector: #selector(restartItem), name: .AVPlayerItemDidPlayToEndTime, object: item)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
|
||||||
|
updatePresentationSizeObservation()
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
func play() {
|
||||||
fatalError("init(coder:) has not been implemented")
|
if player.rate == 0 {
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pause() {
|
||||||
|
player.pause()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updatePresentationSizeObservation() {
|
||||||
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
||||||
|
self.presentationSizeSubject.send(item.presentationSize)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func restartItem() {
|
||||||
|
item.seek(to: .zero) { (success) in
|
||||||
|
guard success else { return }
|
||||||
|
self.player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
item = GifvController.createItem(asset: asset)
|
||||||
|
player.replaceCurrentItem(with: item)
|
||||||
|
self.updatePresentationSizeObservation()
|
||||||
|
player.play()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func createItem(asset: AVAsset) -> AVPlayerItem {
|
private static func createItem(asset: AVAsset) -> AVPlayerItem {
|
||||||
|
@ -63,21 +88,5 @@ class GifvAttachmentView: UIView {
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
item = GifvAttachmentView.createItem(asset: asset)
|
|
||||||
player.replaceCurrentItem(with: item)
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func restartItem() {
|
|
||||||
item.seek(to: .zero) { (success) in
|
|
||||||
guard success else { return }
|
|
||||||
self.player.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -0,0 +1,48 @@
|
||||||
|
//
|
||||||
|
// GifvPlayerView.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 5/12/20.
|
||||||
|
// Copyright © 2020 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
class GifvPlayerView: UIView {
|
||||||
|
|
||||||
|
override class var layerClass: AnyClass {
|
||||||
|
return AVPlayerLayer.self
|
||||||
|
}
|
||||||
|
|
||||||
|
private var playerLayer: AVPlayerLayer {
|
||||||
|
layer as! AVPlayerLayer
|
||||||
|
}
|
||||||
|
|
||||||
|
let controller: GifvController
|
||||||
|
private var presentationSizeCancellable: AnyCancellable?
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
controller.item.presentationSize
|
||||||
|
}
|
||||||
|
|
||||||
|
init(controller: GifvController, gravity: AVLayerVideoGravity) {
|
||||||
|
self.controller = controller
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
playerLayer.player = controller.player
|
||||||
|
playerLayer.videoGravity = gravity
|
||||||
|
|
||||||
|
presentationSizeCancellable = controller.presentationSizeSubject
|
||||||
|
.sink(receiveValue: { [unowned self] _ in
|
||||||
|
self.invalidateIntrinsicContentSize()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,6 +8,7 @@
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
protocol ProfileHeaderViewDelegate: TuskerNavigationDelegate, MenuActionProvider {
|
||||||
|
@ -352,7 +353,8 @@ class ProfileHeaderView: UIView {
|
||||||
let avatar = account.avatar else {
|
let avatar = account.avatar else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delegate?.showLoadingLargeImage(url: avatar, cache: .avatars, description: nil, animatingFrom: avatarImageView)
|
let gallery = GalleryVC.GalleryViewController(dataSource: ImageGalleryDataSource(url: avatar, cache: .avatars, sourceView: avatarImageView), initialItemIndex: 0)
|
||||||
|
delegate?.present(gallery, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func headerPressed() {
|
@objc func headerPressed() {
|
||||||
|
@ -360,7 +362,8 @@ class ProfileHeaderView: UIView {
|
||||||
let header = account.header else {
|
let header = account.header else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delegate?.showLoadingLargeImage(url: header, cache: .headers, description: nil, animatingFrom: headerImageView)
|
let gallery = GalleryVC.GalleryViewController(dataSource: ImageGalleryDataSource(url: header, cache: .headers, sourceView: headerImageView), initialItemIndex: 0)
|
||||||
|
delegate?.present(gallery, animated: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@IBAction func followPressed() {
|
@IBAction func followPressed() {
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
protocol StatusCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate, MenuActionProvider {
|
protocol StatusCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate, MenuActionProvider {
|
||||||
|
@ -328,14 +329,12 @@ extension StatusCollectionViewCell {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension StatusCollectionViewCell {
|
extension StatusCollectionViewCell {
|
||||||
func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
|
func attachmentViewGallery(startingAt index: Int) -> UIViewController? {
|
||||||
guard let delegate = delegate,
|
guard let status = mastodonController.persistentContainer.status(for: statusID) else {
|
||||||
let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
|
return nil
|
||||||
let sourceViews = status.attachments.map(attachmentsView.getAttachmentView(for:))
|
}
|
||||||
let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
|
let sourceViews = attachmentsView.attachmentViews.copy() as! NSHashTable<AttachmentView>
|
||||||
// TODO: PiP
|
return GalleryVC.GalleryViewController(dataSource: StatusAttachmentsGalleryDataSource(attachments: status.attachments, sourceViews: sourceViews), initialItemIndex: index)
|
||||||
// gallery.avPlayerViewControllerDelegate = self
|
|
||||||
return gallery
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
|
||||||
|
|
Loading…
Reference in New Issue