Compare commits
No commits in common. "238f246d644fb36c17c912aabcffb112d2aa2d9d" and "c99c397cf6d4ba2ac36bf40a72874dc8576e67db" have entirely different histories.
238f246d64
...
c99c397cf6
@ -1,13 +1,3 @@
|
|||||||
## 2024.5
|
|
||||||
Features/Improvements:
|
|
||||||
- Improve gallery animations
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Handle right-to-left text in display names
|
|
||||||
- Fix crash during gifv playback
|
|
||||||
- iPadOS: Fix app becoming unresponsive when switching accounts
|
|
||||||
- iPadOS/macOS: Fix Cmd+R shortcuts not working
|
|
||||||
|
|
||||||
## 2024.4
|
## 2024.4
|
||||||
This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements.
|
This release introduces support for iOS 18, including a new sidebar/tab bar on iPad, as well as bugfixes and improvements.
|
||||||
|
|
||||||
|
28
CHANGELOG.md
28
CHANGELOG.md
@ -1,33 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 2024.5 (141)
|
|
||||||
Bugfixes:
|
|
||||||
- Fix gallery controls being positioned incorrectly during dismiss animation on certain devices
|
|
||||||
- Fix gallery controls being positioned incorrectly in landscape orientations
|
|
||||||
|
|
||||||
## 2024.5 (139)
|
|
||||||
Bugfixes:
|
|
||||||
- Fix error decoding certain posts
|
|
||||||
|
|
||||||
## 2024.5 (138)
|
|
||||||
Bugfixes:
|
|
||||||
- Fix potential crash when displaying certain attachments
|
|
||||||
- Fix potential crash due to race condition when opening push notification in app
|
|
||||||
- Fix misaligned text between profile field values/labels
|
|
||||||
- Fix rate limited error message not including reset timestamp
|
|
||||||
- iPadOS/macOS: Fix Cmd+R shortcut not working
|
|
||||||
|
|
||||||
## 2024.5 (137)
|
|
||||||
Features/Improvements:
|
|
||||||
- Improve gallery presentation/dismissal transitions
|
|
||||||
|
|
||||||
Bugfixes:
|
|
||||||
- Account for bidirectional text in display names
|
|
||||||
- Fix crash when playing back gifv
|
|
||||||
- Fix gallery controls not hiding if video loading fails
|
|
||||||
- iPadOS: Fix incorrect gallery dismiss animation on non-fullscreen windows
|
|
||||||
- iPadOS: Fix hang when switching accounts
|
|
||||||
|
|
||||||
## 2024.4 (136)
|
## 2024.4 (136)
|
||||||
Features/Improvements:
|
Features/Improvements:
|
||||||
- Import image description when adding attachments from Photos if possible
|
- Import image description when adding attachments from Photos if possible
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -26,15 +26,9 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "ComposeUI",
|
name: "ComposeUI",
|
||||||
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation"],
|
dependencies: ["Pachyderm", "InstanceFeatures", "TuskerComponents", "MatchedGeometryPresentation"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "ComposeUITests",
|
name: "ComposeUITests",
|
||||||
dependencies: ["ComposeUI"],
|
dependencies: ["ComposeUI"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -23,10 +23,7 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "Duckable",
|
name: "Duckable",
|
||||||
dependencies: [],
|
dependencies: []),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "DuckableTests",
|
// name: "DuckableTests",
|
||||||
// dependencies: ["Duckable"]),
|
// dependencies: ["Duckable"]),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.10
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -14,23 +14,13 @@ let package = Package(
|
|||||||
name: "GalleryVC",
|
name: "GalleryVC",
|
||||||
targets: ["GalleryVC"]),
|
targets: ["GalleryVC"]),
|
||||||
],
|
],
|
||||||
dependencies: [
|
|
||||||
.package(path: "../TuskerComponents"),
|
|
||||||
],
|
|
||||||
targets: [
|
targets: [
|
||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// 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.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "GalleryVC",
|
name: "GalleryVC"),
|
||||||
dependencies: ["TuskerComponents"],
|
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "GalleryVCTests",
|
name: "GalleryVCTests",
|
||||||
dependencies: ["GalleryVC"],
|
dependencies: ["GalleryVC"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -15,7 +15,7 @@ public protocol GalleryContentViewController: UIViewController {
|
|||||||
var caption: String? { get }
|
var caption: String? { get }
|
||||||
var contentOverlayAccessoryViewController: UIViewController? { get }
|
var contentOverlayAccessoryViewController: UIViewController? { get }
|
||||||
var bottomControlsAccessoryViewController: UIViewController? { get }
|
var bottomControlsAccessoryViewController: UIViewController? { get }
|
||||||
var presentationAnimation: GalleryContentPresentationAnimation { get }
|
var canAnimateFromSourceView: Bool { get }
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool)
|
||||||
func galleryContentDidAppear()
|
func galleryContentDidAppear()
|
||||||
@ -31,8 +31,8 @@ public extension GalleryContentViewController {
|
|||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var presentationAnimation: GalleryContentPresentationAnimation {
|
var canAnimateFromSourceView: Bool {
|
||||||
.fromSourceView
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
@ -44,9 +44,3 @@ public extension GalleryContentViewController {
|
|||||||
func galleryContentWillDisappear() {
|
func galleryContentWillDisappear() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum GalleryContentPresentationAnimation {
|
|
||||||
case fade
|
|
||||||
case fromSourceView
|
|
||||||
case fromSourceViewWithoutSnapshot
|
|
||||||
}
|
|
||||||
|
@ -30,37 +30,12 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
|||||||
|
|
||||||
let itemViewController = from.currentItemViewController
|
let itemViewController = from.currentItemViewController
|
||||||
|
|
||||||
if itemViewController.content.presentationAnimation == .fade || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
|
if !itemViewController.content.canAnimateFromSourceView || (UIAccessibility.prefersCrossFadeTransitions && interactiveVelocity == nil) {
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let container = transitionContext.containerView
|
let container = transitionContext.containerView
|
||||||
|
|
||||||
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
|
|
||||||
// is in the window's root presentation.
|
|
||||||
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
|
|
||||||
// `to.view` is already in the view hierarchy at this point; and adding it to the
|
|
||||||
// container causees it to be removed when the transition completes.
|
|
||||||
if to.view.superview == nil {
|
|
||||||
to.view.frame = container.bounds
|
|
||||||
container.addSubview(to.view)
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceSnapshot: UIView? = if itemViewController.content.presentationAnimation == .fromSourceViewWithoutSnapshot {
|
|
||||||
nil
|
|
||||||
} else {
|
|
||||||
sourceView.snapshotView(afterScreenUpdates: false)
|
|
||||||
}
|
|
||||||
if let sourceSnapshot {
|
|
||||||
let snapshotContainer = sourceView.ancestorForInsertingSnapshot
|
|
||||||
snapshotContainer.addSubview(sourceSnapshot)
|
|
||||||
let sourceFrameInShapshotContainer = snapshotContainer.convert(sourceView.bounds, from: sourceView)
|
|
||||||
sourceSnapshot.frame = sourceFrameInShapshotContainer
|
|
||||||
sourceSnapshot.layer.opacity = 1
|
|
||||||
self.sourceView.layer.opacity = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
let sourceFrameInContainer = container.convert(sourceView.bounds, from: sourceView)
|
||||||
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
let destFrameInContainer = container.convert(itemViewController.content.view.bounds, from: itemViewController.content.view)
|
||||||
|
|
||||||
@ -73,39 +48,38 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
|||||||
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
.translatedBy(x: destFrameInContainer.midX - sourceFrameInContainer.midX, y: destFrameInContainer.midY - sourceFrameInContainer.midY)
|
||||||
.scaledBy(x: scale, y: scale)
|
.scaledBy(x: scale, y: scale)
|
||||||
sourceView.transform = sourceToDestTransform
|
sourceView.transform = sourceToDestTransform
|
||||||
sourceSnapshot?.transform = sourceToDestTransform
|
|
||||||
} else {
|
} else {
|
||||||
appliedSourceToDestTransform = false
|
appliedSourceToDestTransform = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moving `to.view` to the container is necessary when the presenting VC (i.e., `to`)
|
||||||
|
// is in the window's root presentation.
|
||||||
|
// But it breaks when the gallery is presented from a sheet-presented VC--in which case
|
||||||
|
// `to.view` is already in the view hierarchy at this point; and adding it to the
|
||||||
|
// container causees it to be removed when the transition completes.
|
||||||
|
if to.view.superview == nil {
|
||||||
|
to.view.frame = container.bounds
|
||||||
|
container.addSubview(to.view)
|
||||||
|
}
|
||||||
|
|
||||||
from.view.frame = container.bounds
|
from.view.frame = container.bounds
|
||||||
container.addSubview(from.view)
|
container.addSubview(from.view)
|
||||||
|
|
||||||
let contentContainer = UIView()
|
|
||||||
contentContainer.layer.masksToBounds = true
|
|
||||||
contentContainer.frame = destFrameInContainer
|
|
||||||
container.addSubview(contentContainer)
|
|
||||||
|
|
||||||
let content = itemViewController.takeContent()
|
let content = itemViewController.takeContent()
|
||||||
content.view.translatesAutoresizingMaskIntoConstraints = true
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
content.view.transform = .identity
|
content.view.layer.masksToBounds = true
|
||||||
content.view.layer.opacity = 1
|
container.addSubview(content.view)
|
||||||
content.view.frame = contentContainer.bounds
|
|
||||||
contentContainer.addSubview(content.view)
|
|
||||||
|
|
||||||
container.layoutIfNeeded()
|
|
||||||
|
|
||||||
// Hide overlaid controls immediately, to prevent the Live Text button's position
|
content.view.frame = destFrameInContainer
|
||||||
// getting caught up in the rest of the animation.
|
content.view.layer.opacity = 1
|
||||||
UIView.animate(withDuration: 0.1) {
|
|
||||||
content.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
container.layoutIfNeeded()
|
||||||
}
|
|
||||||
|
|
||||||
let duration = self.transitionDuration(using: transitionContext)
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
var initialVelocity: CGVector
|
var initialVelocity: CGVector
|
||||||
if let interactiveVelocity,
|
if let interactiveVelocity,
|
||||||
let interactiveTranslation,
|
let interactiveTranslation,
|
||||||
// very short/fast flicks don't transfer their velocity, since it makes size change animation look wacky due to the spring's initial undershoot
|
// 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(interactiveTranslation.x, 2) + pow(interactiveTranslation.y, 2)) > 100,
|
||||||
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
|
sqrt(pow(interactiveVelocity.x, 2) + pow(interactiveVelocity.y, 2)) < 2000 {
|
||||||
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
|
let xDistance = sourceFrameInContainer.midX - destFrameInContainer.midX
|
||||||
@ -128,34 +102,14 @@ class GalleryDismissAnimationController: NSObject, UIViewControllerAnimatedTrans
|
|||||||
|
|
||||||
if appliedSourceToDestTransform {
|
if appliedSourceToDestTransform {
|
||||||
self.sourceView.transform = origSourceTransform
|
self.sourceView.transform = origSourceTransform
|
||||||
sourceSnapshot?.transform = origSourceTransform
|
|
||||||
}
|
}
|
||||||
|
content.view.frame = sourceFrameInContainer
|
||||||
contentContainer.frame = sourceFrameInContainer
|
content.view.layer.opacity = 0
|
||||||
// Using sourceSizeWithDestAspectRatioCenteredInContentContainer does not seem to be necessary here.
|
|
||||||
// I guess autoresizing takes care of it?
|
|
||||||
|
|
||||||
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delay fading out the content because if it's still big while it's semi-transparent,
|
|
||||||
// seeing the stuff behind it looks odd.
|
|
||||||
animator.addAnimations({
|
|
||||||
content.view.layer.opacity = 0
|
|
||||||
}, delayFactor: 0.35)
|
|
||||||
|
|
||||||
if let sourceSnapshot {
|
|
||||||
animator.addAnimations({
|
|
||||||
self.sourceView.layer.opacity = 1
|
|
||||||
sourceSnapshot.layer.opacity = 0
|
|
||||||
}, delayFactor: 0.5)
|
|
||||||
}
|
|
||||||
|
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
sourceSnapshot?.removeFromSuperview()
|
|
||||||
|
|
||||||
// Having dismissed, we don't need to undo any of the changes to the content VC.
|
|
||||||
|
|
||||||
transitionContext.completeTransition(true)
|
transitionContext.completeTransition(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,8 +20,6 @@ class GalleryDismissInteraction: NSObject {
|
|||||||
private(set) var dismissVelocity: CGPoint?
|
private(set) var dismissVelocity: CGPoint?
|
||||||
private(set) var dismissTranslation: CGPoint?
|
private(set) var dismissTranslation: CGPoint?
|
||||||
|
|
||||||
private var cancelAnimator: UIViewPropertyAnimator?
|
|
||||||
|
|
||||||
init(viewController: GalleryViewController) {
|
init(viewController: GalleryViewController) {
|
||||||
self.viewController = viewController
|
self.viewController = viewController
|
||||||
super.init()
|
super.init()
|
||||||
@ -40,8 +38,6 @@ class GalleryDismissInteraction: NSObject {
|
|||||||
content = viewController.currentItemViewController.takeContent()
|
content = viewController.currentItemViewController.takeContent()
|
||||||
content!.view.translatesAutoresizingMaskIntoConstraints = true
|
content!.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
content!.view.frame = origContentFrameInGallery!
|
content!.view.frame = origContentFrameInGallery!
|
||||||
// Make sure the context remains behind the controls
|
|
||||||
content!.view.layer.zPosition = -1000
|
|
||||||
viewController.view.addSubview(content!.view)
|
viewController.view.addSubview(content!.view)
|
||||||
|
|
||||||
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
origControlsVisible = viewController.currentItemViewController.controlsVisible
|
||||||
@ -57,42 +53,12 @@ class GalleryDismissInteraction: NSObject {
|
|||||||
let translation = recognizer.translation(in: viewController.view)
|
let translation = recognizer.translation(in: viewController.view)
|
||||||
let velocity = recognizer.velocity(in: viewController.view)
|
let velocity = recognizer.velocity(in: viewController.view)
|
||||||
|
|
||||||
let translationMagnitude = sqrt(translation.x.magnitudeSquared + translation.y.magnitudeSquared)
|
dismissVelocity = velocity
|
||||||
let velocityMagnitude = sqrt(velocity.x.magnitudeSquared + velocity.y.magnitudeSquared)
|
dismissTranslation = translation
|
||||||
|
viewController.dismiss(animated: true)
|
||||||
if translationMagnitude < 150 && velocityMagnitude < 500 {
|
|
||||||
isActive = false
|
|
||||||
|
|
||||||
cancelAnimator?.stopAnimation(true)
|
|
||||||
|
|
||||||
let spring = UISpringTimingParameters(mass: 1, stiffness: 439, damping: 42, initialVelocity: .zero)
|
|
||||||
cancelAnimator = UIViewPropertyAnimator(duration: 0.2, timingParameters: spring)
|
|
||||||
cancelAnimator!.addAnimations {
|
|
||||||
self.content!.view.frame = self.origContentFrameInGallery!
|
|
||||||
self.viewController.currentItemViewController.setControlsVisible(self.origControlsVisible!, animated: false, dueToUserInteraction: false)
|
|
||||||
}
|
|
||||||
cancelAnimator!.addCompletion { _ in
|
|
||||||
guard !self.isActive else {
|
|
||||||
// bail in case the animation finishing raced with the user's interaction
|
|
||||||
return
|
|
||||||
}
|
|
||||||
self.content!.view.layer.zPosition = 0
|
|
||||||
self.content!.view.removeFromSuperview()
|
|
||||||
self.viewController.currentItemViewController.addContent()
|
|
||||||
self.content = nil
|
|
||||||
self.origContentFrameInGallery = nil
|
|
||||||
self.origControlsVisible = nil
|
|
||||||
}
|
|
||||||
cancelAnimator!.startAnimation()
|
|
||||||
|
|
||||||
} else {
|
// don't unset this until after dismiss is called, so that the dismiss animation controller can read it
|
||||||
dismissVelocity = velocity
|
isActive = false
|
||||||
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:
|
default:
|
||||||
break
|
break
|
||||||
|
@ -11,7 +11,6 @@ import AVFoundation
|
|||||||
@MainActor
|
@MainActor
|
||||||
protocol GalleryItemViewControllerDelegate: AnyObject {
|
protocol GalleryItemViewControllerDelegate: AnyObject {
|
||||||
func isGalleryBeingPresented() -> Bool
|
func isGalleryBeingPresented() -> Bool
|
||||||
func isGalleryBeingDismissed() -> Bool
|
|
||||||
func addPresentationAnimationCompletion(_ block: @escaping () -> Void)
|
func addPresentationAnimationCompletion(_ block: @escaping () -> Void)
|
||||||
func galleryItemClose(_ item: GalleryItemViewController)
|
func galleryItemClose(_ item: GalleryItemViewController)
|
||||||
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]?
|
func galleryItemApplicationActivities(_ item: GalleryItemViewController) -> [UIActivity]?
|
||||||
@ -70,10 +69,6 @@ class GalleryItemViewController: UIViewController {
|
|||||||
scrollView = UIScrollView()
|
scrollView = UIScrollView()
|
||||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
scrollView.delegate = self
|
scrollView.delegate = self
|
||||||
// We calculate zoom/position ignoring the safe area, so content insets need to not incorporate it either.
|
|
||||||
// Otherwise, content that fills the screen (extending into the safe area) may still end up scrollable
|
|
||||||
// (this is readily observable with tall images on a landscape iPad).
|
|
||||||
scrollView.contentInsetAdjustmentBehavior = .never
|
|
||||||
|
|
||||||
view.addSubview(scrollView)
|
view.addSubview(scrollView)
|
||||||
|
|
||||||
@ -377,27 +372,13 @@ class GalleryItemViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateTopControlsInsets() {
|
private func updateTopControlsInsets() {
|
||||||
guard delegate?.isGalleryBeingDismissed() != true else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let notchedDeviceTopInsets: [CGFloat] = [
|
let notchedDeviceTopInsets: [CGFloat] = [
|
||||||
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
|
||||||
48, // iPhone XR, 11
|
48, // iPhone XR, 11
|
||||||
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
|
||||||
50, // iPhone 12 mini, 13 mini
|
50, // iPhone 12 mini, 13 mini
|
||||||
]
|
]
|
||||||
let topInset: CGFloat
|
if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
|
||||||
switch view.window?.windowScene?.interfaceOrientation {
|
|
||||||
case .portraitUpsideDown:
|
|
||||||
topInset = view.safeAreaInsets.bottom
|
|
||||||
case .landscapeLeft:
|
|
||||||
topInset = view.safeAreaInsets.right
|
|
||||||
case .landscapeRight:
|
|
||||||
topInset = view.safeAreaInsets.left
|
|
||||||
default:
|
|
||||||
topInset = view.safeAreaInsets.top
|
|
||||||
}
|
|
||||||
if notchedDeviceTopInsets.contains(topInset) {
|
|
||||||
// the notch width is not the same for the iPhones 13,
|
// the notch width is not the same for the iPhones 13,
|
||||||
// but what we actually want is the same offset from the edges
|
// but what we actually want is the same offset from the edges
|
||||||
// since the corner radius didn't change
|
// since the corner radius didn't change
|
||||||
@ -406,7 +387,7 @@ class GalleryItemViewController: UIViewController {
|
|||||||
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
|
let offset = (earWidth - (shareButton.imageView?.bounds.width ?? 0)) / 2
|
||||||
shareButtonLeadingConstraint.constant = offset
|
shareButtonLeadingConstraint.constant = offset
|
||||||
closeButtonTrailingConstraint.constant = offset
|
closeButtonTrailingConstraint.constant = offset
|
||||||
} else if topInset == 0 {
|
} else if view.safeAreaInsets.top == 0 {
|
||||||
// square corner devices
|
// square corner devices
|
||||||
shareButtonLeadingConstraint.constant = 8
|
shareButtonLeadingConstraint.constant = 8
|
||||||
shareButtonTopConstraint.constant = 8
|
shareButtonTopConstraint.constant = 8
|
||||||
|
@ -25,31 +25,11 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
|||||||
|
|
||||||
let itemViewController = to.currentItemViewController
|
let itemViewController = to.currentItemViewController
|
||||||
|
|
||||||
if itemViewController.content.presentationAnimation == .fade || UIAccessibility.prefersCrossFadeTransitions {
|
if !itemViewController.content.canAnimateFromSourceView || UIAccessibility.prefersCrossFadeTransitions {
|
||||||
animateCrossFadeTransition(using: transitionContext)
|
animateCrossFadeTransition(using: transitionContext)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to effectively "fade out" anything that's on top of the source view.
|
|
||||||
// The 0.1 duration makes this happen faster than the rest of the animation,
|
|
||||||
// and so less noticeable.
|
|
||||||
let sourceSnapshot: UIView? = if itemViewController.content.presentationAnimation == .fromSourceViewWithoutSnapshot {
|
|
||||||
nil
|
|
||||||
} else {
|
|
||||||
sourceView.snapshotView(afterScreenUpdates: false)
|
|
||||||
}
|
|
||||||
if let sourceSnapshot {
|
|
||||||
let snapshotContainer = sourceView.ancestorForInsertingSnapshot
|
|
||||||
snapshotContainer.addSubview(sourceSnapshot)
|
|
||||||
let sourceFrameInShapshotContainer = snapshotContainer.convert(sourceView.bounds, from: sourceView)
|
|
||||||
sourceSnapshot.frame = sourceFrameInShapshotContainer
|
|
||||||
sourceSnapshot.transform = sourceView.transform
|
|
||||||
sourceSnapshot.layer.opacity = 0
|
|
||||||
UIView.animate(withDuration: 0.1) {
|
|
||||||
sourceSnapshot.layer.opacity = 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let container = transitionContext.containerView
|
let container = transitionContext.containerView
|
||||||
to.view.frame = container.bounds
|
to.view.frame = container.bounds
|
||||||
container.addSubview(to.view)
|
container.addSubview(to.view)
|
||||||
@ -76,70 +56,21 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
|||||||
sourceToDestTransform = nil
|
sourceToDestTransform = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grab these before taking the content out and changing the transform.
|
|
||||||
let origContentTransform = itemViewController.content.view.transform
|
|
||||||
let origContentFrame = itemViewController.content.view.frame
|
|
||||||
|
|
||||||
// The content container provides the clipping for the content view,
|
|
||||||
// which, in case the source/dest aspect ratios don't match, makes
|
|
||||||
// it look like the content is expanding out from the source rect.
|
|
||||||
let contentContainer = UIView()
|
|
||||||
contentContainer.layer.masksToBounds = true
|
|
||||||
container.insertSubview(contentContainer, belowSubview: to.view)
|
|
||||||
let content = itemViewController.takeContent()
|
let content = itemViewController.takeContent()
|
||||||
content.view.translatesAutoresizingMaskIntoConstraints = true
|
content.view.translatesAutoresizingMaskIntoConstraints = true
|
||||||
content.view.transform = .identity
|
container.insertSubview(content.view, belowSubview: to.view)
|
||||||
// The fade-in makes the aspect ratio handling look a little bit worse,
|
|
||||||
// but papers over the z-index change and potential corner radius change.
|
|
||||||
content.view.layer.opacity = 0
|
|
||||||
contentContainer.addSubview(content.view)
|
|
||||||
|
|
||||||
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
|
// Use a separate dimming view from to.view, so that the gallery controls can be in front of the moving content.
|
||||||
let dimmingView = UIView()
|
let dimmingView = UIView()
|
||||||
dimmingView.backgroundColor = .black
|
dimmingView.backgroundColor = .black
|
||||||
dimmingView.frame = container.bounds
|
dimmingView.frame = container.bounds
|
||||||
dimmingView.layer.opacity = 0
|
dimmingView.layer.opacity = 0
|
||||||
container.insertSubview(dimmingView, belowSubview: contentContainer)
|
container.insertSubview(dimmingView, belowSubview: content.view)
|
||||||
|
|
||||||
to.view.backgroundColor = nil
|
to.view.backgroundColor = nil
|
||||||
to.view.layer.opacity = 0
|
to.view.layer.opacity = 0
|
||||||
|
content.view.frame = sourceFrameInContainer
|
||||||
contentContainer.frame = sourceFrameInContainer
|
content.view.layer.opacity = 0
|
||||||
|
|
||||||
let sourceAspectRatio: CGFloat = if sourceFrameInContainer.height > 0 {
|
|
||||||
sourceFrameInContainer.width / sourceFrameInContainer.height
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
let destAspectRatio: CGFloat = if destFrameInContainer.height > 0 {
|
|
||||||
destFrameInContainer.width / destFrameInContainer.height
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
}
|
|
||||||
let sourceSizeWithDestAspectRatioCenteredInContentContainer: CGRect
|
|
||||||
if 0.001 < abs(sourceAspectRatio - destAspectRatio) {
|
|
||||||
// asepct ratios are effectively equal
|
|
||||||
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(origin: .zero, size: sourceFrameInContainer.size)
|
|
||||||
} else if sourceAspectRatio < destAspectRatio {
|
|
||||||
// source aspect ratio is narrow/taller than dest
|
|
||||||
let width = sourceFrameInContainer.height * destAspectRatio
|
|
||||||
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(
|
|
||||||
x: -(width - sourceFrameInContainer.width) / 2,
|
|
||||||
y: 0,
|
|
||||||
width: width,
|
|
||||||
height: sourceFrameInContainer.height
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// source aspect ratio is wider/shorter than dest
|
|
||||||
let height = sourceFrameInContainer.width / destAspectRatio
|
|
||||||
sourceSizeWithDestAspectRatioCenteredInContentContainer = CGRect(
|
|
||||||
x: 0,
|
|
||||||
y: -(height - sourceFrameInContainer.height) / 2,
|
|
||||||
width: sourceFrameInContainer.width,
|
|
||||||
height: height
|
|
||||||
)
|
|
||||||
}
|
|
||||||
content.view.frame = sourceSizeWithDestAspectRatioCenteredInContentContainer
|
|
||||||
|
|
||||||
container.layoutIfNeeded()
|
container.layoutIfNeeded()
|
||||||
|
|
||||||
@ -147,14 +78,8 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
|||||||
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
itemViewController.setControlsVisible(false, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
let duration = self.transitionDuration(using: transitionContext)
|
let duration = self.transitionDuration(using: transitionContext)
|
||||||
// less bounce on bigger screens
|
// rougly equivalent to duration: 0.35, bounce: 0.3
|
||||||
let spring = if UIDevice.current.userInterfaceIdiom == .pad {
|
let spring = UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
|
||||||
// roughly equivalent to duration: 0.35, bounce: 0.2
|
|
||||||
UISpringTimingParameters(mass: 1, stiffness: 322, damping: 28, initialVelocity: .zero)
|
|
||||||
} else {
|
|
||||||
// roughly equivalent to duration: 0.35, bounce: 0.3
|
|
||||||
UISpringTimingParameters(mass: 1, stiffness: 322, damping: 25, initialVelocity: .zero)
|
|
||||||
}
|
|
||||||
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
let animator = UIViewPropertyAnimator(duration: duration, timingParameters: spring)
|
||||||
|
|
||||||
animator.addAnimations {
|
animator.addAnimations {
|
||||||
@ -162,35 +87,25 @@ class GalleryPresentationAnimationController: NSObject, UIViewControllerAnimated
|
|||||||
|
|
||||||
to.view.layer.opacity = 1
|
to.view.layer.opacity = 1
|
||||||
|
|
||||||
contentContainer.frame = destFrameInContainer
|
content.view.frame = destFrameInContainer
|
||||||
content.view.frame = contentContainer.bounds
|
|
||||||
content.view.layer.opacity = 1
|
content.view.layer.opacity = 1
|
||||||
|
|
||||||
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
|
itemViewController.setControlsVisible(true, animated: false, dueToUserInteraction: false)
|
||||||
|
|
||||||
if let sourceToDestTransform {
|
if let sourceToDestTransform {
|
||||||
sourceSnapshot?.transform = sourceToDestTransform
|
|
||||||
self.sourceView.transform = sourceToDestTransform
|
self.sourceView.transform = sourceToDestTransform
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animator.addCompletion { _ in
|
animator.addCompletion { _ in
|
||||||
sourceSnapshot?.removeFromSuperview()
|
|
||||||
self.sourceView.layer.opacity = 1
|
|
||||||
if sourceToDestTransform != nil {
|
|
||||||
self.sourceView.transform = origSourceTransform
|
|
||||||
}
|
|
||||||
|
|
||||||
contentContainer.removeFromSuperview()
|
|
||||||
dimmingView.removeFromSuperview()
|
dimmingView.removeFromSuperview()
|
||||||
|
|
||||||
to.view.backgroundColor = .black
|
to.view.backgroundColor = .black
|
||||||
|
|
||||||
// Reset the properties we changed before re-adding the content to the scroll view.
|
if sourceToDestTransform != nil {
|
||||||
// (I would expect UIScrollView to effectively do this itself, but w/e.)
|
self.sourceView.transform = origSourceTransform
|
||||||
content.view.transform = origContentTransform
|
}
|
||||||
content.view.frame = origContentFrame
|
|
||||||
|
|
||||||
itemViewController.addContent()
|
itemViewController.addContent()
|
||||||
|
|
||||||
transitionContext.completeTransition(true)
|
transitionContext.completeTransition(true)
|
||||||
|
@ -139,10 +139,6 @@ extension GalleryViewController: GalleryItemViewControllerDelegate {
|
|||||||
isBeingPresented
|
isBeingPresented
|
||||||
}
|
}
|
||||||
|
|
||||||
func isGalleryBeingDismissed() -> Bool {
|
|
||||||
isBeingDismissed
|
|
||||||
}
|
|
||||||
|
|
||||||
func addPresentationAnimationCompletion(_ block: @escaping () -> Void) {
|
func addPresentationAnimationCompletion(_ block: @escaping () -> Void) {
|
||||||
presentationAnimationCompletionHandlers.append(block)
|
presentationAnimationCompletionHandlers.append(block)
|
||||||
}
|
}
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
//
|
|
||||||
// UIView+Utilities.swift
|
|
||||||
// GalleryVC
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/24/24.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
extension UIView {
|
|
||||||
|
|
||||||
var ancestorForInsertingSnapshot: UIView {
|
|
||||||
var view = self
|
|
||||||
while let superview = view.superview {
|
|
||||||
if superview.layer.masksToBounds {
|
|
||||||
return superview
|
|
||||||
} else if superview is UIScrollView {
|
|
||||||
return self
|
|
||||||
} else {
|
|
||||||
view = superview
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -23,15 +23,9 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "InstanceFeatures",
|
name: "InstanceFeatures",
|
||||||
dependencies: ["Pachyderm"],
|
dependencies: ["Pachyderm"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "InstanceFeaturesTests",
|
name: "InstanceFeaturesTests",
|
||||||
dependencies: ["InstanceFeatures"],
|
dependencies: ["InstanceFeatures"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.8
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -18,10 +18,7 @@ let package = Package(
|
|||||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
// 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.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "MatchedGeometryPresentation",
|
name: "MatchedGeometryPresentation"),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "MatchedGeometryPresentationTests",
|
// name: "MatchedGeometryPresentationTests",
|
||||||
// dependencies: ["MatchedGeometryPresentation"]),
|
// dependencies: ["MatchedGeometryPresentation"]),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.6
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -26,15 +26,9 @@ let package = Package(
|
|||||||
dependencies: [
|
dependencies: [
|
||||||
.product(name: "WebURL", package: "swift-url"),
|
.product(name: "WebURL", package: "swift-url"),
|
||||||
.product(name: "WebURLFoundationExtras", package: "swift-url"),
|
.product(name: "WebURLFoundationExtras", package: "swift-url"),
|
||||||
],
|
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
]),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "PachydermTests",
|
name: "PachydermTests",
|
||||||
dependencies: ["Pachyderm"],
|
dependencies: ["Pachyderm"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -25,30 +25,27 @@ public struct Client: Sendable {
|
|||||||
|
|
||||||
public var timeoutInterval: TimeInterval = 60
|
public var timeoutInterval: TimeInterval = 60
|
||||||
|
|
||||||
private static let dateFormatter: DateFormatter = {
|
static let decoder: JSONDecoder = {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
|
||||||
let formatter = DateFormatter()
|
let formatter = DateFormatter()
|
||||||
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SZ"
|
||||||
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
formatter.timeZone = TimeZone(abbreviation: "UTC")
|
||||||
formatter.locale = Locale(identifier: "en_US_POSIX")
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
||||||
return formatter
|
let iso8601 = ISO8601DateFormatter()
|
||||||
}()
|
|
||||||
private static let iso8601Formatter = ISO8601DateFormatter()
|
|
||||||
private static func decodeDate(string: String) -> Date? {
|
|
||||||
// for the next time mastodon accidentally changes date formats >.>
|
|
||||||
return dateFormatter.date(from: string) ?? iso8601Formatter.date(from: string)
|
|
||||||
}
|
|
||||||
|
|
||||||
static let decoder: JSONDecoder = {
|
|
||||||
let decoder = JSONDecoder()
|
|
||||||
decoder.dateDecodingStrategy = .custom({ (decoder) in
|
decoder.dateDecodingStrategy = .custom({ (decoder) in
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
let str = try container.decode(String.self)
|
let str = try container.decode(String.self)
|
||||||
if let date = Self.decodeDate(string: str) {
|
// for the next time mastodon accidentally changes date formats >.>
|
||||||
|
if let date = formatter.date(from: str) {
|
||||||
|
return date
|
||||||
|
} else if let date = iso8601.date(from: str) {
|
||||||
return date
|
return date
|
||||||
} else {
|
} else {
|
||||||
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
|
throw DecodingError.typeMismatch(Date.self, .init(codingPath: container.codingPath, debugDescription: "unexpected date format: \(str)"))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return decoder
|
return decoder
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@ -108,15 +105,6 @@ public struct Client: Sendable {
|
|||||||
return task
|
return task
|
||||||
}
|
}
|
||||||
|
|
||||||
private func error(from response: HTTPURLResponse) -> ErrorType {
|
|
||||||
if response.statusCode == 429,
|
|
||||||
let date = response.value(forHTTPHeaderField: "X-RateLimit-Reset").flatMap(Self.decodeDate) {
|
|
||||||
return .rateLimited(date)
|
|
||||||
} else {
|
|
||||||
return .unexpectedStatus(response.statusCode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
public func run<Result: Sendable>(_ request: Request<Result>) async throws -> (Result, Pagination?) {
|
||||||
return try await withCheckedThrowingContinuation { continuation in
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
@ -587,8 +575,6 @@ extension Client {
|
|||||||
return "Invalid Model"
|
return "Invalid Model"
|
||||||
case .mastodonError(let code, let error):
|
case .mastodonError(let code, let error):
|
||||||
return "Server Error (\(code)): \(error)"
|
return "Server Error (\(code)): \(error)"
|
||||||
case .rateLimited(let reset):
|
|
||||||
return "Rate Limited Until \(reset.formatted(date: .omitted, time: .standard))"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -599,7 +585,6 @@ extension Client {
|
|||||||
case invalidResponse
|
case invalidResponse
|
||||||
case invalidModel(Swift.Error)
|
case invalidModel(Swift.Error)
|
||||||
case mastodonError(Int, String)
|
case mastodonError(Int, String)
|
||||||
case rateLimited(Date)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum NodeInfoError: LocalizedError {
|
enum NodeInfoError: LocalizedError {
|
||||||
|
@ -22,12 +22,7 @@ public struct Emoji: Codable, Sendable {
|
|||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
self.shortcode = try container.decode(String.self, forKey: .shortcode)
|
||||||
do {
|
self.url = try container.decode(WebURL.self, forKey: .url)
|
||||||
self.url = try container.decode(WebURL.self, forKey: .url)
|
|
||||||
} catch {
|
|
||||||
let s = try? container.decode(String.self, forKey: .url)
|
|
||||||
throw DecodingError.dataCorrupted(.init(codingPath: container.codingPath + [CodingKeys.url], debugDescription: "Could not decode URL '\(s ?? "<failed to decode string>")'", underlyingError: error))
|
|
||||||
}
|
|
||||||
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
|
self.staticURL = try container.decode(WebURL.self, forKey: .staticURL)
|
||||||
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
self.visibleInPicker = try container.decode(Bool.self, forKey: .visibleInPicker)
|
||||||
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
self.category = try container.decodeIfPresent(String.self, forKey: .category)
|
||||||
|
@ -197,72 +197,72 @@ class NotificationGroupTests: XCTestCase {
|
|||||||
|
|
||||||
func testGroupSimple() {
|
func testGroupSimple() {
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
|
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeA2], only: [.favourite])
|
||||||
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!])
|
XCTAssertEqual(groups, [NotificationGroup(notifications: [likeA1, likeA2])!])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testGroupWithOtherGroupableInBetween() {
|
func testGroupWithOtherGroupableInBetween() {
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
|
let groups = NotificationGroup.createGroups(notifications: [likeA1, likeB, likeA2], only: [.favourite])
|
||||||
XCTAssertEqual(groups, [
|
XCTAssertEqual(groups, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA1, likeA2])!,
|
||||||
NotificationGroup(notifications: [likeB], kind: .favourite)!,
|
NotificationGroup(notifications: [likeB])!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDontGroupWithUngroupableInBetween() {
|
func testDontGroupWithUngroupableInBetween() {
|
||||||
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
|
let groups = NotificationGroup.createGroups(notifications: [likeA1, mentionB, likeA2], only: [.favourite])
|
||||||
XCTAssertEqual(groups, [
|
XCTAssertEqual(groups, [
|
||||||
NotificationGroup(notifications: [likeA1], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA1])!,
|
||||||
NotificationGroup(notifications: [mentionB], kind: .mention)!,
|
NotificationGroup(notifications: [mentionB])!,
|
||||||
NotificationGroup(notifications: [likeA2], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA2])!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMergeSimpleGroups() {
|
func testMergeSimpleGroups() {
|
||||||
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)!
|
let group1 = NotificationGroup(notifications: [likeA1])!
|
||||||
let group2 = NotificationGroup(notifications: [likeA2], kind: .favourite)!
|
let group2 = NotificationGroup(notifications: [likeA2])!
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
|
let merged = NotificationGroup.mergeGroups(first: [group1], second: [group2], only: [.favourite])
|
||||||
XCTAssertEqual(merged, [
|
XCTAssertEqual(merged, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!
|
NotificationGroup(notifications: [likeA1, likeA2])!
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMergeGroupsWithOtherGroupableInBetween() {
|
func testMergeGroupsWithOtherGroupableInBetween() {
|
||||||
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)!
|
let group1 = NotificationGroup(notifications: [likeA1])!
|
||||||
let group2 = NotificationGroup(notifications: [likeB], kind: .favourite)!
|
let group2 = NotificationGroup(notifications: [likeB])!
|
||||||
let group3 = NotificationGroup(notifications: [likeA2], kind: .favourite)!
|
let group3 = NotificationGroup(notifications: [likeA2])!
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
||||||
XCTAssertEqual(merged, [
|
XCTAssertEqual(merged, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA1, likeA2])!,
|
||||||
NotificationGroup(notifications: [likeB], kind: .favourite)!,
|
NotificationGroup(notifications: [likeB])!,
|
||||||
])
|
])
|
||||||
|
|
||||||
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
|
let merged2 = NotificationGroup.mergeGroups(first: [group1], second: [group2, group3], only: [.favourite])
|
||||||
XCTAssertEqual(merged2, [
|
XCTAssertEqual(merged2, [
|
||||||
NotificationGroup(notifications: [likeA1, likeA2], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA1, likeA2])!,
|
||||||
NotificationGroup(notifications: [likeB], kind: .favourite)!,
|
NotificationGroup(notifications: [likeB])!,
|
||||||
])
|
])
|
||||||
|
|
||||||
let group4 = NotificationGroup(notifications: [likeB2], kind: .favourite)!
|
let group4 = NotificationGroup(notifications: [likeB2])!
|
||||||
let group5 = NotificationGroup(notifications: [mentionB], kind: .mention)!
|
let group5 = NotificationGroup(notifications: [mentionB])!
|
||||||
let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite])
|
let merged3 = NotificationGroup.mergeGroups(first: [group1, group5, group2], second: [group4, group3], only: [.favourite])
|
||||||
print(merged3.count)
|
print(merged3.count)
|
||||||
XCTAssertEqual(merged3, [
|
XCTAssertEqual(merged3, [
|
||||||
group1,
|
group1,
|
||||||
group5,
|
group5,
|
||||||
NotificationGroup(notifications: [likeB, likeB2], kind: .favourite),
|
NotificationGroup(notifications: [likeB, likeB2]),
|
||||||
group3
|
group3
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDontMergeWithUngroupableInBetween() {
|
func testDontMergeWithUngroupableInBetween() {
|
||||||
let group1 = NotificationGroup(notifications: [likeA1], kind: .favourite)!
|
let group1 = NotificationGroup(notifications: [likeA1])!
|
||||||
let group2 = NotificationGroup(notifications: [mentionB], kind: .mention)!
|
let group2 = NotificationGroup(notifications: [mentionB])!
|
||||||
let group3 = NotificationGroup(notifications: [likeA2], kind: .favourite)!
|
let group3 = NotificationGroup(notifications: [likeA2])!
|
||||||
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
let merged = NotificationGroup.mergeGroups(first: [group1, group2], second: [group3], only: [.favourite])
|
||||||
XCTAssertEqual(merged, [
|
XCTAssertEqual(merged, [
|
||||||
NotificationGroup(notifications: [likeA1], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA1])!,
|
||||||
NotificationGroup(notifications: [mentionB], kind: .mention)!,
|
NotificationGroup(notifications: [mentionB])!,
|
||||||
NotificationGroup(notifications: [likeA2], kind: .favourite)!,
|
NotificationGroup(notifications: [likeA2])!,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.10
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -23,17 +23,10 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "PushNotifications",
|
name: "PushNotifications",
|
||||||
dependencies: ["UserAccounts", "Pachyderm"],
|
dependencies: ["UserAccounts", "Pachyderm"]
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "PushNotificationsTests",
|
name: "PushNotificationsTests",
|
||||||
dependencies: ["PushNotifications"],
|
dependencies: ["PushNotifications"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -23,15 +23,9 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "TTTKit",
|
name: "TTTKit",
|
||||||
dependencies: [],
|
dependencies: []),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "TTTKitTests",
|
name: "TTTKitTests",
|
||||||
dependencies: ["TTTKit"],
|
dependencies: ["TTTKit"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -23,10 +23,7 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "TuskerComponents",
|
name: "TuskerComponents",
|
||||||
dependencies: [],
|
dependencies: []),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "TuskerComponentsTests",
|
// name: "TuskerComponentsTests",
|
||||||
// dependencies: ["TuskerComponents"]),
|
// dependencies: ["TuskerComponents"]),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.8
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -22,17 +22,11 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package and products from dependencies.
|
// Targets can depend on other targets in this package and products from dependencies.
|
||||||
.target(
|
.target(
|
||||||
name: "TuskerPreferences",
|
name: "TuskerPreferences",
|
||||||
dependencies: ["Pachyderm"],
|
dependencies: ["Pachyderm"]
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]
|
|
||||||
),
|
),
|
||||||
.testTarget(
|
.testTarget(
|
||||||
name: "TuskerPreferencesTests",
|
name: "TuskerPreferencesTests",
|
||||||
dependencies: ["TuskerPreferences"],
|
dependencies: ["TuskerPreferences"]
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
// swift-tools-version: 6.0
|
// swift-tools-version: 5.7
|
||||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
import PackageDescription
|
import PackageDescription
|
||||||
@ -23,10 +23,7 @@ let package = Package(
|
|||||||
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
// Targets can depend on other targets in this package, and on products in packages this package depends on.
|
||||||
.target(
|
.target(
|
||||||
name: "UserAccounts",
|
name: "UserAccounts",
|
||||||
dependencies: ["Pachyderm"],
|
dependencies: ["Pachyderm"]),
|
||||||
swiftSettings: [
|
|
||||||
.swiftLanguageMode(.v5)
|
|
||||||
]),
|
|
||||||
// .testTarget(
|
// .testTarget(
|
||||||
// name: "UserAccountsTests",
|
// name: "UserAccountsTests",
|
||||||
// dependencies: ["UserAccounts"]),
|
// dependencies: ["UserAccounts"]),
|
||||||
|
@ -204,16 +204,20 @@
|
|||||||
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 */; };
|
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 /* GrayscalableImageGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.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 */; };
|
D6934F342BA8D65A002B1C8D /* ImageGalleryDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */; };
|
||||||
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
|
D6934F362BA8E020002B1C8D /* GifvGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */; };
|
||||||
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
|
D6934F382BA8E2B7002B1C8D /* GifvController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F372BA8E2B7002B1C8D /* GifvController.swift */; };
|
||||||
D6934F3C2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.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 */; };
|
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 */; };
|
||||||
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
D693DE5723FE1A6A0061E07D /* EnhancedNavigationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */; };
|
||||||
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
D693DE5923FE24310061E07D /* InteractivePushTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = D693DE5823FE24300061E07D /* InteractivePushTransition.swift */; };
|
||||||
@ -635,15 +639,19 @@
|
|||||||
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>"; };
|
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 /* GrayscalableImageGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableImageGalleryContentViewController.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>"; };
|
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>"; };
|
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>"; };
|
D6934F372BA8E2B7002B1C8D /* GifvController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GifvController.swift; sourceTree = "<group>"; };
|
||||||
D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GrayscalableVideoGalleryContentViewController.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>"; };
|
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>"; };
|
||||||
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
D693DE5623FE1A6A0061E07D /* EnhancedNavigationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedNavigationViewController.swift; sourceTree = "<group>"; };
|
||||||
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
D693DE5823FE24300061E07D /* InteractivePushTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractivePushTransition.swift; sourceTree = "<group>"; };
|
||||||
@ -893,9 +901,13 @@
|
|||||||
children = (
|
children = (
|
||||||
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
|
D6934F2D2BA7AEF9002B1C8D /* StatusAttachmentsGalleryDataSource.swift */,
|
||||||
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
|
D6934F332BA8D65A002B1C8D /* ImageGalleryDataSource.swift */,
|
||||||
D6934F2F2BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift */,
|
D6934F312BA7E43E002B1C8D /* LoadingGalleryContentViewController.swift */,
|
||||||
|
D6934F2F2BA7AF91002B1C8D /* ImageGalleryContentViewController.swift */,
|
||||||
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
D6934F352BA8E020002B1C8D /* GifvGalleryContentViewController.swift */,
|
||||||
D6934F3B2BAA0F80002B1C8D /* GrayscalableVideoGalleryContentViewController.swift */,
|
D6934F3B2BAA0F80002B1C8D /* VideoGalleryContentViewController.swift */,
|
||||||
|
D69261222BB3AEFB0023152C /* VideoOverlayViewController.swift */,
|
||||||
|
D6934F412BAC7D6E002B1C8D /* VideoControlsViewController.swift */,
|
||||||
|
D6934F392BA8F3D7002B1C8D /* FallbackGalleryContentViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Gallery;
|
path = Gallery;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -2113,6 +2125,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 */,
|
||||||
@ -2159,7 +2172,8 @@
|
|||||||
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 /* GrayscalableVideoGalleryContentViewController.swift in Sources */,
|
D69261232BB3AEFB0023152C /* VideoOverlayViewController.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 */,
|
||||||
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
|
D698F46B2BD079F00054DB14 /* AnnouncementListRow.swift in Sources */,
|
||||||
@ -2179,6 +2193,7 @@
|
|||||||
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 */,
|
||||||
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
D64A50482C739DEA009D7193 /* BaseMainTabBarViewController.swift in Sources */,
|
||||||
@ -2206,6 +2221,7 @@
|
|||||||
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.swift in Sources */,
|
D630C3CC2BC5FD4600208903 /* GetAuthorizationTokenService.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 */,
|
||||||
@ -2328,7 +2344,7 @@
|
|||||||
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
D61A45EA28DF51EE002BE511 /* TimelineLikeCollectionViewController.swift in Sources */,
|
||||||
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
D64D0AB12128D9AE005A6F37 /* OnboardingViewController.swift in Sources */,
|
||||||
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
|
D64A50BE2C752247009D7193 /* AdaptableNavigationController.swift in Sources */,
|
||||||
D6934F302BA7AF91002B1C8D /* GrayscalableImageGalleryContentViewController.swift in Sources */,
|
D6934F302BA7AF91002B1C8D /* ImageGalleryContentViewController.swift in Sources */,
|
||||||
D6934F3E2BAA19D5002B1C8D /* ImageActivityItemSource.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 */,
|
||||||
|
@ -375,14 +375,13 @@ class MastodonCachePersistentStore: NSPersistentCloudKitContainer, @unchecked Se
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAll(notifications: [Pachyderm.Notification], in context: NSManagedObjectContext? = nil, completion: (() -> Void)? = nil) {
|
func addAll(notifications: [Pachyderm.Notification], completion: (() -> Void)? = nil) {
|
||||||
let context = context ?? backgroundContext
|
backgroundContext.perform {
|
||||||
context.perform {
|
|
||||||
let statuses = notifications.compactMap { $0.status }
|
let statuses = notifications.compactMap { $0.status }
|
||||||
let accounts = notifications.map { $0.account }
|
let accounts = notifications.map { $0.account }
|
||||||
statuses.forEach { self.upsert(status: $0, context: context) }
|
statuses.forEach { self.upsert(status: $0, context: self.backgroundContext) }
|
||||||
accounts.forEach { self.upsert(account: $0, in: context) }
|
accounts.forEach { self.upsert(account: $0, in: self.backgroundContext) }
|
||||||
self.save(context: context)
|
self.save(context: self.backgroundContext)
|
||||||
completion?()
|
completion?()
|
||||||
statuses.forEach { self.statusSubject.send($0.id) }
|
statuses.forEach { self.statusSubject.send($0.id) }
|
||||||
accounts.forEach { self.accountSubject.send($0.id) }
|
accounts.forEach { self.accountSubject.send($0.id) }
|
||||||
|
@ -484,11 +484,3 @@ extension ConversationViewController: StatusBarTappableViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConversationViewController: RefreshableViewController {
|
|
||||||
func refresh() {
|
|
||||||
Task {
|
|
||||||
await refreshContext()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -16,11 +16,15 @@ struct TrendingLinkCardView: View {
|
|||||||
let card: Card
|
let card: Card
|
||||||
|
|
||||||
private var imageURL: URL? {
|
private var imageURL: URL? {
|
||||||
card.image.flatMap { URL($0) }
|
if let image = card.image {
|
||||||
|
URL(image)
|
||||||
|
} else {
|
||||||
|
nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var descriptionText: String {
|
private var descriptionText: String {
|
||||||
let converter = TextConverter(configuration: .init(insertNewlines: false))
|
var converter = TextConverter(configuration: .init(insertNewlines: false))
|
||||||
return converter.convert(html: card.description)
|
return converter.convert(html: card.description)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,15 @@
|
|||||||
//
|
//
|
||||||
// FallbackGalleryContentViewController.swift
|
// FallbackGalleryContentViewController.swift
|
||||||
// GalleryVC
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/18/24.
|
// Created by Shadowfacts on 3/18/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
import QuickLook
|
import QuickLook
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
private class FallbackGalleryContentViewController: QLPreviewController {
|
private class FallbackGalleryContentViewController: QLPreviewController {
|
||||||
private let previewItem = GalleryPreviewItem()
|
private let previewItem = GalleryPreviewItem()
|
||||||
@ -50,40 +52,40 @@ extension FallbackGalleryContentViewController: QLPreviewControllerDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController {
|
class FallbackGalleryNavigationController: UINavigationController, GalleryContentViewController {
|
||||||
public init(url: URL) {
|
init(url: URL) {
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
self.viewControllers = [FallbackGalleryContentViewController(url: url)]
|
self.viewControllers = [FallbackGalleryContentViewController(url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
container?.disableGalleryScrollAndZoom()
|
container?.disableGalleryScrollAndZoom()
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder aDecoder: NSCoder) {
|
required init?(coder aDecoder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
public weak var container: (any GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
public var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
.zero
|
.zero
|
||||||
}
|
}
|
||||||
|
|
||||||
public var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
[]
|
[]
|
||||||
}
|
}
|
||||||
|
|
||||||
public var caption: String? {
|
var caption: String? {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public var presentationAnimation: GalleryContentPresentationAnimation {
|
var canAnimateFromSourceView: Bool {
|
||||||
.fade
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,8 +69,4 @@ class GifvGalleryContentViewController: UIViewController, GalleryContentViewCont
|
|||||||
[VideoActivityItemSource(asset: controller.item.asset, url: url)]
|
[VideoActivityItemSource(asset: controller.item.asset, url: url)]
|
||||||
}
|
}
|
||||||
|
|
||||||
var presentationAnimation: GalleryContentPresentationAnimation {
|
|
||||||
.fromSourceViewWithoutSnapshot
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
//
|
|
||||||
// GrayscalableImageGalleryContentViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/21/24.
|
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import TuskerComponents
|
|
||||||
import GalleryVC
|
|
||||||
|
|
||||||
class GrayscalableImageGalleryContentViewController: GalleryVC.ImageGalleryContentViewController {
|
|
||||||
private let url: URL
|
|
||||||
private let originalImage: UIImage
|
|
||||||
private let originalData: Data?
|
|
||||||
private var isGrayscale = false
|
|
||||||
|
|
||||||
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
|
|
||||||
self.url = url
|
|
||||||
self.originalImage = image
|
|
||||||
self.originalData = originalData
|
|
||||||
|
|
||||||
super.init(image: image, caption: caption, gifController: gifController)
|
|
||||||
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
if isGrayscale {
|
|
||||||
self.image = ImageGrayscalifier.convert(url: url, image: image) ?? image
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
let image = if isGrayscale {
|
|
||||||
ImageGrayscalifier.convert(url: url, image: originalImage)
|
|
||||||
} else {
|
|
||||||
originalImage
|
|
||||||
}
|
|
||||||
if let image {
|
|
||||||
self.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override var activityItemsForSharing: [Any] {
|
|
||||||
if let data = originalData ?? image.pngData() {
|
|
||||||
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
|
||||||
} else {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,83 +0,0 @@
|
|||||||
//
|
|
||||||
// GrayscalableVideoGalleryContentViewController.swift
|
|
||||||
// Tusker
|
|
||||||
//
|
|
||||||
// Created by Shadowfacts on 11/21/24.
|
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import UIKit
|
|
||||||
import GalleryVC
|
|
||||||
import AVFoundation
|
|
||||||
|
|
||||||
class GrayscalableVideoGalleryContentViewController: GalleryVC.VideoGalleryContentViewController {
|
|
||||||
private var audioSessionToken: AudioSessionCoordinator.Token?
|
|
||||||
private var isGrayscale: Bool
|
|
||||||
private var isFirstAppearance = true
|
|
||||||
|
|
||||||
override init(url: URL, caption: String?) {
|
|
||||||
self.isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
|
|
||||||
super.init(url: url, caption: caption)
|
|
||||||
|
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainActor required init?(coder: NSCoder) {
|
|
||||||
fatalError("init(coder:) has not been implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override class func createItem(asset: AVAsset) -> AVPlayerItem {
|
|
||||||
let item = AVPlayerItem(asset: asset)
|
|
||||||
if Preferences.shared.grayscaleImages {
|
|
||||||
#if os(visionOS)
|
|
||||||
#warning("Use async AVVideoComposition CIFilter initializer")
|
|
||||||
#else
|
|
||||||
let filter = CIFilter(name: "CIColorMonochrome")!
|
|
||||||
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
|
|
||||||
filter.setValue(1.0, forKey: "inputIntensity")
|
|
||||||
|
|
||||||
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
|
|
||||||
filter.setValue(request.sourceImage, forKey: "inputImage")
|
|
||||||
request.finish(with: filter.outputImage!, context: nil)
|
|
||||||
})
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
return item
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func preferencesChanged() {
|
|
||||||
if isGrayscale != Preferences.shared.grayscaleImages {
|
|
||||||
let isPlaying = player.rate > 0
|
|
||||||
isGrayscale = Preferences.shared.grayscaleImages
|
|
||||||
replaceCurrentItem(with: Self.createItem(asset: item.asset))
|
|
||||||
if isPlaying {
|
|
||||||
player.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func galleryContentDidAppear() {
|
|
||||||
super.galleryContentDidAppear()
|
|
||||||
|
|
||||||
let wasFirstAppearance = isFirstAppearance
|
|
||||||
isFirstAppearance = false
|
|
||||||
|
|
||||||
audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
|
|
||||||
if wasFirstAppearance {
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
self.player.play()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override func galleryContentWillDisappear() {
|
|
||||||
super.galleryContentWillDisappear()
|
|
||||||
|
|
||||||
if let audioSessionToken {
|
|
||||||
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -1,22 +1,22 @@
|
|||||||
//
|
//
|
||||||
// ImageGalleryContentViewController.swift
|
// ImageGalleryContentViewController.swift
|
||||||
// GalleryVC
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/17/24.
|
// Created by Shadowfacts on 3/17/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
import Pachyderm
|
||||||
import TuskerComponents
|
import TuskerComponents
|
||||||
@preconcurrency import VisionKit
|
@preconcurrency import VisionKit
|
||||||
|
|
||||||
open class ImageGalleryContentViewController: UIViewController, GalleryContentViewController {
|
class ImageGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
public let caption: String?
|
let url: URL
|
||||||
public var image: UIImage {
|
let caption: String?
|
||||||
didSet {
|
let originalData: Data?
|
||||||
imageView?.image = image
|
let image: UIImage
|
||||||
}
|
|
||||||
}
|
|
||||||
let gifController: GIFController?
|
let gifController: GIFController?
|
||||||
|
|
||||||
private var imageView: GIFImageView!
|
private var imageView: GIFImageView!
|
||||||
@ -27,8 +27,12 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
@available(iOS 16.0, macCatalyst 17.0, *)
|
@available(iOS 16.0, macCatalyst 17.0, *)
|
||||||
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
|
private var analysisInteraction: ImageAnalysisInteraction? { _analysisInteraction as? ImageAnalysisInteraction }
|
||||||
|
|
||||||
public init(image: UIImage, caption: String?, gifController: GIFController?) {
|
private var isGrayscale = false
|
||||||
|
|
||||||
|
init(url: URL, caption: String?, originalData: Data?, image: UIImage, gifController: GIFController?) {
|
||||||
|
self.url = url
|
||||||
self.caption = caption
|
self.caption = caption
|
||||||
|
self.originalData = originalData
|
||||||
self.image = image
|
self.image = image
|
||||||
self.gifController = gifController
|
self.gifController = gifController
|
||||||
|
|
||||||
@ -37,14 +41,21 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
preferredContentSize = image.size
|
preferredContentSize = image.size
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
imageView = GIFImageView(image: image)
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
let maybeGrayscaleImage = if isGrayscale {
|
||||||
|
ImageGrayscalifier.convert(url: url, image: image) ?? image
|
||||||
|
} else {
|
||||||
|
image
|
||||||
|
}
|
||||||
|
|
||||||
|
imageView = GIFImageView(image: maybeGrayscaleImage)
|
||||||
imageView.translatesAutoresizingMaskIntoConstraints = false
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
imageView.contentMode = .scaleAspectFill
|
imageView.contentMode = .scaleAspectFill
|
||||||
imageView.isUserInteractionEnabled = true
|
imageView.isUserInteractionEnabled = true
|
||||||
@ -75,9 +86,11 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func viewWillAppear(_ animated: Bool) {
|
override func viewWillAppear(_ animated: Bool) {
|
||||||
super.viewWillAppear(animated)
|
super.viewWillAppear(animated)
|
||||||
|
|
||||||
if let gifController {
|
if let gifController {
|
||||||
@ -85,23 +98,37 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
let image = if isGrayscale {
|
||||||
|
ImageGrayscalifier.convert(url: url, image: image)
|
||||||
|
} else {
|
||||||
|
image
|
||||||
|
}
|
||||||
|
if let image {
|
||||||
|
imageView.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
public weak var container: (any GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
public var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
image.size
|
image.size
|
||||||
}
|
}
|
||||||
|
|
||||||
open var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
return [image]
|
if let data = originalData ?? image.pngData() {
|
||||||
|
return [ImageActivityItemSource(data: data, url: url, image: image)]
|
||||||
|
} else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var presentationAnimation: GalleryContentPresentationAnimation {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
gifController != nil ? .fromSourceViewWithoutSnapshot : .fromSourceView
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
|
||||||
if #available(iOS 16.0, macCatalyst 17.0, *),
|
if #available(iOS 16.0, macCatalyst 17.0, *),
|
||||||
let analysisInteraction {
|
let analysisInteraction {
|
||||||
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
analysisInteraction.setSupplementaryInterfaceHidden(!visible, animated: animated)
|
||||||
@ -111,7 +138,7 @@ open class ImageGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
|
|
||||||
@available(iOS 16.0, macCatalyst 17.0, *)
|
@available(iOS 16.0, macCatalyst 17.0, *)
|
||||||
extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate {
|
extension ImageGalleryContentViewController: ImageAnalysisInteractionDelegate {
|
||||||
public func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool {
|
func interaction(_ interaction: ImageAnalysisInteraction, shouldBeginAt point: CGPoint, for interactionType: ImageAnalysisInteraction.InteractionTypes) -> Bool {
|
||||||
return container?.galleryControlsVisible ?? true
|
return container?.galleryControlsVisible ?? true
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -34,7 +34,7 @@ class ImageGalleryDataSource: GalleryDataSource {
|
|||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return GrayscalableImageGalleryContentViewController(
|
return ImageGalleryContentViewController(
|
||||||
url: url,
|
url: url,
|
||||||
caption: nil,
|
caption: nil,
|
||||||
originalData: entry.data,
|
originalData: entry.data,
|
||||||
@ -52,7 +52,7 @@ class ImageGalleryDataSource: GalleryDataSource {
|
|||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return GrayscalableImageGalleryContentViewController(
|
return ImageGalleryContentViewController(
|
||||||
url: self.url,
|
url: self.url,
|
||||||
caption: nil,
|
caption: nil,
|
||||||
originalData: data,
|
originalData: data,
|
||||||
|
@ -7,42 +7,43 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
|
|
||||||
public class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
class LoadingGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
private let fallbackCaption: String?
|
private let fallbackCaption: String?
|
||||||
private let provider: () async -> (any GalleryContentViewController)?
|
private let provider: () async -> (any GalleryContentViewController)?
|
||||||
private var wrapped: (any GalleryContentViewController)!
|
private var wrapped: (any GalleryContentViewController)!
|
||||||
|
|
||||||
public weak var container: GalleryContentViewControllerContainer?
|
weak var container: GalleryContentViewControllerContainer?
|
||||||
|
|
||||||
public var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
wrapped?.contentSize ?? .zero
|
wrapped?.contentSize ?? .zero
|
||||||
}
|
}
|
||||||
|
|
||||||
public var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
wrapped?.activityItemsForSharing ?? []
|
wrapped?.activityItemsForSharing ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
public var caption: String? {
|
var caption: String? {
|
||||||
wrapped?.caption ?? fallbackCaption
|
wrapped?.caption ?? fallbackCaption
|
||||||
}
|
}
|
||||||
|
|
||||||
public var presentationAnimation: GalleryContentPresentationAnimation {
|
var canAnimateFromSourceView: Bool {
|
||||||
wrapped?.presentationAnimation ?? .fade
|
wrapped?.canAnimateFromSourceView ?? true
|
||||||
}
|
}
|
||||||
|
|
||||||
public init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {
|
init(caption: String?, provider: @escaping () async -> (any GalleryContentViewController)?) {
|
||||||
self.fallbackCaption = caption
|
self.fallbackCaption = caption
|
||||||
self.provider = provider
|
self.provider = provider
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
container?.setGalleryContentLoading(true)
|
container?.setGalleryContentLoading(true)
|
||||||
@ -80,7 +81,7 @@ public class LoadingGalleryContentViewController: UIViewController, GalleryConte
|
|||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "Error Loading"
|
label.text = "Error Loading"
|
||||||
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0)
|
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
label.textColor = .secondaryLabel
|
label.textColor = .secondaryLabel
|
||||||
label.adjustsFontForContentSizeCategory = true
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
@ -101,15 +102,15 @@ public class LoadingGalleryContentViewController: UIViewController, GalleryConte
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
wrapped?.setControlsVisible(visible, animated: animated, dueToUserInteraction: dueToUserInteraction)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func galleryContentDidAppear() {
|
func galleryContentDidAppear() {
|
||||||
wrapped?.galleryContentDidAppear()
|
wrapped?.galleryContentDidAppear()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func galleryContentWillDisappear() {
|
func galleryContentWillDisappear() {
|
||||||
wrapped?.galleryContentWillDisappear()
|
wrapped?.galleryContentWillDisappear()
|
||||||
}
|
}
|
||||||
|
|
@ -33,7 +33,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
|||||||
case .image:
|
case .image:
|
||||||
if let view = attachmentView(for: attachment),
|
if let view = attachmentView(for: attachment),
|
||||||
let image = view.attachmentImage {
|
let image = view.attachmentImage {
|
||||||
return GrayscalableImageGalleryContentViewController(
|
return ImageGalleryContentViewController(
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
caption: attachment.description,
|
caption: attachment.description,
|
||||||
originalData: view.originalData,
|
originalData: view.originalData,
|
||||||
@ -49,7 +49,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
|||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return GrayscalableImageGalleryContentViewController(
|
return ImageGalleryContentViewController(
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
caption: attachment.description,
|
caption: attachment.description,
|
||||||
originalData: entry.data,
|
originalData: entry.data,
|
||||||
@ -68,7 +68,7 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
|||||||
} else {
|
} else {
|
||||||
nil
|
nil
|
||||||
}
|
}
|
||||||
return GrayscalableImageGalleryContentViewController(
|
return ImageGalleryContentViewController(
|
||||||
url: attachment.url,
|
url: attachment.url,
|
||||||
caption: attachment.description,
|
caption: attachment.description,
|
||||||
originalData: data,
|
originalData: data,
|
||||||
@ -91,10 +91,10 @@ class StatusAttachmentsGalleryDataSource: GalleryDataSource {
|
|||||||
}
|
}
|
||||||
return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description)
|
return GifvGalleryContentViewController(controller: controller, url: attachment.url, caption: attachment.description)
|
||||||
case .video:
|
case .video:
|
||||||
return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
case .audio:
|
case .audio:
|
||||||
// TODO: use separate content VC with audio visualization?
|
// TODO: use separate content VC with audio visualization?
|
||||||
return GrayscalableVideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
return VideoGalleryContentViewController(url: attachment.url, caption: attachment.description)
|
||||||
case .unknown:
|
case .unknown:
|
||||||
return LoadingGalleryContentViewController(caption: nil) {
|
return LoadingGalleryContentViewController(caption: nil) {
|
||||||
do {
|
do {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// VideoControlsViewController.swift
|
// VideoControlsViewController.swift
|
||||||
// GalleryVC
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/21/24.
|
// Created by Shadowfacts on 3/21/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
@ -9,15 +9,6 @@
|
|||||||
import UIKit
|
import UIKit
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
|
|
||||||
@propertyWrapper
|
|
||||||
final class Box<T> {
|
|
||||||
var wrappedValue: T
|
|
||||||
|
|
||||||
init(wrappedValue: T) {
|
|
||||||
self.wrappedValue = wrappedValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class VideoControlsViewController: UIViewController {
|
class VideoControlsViewController: UIViewController {
|
||||||
private static let formatter: DateComponentsFormatter = {
|
private static let formatter: DateComponentsFormatter = {
|
||||||
let f = DateComponentsFormatter()
|
let f = DateComponentsFormatter()
|
||||||
@ -31,35 +22,27 @@ class VideoControlsViewController: UIViewController {
|
|||||||
@Box private var playbackSpeed: Float
|
@Box private var playbackSpeed: Float
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
private lazy var muteButton: MuteButton = {
|
private lazy var muteButton = MuteButton().configure {
|
||||||
let button = MuteButton()
|
$0.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
||||||
button.addTarget(self, action: #selector(muteButtonPressed), for: .touchUpInside)
|
$0.setMuted(false, animated: false)
|
||||||
button.setMuted(false, animated: false)
|
}
|
||||||
return button
|
|
||||||
}()
|
|
||||||
|
|
||||||
private let timestampLabel: UILabel = {
|
private let timestampLabel = UILabel().configure {
|
||||||
let label = UILabel()
|
$0.text = "0:00"
|
||||||
label.text = "0:00"
|
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
}
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var scrubbingControl: VideoScrubbingControl = {
|
private lazy var scrubbingControl = VideoScrubbingControl().configure {
|
||||||
let control = VideoScrubbingControl()
|
$0.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
||||||
control.heightAnchor.constraint(equalToConstant: 44).isActive = true
|
$0.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
|
||||||
control.addTarget(self, action: #selector(scrubbingStarted), for: .editingDidBegin)
|
$0.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
|
||||||
control.addTarget(self, action: #selector(scrubbingChanged), for: .editingChanged)
|
$0.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
|
||||||
control.addTarget(self, action: #selector(scrubbingEnded), for: .editingDidEnd)
|
}
|
||||||
return control
|
|
||||||
}()
|
|
||||||
|
|
||||||
private let timeRemainingLabel: UILabel = {
|
private let timeRemainingLabel = UILabel().configure {
|
||||||
let label = UILabel()
|
$0.text = "-0:00"
|
||||||
label.text = "-0:00"
|
$0.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
||||||
label.font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .monospacedDigitSystemFont(ofSize: 13, weight: .regular))
|
}
|
||||||
return label
|
|
||||||
}()
|
|
||||||
|
|
||||||
private lazy var optionsButton = MenuButton { [unowned self] in
|
private lazy var optionsButton = MenuButton { [unowned self] in
|
||||||
let imageName: String
|
let imageName: String
|
||||||
@ -99,19 +82,17 @@ class VideoControlsViewController: UIViewController {
|
|||||||
return UIMenu(children: [speedMenu])
|
return UIMenu(children: [speedMenu])
|
||||||
}
|
}
|
||||||
|
|
||||||
private lazy var hStack: UIStackView = {
|
private lazy var hStack = UIStackView(arrangedSubviews: [
|
||||||
let stack = UIStackView(arrangedSubviews: [
|
muteButton,
|
||||||
muteButton,
|
timestampLabel,
|
||||||
timestampLabel,
|
scrubbingControl,
|
||||||
scrubbingControl,
|
timeRemainingLabel,
|
||||||
timeRemainingLabel,
|
optionsButton,
|
||||||
optionsButton,
|
]).configure {
|
||||||
])
|
$0.axis = .horizontal
|
||||||
stack.axis = .horizontal
|
$0.spacing = 8
|
||||||
stack.spacing = 8
|
$0.alignment = .center
|
||||||
stack.alignment = .center
|
}
|
||||||
return stack
|
|
||||||
}()
|
|
||||||
|
|
||||||
private var timestampObserverToken: Any?
|
private var timestampObserverToken: Any?
|
||||||
private var scrubberObserverToken: Any?
|
private var scrubberObserverToken: Any?
|
@ -1,58 +1,73 @@
|
|||||||
//
|
//
|
||||||
// VideoGalleryContentViewController.swift
|
// VideoGalleryContentViewController.swift
|
||||||
// GalleryVC
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/19/24.
|
// Created by Shadowfacts on 3/19/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import GalleryVC
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import CoreImage
|
import CoreImage
|
||||||
|
|
||||||
open class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
|
class VideoGalleryContentViewController: UIViewController, GalleryContentViewController {
|
||||||
public let url: URL
|
private let url: URL
|
||||||
public let caption: String?
|
let caption: String?
|
||||||
public private(set) var item: AVPlayerItem
|
private var item: AVPlayerItem
|
||||||
public let player: AVPlayer
|
let player: AVPlayer
|
||||||
|
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
@available(iOS, obsoleted: 16.0, message: "Use AVPlayer.defaultRate")
|
||||||
@Box private var playbackSpeed: Float = 1
|
@Box private var playbackSpeed: Float = 1
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
private var isGrayscale: Bool
|
||||||
|
|
||||||
private var presentationSizeObservation: NSKeyValueObservation?
|
private var presentationSizeObservation: NSKeyValueObservation?
|
||||||
private var statusObservation: NSKeyValueObservation?
|
private var statusObservation: NSKeyValueObservation?
|
||||||
private var rateObservation: NSKeyValueObservation?
|
private var rateObservation: NSKeyValueObservation?
|
||||||
|
private var isFirstAppearance = true
|
||||||
private var hideControlsWorkItem: DispatchWorkItem?
|
private var hideControlsWorkItem: DispatchWorkItem?
|
||||||
private var isShowingError = false
|
private var audioSessionToken: AudioSessionCoordinator.Token?
|
||||||
|
|
||||||
public init(url: URL, caption: String?) {
|
init(url: URL, caption: String?) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.caption = caption
|
self.caption = caption
|
||||||
|
|
||||||
|
self.isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
|
||||||
let asset = AVAsset(url: url)
|
let asset = AVAsset(url: url)
|
||||||
self.item = Self.createItem(asset: asset)
|
self.item = VideoGalleryContentViewController.createItem(asset: asset)
|
||||||
self.player = AVPlayer(playerItem: item)
|
self.player = AVPlayer(playerItem: item)
|
||||||
|
|
||||||
super.init(nibName: nil, bundle: nil)
|
super.init(nibName: nil, bundle: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
public required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
open class func createItem(asset: AVAsset) -> AVPlayerItem {
|
private static func createItem(asset: AVAsset) -> AVPlayerItem {
|
||||||
return AVPlayerItem(asset: asset)
|
let item = AVPlayerItem(asset: asset)
|
||||||
|
if Preferences.shared.grayscaleImages {
|
||||||
|
#if os(visionOS)
|
||||||
|
#warning("Use async AVVideoComposition CIFilter initializer")
|
||||||
|
#else
|
||||||
|
let filter = CIFilter(name: "CIColorMonochrome")!
|
||||||
|
filter.setValue(CIColor(red: 0.85, green: 0.85, blue: 0.85), forKey: "inputColor")
|
||||||
|
filter.setValue(1.0, forKey: "inputIntensity")
|
||||||
|
|
||||||
|
item.videoComposition = AVVideoComposition(asset: asset, applyingCIFiltersWithHandler: { request in
|
||||||
|
filter.setValue(request.sourceImage, forKey: "inputImage")
|
||||||
|
request.finish(with: filter.outputImage!, context: nil)
|
||||||
|
})
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
public func replaceCurrentItem(with item: AVPlayerItem) {
|
override func viewDidLoad() {
|
||||||
self.item = item
|
|
||||||
player.replaceCurrentItem(with: item)
|
|
||||||
updateItemObservations()
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func viewDidLoad() {
|
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
|
|
||||||
container?.setGalleryContentLoading(true)
|
container?.setGalleryContentLoading(true)
|
||||||
@ -77,17 +92,19 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
scheduleControlsHide()
|
scheduleControlsHide()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateItemObservations() {
|
private func updateItemObservations() {
|
||||||
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
||||||
MainActor.assumeIsolated {
|
MainActor.runUnsafely {
|
||||||
self.preferredContentSize = item.presentationSize
|
self.preferredContentSize = item.presentationSize
|
||||||
self.container?.galleryContentChanged()
|
self.container?.galleryContentChanged()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
|
statusObservation = item.observe(\.status, changeHandler: { [unowned self] item, _ in
|
||||||
MainActor.assumeIsolated {
|
MainActor.runUnsafely {
|
||||||
if item.status == .readyToPlay {
|
if item.status == .readyToPlay {
|
||||||
self.container?.setGalleryContentLoading(false)
|
self.container?.setGalleryContentLoading(false)
|
||||||
self.statusObservation = nil
|
self.statusObservation = nil
|
||||||
@ -96,22 +113,19 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
self.container?.setGalleryContentLoading(false)
|
self.container?.setGalleryContentLoading(false)
|
||||||
self.showErrorView(error)
|
self.showErrorView(error)
|
||||||
self.statusObservation = nil
|
self.statusObservation = nil
|
||||||
self.overlayVC.setVisible(false)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private func showErrorView(_ error: any Error) {
|
private func showErrorView(_ error: any Error) {
|
||||||
isShowingError = true
|
|
||||||
|
|
||||||
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
let image = UIImageView(image: UIImage(systemName: "exclamationmark.triangle.fill")!)
|
||||||
image.tintColor = .secondaryLabel
|
image.tintColor = .secondaryLabel
|
||||||
image.contentMode = .scaleAspectFit
|
image.contentMode = .scaleAspectFit
|
||||||
|
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.text = "Error Loading"
|
label.text = "Error Loading"
|
||||||
label.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .title1).withSymbolicTraits(.traitBold)!, size: 0)
|
label.font = .preferredFont(forTextStyle: .title1).withTraits(.traitBold)!
|
||||||
label.textColor = .secondaryLabel
|
label.textColor = .secondaryLabel
|
||||||
label.adjustsFontForContentSizeCategory = true
|
label.adjustsFontForContentSizeCategory = true
|
||||||
|
|
||||||
@ -139,9 +153,26 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
if isGrayscale != Preferences.shared.grayscaleImages {
|
||||||
|
let isPlaying = player.rate > 0
|
||||||
|
isGrayscale = Preferences.shared.grayscaleImages
|
||||||
|
item = VideoGalleryContentViewController.createItem(asset: item.asset)
|
||||||
|
player.replaceCurrentItem(with: item)
|
||||||
|
updateItemObservations()
|
||||||
|
if isPlaying {
|
||||||
|
#if os(visionOS)
|
||||||
|
player.play()
|
||||||
|
#else
|
||||||
|
player.rate = playbackSpeed
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func scheduleControlsHide() {
|
private func scheduleControlsHide() {
|
||||||
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
hideControlsWorkItem = DispatchWorkItem { [weak self] in
|
||||||
MainActor.assumeIsolated {
|
MainActor.runUnsafely {
|
||||||
guard let self,
|
guard let self,
|
||||||
let container = self.container,
|
let container = self.container,
|
||||||
container.galleryControlsVisible else {
|
container.galleryControlsVisible else {
|
||||||
@ -155,19 +186,14 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
|
|
||||||
// MARK: GalleryContentViewController
|
// MARK: GalleryContentViewController
|
||||||
|
|
||||||
public weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
weak var container: (any GalleryVC.GalleryContentViewControllerContainer)?
|
||||||
|
|
||||||
public var contentSize: CGSize {
|
var contentSize: CGSize {
|
||||||
item.presentationSize
|
item.presentationSize
|
||||||
}
|
}
|
||||||
|
|
||||||
open var activityItemsForSharing: [Any] {
|
var activityItemsForSharing: [Any] {
|
||||||
// [VideoActivityItemSource(asset: item.asset, url: url)]
|
[VideoActivityItemSource(asset: item.asset, url: url)]
|
||||||
[]
|
|
||||||
}
|
|
||||||
|
|
||||||
public var presentationAnimation: GalleryContentPresentationAnimation {
|
|
||||||
isShowingError ? .fade : .fromSourceViewWithoutSnapshot
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
@ -175,20 +201,18 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
#else
|
#else
|
||||||
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
private lazy var overlayVC = VideoOverlayViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
#endif
|
#endif
|
||||||
public var contentOverlayAccessoryViewController: UIViewController? {
|
var contentOverlayAccessoryViewController: UIViewController? {
|
||||||
overlayVC
|
overlayVC
|
||||||
}
|
}
|
||||||
|
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player)
|
||||||
#else
|
#else
|
||||||
public private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
private(set) lazy var bottomControlsAccessoryViewController: UIViewController? = VideoControlsViewController(player: player, playbackSpeed: _playbackSpeed)
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
func setControlsVisible(_ visible: Bool, animated: Bool, dueToUserInteraction: Bool) {
|
||||||
if !isShowingError {
|
overlayVC.setVisible(visible)
|
||||||
overlayVC.setVisible(visible)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !visible {
|
if !visible {
|
||||||
hideControlsWorkItem?.cancel()
|
hideControlsWorkItem?.cancel()
|
||||||
@ -198,11 +222,25 @@ open class VideoGalleryContentViewController: UIViewController, GalleryContentVi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open func galleryContentDidAppear() {
|
func galleryContentDidAppear() {
|
||||||
|
let wasFirstAppearance = isFirstAppearance
|
||||||
|
isFirstAppearance = false
|
||||||
|
|
||||||
|
audioSessionToken = AudioSessionCoordinator.shared.beginPlayback(mode: .video) {
|
||||||
|
if wasFirstAppearance {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
self.player.play()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
open func galleryContentWillDisappear() {
|
func galleryContentWillDisappear() {
|
||||||
player.pause()
|
player.pause()
|
||||||
|
|
||||||
|
if let audioSessionToken {
|
||||||
|
AudioSessionCoordinator.shared.endPlayback(token: audioSessionToken)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -231,9 +269,9 @@ private class PlayerView: UIView {
|
|||||||
playerLayer.player = player
|
playerLayer.player = player
|
||||||
playerLayer.videoGravity = .resizeAspect
|
playerLayer.videoGravity = .resizeAspect
|
||||||
|
|
||||||
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] _, _ in
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] _, _ in
|
||||||
MainActor.assumeIsolated {
|
MainActor.runUnsafely {
|
||||||
self?.invalidateIntrinsicContentSize()
|
self.invalidateIntrinsicContentSize()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
//
|
//
|
||||||
// VideoOverlayViewController.swift
|
// VideoOverlayViewController.swift
|
||||||
// GalleryVC
|
// Tusker
|
||||||
//
|
//
|
||||||
// Created by Shadowfacts on 3/26/24.
|
// Created by Shadowfacts on 3/26/24.
|
||||||
// Copyright © 2024 Shadowfacts. All rights reserved.
|
// Copyright © 2024 Shadowfacts. All rights reserved.
|
||||||
@ -90,7 +90,7 @@ class VideoOverlayViewController: UIViewController {
|
|||||||
])
|
])
|
||||||
|
|
||||||
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
|
rateObservation = player.observe(\.rate, changeHandler: { player, _ in
|
||||||
MainActor.assumeIsolated {
|
MainActor.runUnsafely {
|
||||||
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
|
playPauseButton.image = player.rate > 0 ? VideoOverlayViewController.pauseImage : VideoOverlayViewController.playImage
|
||||||
}
|
}
|
||||||
})
|
})
|
@ -151,22 +151,6 @@ class BaseMainTabBarViewController: UITabBarController, FastAccountSwitcherViewC
|
|||||||
return false
|
return false
|
||||||
#endif // !os(visionOS)
|
#endif // !os(visionOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Keyboard shortcuts
|
|
||||||
|
|
||||||
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
|
||||||
// This is a silly workaround for when the sidebar is focused (and therefore first responder), which,
|
|
||||||
// unfortunately, is almost always. Because the content view controller then isn't in the responder chain,
|
|
||||||
// we manually delegate to the top view controller if possible.
|
|
||||||
if action == #selector(RefreshableViewController.refresh),
|
|
||||||
let selected = selectedViewController as? NavigationControllerProtocol,
|
|
||||||
let top = selected.topViewController as? RefreshableViewController {
|
|
||||||
return top
|
|
||||||
} else {
|
|
||||||
return super.target(forAction: action, withSender: sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
extension BaseMainTabBarViewController: TuskerNavigationDelegate {
|
||||||
|
@ -219,19 +219,6 @@ class MainSplitViewController: UISplitViewController {
|
|||||||
@objc func handleComposeKeyCommand() {
|
@objc func handleComposeKeyCommand() {
|
||||||
compose(editing: nil)
|
compose(editing: nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func target(forAction action: Selector, withSender sender: Any?) -> Any? {
|
|
||||||
// This is a silly workaround for when the sidebar is focused (and therefore first responder), which,
|
|
||||||
// unfortunately, is almost always. Because the content view controller then isn't in the responder chain,
|
|
||||||
// we manually delegate to the top view controller if possible.
|
|
||||||
if action == #selector(RefreshableViewController.refresh),
|
|
||||||
traitCollection.horizontalSizeClass == .regular,
|
|
||||||
let top = secondaryNavController.topViewController as? RefreshableViewController {
|
|
||||||
return top
|
|
||||||
} else {
|
|
||||||
return super.target(forAction: action, withSender: sender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,13 +33,6 @@ final class NewMainTabBarViewController: BaseMainTabBarViewController {
|
|||||||
private var isCompact: Bool?
|
private var isCompact: Bool?
|
||||||
@Box fileprivate var myProfileCell: UIView?
|
@Box fileprivate var myProfileCell: UIView?
|
||||||
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
private var sidebarTapRecognizer: UITapGestureRecognizer?
|
||||||
|
|
||||||
private lazy var fastAccountSwitcherIndicator: UIView = {
|
|
||||||
let indicator = FastAccountSwitcherIndicatorView()
|
|
||||||
// need to explicitly set the frame to get it vertically centered
|
|
||||||
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
|
||||||
return indicator
|
|
||||||
}()
|
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
@ -461,20 +454,15 @@ extension NewMainTabBarViewController {
|
|||||||
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
||||||
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
||||||
if tab.identifier == Tab.compose.rawValue {
|
if tab.identifier == Tab.compose.rawValue {
|
||||||
if #unavailable(iOS 18.1) {
|
let currentTab = selectedTab
|
||||||
let currentTab = selectedTab
|
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
|
||||||
// returning false for shouldSelectTab doesn't prevent the UITabBar from being updated (FB14857254)
|
// returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state)
|
||||||
// returning false and then setting selectedTab=tab and selectedTab=currentTab seems to leave things in a bad state (currentTab's VC is on screen but in the disappeared state)
|
// so return true, and then after the tab bar VC has finished updating, go back to currentTab
|
||||||
// so return true, and then after the tab bar VC has finished updating, go back to currentTab
|
DispatchQueue.main.async {
|
||||||
DispatchQueue.main.async {
|
self.selectedTab = currentTab
|
||||||
self.selectedTab = currentTab
|
|
||||||
}
|
|
||||||
compose(editing: nil)
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
compose(editing: nil)
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
compose(editing: nil)
|
||||||
|
return true
|
||||||
} else if let selectedTab,
|
} else if let selectedTab,
|
||||||
selectedTab == tab,
|
selectedTab == tab,
|
||||||
let nav = selectedViewController as? any NavigationControllerProtocol {
|
let nav = selectedViewController as? any NavigationControllerProtocol {
|
||||||
@ -520,6 +508,13 @@ extension NewMainTabBarViewController: UITabBarControllerDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var fastAccountSwitcherIndicator: UIView = {
|
||||||
|
let indicator = FastAccountSwitcherIndicatorView()
|
||||||
|
// need to explicitly set the frame to get it vertically centered
|
||||||
|
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
||||||
|
return indicator
|
||||||
|
}()
|
||||||
|
|
||||||
@available(iOS 18.0, *)
|
@available(iOS 18.0, *)
|
||||||
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
extension NewMainTabBarViewController: UITabBarController.Sidebar.Delegate {
|
||||||
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
|
func tabBarController(_ tabBarController: UITabBarController, sidebarVisibilityWillChange sidebar: UITabBarController.Sidebar, animator: any UITabBarController.Sidebar.Animating) {
|
||||||
|
@ -48,9 +48,7 @@ class NotificationLoadingViewController: UIViewController {
|
|||||||
do {
|
do {
|
||||||
let (notification, _) = try await mastodonController.run(request)
|
let (notification, _) = try await mastodonController.run(request)
|
||||||
await withCheckedContinuation { continuation in
|
await withCheckedContinuation { continuation in
|
||||||
let container = mastodonController.persistentContainer
|
mastodonController.persistentContainer.addAll(notifications: [notification]) {
|
||||||
let context = container.viewContext
|
|
||||||
container.addAll(notifications: [notification], in: context) {
|
|
||||||
continuation.resume()
|
continuation.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -180,9 +180,3 @@ extension NotificationsPageViewController: StateRestorableViewController {
|
|||||||
return currentPage.userActivity(accountID: mastodonController.accountInfo!.id)
|
return currentPage.userActivity(accountID: mastodonController.accountInfo!.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NotificationsPageViewController: RefreshableViewController {
|
|
||||||
func refresh() {
|
|
||||||
(currentViewController as? RefreshableViewController)?.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -393,9 +393,3 @@ extension ProfileViewController: StatusBarTappableViewController {
|
|||||||
return currentViewController.handleStatusBarTapped(xPosition: xPosition)
|
return currentViewController.handleStatusBarTapped(xPosition: xPosition)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileViewController: RefreshableViewController {
|
|
||||||
func refresh() {
|
|
||||||
currentViewController.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -77,25 +77,11 @@ class InstanceTimelineViewController: TimelineViewController {
|
|||||||
cell.updateUI(statusID: id, state: state, filterResult: filterResult, precomputedContent: precomputedContent)
|
cell.updateUI(statusID: id, state: state, filterResult: filterResult, precomputedContent: precomputedContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
|
|
||||||
guard browsingEnabled else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return super.collectionView(collectionView, shouldSelectItemAt: indexPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
|
||||||
guard browsingEnabled else { return }
|
guard browsingEnabled else { return }
|
||||||
super.collectionView(collectionView, didSelectItemAt: indexPath)
|
super.collectionView(collectionView, didSelectItemAt: indexPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
guard browsingEnabled else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return super.collectionView(collectionView, contextMenuConfigurationForItemAt: indexPath, point: point)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: Timeline
|
// MARK: Timeline
|
||||||
|
|
||||||
override func handleLoadAllError(_ error: Swift.Error) async {
|
override func handleLoadAllError(_ error: Swift.Error) async {
|
||||||
|
@ -212,9 +212,3 @@ extension TimelinesPageViewController: StateRestorableViewController {
|
|||||||
return (currentViewController as? TimelineViewController)?.stateRestorationActivity()
|
return (currentViewController as? TimelineViewController)?.stateRestorationActivity()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension TimelinesPageViewController: RefreshableViewController {
|
|
||||||
func refresh() {
|
|
||||||
(currentViewController as? RefreshableViewController)?.refresh()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -30,9 +30,7 @@ class AccountDisplayAndUserNameLabel: EmojiLabel {
|
|||||||
|
|
||||||
private func makeAttributedText(state: State) -> NSAttributedString {
|
private func makeAttributedText(state: State) -> NSAttributedString {
|
||||||
let s = NSMutableAttributedString()
|
let s = NSMutableAttributedString()
|
||||||
// U+2068 FIRST-STRONG ISOLATE and U+2069 POP DIRECTIONAL ISOLATE
|
s.append(NSAttributedString(string: state.displayName, attributes: [
|
||||||
// to prevent bidi text in the display name influencing the username
|
|
||||||
s.append(NSAttributedString(string: "\u{2068}\(state.displayName)\u{2069}", attributes: [
|
|
||||||
.font: UIFont(descriptor: baseFont.addingAttributes([
|
.font: UIFont(descriptor: baseFont.addingAttributes([
|
||||||
.traits: [
|
.traits: [
|
||||||
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
|
||||||
|
@ -412,7 +412,7 @@ class AttachmentView: GIFImageView {
|
|||||||
makeBadgeView(text: "ALT")
|
makeBadgeView(text: "ALT")
|
||||||
}
|
}
|
||||||
if badges.contains(.noAlt) {
|
if badges.contains(.noAlt) {
|
||||||
makeBadgeView(text: "NO ALT")
|
makeBadgeView(text: "No ALT")
|
||||||
}
|
}
|
||||||
|
|
||||||
let first = stack.arrangedSubviews.first!
|
let first = stack.arrangedSubviews.first!
|
||||||
@ -477,12 +477,12 @@ extension AttachmentView: UIContextMenuInteractionDelegate {
|
|||||||
return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in
|
return UIContextMenuConfiguration { [unowned self] () -> UIViewController? in
|
||||||
if self.attachment.kind == .image,
|
if self.attachment.kind == .image,
|
||||||
let image {
|
let image {
|
||||||
return GrayscalableImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController)
|
return ImageGalleryContentViewController(url: self.attachment.url, caption: nil, originalData: nil, image: image, gifController: self.gifController)
|
||||||
} else if self.attachment.kind == .gifv,
|
} else if self.attachment.kind == .gifv,
|
||||||
let gifvView {
|
let gifvView {
|
||||||
return GifvGalleryContentViewController(controller: gifvView.controller, url: self.attachment.url, caption: nil)
|
return GifvGalleryContentViewController(controller: gifvView.controller, url: self.attachment.url, caption: nil)
|
||||||
} else if self.attachment.kind == .video || self.attachment.kind == .audio {
|
} else if self.attachment.kind == .video || self.attachment.kind == .audio {
|
||||||
let vc = GrayscalableVideoGalleryContentViewController(url: self.attachment.url, caption: nil)
|
let vc = VideoGalleryContentViewController(url: self.attachment.url, caption: nil)
|
||||||
vc.player.isMuted = true
|
vc.player.isMuted = true
|
||||||
return vc
|
return vc
|
||||||
} else {
|
} else {
|
||||||
|
@ -295,11 +295,7 @@ class AttachmentsContainerView: UIView {
|
|||||||
accessibilityElements.append(moreView)
|
accessibilityElements.append(moreView)
|
||||||
}
|
}
|
||||||
|
|
||||||
self.aspectRatio = if aspectRatio.isNaN || aspectRatio.isInfinite {
|
self.aspectRatio = aspectRatio
|
||||||
16/9
|
|
||||||
} else {
|
|
||||||
aspectRatio
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
self.isHidden = true
|
self.isHidden = true
|
||||||
}
|
}
|
||||||
|
@ -60,9 +60,9 @@ class GifvController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updatePresentationSizeObservation() {
|
private func updatePresentationSizeObservation() {
|
||||||
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [weak self] item, _ in
|
presentationSizeObservation = item.observe(\.presentationSize, changeHandler: { [unowned self] item, _ in
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self?.presentationSizeSubject.send(item.presentationSize)
|
self.presentationSizeSubject.send(item.presentationSize)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,11 @@ import SwiftUI
|
|||||||
import SafariServices
|
import SafariServices
|
||||||
|
|
||||||
class ProfileFieldValueView: UIView {
|
class ProfileFieldValueView: UIView {
|
||||||
weak var navigationDelegate: TuskerNavigationDelegate?
|
weak var navigationDelegate: TuskerNavigationDelegate? {
|
||||||
|
didSet {
|
||||||
|
textView.navigationDelegate = navigationDelegate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static let converter = HTMLConverter(
|
private static let converter = HTMLConverter(
|
||||||
font: .preferredFont(forTextStyle: .body),
|
font: .preferredFont(forTextStyle: .body),
|
||||||
@ -24,9 +28,8 @@ class ProfileFieldValueView: UIView {
|
|||||||
|
|
||||||
private let account: AccountMO
|
private let account: AccountMO
|
||||||
private let field: Account.Field
|
private let field: Account.Field
|
||||||
private var link: (String, URL)?
|
|
||||||
|
|
||||||
private let label = EmojiLabel()
|
private let textView = ContentTextView()
|
||||||
private var iconView: UIView?
|
private var iconView: UIView?
|
||||||
|
|
||||||
private var currentTargetedPreview: UITargetedPreview?
|
private var currentTargetedPreview: UITargetedPreview?
|
||||||
@ -39,34 +42,28 @@ class ProfileFieldValueView: UIView {
|
|||||||
|
|
||||||
let converted = NSMutableAttributedString(attributedString: ProfileFieldValueView.converter.convert(field.value))
|
let converted = NSMutableAttributedString(attributedString: ProfileFieldValueView.converter.convert(field.value))
|
||||||
|
|
||||||
converted.enumerateAttribute(.link, in: converted.fullRange) { value, range, stop in
|
#if os(visionOS)
|
||||||
guard value != nil else { return }
|
textView.linkTextAttributes = [
|
||||||
if self.link == nil {
|
.foregroundColor: UIColor.link
|
||||||
self.link = (converted.attributedSubstring(from: range).string, value as! URL)
|
]
|
||||||
}
|
#else
|
||||||
#if os(visionOS)
|
textView.linkTextAttributes = [
|
||||||
converted.addAttribute(.foregroundColor, value: UIColor.link, range: range)
|
.foregroundColor: UIColor.tintColor
|
||||||
#else
|
]
|
||||||
converted.addAttribute(.foregroundColor, value: UIColor.tintColor, range: range)
|
#endif
|
||||||
#endif
|
textView.backgroundColor = nil
|
||||||
// the .link attribute in a UILabel always makes the color blue >.>
|
textView.isScrollEnabled = false
|
||||||
converted.removeAttribute(.link, range: range)
|
textView.isSelectable = false
|
||||||
}
|
textView.isEditable = false
|
||||||
|
textView.font = .preferredFont(forTextStyle: .body)
|
||||||
if link != nil {
|
updateTextContainerInset()
|
||||||
label.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(linkTapped)))
|
textView.adjustsFontForContentSizeCategory = true
|
||||||
label.addInteraction(UIContextMenuInteraction(delegate: self))
|
textView.attributedText = converted
|
||||||
label.isUserInteractionEnabled = true
|
textView.setEmojis(account.emojis, identifier: account.id)
|
||||||
}
|
textView.isUserInteractionEnabled = true
|
||||||
|
textView.setContentCompressionResistancePriority(.required, for: .vertical)
|
||||||
label.numberOfLines = 0
|
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.font = .preferredFont(forTextStyle: .body)
|
addSubview(textView)
|
||||||
label.adjustsFontForContentSizeCategory = true
|
|
||||||
label.attributedText = converted
|
|
||||||
label.setEmojis(account.emojis, identifier: account.id)
|
|
||||||
label.setContentCompressionResistancePriority(.required, for: .vertical)
|
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
addSubview(label)
|
|
||||||
|
|
||||||
let labelTrailingConstraint: NSLayoutConstraint
|
let labelTrailingConstraint: NSLayoutConstraint
|
||||||
|
|
||||||
@ -83,20 +80,20 @@ class ProfileFieldValueView: UIView {
|
|||||||
icon.isPointerInteractionEnabled = true
|
icon.isPointerInteractionEnabled = true
|
||||||
icon.accessibilityLabel = "Verified link"
|
icon.accessibilityLabel = "Verified link"
|
||||||
addSubview(icon)
|
addSubview(icon)
|
||||||
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
|
labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: icon.leadingAnchor)
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
icon.centerYAnchor.constraint(equalTo: label.centerYAnchor),
|
icon.centerYAnchor.constraint(equalTo: textView.centerYAnchor),
|
||||||
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
icon.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchor),
|
||||||
])
|
])
|
||||||
} else {
|
} else {
|
||||||
labelTrailingConstraint = label.trailingAnchor.constraint(equalTo: trailingAnchor)
|
labelTrailingConstraint = textView.trailingAnchor.constraint(equalTo: trailingAnchor)
|
||||||
}
|
}
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
label.leadingAnchor.constraint(equalTo: leadingAnchor),
|
textView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
labelTrailingConstraint,
|
labelTrailingConstraint,
|
||||||
label.topAnchor.constraint(equalTo: topAnchor),
|
textView.topAnchor.constraint(equalTo: topAnchor),
|
||||||
label.bottomAnchor.constraint(equalTo: bottomAnchor),
|
textView.bottomAnchor.constraint(equalTo: bottomAnchor),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,37 +102,36 @@ class ProfileFieldValueView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||||
var size = label.sizeThatFits(size)
|
var size = textView.sizeThatFits(size)
|
||||||
if let iconView {
|
if let iconView {
|
||||||
size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
size.width += iconView.sizeThatFits(UIView.layoutFittingCompressedSize).width
|
||||||
}
|
}
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||||
|
super.traitCollectionDidChange(previousTraitCollection)
|
||||||
|
if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory {
|
||||||
|
updateTextContainerInset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateTextContainerInset() {
|
||||||
|
// blergh
|
||||||
|
switch traitCollection.preferredContentSizeCategory {
|
||||||
|
case .extraSmall:
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 4, left: 0, bottom: 0, right: 0)
|
||||||
|
case .small:
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 3, left: 0, bottom: 0, right: 0)
|
||||||
|
case .medium, .large:
|
||||||
|
textView.textContainerInset = UIEdgeInsets(top: 2, left: 0, bottom: 0, right: 0)
|
||||||
|
default:
|
||||||
|
textView.textContainerInset = .zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func setTextAlignment(_ alignment: NSTextAlignment) {
|
func setTextAlignment(_ alignment: NSTextAlignment) {
|
||||||
label.textAlignment = alignment
|
textView.textAlignment = alignment
|
||||||
}
|
|
||||||
|
|
||||||
func getHashtagOrURL() -> (Hashtag?, URL)? {
|
|
||||||
guard let (text, url) = link else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if text.starts(with: "#") {
|
|
||||||
return (Hashtag(name: String(text.dropFirst()), url: url), url)
|
|
||||||
} else {
|
|
||||||
return (nil, url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc private func linkTapped() {
|
|
||||||
guard let (hashtag, url) = getHashtagOrURL() else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let hashtag {
|
|
||||||
navigationDelegate?.selected(tag: hashtag)
|
|
||||||
} else {
|
|
||||||
navigationDelegate?.selected(url: url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc private func verifiedIconTapped() {
|
@objc private func verifiedIconTapped() {
|
||||||
@ -145,7 +141,7 @@ class ProfileFieldValueView: UIView {
|
|||||||
let view = ProfileFieldVerificationView(
|
let view = ProfileFieldVerificationView(
|
||||||
acct: account.acct,
|
acct: account.acct,
|
||||||
verifiedAt: field.verifiedAt!,
|
verifiedAt: field.verifiedAt!,
|
||||||
linkText: label.text ?? "",
|
linkText: textView.text ?? "",
|
||||||
navigationDelegate: navigationDelegate
|
navigationDelegate: navigationDelegate
|
||||||
)
|
)
|
||||||
let host = UIHostingController(rootView: view)
|
let host = UIHostingController(rootView: view)
|
||||||
@ -169,49 +165,3 @@ class ProfileFieldValueView: UIView {
|
|||||||
navigationDelegate.present(toPresent, animated: true)
|
navigationDelegate.present(toPresent, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ProfileFieldValueView: UIContextMenuInteractionDelegate, MenuActionProvider {
|
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
|
|
||||||
guard let (hashtag, url) = getHashtagOrURL(),
|
|
||||||
let navigationDelegate else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if let hashtag {
|
|
||||||
return UIContextMenuConfiguration {
|
|
||||||
HashtagTimelineViewController(for: hashtag, mastodonController: navigationDelegate.apiController)
|
|
||||||
} actionProvider: { _ in
|
|
||||||
UIMenu(children: self.actionsForHashtag(hashtag, source: .view(self)))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return UIContextMenuConfiguration {
|
|
||||||
let vc = SFSafariViewController(url: url)
|
|
||||||
#if !os(visionOS)
|
|
||||||
vc.preferredControlTintColor = Preferences.shared.accentColor.color
|
|
||||||
#endif
|
|
||||||
return vc
|
|
||||||
} actionProvider: { _ in
|
|
||||||
UIMenu(children: self.actionsForURL(url, source: .view(self)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, highlightPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? {
|
|
||||||
var rect = label.textRect(forBounds: label.bounds, limitedToNumberOfLines: 0)
|
|
||||||
// the rect should be vertically centered, but textRect doesn't seem to take the label's vertical alignment into account
|
|
||||||
rect.origin.x = 0
|
|
||||||
rect.origin.y = (bounds.height - rect.height) / 2
|
|
||||||
let parameters = UIPreviewParameters(textLineRects: [rect as NSValue])
|
|
||||||
let preview = UITargetedPreview(view: label, parameters: parameters)
|
|
||||||
currentTargetedPreview = preview
|
|
||||||
return preview
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configuration: UIContextMenuConfiguration, dismissalPreviewForItemWithIdentifier identifier: NSCopying) -> UITargetedPreview? {
|
|
||||||
return currentTargetedPreview
|
|
||||||
}
|
|
||||||
|
|
||||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
|
||||||
MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: navigationDelegate!)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -9,8 +9,8 @@
|
|||||||
// Configuration settings file format documentation can be found at:
|
// Configuration settings file format documentation can be found at:
|
||||||
// https://help.apple.com/xcode/#/dev745c5c974
|
// https://help.apple.com/xcode/#/dev745c5c974
|
||||||
|
|
||||||
MARKETING_VERSION = 2024.5
|
MARKETING_VERSION = 2024.4
|
||||||
CURRENT_PROJECT_VERSION = 141
|
CURRENT_PROJECT_VERSION = 136
|
||||||
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
CURRENT_PROJECT_VERSION = $(inherited)$(CURRENT_PROJECT_VERSION_BUILD_SUFFIX_$(CONFIGURATION))
|
||||||
|
|
||||||
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
CURRENT_PROJECT_VERSION_BUILD_SUFFIX_Debug=-dev
|
||||||
|
Loading…
x
Reference in New Issue
Block a user