parent
42e29862ac
commit
6857529d06
|
@ -13,6 +13,7 @@ public protocol GalleryContentViewController: UIViewController {
|
||||||
var contentSize: CGSize { get }
|
var contentSize: CGSize { get }
|
||||||
var activityItemsForSharing: [Any] { get }
|
var activityItemsForSharing: [Any] { get }
|
||||||
var caption: String? { get }
|
var caption: String? { get }
|
||||||
|
var contentOverlayAccessoryViewController: UIViewController? { get }
|
||||||
var bottomControlsAccessoryViewController: UIViewController? { get }
|
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||||
var canAnimateFromSourceView: Bool { get }
|
var canAnimateFromSourceView: Bool { get }
|
||||||
|
|
||||||
|
@ -20,6 +21,10 @@ public protocol GalleryContentViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension GalleryContentViewController {
|
public extension GalleryContentViewController {
|
||||||
|
var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
var bottomControlsAccessoryViewController: UIViewController? {
|
var bottomControlsAccessoryViewController: UIViewController? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,4 +14,5 @@ public protocol GalleryContentViewControllerContainer {
|
||||||
func setGalleryContentLoading(_ loading: Bool)
|
func setGalleryContentLoading(_ loading: Bool)
|
||||||
func galleryContentChanged()
|
func galleryContentChanged()
|
||||||
func disableGalleryScrollAndZoom()
|
func disableGalleryScrollAndZoom()
|
||||||
|
func setGalleryControlsVisible(_ visible: Bool, animated: Bool)
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
|
|
||||||
let itemIndex: Int
|
let itemIndex: Int
|
||||||
let content: GalleryContentViewController
|
let content: GalleryContentViewController
|
||||||
|
private var overlayVC: UIViewController?
|
||||||
|
|
||||||
private var activityIndicator: UIActivityIndicatorView?
|
private var activityIndicator: UIActivityIndicatorView?
|
||||||
private(set) var scrollView: UIScrollView!
|
private(set) var scrollView: UIScrollView!
|
||||||
|
@ -33,6 +34,9 @@ class GalleryItemViewController: UIViewController {
|
||||||
private var bottomControlsView: UIStackView!
|
private var bottomControlsView: UIStackView!
|
||||||
private(set) var captionTextView: UITextView!
|
private(set) var captionTextView: UITextView!
|
||||||
|
|
||||||
|
private var singleTap: UITapGestureRecognizer!
|
||||||
|
private var doubleTap: UITapGestureRecognizer!
|
||||||
|
|
||||||
private var contentViewLeadingConstraint: NSLayoutConstraint?
|
private var contentViewLeadingConstraint: NSLayoutConstraint?
|
||||||
private var contentViewTopConstraint: NSLayoutConstraint?
|
private var contentViewTopConstraint: NSLayoutConstraint?
|
||||||
|
|
||||||
|
@ -70,6 +74,19 @@ class GalleryItemViewController: UIViewController {
|
||||||
addContent()
|
addContent()
|
||||||
centerContent()
|
centerContent()
|
||||||
|
|
||||||
|
overlayVC = content.contentOverlayAccessoryViewController
|
||||||
|
if let overlayVC {
|
||||||
|
overlayVC.view.isHidden = activityIndicator != nil
|
||||||
|
overlayVC.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(overlayVC.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
overlayVC.view.leadingAnchor.constraint(equalTo: content.view.leadingAnchor),
|
||||||
|
overlayVC.view.trailingAnchor.constraint(equalTo: content.view.trailingAnchor),
|
||||||
|
overlayVC.view.topAnchor.constraint(equalTo: content.view.topAnchor),
|
||||||
|
overlayVC.view.bottomAnchor.constraint(equalTo: content.view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
topControlsView = UIView()
|
topControlsView = UIView()
|
||||||
topControlsView.translatesAutoresizingMaskIntoConstraints = false
|
topControlsView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(topControlsView)
|
view.addSubview(topControlsView)
|
||||||
|
@ -118,6 +135,7 @@ class GalleryItemViewController: UIViewController {
|
||||||
captionTextView.isSelectable = true
|
captionTextView.isSelectable = true
|
||||||
captionTextView.font = .preferredFont(forTextStyle: .body)
|
captionTextView.font = .preferredFont(forTextStyle: .body)
|
||||||
captionTextView.adjustsFontForContentSizeCategory = true
|
captionTextView.adjustsFontForContentSizeCategory = true
|
||||||
|
captionTextView.alwaysBounceVertical = true
|
||||||
updateCaptionTextView()
|
updateCaptionTextView()
|
||||||
bottomControlsView.addArrangedSubview(captionTextView)
|
bottomControlsView.addArrangedSubview(captionTextView)
|
||||||
|
|
||||||
|
@ -151,8 +169,10 @@ class GalleryItemViewController: UIViewController {
|
||||||
captionTextView.heightAnchor.constraint(equalToConstant: 150),
|
captionTextView.heightAnchor.constraint(equalToConstant: 150),
|
||||||
])
|
])
|
||||||
|
|
||||||
let singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
|
singleTap = UITapGestureRecognizer(target: self, action: #selector(viewPressed))
|
||||||
let doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
|
singleTap.delegate = self
|
||||||
|
doubleTap = UITapGestureRecognizer(target: self, action: #selector(viewDoublePressed))
|
||||||
|
doubleTap.delegate = self
|
||||||
doubleTap.numberOfTapsRequired = 2
|
doubleTap.numberOfTapsRequired = 2
|
||||||
// this requirement is needed to make sure the double tap is ever recognized
|
// this requirement is needed to make sure the double tap is ever recognized
|
||||||
singleTap.require(toFail: doubleTap)
|
singleTap.require(toFail: doubleTap)
|
||||||
|
@ -192,6 +212,8 @@ class GalleryItemViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func addContent() {
|
func addContent() {
|
||||||
|
content.loadViewIfNeeded()
|
||||||
|
|
||||||
content.setControlsVisible(controlsVisible, animated: false)
|
content.setControlsVisible(controlsVisible, animated: false)
|
||||||
|
|
||||||
content.view.translatesAutoresizingMaskIntoConstraints = false
|
content.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
@ -219,6 +241,16 @@ class GalleryItemViewController: UIViewController {
|
||||||
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
content.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let overlayVC {
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
overlayVC.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
overlayVC.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
overlayVC.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
overlayVC.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
content.view.layoutIfNeeded()
|
content.view.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,6 +437,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
||||||
|
|
||||||
func setGalleryContentLoading(_ loading: Bool) {
|
func setGalleryContentLoading(_ loading: Bool) {
|
||||||
if loading {
|
if loading {
|
||||||
|
overlayVC?.view.isHidden = true
|
||||||
if activityIndicator == nil {
|
if activityIndicator == nil {
|
||||||
let activityIndicator = UIActivityIndicatorView(style: .large)
|
let activityIndicator = UIActivityIndicatorView(style: .large)
|
||||||
self.activityIndicator = activityIndicator
|
self.activityIndicator = activityIndicator
|
||||||
|
@ -430,6 +463,7 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
||||||
} else {
|
} else {
|
||||||
activityIndicator.removeFromSuperview()
|
activityIndicator.removeFromSuperview()
|
||||||
self.activityIndicator = nil
|
self.activityIndicator = nil
|
||||||
|
self.overlayVC?.view.isHidden = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -450,6 +484,10 @@ extension GalleryItemViewController: GalleryContentViewControllerContainer {
|
||||||
addContent()
|
addContent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setGalleryControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
|
setControlsVisible(visible, animated: animated)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GalleryItemViewController: UIScrollViewDelegate {
|
extension GalleryItemViewController: UIScrollViewDelegate {
|
||||||
|
@ -472,3 +510,17 @@ extension GalleryItemViewController: UIScrollViewDelegate {
|
||||||
scrollView.layoutIfNeeded()
|
scrollView.layoutIfNeeded()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension GalleryItemViewController: UIGestureRecognizerDelegate {
|
||||||
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer == singleTap {
|
||||||
|
let loc = gestureRecognizer.location(in: view)
|
||||||
|
return !topControlsView.frame.contains(loc) && !bottomControlsView.frame.contains(loc)
|
||||||
|
} else if gestureRecognizer == doubleTap {
|
||||||
|
let loc = gestureRecognizer.location(in: content.view)
|
||||||
|
return content.view.bounds.contains(loc)
|
||||||
|
} else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -185,6 +185,8 @@
|
||||||
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 */; };
|
||||||
|
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */; };
|
||||||
|
D69261272BB3BA610023152C /* Box.swift in Sources */ = {isa = PBXBuildFile; fileRef = D69261262BB3BA610023152C /* Box.swift */; };
|
||||||
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
|
D6934F2C2BA7AD32002B1C8D /* GalleryVC in Frameworks */ = {isa = PBXBuildFile; productRef = D6934F2B2BA7AD32002B1C8D /* GalleryVC */; };
|
||||||
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
|
D6934F2E2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */; };
|
||||||
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
|
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */; };
|
||||||
|
@ -196,6 +198,7 @@
|
||||||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
|
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */; };
|
||||||
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
|
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */; };
|
||||||
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
D6934F402BAA19EC002B1C8D /* VideoActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */; };
|
||||||
|
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.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 */; };
|
||||||
|
@ -587,6 +590,8 @@
|
||||||
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>"; };
|
||||||
|
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoOverlayViewController.swift; sourceTree = "<group>"; };
|
||||||
|
D69261262BB3BA610023152C /* Box.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Box.swift; sourceTree = "<group>"; };
|
||||||
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusAttachmentsGalleryDataSource.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>"; };
|
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>"; };
|
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingGalleryContentViewController.swift; sourceTree = "<group>"; };
|
||||||
|
@ -597,6 +602,7 @@
|
||||||
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoGalleryContentViewController.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>"; };
|
D6934F3D2BAA19D5002B1C8D /* ImageActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
D6934F3F2BAA19EC002B1C8D /* VideoActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoActivityItemSource.swift; sourceTree = "<group>"; };
|
||||||
|
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoControlsViewController.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>"; };
|
||||||
|
@ -829,6 +835,8 @@
|
||||||
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
|
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
|
||||||
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
||||||
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
|
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
|
||||||
|
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */,
|
||||||
|
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */,
|
||||||
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
|
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Gallery;
|
path = Gallery;
|
||||||
|
@ -1487,6 +1495,7 @@
|
||||||
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
D6D4DDCF212518A000E1C4BB /* AppDelegate.swift */,
|
||||||
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
D6CA6A91249FAD8900AD45C1 /* AudioSessionHelper.swift */,
|
||||||
D6D79F582A13293200AB2315 /* BackgroundManager.swift */,
|
D6D79F582A13293200AB2315 /* BackgroundManager.swift */,
|
||||||
|
D69261262BB3BA610023152C /* Box.swift */,
|
||||||
D61F75B6293C119700C0B37F /* Filterer.swift */,
|
D61F75B6293C119700C0B37F /* Filterer.swift */,
|
||||||
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
D6E4269C2532A3E100C02E1C /* FuzzyMatcher.swift */,
|
||||||
D61F75BA293C183100C0B37F /* HTMLConverter.swift */,
|
D61F75BA293C183100C0B37F /* HTMLConverter.swift */,
|
||||||
|
@ -1946,6 +1955,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
D6934F422BAC7D6E002B1C8D /* VideoControlsViewController.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 */,
|
||||||
|
@ -1989,6 +1999,7 @@
|
||||||
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 */,
|
||||||
|
D69261232BB3AEFB0023152C /* VideoOverlayViewController.swift in Sources */,
|
||||||
D6934F3C2BAA0F80002B1C8D /* VideoGalleryContentViewController.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 */,
|
||||||
|
@ -2162,6 +2173,7 @@
|
||||||
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
04586B4322B301470021BD04 /* AppearancePrefsView.swift in Sources */,
|
||||||
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
D6FF9860255C717400845181 /* AccountSwitchingContainerViewController.swift in Sources */,
|
||||||
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
D646DCD42A0729440059ECEB /* ActionNotificationGroupCollectionViewCell.swift in Sources */,
|
||||||
|
D69261272BB3BA610023152C /* Box.swift in Sources */,
|
||||||
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 */,
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
//
|
||||||
|
// Box.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/26/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
@propertyWrapper
|
||||||
|
class Box<Value> {
|
||||||
|
var wrappedValue: Value
|
||||||
|
|
||||||
|
init(wrappedValue: Value) {
|
||||||
|
self.wrappedValue = wrappedValue
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,459 @@
|
||||||
|
//
|
||||||
|
// VideoControlsViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/21/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class VideoControlsViewController: UIViewController {
|
||||||
|
private static let formatter: DateComponentsFormatter = {
|
||||||
|
let f = DateComponentsFormatter()
|
||||||
|
f.allowedUnits = [.minute, .second]
|
||||||
|
f.zeroFormattingBehavior = .pad
|
||||||
|
return f
|
||||||
|
}()
|
||||||
|
|
||||||
|
private let player: AVPlayer
|
||||||
|
@Box private var playbackSpeed: Float
|
||||||
|
|
||||||
|
private lazy var muteButton = MuteButton().configure {
|
||||||
|
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||||
|
$0.setMuted(false, animated: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let timestampLabel = UILabel().configure {
|
||||||
|
$0.text = "0:00"
|
||||||
|
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var scrubbingControl = VideoScrubbingControl().configure {
|
||||||
|
$0.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
||||||
|
$0.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
|
||||||
|
$0.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
|
||||||
|
$0.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
|
||||||
|
}
|
||||||
|
|
||||||
|
private let timeRemainingLabel = UILabel().configure {
|
||||||
|
$0.text = "-0:00"
|
||||||
|
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||||
|
let imageName: String
|
||||||
|
if #available(iOS 17.0, *) {
|
||||||
|
switch self.playbackSpeed {
|
||||||
|
case 0.5:
|
||||||
|
imageName = "gauge.with.dots.needle.0percent"
|
||||||
|
case 1:
|
||||||
|
imageName = "gauge.with.dots.needle.33percent"
|
||||||
|
case 1.25:
|
||||||
|
imageName = "gauge.with.dots.needle.50percent"
|
||||||
|
case 2:
|
||||||
|
imageName = "gauge.with.dots.needle.100percent"
|
||||||
|
default:
|
||||||
|
imageName = "gauge.with.dots.needle.67percent"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
imageName = "speedometer"
|
||||||
|
}
|
||||||
|
let speedMenu = UIMenu(title: "Playback Speed", image: UIImage(systemName: imageName), children: PlaybackSpeed.allCases.map { speed in
|
||||||
|
UIAction(title: speed.displayName, state: self.playbackSpeed == speed.rate ? .on : .off) { [unowned self] _ in
|
||||||
|
self.playbackSpeed = speed.rate
|
||||||
|
if self.player.rate > 0 {
|
||||||
|
self.player.rate = speed.rate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return UIMenu(children: [speedMenu])
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy var hStack = UIStackView(arrangedSubviews: [
|
||||||
|
muteButton,
|
||||||
|
timestampLabel,
|
||||||
|
scrubbingControl,
|
||||||
|
timeRemainingLabel,
|
||||||
|
optionsButton,
|
||||||
|
]).configure {
|
||||||
|
$0.axis = .horizontal
|
||||||
|
$0.spacing = 8
|
||||||
|
$0.alignment = .center
|
||||||
|
}
|
||||||
|
|
||||||
|
private var timestampObserverToken: Any?
|
||||||
|
private var scrubberObserverToken: Any?
|
||||||
|
|
||||||
|
private var wasPlayingWhenScrubbingStarted = false
|
||||||
|
private var scrubbingTargetTime: CMTime?
|
||||||
|
private var isSeeking = false
|
||||||
|
|
||||||
|
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||||
|
self.player = player
|
||||||
|
self._playbackSpeed = playbackSpeed
|
||||||
|
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
hStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(hStack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
hStack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 4),
|
||||||
|
hStack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -4),
|
||||||
|
hStack.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
hStack.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
timestampObserverToken = player.addPeriodicTimeObserver(forInterval: CMTime(value: 1, timescale: 2), queue: .main) { [unowned self] _ in
|
||||||
|
self.updateTimestamps()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTimestamps() {
|
||||||
|
let current = player.currentTime()
|
||||||
|
timestampLabel.text = VideoControlsViewController.formatter.string(from: current.seconds)!
|
||||||
|
let duration = player.currentItem!.duration
|
||||||
|
if duration != .indefinite {
|
||||||
|
let remaining = duration - current
|
||||||
|
timeRemainingLabel.text = "-" + VideoControlsViewController.formatter.string(from: remaining.seconds)!
|
||||||
|
|
||||||
|
if scrubberObserverToken == nil {
|
||||||
|
let interval = CMTime(value: 1, timescale: CMTimeScale(self.scrubbingControl.bounds.width))
|
||||||
|
if interval.isValid {
|
||||||
|
self.scrubberObserverToken = self.player.addPeriodicTimeObserver(forInterval: interval, queue: .main, using: { _ in
|
||||||
|
self.scrubbingControl.fractionComplete = self.player.currentTime().seconds / duration.seconds
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func scrubbingStarted() {
|
||||||
|
wasPlayingWhenScrubbingStarted = player.rate > 0
|
||||||
|
player.rate = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func scrubbingChanged() {
|
||||||
|
let duration = player.currentItem!.duration
|
||||||
|
let time = CMTime(value: CMTimeValue(scrubbingControl.fractionComplete * duration.seconds * 1_000_000_000), timescale: 1_000_000_000)
|
||||||
|
scrubbingTargetTime = time
|
||||||
|
if !isSeeking {
|
||||||
|
seekToScrubbingTime()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func seekToScrubbingTime() {
|
||||||
|
guard let scrubbingTargetTime else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
isSeeking = true
|
||||||
|
player.seek(to: scrubbingTargetTime) { finished in
|
||||||
|
if finished {
|
||||||
|
if self.scrubbingTargetTime != scrubbingTargetTime {
|
||||||
|
self.seekToScrubbingTime()
|
||||||
|
} else {
|
||||||
|
self.isSeeking = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func scrubbingEnded() {
|
||||||
|
scrubbingChanged()
|
||||||
|
if wasPlayingWhenScrubbingStarted {
|
||||||
|
player.rate = playbackSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func muteButtonPressed() {
|
||||||
|
player.isMuted.toggle()
|
||||||
|
muteButton.setMuted(player.isMuted, animated: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class VideoScrubbingControl: UIControl {
|
||||||
|
var fractionComplete: Double = 0 {
|
||||||
|
didSet {
|
||||||
|
updateFillLayerMask()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let trackLayer = CAShapeLayer()
|
||||||
|
private let fillLayer = CAShapeLayer()
|
||||||
|
private let fillMaskLayer = CALayer()
|
||||||
|
|
||||||
|
private var scrubbingStartFraction: Double?
|
||||||
|
private var touchStartLocation: CGPoint?
|
||||||
|
private var animator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
private var feedbackGenerator: UIImpactFeedbackGenerator?
|
||||||
|
#endif
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
trackLayer.fillColor = UIColor.systemGray.cgColor
|
||||||
|
trackLayer.shadowColor = UIColor.black.cgColor
|
||||||
|
layer.addSublayer(trackLayer)
|
||||||
|
|
||||||
|
fillLayer.fillColor = UIColor.white.cgColor
|
||||||
|
fillLayer.mask = fillMaskLayer
|
||||||
|
layer.addSublayer(fillLayer)
|
||||||
|
|
||||||
|
fillMaskLayer.backgroundColor = UIColor.black.cgColor
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSublayers(of layer: CALayer) {
|
||||||
|
super.layoutSublayers(of: layer)
|
||||||
|
|
||||||
|
let trackFrame = CGRect(x: 0, y: (layer.bounds.height - 8) / 2, width: layer.bounds.width, height: 8)
|
||||||
|
|
||||||
|
trackLayer.frame = trackFrame
|
||||||
|
trackLayer.path = CGPath(roundedRect: CGRect(x: 0, y: 0, width: trackFrame.width, height: trackFrame.height), cornerWidth: 4, cornerHeight: 4, transform: nil)
|
||||||
|
trackLayer.shadowPath = trackLayer.path
|
||||||
|
|
||||||
|
fillLayer.frame = trackFrame
|
||||||
|
fillLayer.path = trackLayer.path
|
||||||
|
updateFillLayerMask()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateFillLayerMask() {
|
||||||
|
// I don't know where this animation is coming from
|
||||||
|
CATransaction.begin()
|
||||||
|
CATransaction.setDisableActions(true)
|
||||||
|
fillMaskLayer.frame = CGRect(x: 0, y: 0, width: fractionComplete * bounds.width, height: 8)
|
||||||
|
CATransaction.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
touchStartLocation = touch.location(in: self)
|
||||||
|
scrubbingStartFraction = fractionComplete
|
||||||
|
|
||||||
|
animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear)
|
||||||
|
animator!.addAnimations {
|
||||||
|
self.transform = CGAffineTransform(scaleX: 1, y: 1.5)
|
||||||
|
}
|
||||||
|
animator!.startAnimation()
|
||||||
|
|
||||||
|
sendActions(for: .editingDidBegin)
|
||||||
|
|
||||||
|
#if !os(visionOS)
|
||||||
|
feedbackGenerator = UIImpactFeedbackGenerator(style: .light)
|
||||||
|
feedbackGenerator!.prepare()
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
guard let touchStartLocation,
|
||||||
|
let scrubbingStartFraction else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let location = touch.location(in: self)
|
||||||
|
let translation = CGPoint(x: location.x - touchStartLocation.x, y: location.y - touchStartLocation.y)
|
||||||
|
let scrubbingAmount = translation.x / bounds.width
|
||||||
|
let unclampedFractionComplete = scrubbingStartFraction + scrubbingAmount
|
||||||
|
let newFractionComplete = max(0, min(1, unclampedFractionComplete))
|
||||||
|
#if !os(visionOS)
|
||||||
|
if newFractionComplete != fractionComplete && (newFractionComplete == 0 || newFractionComplete == 1) {
|
||||||
|
feedbackGenerator!.impactOccurred(intensity: 0.5)
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
fractionComplete = newFractionComplete
|
||||||
|
sendActions(for: .editingChanged)
|
||||||
|
|
||||||
|
if unclampedFractionComplete < 0 || unclampedFractionComplete > 1 {
|
||||||
|
let stretchFactor: CGFloat
|
||||||
|
if unclampedFractionComplete < 0 {
|
||||||
|
stretchFactor = 1/(unclampedFractionComplete * bounds.width / 10 - 1) + 1
|
||||||
|
} else {
|
||||||
|
stretchFactor = -1/((unclampedFractionComplete-1) * bounds.width / 10 + 1) + 1
|
||||||
|
}
|
||||||
|
let stretchAmount = 8 * stretchFactor
|
||||||
|
transform = CGAffineTransform(scaleX: 1 + stretchAmount / bounds.width, y: 1 + 0.5 * (1 - stretchFactor))
|
||||||
|
.translatedBy(x: sign(unclampedFractionComplete) * stretchAmount / 2, y: 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||||
|
touchStartLocation = nil
|
||||||
|
resetScale()
|
||||||
|
sendActions(for: .editingDidEnd)
|
||||||
|
#if !os(visionOS)
|
||||||
|
feedbackGenerator = nil
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
override func cancelTracking(with event: UIEvent?) {
|
||||||
|
touchStartLocation = nil
|
||||||
|
resetScale()
|
||||||
|
sendActions(for: .editingDidEnd)
|
||||||
|
#if !os(visionOS)
|
||||||
|
feedbackGenerator = nil
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resetScale() {
|
||||||
|
if let animator,
|
||||||
|
animator.isRunning {
|
||||||
|
animator.isReversed = true
|
||||||
|
animator.startAnimation()
|
||||||
|
} else {
|
||||||
|
animator?.pauseAnimation()
|
||||||
|
animator = UIViewPropertyAnimator(duration: 0.1, curve: .linear)
|
||||||
|
animator!.addAnimations {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
animator!.startAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MuteButton: UIControl {
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
CGSize(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.tintColor = .white
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMuted(_ muted: Bool, animated: Bool) {
|
||||||
|
let image = UIImage(systemName: muted ? "speaker.slash.fill" : "speaker.wave.3.fill")!
|
||||||
|
if animated,
|
||||||
|
#available(iOS 17.0, *) {
|
||||||
|
imageView.setSymbolImage(image, contentTransition: .replace.wholeSymbol, options: .speed(5))
|
||||||
|
} else {
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
!(gestureRecognizer is UITapGestureRecognizer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MenuButton: UIControl {
|
||||||
|
private let menuProvider: () -> UIMenu
|
||||||
|
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
CGSize(width: 32, height: 32)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(menuProvider: @escaping () -> UIMenu) {
|
||||||
|
self.menuProvider = menuProvider
|
||||||
|
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
imageView.image = UIImage(systemName: "ellipsis.circle")
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.tintColor = .white
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.centerXAnchor.constraint(equalTo: centerXAnchor),
|
||||||
|
imageView.centerYAnchor.constraint(equalTo: centerYAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
isContextMenuInteractionEnabled = true
|
||||||
|
showsMenuAsPrimaryAction = true
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
|
UIContextMenuConfiguration(actionProvider: { _ in
|
||||||
|
self.menuProvider()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willDisplayMenuFor configuration: UIContextMenuConfiguration, animator: (any UIContextMenuInteractionAnimating)?) {
|
||||||
|
animator?.addAnimations {
|
||||||
|
self.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willEndFor configuration: UIContextMenuConfiguration, animator: (any UIContextMenuInteractionAnimating)?) {
|
||||||
|
if let animator {
|
||||||
|
animator.addAnimations {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum PlaybackSpeed: CaseIterable {
|
||||||
|
case half, regular, oneAndAQuarter, oneAndAHalf, two
|
||||||
|
|
||||||
|
var rate: Float {
|
||||||
|
switch self {
|
||||||
|
case .half:
|
||||||
|
0.5
|
||||||
|
case .regular:
|
||||||
|
1
|
||||||
|
case .oneAndAQuarter:
|
||||||
|
1.25
|
||||||
|
case .oneAndAHalf:
|
||||||
|
1.5
|
||||||
|
case .two:
|
||||||
|
2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName: String {
|
||||||
|
switch self {
|
||||||
|
case .half:
|
||||||
|
"0.5×"
|
||||||
|
case .regular:
|
||||||
|
"1×"
|
||||||
|
case .oneAndAQuarter:
|
||||||
|
"1.25×"
|
||||||
|
case .oneAndAHalf:
|
||||||
|
"1.5×"
|
||||||
|
case .two:
|
||||||
|
"2×"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,9 +16,14 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
private let item: AVPlayerItem
|
private let item: AVPlayerItem
|
||||||
let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
|
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
||||||
|
@Box private var playbackSpeed: Float = 1
|
||||||
|
|
||||||
private var presentationSizeObservation: NSKeyValueObservation?
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
private var statusObservation: NSKeyValueObservation?
|
private var statusObservation: NSKeyValueObservation?
|
||||||
|
private var rateObservation: NSKeyValueObservation?
|
||||||
private var isFirstAppearance = true
|
private var isFirstAppearance = true
|
||||||
|
private var hideControlsWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
init(url: URL, caption: String?) {
|
init(url: URL, caption: String?) {
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -64,6 +69,20 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
rateObservation = player.observe(\.rate, options: .old, changeHandler: { [unowned self] player, info in
|
||||||
|
hideControlsWorkItem?.cancel()
|
||||||
|
if player.rate > 0 && info.oldValue == 0 {
|
||||||
|
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||||
|
guard let self,
|
||||||
|
let container = self.container,
|
||||||
|
container.galleryControlsVisible else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
container.setGalleryControlsVisible(false, animated: true)
|
||||||
|
}
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(5), execute: hideControlsWorkItem!)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
preferredContentSize = item.presentationSize
|
preferredContentSize = item.presentationSize
|
||||||
}
|
}
|
||||||
|
@ -95,6 +114,18 @@ class VideoGalleryContentViewController: UIViewController, GalleryContentViewCon
|
||||||
[VideoActivityItemSource(asset: item.asset, url: url)]
|
[VideoActivityItemSource(asset: item.asset, url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
|
var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
|
overlayVC
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
|
|
||||||
|
func setControlsVisible(_ visible: Bool, animated: Bool) {
|
||||||
|
overlayVC.setVisible(visible)
|
||||||
|
hideControlsWorkItem?.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PlayerView: UIView {
|
private class PlayerView: UIView {
|
||||||
|
|
|
@ -0,0 +1,200 @@
|
||||||
|
//
|
||||||
|
// VideoOverlayViewController.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 3/26/24.
|
||||||
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import AVFoundation
|
||||||
|
|
||||||
|
class VideoOverlayViewController: UIViewController {
|
||||||
|
|
||||||
|
private static let playImage = UIImage(systemName: "play.fill")!
|
||||||
|
private static let pauseImage = UIImage(systemName: "pause.fill")!
|
||||||
|
|
||||||
|
private let player: AVPlayer
|
||||||
|
@Box private var playbackSpeed: Float
|
||||||
|
|
||||||
|
private var dimmingView: UIView!
|
||||||
|
private var controlsStack: UIStackView!
|
||||||
|
private var skipBackButton: VideoOverlayButton!
|
||||||
|
private var skipForwardButton: VideoOverlayButton!
|
||||||
|
|
||||||
|
private var rateObservation: NSKeyValueObservation?
|
||||||
|
|
||||||
|
init(player: AVPlayer, playbackSpeed: Box<Float>) {
|
||||||
|
self.player = player
|
||||||
|
self._playbackSpeed = playbackSpeed
|
||||||
|
super.init(nibName: nil, bundle: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func viewDidLoad() {
|
||||||
|
super.viewDidLoad()
|
||||||
|
|
||||||
|
dimmingView = UIView()
|
||||||
|
dimmingView.backgroundColor = .black
|
||||||
|
dimmingView.alpha = 0.2
|
||||||
|
dimmingView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(dimmingView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
dimmingView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
dimmingView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
dimmingView.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
dimmingView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
skipBackButton = VideoOverlayButton(image: UIImage(systemName: "gobackward.10")!)
|
||||||
|
skipBackButton.addTarget(self, action: #selector(skipBackPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
let playPauseButton = VideoOverlayButton(image: VideoOverlayViewController.pauseImage)
|
||||||
|
playPauseButton.addTarget(self, action: #selector(playPausePressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
skipForwardButton = VideoOverlayButton(image: UIImage(systemName: "goforward.10")!)
|
||||||
|
skipForwardButton.addTarget(self, action: #selector(skipForwardPressed), for: .touchUpInside)
|
||||||
|
|
||||||
|
controlsStack = UIStackView(arrangedSubviews: [
|
||||||
|
skipBackButton,
|
||||||
|
playPauseButton,
|
||||||
|
skipForwardButton,
|
||||||
|
])
|
||||||
|
controlsStack.axis = .horizontal
|
||||||
|
controlsStack.alignment = .center
|
||||||
|
controlsStack.spacing = 24
|
||||||
|
controlsStack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
view.addSubview(controlsStack)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
controlsStack.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||||
|
controlsStack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
|
||||||
|
skipBackButton.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
skipBackButton.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
playPauseButton.widthAnchor.constraint(equalToConstant: 66),
|
||||||
|
playPauseButton.heightAnchor.constraint(equalToConstant: 66),
|
||||||
|
skipForwardButton.widthAnchor.constraint(equalToConstant: 50),
|
||||||
|
skipForwardButton.heightAnchor.constraint(equalToConstant: 50),
|
||||||
|
])
|
||||||
|
|
||||||
|
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
|
||||||
|
MainActor.runUnsafely {
|
||||||
|
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVisible(_ visible: Bool) {
|
||||||
|
loadViewIfNeeded()
|
||||||
|
|
||||||
|
view.alpha = visible ? 1 : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func playPausePressed() {
|
||||||
|
if player.rate > 0 {
|
||||||
|
player.rate = 0
|
||||||
|
} else {
|
||||||
|
player.rate = playbackSpeed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func skipBackPressed() {
|
||||||
|
player.seek(to: player.currentTime() - CMTime(value: 10, timescale: 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func skipForwardPressed() {
|
||||||
|
player.seek(to: player.currentTime() + CMTime(value: 10, timescale: 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private class VideoOverlayButton: UIControl {
|
||||||
|
var image: UIImage? {
|
||||||
|
get {
|
||||||
|
imageView.image
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
imageView.image = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private let backgroundView = UIView()
|
||||||
|
private let imageView = UIImageView()
|
||||||
|
|
||||||
|
private var animator: UIViewPropertyAnimator?
|
||||||
|
|
||||||
|
override var isEnabled: Bool {
|
||||||
|
didSet {
|
||||||
|
imageView.tintColor = isEnabled ? .white : .lightGray
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(image: UIImage) {
|
||||||
|
super.init(frame: .zero)
|
||||||
|
|
||||||
|
backgroundView.alpha = 0
|
||||||
|
backgroundView.backgroundColor = .lightGray.withAlphaComponent(0.5)
|
||||||
|
backgroundView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(backgroundView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
backgroundView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
backgroundView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
backgroundView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
backgroundView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
imageView.image = image
|
||||||
|
imageView.tintColor = .white
|
||||||
|
imageView.contentMode = .scaleAspectFit
|
||||||
|
imageView.preferredSymbolConfiguration = .init(scale: .large)
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
imageView.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
|
||||||
|
imageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
override func layoutSubviews() {
|
||||||
|
super.layoutSubviews()
|
||||||
|
|
||||||
|
backgroundView.layer.cornerRadius = bounds.height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
bounds.contains(point) ? self : nil
|
||||||
|
}
|
||||||
|
|
||||||
|
override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
self.backgroundView.alpha = 1
|
||||||
|
self.backgroundView.transform = CGAffineTransform(scaleX: 1/0.8, y: 1/0.8)
|
||||||
|
self.transform = CGAffineTransform(scaleX: 0.8, y: 0.8)
|
||||||
|
}
|
||||||
|
return super.beginTracking(touch, with: event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||||
|
UIView.animate(withDuration: 0.2) {
|
||||||
|
self.backgroundView.alpha = 0
|
||||||
|
self.backgroundView.transform = .identity
|
||||||
|
self.transform = .identity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
|
if gestureRecognizer is UITapGestureRecognizer {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue