mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-15 13:35:19 +00:00
Camera and editor improvements
This commit is contained in:
parent
d086a8f674
commit
eeb1c469f3
@ -774,8 +774,25 @@ public struct StoryCameraTransitionOut {
|
||||
}
|
||||
}
|
||||
|
||||
public struct StoryCameraTransitionInCoordinator {
|
||||
public let animateIn: () -> Void
|
||||
public let updateTransitionProgress: (CGFloat) -> Void
|
||||
public let completeWithTransitionProgressAndVelocity: (CGFloat, CGFloat) -> Void
|
||||
|
||||
public init(
|
||||
animateIn: @escaping () -> Void,
|
||||
updateTransitionProgress: @escaping (CGFloat) -> Void,
|
||||
completeWithTransitionProgressAndVelocity: @escaping (CGFloat, CGFloat) -> Void
|
||||
) {
|
||||
self.animateIn = animateIn
|
||||
self.updateTransitionProgress = updateTransitionProgress
|
||||
self.completeWithTransitionProgressAndVelocity = completeWithTransitionProgressAndVelocity
|
||||
}
|
||||
}
|
||||
|
||||
public protocol TelegramRootControllerInterface: NavigationController {
|
||||
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?)
|
||||
@discardableResult
|
||||
func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
|
||||
}
|
||||
|
||||
public protocol SharedAccountContext: AnyObject {
|
||||
@ -874,7 +891,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
|
||||
func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController
|
||||
|
||||
func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController
|
||||
func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
||||
|
||||
|
@ -46,9 +46,10 @@ private final class CameraContext {
|
||||
let ciContext = CIContext()
|
||||
var ciImage = CIImage(cvImageBuffer: pixelBuffer)
|
||||
ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33))
|
||||
ciImage = ciImage.clampedToExtent()
|
||||
if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) {
|
||||
let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right)
|
||||
CameraSimplePreviewView.saveLastState(uiImage)
|
||||
CameraSimplePreviewView.saveLastStateImage(uiImage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ public class CameraSimplePreviewView: UIView {
|
||||
}
|
||||
}
|
||||
|
||||
static func saveLastState(_ image: UIImage) {
|
||||
static func saveLastStateImage(_ image: UIImage) {
|
||||
let imagePath = NSTemporaryDirectory() + "cameraImage.jpg"
|
||||
if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) {
|
||||
try? data.write(to: URL(fileURLWithPath: imagePath))
|
||||
|
@ -93,7 +93,7 @@ final class VideoRecorder {
|
||||
|
||||
self.isRecording = true
|
||||
|
||||
assetWriter.startWriting()
|
||||
//assetWriter.startWriting()
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,17 +132,15 @@ final class VideoRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
private var skippedCount = 0
|
||||
func appendVideo(sampleBuffer: CMSampleBuffer) {
|
||||
if self.skippedCount < 2 {
|
||||
self.skippedCount += 1
|
||||
return
|
||||
}
|
||||
self.queue.async {
|
||||
guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else {
|
||||
return
|
||||
}
|
||||
let timestamp = sampleBuffer.presentationTimestamp
|
||||
if let startTimestamp = self.captureStartTimestamp, timestamp.seconds < startTimestamp {
|
||||
return
|
||||
}
|
||||
|
||||
switch assetWriter.status {
|
||||
case .unknown:
|
||||
|
@ -2439,17 +2439,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
}
|
||||
|
||||
var cameraTransitionIn: StoryCameraTransitionIn?
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
cameraTransitionIn = StoryCameraTransitionIn(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionView.bounds,
|
||||
sourceCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var initialFocusedId: AnyHashable?
|
||||
if let peer {
|
||||
initialFocusedId = AnyHashable(peer.id)
|
||||
@ -2457,11 +2446,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
|
||||
if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty {
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in
|
||||
let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
@ -2472,6 +2461,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
}
|
||||
return nil
|
||||
})
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
|
||||
return
|
||||
@ -4862,6 +4852,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
||||
self.view.addSubview(ConfettiView(frame: self.view.bounds))
|
||||
}
|
||||
}
|
||||
|
||||
private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator?
|
||||
func storyCameraPanGestureChanged(transitionFraction: CGFloat) {
|
||||
guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else {
|
||||
return
|
||||
}
|
||||
|
||||
let coordinator: StoryCameraTransitionInCoordinator?
|
||||
if let current = self.storyCameraTransitionInCoordinator {
|
||||
coordinator = current
|
||||
} else {
|
||||
coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
}
|
||||
if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
|
||||
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: transitionView,
|
||||
destinationRect: transitionView.bounds,
|
||||
destinationCornerRadius: transitionView.bounds.height * 0.5
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
self.storyCameraTransitionInCoordinator = coordinator
|
||||
}
|
||||
coordinator?.updateTransitionProgress(transitionFraction)
|
||||
}
|
||||
|
||||
func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) {
|
||||
if let coordinator = self.storyCameraTransitionInCoordinator {
|
||||
coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity)
|
||||
self.storyCameraTransitionInCoordinator = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource {
|
||||
|
@ -1081,8 +1081,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
}
|
||||
|
||||
if selectedIndex <= 0 && translation.x > 0.0 {
|
||||
let overscroll = translation.x
|
||||
transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
||||
//let overscroll = translation.x
|
||||
//transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width
|
||||
transitionFraction = 0.0
|
||||
|
||||
self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width)
|
||||
}
|
||||
|
||||
if selectedIndex >= maxFilterIndex && translation.x < 0.0 {
|
||||
@ -1159,6 +1162,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele
|
||||
self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, transition: .immediate)
|
||||
}
|
||||
}
|
||||
|
||||
self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x)
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
@ -36,6 +36,7 @@ swift_library(
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/QrCodeUI:QrCodeUI",
|
||||
"//submodules/LocalizedPeerData:LocalizedPeerData"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -22,6 +22,7 @@ import TelegramPermissionsUI
|
||||
import AppBundle
|
||||
import ContextUI
|
||||
import PhoneNumberFormat
|
||||
import LocalizedPeerData
|
||||
|
||||
private let dropDownIcon = { () -> UIImage in
|
||||
UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0)
|
||||
@ -369,72 +370,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable {
|
||||
}
|
||||
}
|
||||
|
||||
private extension EnginePeer.IndexName {
|
||||
func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult {
|
||||
switch self {
|
||||
case let .title(lhsTitle, _):
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsTitle.caseInsensitiveCompare(rhsString)
|
||||
case let .personName(lhsFirst, lhsLast, _, _):
|
||||
let lhsString: String
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if lhsFirst.isEmpty {
|
||||
lhsString = lhsLast
|
||||
} else {
|
||||
lhsString = lhsFirst + lhsLast
|
||||
}
|
||||
case .lastFirst:
|
||||
if lhsLast.isEmpty {
|
||||
lhsString = lhsFirst
|
||||
} else {
|
||||
lhsString = lhsLast + lhsFirst
|
||||
}
|
||||
}
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsString.caseInsensitiveCompare(rhsString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set<EnginePeer.Id>, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool) -> [ContactListNodeEntry] {
|
||||
var entries: [ContactListNodeEntry] = []
|
||||
|
||||
|
@ -21,7 +21,6 @@ public protocol ContainableController: AnyObject {
|
||||
|
||||
func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition)
|
||||
func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation)
|
||||
func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition)
|
||||
func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize?
|
||||
|
||||
func viewWillAppear(_ animated: Bool)
|
||||
|
@ -1106,9 +1106,13 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
|
||||
func updateTransform(node: ASDisplayNode, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion)
|
||||
}
|
||||
|
||||
func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) {
|
||||
let transform = CATransform3DMakeAffineTransform(transform)
|
||||
|
||||
if CATransform3DEqualToTransform(node.layer.transform, transform) {
|
||||
if CATransform3DEqualToTransform(layer.transform, transform) {
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
@ -1117,19 +1121,19 @@ public extension ContainedViewLayoutTransition {
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.transform = transform
|
||||
layer.transform = transform
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let previousTransform: CATransform3D
|
||||
if beginWithCurrentState, let presentation = node.layer.presentation() {
|
||||
if beginWithCurrentState, let presentation = layer.presentation() {
|
||||
previousTransform = presentation.transform
|
||||
} else {
|
||||
previousTransform = node.layer.transform
|
||||
previousTransform = layer.transform
|
||||
}
|
||||
node.layer.transform = transform
|
||||
node.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in
|
||||
layer.transform = transform
|
||||
layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in
|
||||
completion?(value)
|
||||
})
|
||||
}
|
||||
@ -1262,13 +1266,8 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
let t = node.layer.sublayerTransform
|
||||
func updateSublayerTransformScaleAndOffset(layer: CALayer, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
let t = layer.sublayerTransform
|
||||
let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13))
|
||||
let currentOffset = CGPoint(x: t.m41 / currentScale, y: t.m42 / currentScale)
|
||||
if abs(currentScale - scale) <= CGFloat.ulpOfOne && abs(currentOffset.x - offset.x) <= CGFloat.ulpOfOne && abs(currentOffset.y - offset.y) <= CGFloat.ulpOfOne {
|
||||
@ -1282,21 +1281,21 @@ public extension ContainedViewLayoutTransition {
|
||||
|
||||
switch self {
|
||||
case .immediate:
|
||||
node.layer.removeAnimation(forKey: "sublayerTransform")
|
||||
node.layer.sublayerTransform = transform
|
||||
layer.removeAnimation(forKey: "sublayerTransform")
|
||||
layer.sublayerTransform = transform
|
||||
if let completion = completion {
|
||||
completion(true)
|
||||
}
|
||||
case let .animated(duration, curve):
|
||||
let initialTransform: CATransform3D
|
||||
if beginWithCurrentState, node.isNodeLoaded {
|
||||
initialTransform = node.layer.presentation()?.sublayerTransform ?? t
|
||||
if beginWithCurrentState {
|
||||
initialTransform = layer.presentation()?.sublayerTransform ?? t
|
||||
} else {
|
||||
initialTransform = t
|
||||
}
|
||||
|
||||
node.layer.sublayerTransform = transform
|
||||
node.layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
layer.sublayerTransform = transform
|
||||
layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: {
|
||||
result in
|
||||
if let completion = completion {
|
||||
completion(result)
|
||||
@ -1305,6 +1304,15 @@ public extension ContainedViewLayoutTransition {
|
||||
}
|
||||
}
|
||||
|
||||
func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0)
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
return updateSublayerTransformScaleAndOffset(layer: node.layer, scale: scale, offset: offset, beginWithCurrentState: beginWithCurrentState, completion: completion)
|
||||
}
|
||||
|
||||
func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||
if !node.isNodeLoaded {
|
||||
node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0)
|
||||
|
@ -185,13 +185,15 @@ public enum DeviceMetrics: CaseIterable, Equatable {
|
||||
case .iPhoneX, .iPhoneXSMax:
|
||||
return 39.0
|
||||
case .iPhoneXr:
|
||||
return 41.0 + UIScreenPixel
|
||||
return 41.5
|
||||
case .iPhone12Mini:
|
||||
return 44.0
|
||||
case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed:
|
||||
case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14ProZoomed:
|
||||
return 47.0 + UIScreenPixel
|
||||
case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone14ProMaxZoomed:
|
||||
case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMaxZoomed:
|
||||
return 53.0 + UIScreenPixel
|
||||
case .iPhone14Pro, .iPhone14ProMax:
|
||||
return 55.0
|
||||
case let .unknown(_, _, onScreenNavigationHeight):
|
||||
if let _ = onScreenNavigationHeight {
|
||||
return 39.0
|
||||
|
@ -566,7 +566,7 @@ public class DrawingContext {
|
||||
f(self.context)
|
||||
}
|
||||
|
||||
public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil) {
|
||||
public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil, colorSpace: CGColorSpace? = nil) {
|
||||
if size.width <= 0.0 || size.height <= 0.0 {
|
||||
return nil
|
||||
}
|
||||
@ -601,7 +601,7 @@ public class DrawingContext {
|
||||
height: Int(self.scaledSize.height),
|
||||
bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent,
|
||||
bytesPerRow: self.bytesPerRow,
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: self.bitmapInfo.rawValue,
|
||||
releaseCallback: nil,
|
||||
releaseInfo: nil
|
||||
@ -616,7 +616,7 @@ public class DrawingContext {
|
||||
}
|
||||
}
|
||||
|
||||
public func generateImage() -> UIImage? {
|
||||
public func generateImage(colorSpace: CGColorSpace? = nil) -> UIImage? {
|
||||
if self.scaledSize.width.isZero || self.scaledSize.height.isZero {
|
||||
return nil
|
||||
}
|
||||
@ -633,7 +633,7 @@ public class DrawingContext {
|
||||
bitsPerComponent: self.context.bitsPerComponent,
|
||||
bitsPerPixel: self.context.bitsPerPixel,
|
||||
bytesPerRow: self.context.bytesPerRow,
|
||||
space: DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace,
|
||||
bitmapInfo: self.context.bitmapInfo,
|
||||
provider: dataProvider,
|
||||
decode: nil,
|
||||
|
@ -1274,9 +1274,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
self.filterController(controller, animated: false)
|
||||
}
|
||||
|
||||
public func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
}
|
||||
|
||||
private func scrollToTop(_ subject: NavigationSplitContainerScrollToTop) {
|
||||
if let _ = self.inCallStatusBar {
|
||||
self.inCallNavigate?()
|
||||
@ -1765,4 +1762,11 @@ open class NavigationController: UINavigationController, ContainableController,
|
||||
private func notifyAccessibilityScreenChanged() {
|
||||
UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil)
|
||||
}
|
||||
|
||||
public func updateRootContainerTransitionOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let rootContainer = self.rootContainer, case let .flat(container) = rootContainer else {
|
||||
return
|
||||
}
|
||||
transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0))
|
||||
}
|
||||
}
|
||||
|
@ -166,10 +166,12 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
|
||||
public private(set) var modalStyleOverlayTransitionFactor: CGFloat = 0.0
|
||||
public var modalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
public var customModalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)?
|
||||
public func updateModalStyleOverlayTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
if self.modalStyleOverlayTransitionFactor != value {
|
||||
self.modalStyleOverlayTransitionFactor = value
|
||||
self.modalStyleOverlayTransitionFactorUpdated?(transition)
|
||||
self.customModalStyleOverlayTransitionFactorUpdated?(transition)
|
||||
}
|
||||
}
|
||||
|
||||
@ -452,10 +454,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject {
|
||||
}
|
||||
}
|
||||
|
||||
open func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
|
||||
}
|
||||
|
||||
open func navigationStackConfigurationUpdated(next: [ViewController]) {
|
||||
}
|
||||
|
||||
|
@ -51,7 +51,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
private let size: CGSize
|
||||
|
||||
weak var drawingView: DrawingView?
|
||||
weak var selectionContainerView: DrawingSelectionContainerView?
|
||||
public weak var selectionContainerView: DrawingSelectionContainerView?
|
||||
|
||||
private var tapGestureRecognizer: UITapGestureRecognizer!
|
||||
private(set) var selectedEntityView: DrawingEntityView?
|
||||
@ -220,7 +220,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
return CGSize(width: width, height: width)
|
||||
}
|
||||
|
||||
func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
public func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) {
|
||||
let center = self.startPosition(relativeTo: relativeTo)
|
||||
let rotation = self.getEntityInitialRotation()
|
||||
let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0)
|
||||
@ -485,7 +485,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
}
|
||||
}
|
||||
|
||||
func selectEntity(_ entity: DrawingEntity?) {
|
||||
public func selectEntity(_ entity: DrawingEntity?) {
|
||||
if entity?.isMedia == true {
|
||||
return
|
||||
}
|
||||
@ -596,7 +596,7 @@ public class DrawingEntityView: UIView {
|
||||
let entity: DrawingEntity
|
||||
var isTracking = false
|
||||
|
||||
weak var selectionView: DrawingEntitySelectionView?
|
||||
public weak var selectionView: DrawingEntitySelectionView?
|
||||
weak var containerView: DrawingEntitiesView?
|
||||
|
||||
var onSnapToXAxis: (Bool) -> Void = { _ in }
|
||||
|
@ -22,6 +22,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia
|
||||
didSet {
|
||||
if let previewView = self.previewView {
|
||||
previewView.isUserInteractionEnabled = false
|
||||
previewView.layer.allowsEdgeAntialiasing = true
|
||||
self.addSubview(previewView)
|
||||
}
|
||||
}
|
||||
|
@ -2310,8 +2310,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
|
||||
private var _selectionContainerView: DrawingSelectionContainerView?
|
||||
var selectionContainerView: DrawingSelectionContainerView {
|
||||
if self._selectionContainerView == nil {
|
||||
self._selectionContainerView = DrawingSelectionContainerView(frame: .zero)
|
||||
if self._selectionContainerView == nil, let controller = self.controller {
|
||||
if let externalSelectionContainerView = controller.externalSelectionContainerView {
|
||||
self._selectionContainerView = externalSelectionContainerView
|
||||
} else {
|
||||
self._selectionContainerView = DrawingSelectionContainerView(frame: .zero)
|
||||
}
|
||||
|
||||
}
|
||||
return self._selectionContainerView!
|
||||
}
|
||||
@ -3013,6 +3018,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
private let isAvatar: Bool
|
||||
private let externalDrawingView: DrawingView?
|
||||
private let externalEntitiesView: DrawingEntitiesView?
|
||||
private let externalSelectionContainerView: DrawingSelectionContainerView?
|
||||
private let existingStickerPickerInputData: Promise<StickerPickerInputData>?
|
||||
|
||||
public var requestDismiss: () -> Void = {}
|
||||
@ -3020,7 +3026,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
public var getCurrentImage: () -> UIImage? = { return nil }
|
||||
public var updateVideoPlayback: (Bool) -> Void = { _ in }
|
||||
|
||||
public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, existingStickerPickerInputData: Promise<StickerPickerInputData>? = nil) {
|
||||
public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise<StickerPickerInputData>? = nil) {
|
||||
self.context = context
|
||||
self.sourceHint = sourceHint
|
||||
self.size = size
|
||||
@ -3041,6 +3047,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
self.externalEntitiesView = nil
|
||||
}
|
||||
|
||||
if let selectionContainerView = selectionContainerView {
|
||||
self.externalSelectionContainerView = selectionContainerView
|
||||
} else {
|
||||
self.externalSelectionContainerView = nil
|
||||
}
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .Hide
|
||||
@ -3081,13 +3093,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
||||
return nil
|
||||
}
|
||||
|
||||
let drawingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in
|
||||
let bounds = CGRect(origin: .zero, size: size)
|
||||
context.clear(bounds)
|
||||
if let cgImage = self.drawingView.drawingImage?.cgImage {
|
||||
context.draw(cgImage, in: bounds)
|
||||
}
|
||||
}, opaque: false, scale: 1.0)
|
||||
let drawingImage = self.drawingView.drawingImage
|
||||
|
||||
let _ = self.entitiesView.entitiesData
|
||||
let codableEntities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) })
|
||||
|
@ -201,7 +201,7 @@ private final class StickerSelectionComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
class StickerPickerScreen: ViewController {
|
||||
public class StickerPickerScreen: ViewController {
|
||||
final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private var presentationData: PresentationData
|
||||
private weak var controller: StickerPickerScreen?
|
||||
@ -999,9 +999,9 @@ class StickerPickerScreen: ViewController {
|
||||
public var pushController: (ViewController) -> Void = { _ in }
|
||||
public var presentController: (ViewController) -> Void = { _ in }
|
||||
|
||||
var completion: (TelegramMediaFile?) -> Void = { _ in }
|
||||
public var completion: (TelegramMediaFile?) -> Void = { _ in }
|
||||
|
||||
init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>) {
|
||||
public init(context: AccountContext, inputData: Signal<StickerPickerInputData, NoError>) {
|
||||
self.context = context
|
||||
self.theme = defaultDarkColorPresentationTheme
|
||||
self.inputData = inputData
|
||||
@ -1015,14 +1015,14 @@ class StickerPickerScreen: ViewController {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func loadDisplayNode() {
|
||||
public override func loadDisplayNode() {
|
||||
self.displayNode = Node(context: self.context, controller: self, theme: self.theme)
|
||||
self.displayNodeDidLoad()
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
}
|
||||
|
||||
override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
self.view.endEditing(true)
|
||||
if flag {
|
||||
self.node.animateOut(completion: {
|
||||
@ -1035,19 +1035,19 @@ class StickerPickerScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
public override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
|
||||
self.node.updateIsVisible(isVisible: true)
|
||||
}
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
public override func viewDidDisappear(_ animated: Bool) {
|
||||
super.viewDidDisappear(animated)
|
||||
|
||||
self.node.updateIsVisible(isVisible: false)
|
||||
}
|
||||
|
||||
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.currentLayout = layout
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
@ -579,7 +579,7 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon
|
||||
let interfaceController: TGPhotoDrawingInterfaceController
|
||||
|
||||
init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) {
|
||||
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView)
|
||||
let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView, selectionContainerView: nil)
|
||||
self.interfaceController = interfaceController
|
||||
self.drawingView = interfaceController.drawingView
|
||||
self.drawingEntitiesView = interfaceController.entitiesView
|
||||
|
@ -59,3 +59,69 @@ public extension EnginePeer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public extension EnginePeer.IndexName {
|
||||
func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult {
|
||||
switch self {
|
||||
case let .title(lhsTitle, _):
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsTitle.caseInsensitiveCompare(rhsString)
|
||||
case let .personName(lhsFirst, lhsLast, _, _):
|
||||
let lhsString: String
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if lhsFirst.isEmpty {
|
||||
lhsString = lhsLast
|
||||
} else {
|
||||
lhsString = lhsFirst + lhsLast
|
||||
}
|
||||
case .lastFirst:
|
||||
if lhsLast.isEmpty {
|
||||
lhsString = lhsFirst
|
||||
} else {
|
||||
lhsString = lhsLast + lhsFirst
|
||||
}
|
||||
}
|
||||
let rhsString: String
|
||||
switch other {
|
||||
case let .title(title, _):
|
||||
rhsString = title
|
||||
case let .personName(first, last, _, _):
|
||||
switch ordering {
|
||||
case .firstLast:
|
||||
if first.isEmpty {
|
||||
rhsString = last
|
||||
} else {
|
||||
rhsString = first + last
|
||||
}
|
||||
case .lastFirst:
|
||||
if last.isEmpty {
|
||||
rhsString = first
|
||||
} else {
|
||||
rhsString = last + first
|
||||
}
|
||||
}
|
||||
}
|
||||
return lhsString.caseInsensitiveCompare(rhsString)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,30 +29,32 @@ final class MediaPickerGridItem: GridItem {
|
||||
let theme: PresentationTheme
|
||||
let selectable: Bool
|
||||
let enableAnimations: Bool
|
||||
let stories: Bool
|
||||
|
||||
let section: GridSection? = nil
|
||||
|
||||
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.content = content
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
self.enableAnimations = enableAnimations
|
||||
self.stories = stories
|
||||
}
|
||||
|
||||
func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode {
|
||||
switch self.content {
|
||||
case let .asset(fetchResult, index):
|
||||
let node = MediaPickerGridItemNode()
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
return node
|
||||
case let .media(media, index):
|
||||
let node = MediaPickerGridItemNode()
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
return node
|
||||
case let .draft(draft, index):
|
||||
let node = MediaPickerGridItemNode()
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
return node
|
||||
}
|
||||
}
|
||||
@ -64,11 +66,11 @@ final class MediaPickerGridItem: GridItem {
|
||||
}
|
||||
switch self.content {
|
||||
case let .asset(fetchResult, index):
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
case let .media(media, index):
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
case let .draft(draft, index):
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations)
|
||||
node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -151,7 +153,11 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
var identifier: String {
|
||||
return self.selectableItem?.uniqueIdentifier ?? ""
|
||||
if let (draft, _) = self.currentDraftState {
|
||||
return draft.path
|
||||
} else {
|
||||
return self.selectableItem?.uniqueIdentifier ?? ""
|
||||
}
|
||||
}
|
||||
|
||||
var selectableItem: TGMediaSelectableItem? {
|
||||
@ -235,7 +241,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:))))
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
@ -262,7 +268,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.updateHiddenMedia()
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
@ -279,7 +285,7 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
self.updateHiddenMedia()
|
||||
}
|
||||
|
||||
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) {
|
||||
func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult<PHAsset>, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) {
|
||||
self.interaction = interaction
|
||||
self.theme = theme
|
||||
self.selectable = selectable
|
||||
@ -315,7 +321,12 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
|
||||
let scale = min(2.0, UIScreenScale)
|
||||
let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
let targetSize: CGSize
|
||||
if stories {
|
||||
targetSize = CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale)
|
||||
} else {
|
||||
targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale)
|
||||
}
|
||||
|
||||
let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true)
|
||||
|> then(
|
||||
@ -456,10 +467,18 @@ final class MediaPickerGridItemNode: GridItemNode {
|
||||
}
|
||||
}
|
||||
|
||||
func transitionView() -> UIView {
|
||||
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
|
||||
view.frame = self.convert(self.bounds, to: nil)
|
||||
return view
|
||||
func transitionView(snapshot: Bool) -> UIView {
|
||||
if snapshot {
|
||||
let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
|
||||
view.frame = self.convert(self.bounds, to: nil)
|
||||
return view
|
||||
} else {
|
||||
return self.imageNode.view
|
||||
}
|
||||
}
|
||||
|
||||
func transitionImage() -> UIImage? {
|
||||
return self.imageNode.image
|
||||
}
|
||||
|
||||
@objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) {
|
||||
|
@ -53,13 +53,14 @@ private struct MediaPickerGridEntry: Comparable, Identifiable {
|
||||
let stableId: Int
|
||||
let content: MediaPickerGridItemContent
|
||||
let selectable: Bool
|
||||
let stories: Bool
|
||||
|
||||
static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool {
|
||||
return lhs.stableId < rhs.stableId
|
||||
}
|
||||
|
||||
func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme) -> MediaPickerGridItem {
|
||||
return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency)
|
||||
return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories)
|
||||
}
|
||||
}
|
||||
|
||||
@ -583,9 +584,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
var updateLayout = false
|
||||
|
||||
var stories = false
|
||||
var selectable = true
|
||||
if case let .assets(_, mode) = controller.subject, mode != .default {
|
||||
selectable = false
|
||||
if mode == .story {
|
||||
stories = true
|
||||
}
|
||||
}
|
||||
|
||||
switch state {
|
||||
@ -606,7 +611,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
var draftIndex = 0
|
||||
for draft in drafts {
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable))
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable, stories: stories))
|
||||
stableId += 1
|
||||
draftIndex += 1
|
||||
}
|
||||
@ -618,7 +623,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
} else {
|
||||
index = totalCount - i - 1
|
||||
}
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable))
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable, stories: stories))
|
||||
stableId += 1
|
||||
}
|
||||
|
||||
@ -647,7 +652,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
case let .media(media):
|
||||
let count = media.count
|
||||
for i in 0 ..< count {
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true))
|
||||
entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true, stories: stories))
|
||||
stableId += 1
|
||||
}
|
||||
}
|
||||
@ -976,7 +981,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
private func transitionView(for identifier: String, hideSource: Bool = false) -> UIView? {
|
||||
fileprivate func transitionView(for identifier: String, snapshot: Bool = true, hideSource: Bool = false) -> UIView? {
|
||||
if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 {
|
||||
return selectionNode.transitionView(for: identifier, hideSource: hideSource)
|
||||
} else {
|
||||
@ -986,7 +991,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
transitionNode = itemNode
|
||||
}
|
||||
}
|
||||
let transitionView = transitionNode?.transitionView()
|
||||
let transitionView = transitionNode?.transitionView(snapshot: snapshot)
|
||||
if hideSource {
|
||||
transitionNode?.isHidden = true
|
||||
}
|
||||
@ -994,6 +999,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func transitionImage(for identifier: String) -> UIImage? {
|
||||
var transitionNode: MediaPickerGridItemNode?
|
||||
self.gridNode.forEachItemNode { itemNode in
|
||||
if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier {
|
||||
transitionNode = itemNode
|
||||
}
|
||||
}
|
||||
return transitionNode?.transitionImage()
|
||||
}
|
||||
|
||||
private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) {
|
||||
self.enqueuedTransactions.append(transaction)
|
||||
|
||||
@ -1189,7 +1204,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
|
||||
var itemHeight = itemWidth
|
||||
if case let .assets(_, mode) = controller.subject, case .story = mode {
|
||||
itemHeight = 180.0
|
||||
itemHeight = round(itemWidth / 9.0 * 16.0)
|
||||
}
|
||||
|
||||
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemHeight * 3.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
|
||||
@ -1905,6 +1920,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? {
|
||||
return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource)
|
||||
}
|
||||
|
||||
fileprivate func transitionImage(for identifier: String) -> UIImage? {
|
||||
return self.controllerNode.transitionImage(for: identifier)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
@ -2112,7 +2135,8 @@ public func wallpaperMediaPickerController(
|
||||
|
||||
public func storyMediaPickerController(
|
||||
context: AccountContext,
|
||||
completion: @escaping (Any) -> Void = { _ in }
|
||||
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void = { _, _, _, _, _ in },
|
||||
dismissed: @escaping () -> Void
|
||||
) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
let updatedPresentationData: (PresentationData, Signal<PresentationData, NoError>) = (presentationData, .single(presentationData))
|
||||
@ -2121,9 +2145,37 @@ public func storyMediaPickerController(
|
||||
})
|
||||
controller.requestController = { _, present in
|
||||
let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil)
|
||||
mediaPickerController.customSelection = completion
|
||||
mediaPickerController.customSelection = { [weak mediaPickerController] result in
|
||||
guard let controller = mediaPickerController else {
|
||||
return
|
||||
}
|
||||
if let result = result as? MediaEditorDraft {
|
||||
if let transitionView = controller.transitionView(for: result.path, snapshot: false) {
|
||||
let transitionOut: () -> (UIView, CGRect)? = {
|
||||
if let transitionView = controller.transitionView(for: result.path, snapshot: false) {
|
||||
return (transitionView, transitionView.bounds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.path), transitionOut)
|
||||
}
|
||||
} else if let result = result as? PHAsset {
|
||||
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
|
||||
let transitionOut: () -> (UIView, CGRect)? = {
|
||||
if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) {
|
||||
return (transitionView, transitionView.bounds)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut)
|
||||
}
|
||||
}
|
||||
}
|
||||
present(mediaPickerController, mediaPickerController.mediaPickerContext)
|
||||
}
|
||||
controller.willDismiss = {
|
||||
dismissed()
|
||||
}
|
||||
controller.navigationPresentation = .flatModal
|
||||
controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
return controller
|
||||
|
@ -328,8 +328,8 @@ public func chatMessageLegacySticker(account: Account, userLocation: MediaResour
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad)
|
||||
public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad, colorSpace: colorSpace)
|
||||
}
|
||||
|
||||
public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
@ -385,7 +385,7 @@ public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaRes
|
||||
}
|
||||
}
|
||||
|
||||
public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal: Signal<Tuple3<Data?, Data?, Bool>, NoError>
|
||||
if thumbnail {
|
||||
signal = chatMessageStickerThumbnailData(postbox: postbox, userLocation: userLocation, file: file, synchronousLoad: synchronousLoad)
|
||||
@ -408,7 +408,7 @@ public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUser
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) else {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil, colorSpace: colorSpace) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -64,6 +64,9 @@ extension TelegramUser {
|
||||
if (flags & (1 << 28)) != 0 {
|
||||
userFlags.insert(.isPremium)
|
||||
}
|
||||
if (flags2 & (1 << 2)) != 0 {
|
||||
userFlags.insert(.isCloseFriend)
|
||||
}
|
||||
|
||||
var botInfo: BotUserInfo?
|
||||
if (flags & (1 << 14)) != 0 {
|
||||
@ -96,7 +99,7 @@ extension TelegramUser {
|
||||
|
||||
static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? {
|
||||
switch rhs {
|
||||
case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _):
|
||||
case let .user(flags, flags2, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _):
|
||||
let isMin = (flags & (1 << 20)) != 0
|
||||
if !isMin {
|
||||
return TelegramUser(user: rhs)
|
||||
@ -129,6 +132,9 @@ extension TelegramUser {
|
||||
if (flags & (1 << 28)) != 0 {
|
||||
userFlags.insert(.isPremium)
|
||||
}
|
||||
if (flags2 & (1 << 2)) != 0 {
|
||||
userFlags.insert(.isCloseFriend)
|
||||
}
|
||||
|
||||
var botInfo: BotUserInfo?
|
||||
if (flags & (1 << 14)) != 0 {
|
||||
|
@ -16,6 +16,7 @@ public struct UserInfoFlags: OptionSet {
|
||||
public static let isScam = UserInfoFlags(rawValue: (1 << 2))
|
||||
public static let isFake = UserInfoFlags(rawValue: (1 << 3))
|
||||
public static let isPremium = UserInfoFlags(rawValue: (1 << 4))
|
||||
public static let isCloseFriend = UserInfoFlags(rawValue: (1 << 5))
|
||||
}
|
||||
|
||||
public struct BotUserInfoFlags: OptionSet {
|
||||
@ -351,4 +352,8 @@ public final class TelegramUser: Peer, Equatable {
|
||||
public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramUser {
|
||||
return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames)
|
||||
}
|
||||
|
||||
public func withUpdatedFlags(_ flags: UserInfoFlags) -> TelegramUser {
|
||||
return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,36 @@
|
||||
import Foundation
|
||||
import Postbox
|
||||
import SwiftSignalKit
|
||||
import TelegramApi
|
||||
import MtProtoKit
|
||||
|
||||
public func _internal_updateCloseFriends(account: Account, peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
|
||||
let ids: [Int64] = peerIds.map { $0.id._internalGetInt64Value() }
|
||||
return account.network.request(Api.functions.contacts.editCloseFriends(id: ids))
|
||||
|> retryRequest
|
||||
|> mapToSignal { result -> Signal<Void, NoError> in
|
||||
return account.postbox.transaction { transaction in
|
||||
let contactPeerIds = transaction.getContactPeerIds()
|
||||
var updatedPeers: [Peer] = []
|
||||
for peerId in contactPeerIds {
|
||||
if let peer = transaction.getPeer(peerId) as? TelegramUser {
|
||||
if peerIds.contains(peerId) {
|
||||
var updatedFlags = peer.flags
|
||||
updatedFlags.insert(.isCloseFriend)
|
||||
let updatedPeer = peer.withUpdatedFlags(updatedFlags)
|
||||
updatedPeers.append(updatedPeer)
|
||||
} else if peer.flags.contains(.isCloseFriend) {
|
||||
var updatedFlags = peer.flags
|
||||
updatedFlags.remove(.isCloseFriend)
|
||||
let updatedPeer = peer.withUpdatedFlags(updatedFlags)
|
||||
updatedPeers.append(updatedPeer)
|
||||
}
|
||||
}
|
||||
}
|
||||
updatePeers(transaction: transaction, peers: updatedPeers, update: { _, updated in
|
||||
return updated
|
||||
})
|
||||
}
|
||||
}
|
||||
|> ignoreValues
|
||||
}
|
@ -44,5 +44,9 @@ public extension TelegramEngine {
|
||||
public func updateSelectiveAccountPrivacySettings(type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal<Void, NoError> {
|
||||
return _internal_updateSelectiveAccountPrivacySettings(account: self.account, type: type, settings: settings)
|
||||
}
|
||||
|
||||
public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal<Never, NoError> {
|
||||
return _internal_updateCloseFriends(account: self.account, peerIds: peerIds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -70,6 +70,7 @@ swift_library(
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BlurredBackgroundComponent",
|
||||
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
|
||||
"//submodules/Components/BundleIconComponent:BundleIconComponent",
|
||||
"//submodules/TooltipUI",
|
||||
"//submodules/TelegramUI/Components/MediaEditor",
|
||||
],
|
||||
|
@ -3,14 +3,14 @@ import UIKit
|
||||
import ComponentFlow
|
||||
|
||||
final class CameraButton: Component {
|
||||
let content: AnyComponent<Empty>
|
||||
let content: AnyComponentWithIdentity<Empty>
|
||||
let minSize: CGSize?
|
||||
let tag: AnyObject?
|
||||
let isEnabled: Bool
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
content: AnyComponent<Empty>,
|
||||
content: AnyComponentWithIdentity<Empty>,
|
||||
minSize: CGSize? = nil,
|
||||
tag: AnyObject? = nil,
|
||||
isEnabled: Bool = true,
|
||||
@ -50,7 +50,7 @@ final class CameraButton: Component {
|
||||
}
|
||||
|
||||
final class View: UIButton, ComponentTaggedView {
|
||||
private let contentView: ComponentHostView<Empty>
|
||||
private var contentView: ComponentHostView<Empty>
|
||||
|
||||
private var component: CameraButton?
|
||||
private var currentIsHighlighted: Bool = false {
|
||||
@ -123,9 +123,17 @@ final class CameraButton: Component {
|
||||
}
|
||||
|
||||
func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
if let currentId = self.component?.content.id, currentId != component.content.id {
|
||||
self.contentView.removeFromSuperview()
|
||||
|
||||
self.contentView = ComponentHostView<Empty>()
|
||||
self.contentView.isUserInteractionEnabled = false
|
||||
self.contentView.layer.allowsGroupOpacity = true
|
||||
self.addSubview(self.contentView)
|
||||
}
|
||||
let contentSize = self.contentView.update(
|
||||
transition: transition,
|
||||
component: component.content,
|
||||
component: component.content.component,
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
|
@ -17,6 +17,7 @@ import Photos
|
||||
import LottieAnimationComponent
|
||||
import TooltipUI
|
||||
import MediaEditor
|
||||
import BundleIconComponent
|
||||
|
||||
let videoRedColor = UIColor(rgb: 0xff3b30)
|
||||
|
||||
@ -33,29 +34,31 @@ private struct CameraState {
|
||||
}
|
||||
let mode: CameraMode
|
||||
let flashMode: Camera.FlashMode
|
||||
let flashModeDidChange: Bool
|
||||
let recording: Recording
|
||||
let duration: Double
|
||||
|
||||
func updatedMode(_ mode: CameraMode) -> CameraState {
|
||||
return CameraState(mode: mode, flashMode: self.flashMode, recording: self.recording, duration: self.duration)
|
||||
return CameraState(mode: mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration)
|
||||
}
|
||||
|
||||
func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState {
|
||||
return CameraState(mode: self.mode, flashMode: flashMode, recording: self.recording, duration: self.duration)
|
||||
return CameraState(mode: self.mode, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration)
|
||||
}
|
||||
|
||||
func updatedRecording(_ recording: Recording) -> CameraState {
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, recording: recording, duration: self.duration)
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration)
|
||||
}
|
||||
|
||||
func updatedDuration(_ duration: Double) -> CameraState {
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, recording: self.recording, duration: duration)
|
||||
return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration)
|
||||
}
|
||||
}
|
||||
|
||||
enum CameraScreenTransition {
|
||||
case animateIn
|
||||
case animateOut
|
||||
case finishedAnimateIn
|
||||
}
|
||||
|
||||
private let cancelButtonTag = GenericComponentViewTag()
|
||||
@ -71,7 +74,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
let context: AccountContext
|
||||
let camera: Camera
|
||||
let changeMode: ActionSlot<CameraMode>
|
||||
let isDismissing: Bool
|
||||
let hasAppeared: Bool
|
||||
let present: (ViewController) -> Void
|
||||
let push: (ViewController) -> Void
|
||||
let completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@ -80,7 +83,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
context: AccountContext,
|
||||
camera: Camera,
|
||||
changeMode: ActionSlot<CameraMode>,
|
||||
isDismissing: Bool,
|
||||
hasAppeared: Bool,
|
||||
present: @escaping (ViewController) -> Void,
|
||||
push: @escaping (ViewController) -> Void,
|
||||
completion: ActionSlot<Signal<CameraScreen.Result, NoError>>
|
||||
@ -88,7 +91,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
self.context = context
|
||||
self.camera = camera
|
||||
self.changeMode = changeMode
|
||||
self.isDismissing = isDismissing
|
||||
self.hasAppeared = hasAppeared
|
||||
self.present = present
|
||||
self.push = push
|
||||
self.completion = completion
|
||||
@ -98,7 +101,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.isDismissing != rhs.isDismissing {
|
||||
if lhs.hasAppeared != rhs.hasAppeared {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@ -108,7 +111,6 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
enum ImageKey: Hashable {
|
||||
case cancel
|
||||
case flip
|
||||
case flash
|
||||
}
|
||||
private var cachedImages: [ImageKey: UIImage] = [:]
|
||||
func image(_ key: ImageKey) -> UIImage {
|
||||
@ -121,8 +123,6 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
image = UIImage(bundleImageName: "Camera/CloseIcon")!
|
||||
case .flip:
|
||||
image = UIImage(bundleImageName: "Camera/FlipIcon")!
|
||||
case .flash:
|
||||
image = UIImage(bundleImageName: "Camera/FlashIcon")!
|
||||
}
|
||||
cachedImages[key] = image
|
||||
return image
|
||||
@ -141,7 +141,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
fileprivate var lastGalleryAsset: PHAsset?
|
||||
private var lastGalleryAssetsDisposable: Disposable?
|
||||
|
||||
var cameraState = CameraState(mode: .photo, flashMode: .off, recording: .none, duration: 0.0)
|
||||
var cameraState = CameraState(mode: .photo, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0)
|
||||
var swipeHint: CaptureControlsComponent.SwipeHint = .none
|
||||
|
||||
init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot<Signal<CameraScreen.Result, NoError>>) {
|
||||
@ -188,6 +188,9 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
|
||||
func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) {
|
||||
guard hint != self.swipeHint else {
|
||||
return
|
||||
}
|
||||
self.swipeHint = hint
|
||||
self.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
@ -274,10 +277,15 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
if case .none = state.cameraState.recording {
|
||||
let cancelButton = cancelButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponent(Image(
|
||||
image: state.image(.cancel),
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
)),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "cancel",
|
||||
component: AnyComponent(
|
||||
Image(
|
||||
image: state.image(.cancel),
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
guard let controller = controller() as? CameraScreen else {
|
||||
return
|
||||
@ -294,32 +302,50 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
.disappear(.default(scale: true))
|
||||
.cornerRadius(20.0)
|
||||
)
|
||||
|
||||
let flashIconName: String
|
||||
switch state.cameraState.flashMode {
|
||||
case .off:
|
||||
flashIconName = "flash_off"
|
||||
case .on:
|
||||
flashIconName = "flash_on"
|
||||
case .auto:
|
||||
flashIconName = "flash_auto"
|
||||
@unknown default:
|
||||
flashIconName = "flash_off"
|
||||
}
|
||||
|
||||
let flashButton = flashButton.update(
|
||||
component: CameraButton(
|
||||
content: AnyComponent(
|
||||
|
||||
let flashContentComponent: AnyComponentWithIdentity<Empty>
|
||||
if component.hasAppeared {
|
||||
let flashIconName: String
|
||||
switch state.cameraState.flashMode {
|
||||
case .off:
|
||||
flashIconName = "flash_off"
|
||||
case .on:
|
||||
flashIconName = "flash_on"
|
||||
case .auto:
|
||||
flashIconName = "flash_auto"
|
||||
@unknown default:
|
||||
flashIconName = "flash_off"
|
||||
}
|
||||
|
||||
flashContentComponent = AnyComponentWithIdentity(
|
||||
id: "animatedIcon",
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: flashIconName,
|
||||
mode: .animating(loop: false),
|
||||
mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false),
|
||||
range: nil
|
||||
),
|
||||
colors: [:],
|
||||
size: CGSize(width: 40.0, height: 40.0)
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
} else {
|
||||
flashContentComponent = AnyComponentWithIdentity(
|
||||
id: "staticIcon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Camera/FlashOffIcon",
|
||||
tintColor: nil
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
let flashButton = flashButton.update(
|
||||
component: CameraButton(
|
||||
content: flashContentComponent,
|
||||
action: { [weak state] in
|
||||
guard let state else {
|
||||
return
|
||||
@ -514,7 +540,7 @@ private final class CameraScreenComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
|
||||
if case .none = state.cameraState.recording, !component.isDismissing {
|
||||
if case .none = state.cameraState.recording {
|
||||
let modeControl = modeControl.update(
|
||||
component: ModeComponent(
|
||||
availableModes: [.photo, .video],
|
||||
@ -638,13 +664,16 @@ public class CameraScreen: ViewController {
|
||||
private let context: AccountContext
|
||||
private let updateState: ActionSlot<CameraState>
|
||||
|
||||
private let backgroundEffectView: UIVisualEffectView
|
||||
private let backgroundDimView: UIView
|
||||
fileprivate let backgroundView: UIView
|
||||
fileprivate let containerView: UIView
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
private let previewContainerView: UIView
|
||||
fileprivate let previewView: CameraPreviewView?
|
||||
fileprivate let simplePreviewView: CameraSimplePreviewView?
|
||||
fileprivate let previewBlurView: BlurView
|
||||
private var previewSnapshotView: UIView?
|
||||
fileprivate let transitionDimView: UIView
|
||||
fileprivate let transitionCornersView: UIImageView
|
||||
fileprivate let camera: Camera
|
||||
|
||||
private var presentationData: PresentationData
|
||||
@ -665,7 +694,7 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var previewBlurPromise = ValuePromise<Bool>(false)
|
||||
fileprivate var previewBlurPromise = ValuePromise<Bool>(false)
|
||||
|
||||
init(controller: CameraScreen) {
|
||||
self.controller = controller
|
||||
@ -674,9 +703,11 @@ public class CameraScreen: ViewController {
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.backgroundEffectView = UIVisualEffectView(effect: nil)
|
||||
self.backgroundDimView = UIView()
|
||||
self.backgroundDimView.backgroundColor = UIColor(rgb: 0x000000)
|
||||
self.backgroundView = UIView()
|
||||
self.backgroundView.backgroundColor = UIColor(rgb: 0x000000)
|
||||
|
||||
self.containerView = UIView()
|
||||
self.containerView.clipsToBounds = true
|
||||
|
||||
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
|
||||
|
||||
@ -710,17 +741,25 @@ public class CameraScreen: ViewController {
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
self.transitionDimView = UIView()
|
||||
self.transitionDimView.backgroundColor = UIColor(rgb: 0x000000)
|
||||
self.transitionDimView.isUserInteractionEnabled = false
|
||||
|
||||
self.transitionCornersView = UIImageView()
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.view.addSubview(self.backgroundEffectView)
|
||||
self.view.addSubview(self.backgroundDimView)
|
||||
self.view.addSubview(self.backgroundView)
|
||||
self.view.addSubview(self.containerView)
|
||||
|
||||
self.view.addSubview(self.previewContainerView)
|
||||
self.containerView.addSubview(self.previewContainerView)
|
||||
self.previewContainerView.addSubview(self.effectivePreviewView)
|
||||
self.previewContainerView.addSubview(self.previewBlurView)
|
||||
self.containerView.addSubview(self.transitionDimView)
|
||||
self.view.addSubview(self.transitionCornersView)
|
||||
|
||||
self.changingPositionDisposable = combineLatest(
|
||||
queue: Queue.mainQueue(),
|
||||
@ -733,7 +772,9 @@ public class CameraScreen: ViewController {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
})
|
||||
} else if forceBlur {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.previewBlurView.effect = UIBlurEffect(style: .dark)
|
||||
}
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.4) {
|
||||
self.previewBlurView.effect = nil
|
||||
@ -776,7 +817,8 @@ public class CameraScreen: ViewController {
|
||||
self.camera.stopCapture()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
nil
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -814,96 +856,43 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private var panTranslation: CGFloat?
|
||||
private var previewInitialPosition: CGPoint?
|
||||
private var controlsInitialPosition: CGPoint?
|
||||
@objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
||||
switch gestureRecognizer.state {
|
||||
case .began:
|
||||
self.panTranslation = nil
|
||||
self.previewInitialPosition = self.previewContainerView.center
|
||||
self.controlsInitialPosition = self.componentHost.view?.center
|
||||
case .changed:
|
||||
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
|
||||
if !"".isEmpty {
|
||||
|
||||
} else {
|
||||
if abs(translation.x) > 50.0 && abs(translation.y) < 50.0, self.panTranslation == nil {
|
||||
self.changeMode.invoke(translation.x > 0.0 ? .photo : .video)
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
} else if translation.y > 10.0 {
|
||||
let isFirstPanChange = self.panTranslation == nil
|
||||
self.panTranslation = translation.y
|
||||
if let previewInitialPosition = self.previewInitialPosition {
|
||||
self.previewContainerView.center = CGPoint(x: previewInitialPosition.x, y: previewInitialPosition.y + translation.y)
|
||||
}
|
||||
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
||||
view.center = CGPoint(x: controlsInitialPosition.x, y: controlsInitialPosition.y + translation.y)
|
||||
}
|
||||
|
||||
if self.backgroundEffectView.isHidden {
|
||||
self.backgroundEffectView.isHidden = false
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = nil
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
})
|
||||
}
|
||||
|
||||
if isFirstPanChange {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
if translation.x < -10.0 {
|
||||
let transitionFraction = 1.0 - abs(translation.x) / self.frame.width
|
||||
controller.updateTransitionProgress(transitionFraction, transition: .immediate)
|
||||
} else if translation.y < -10.0 {
|
||||
self.controller?.presentGallery()
|
||||
controller.presentGallery()
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
}
|
||||
}
|
||||
case .ended:
|
||||
let velocity = gestureRecognizer.velocity(in: self.view)
|
||||
if velocity.y > 1000.0 {
|
||||
self.controller?.requestDismiss(animated: true)
|
||||
} else if let panTranslation = self.panTranslation, abs(panTranslation) > 300.0 {
|
||||
self.controller?.requestDismiss(animated: true)
|
||||
} else {
|
||||
let transition = Transition(animation: .curve(duration: 0.3, curve: .spring))
|
||||
if let previewInitialPosition = self.previewInitialPosition {
|
||||
transition.setPosition(view: self.previewContainerView, position: previewInitialPosition)
|
||||
}
|
||||
if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view {
|
||||
transition.setPosition(view: view, position: controlsInitialPosition)
|
||||
}
|
||||
if !self.backgroundEffectView.isHidden {
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
}, completion: { _ in
|
||||
self.backgroundEffectView.isHidden = true
|
||||
})
|
||||
}
|
||||
}
|
||||
if let _ = self.panTranslation {
|
||||
self.panTranslation = nil
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
}
|
||||
let transitionFraction = 1.0 - abs(translation.x) / self.frame.width
|
||||
controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
self.backgroundView.alpha = 0.0
|
||||
UIView.animate(withDuration: 0.4, animations: {
|
||||
self.backgroundEffectView.effect = UIBlurEffect(style: .dark)
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
|
||||
}, completion: { _ in
|
||||
self.backgroundEffectView.isHidden = true
|
||||
self.backgroundView.alpha = 1.0
|
||||
})
|
||||
|
||||
if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView {
|
||||
@ -926,23 +915,13 @@ public class CameraScreen: ViewController {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||
view.layer.shadowRadius = 4.0
|
||||
view.layer.shadowColor = UIColor.black.cgColor
|
||||
view.layer.shadowOpacity = 0.2
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
self.camera.stopCapture(invalidate: true)
|
||||
|
||||
self.backgroundEffectView.isHidden = false
|
||||
|
||||
|
||||
UIView.animate(withDuration: 0.25, animations: {
|
||||
self.backgroundEffectView.effect = nil
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
self.backgroundView.alpha = 0.0
|
||||
})
|
||||
|
||||
if let transitionOut = self.controller?.transitionOut(false), let destinationView = transitionOut.destinationView {
|
||||
@ -994,13 +973,15 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func commitTransitionToEditor() {
|
||||
self.previewContainerView.alpha = 0.0
|
||||
func pauseCameraCapture() {
|
||||
self.simplePreviewView?.isEnabled = false
|
||||
Queue.mainQueue().after(0.3) {
|
||||
self.previewBlurPromise.set(true)
|
||||
}
|
||||
self.camera.stopCapture()
|
||||
}
|
||||
|
||||
private var previewSnapshotView: UIView?
|
||||
func animateInFromEditor() {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
func resumeCameraCapture() {
|
||||
if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) {
|
||||
self.simplePreviewView?.addSubview(snapshot)
|
||||
self.previewSnapshotView = snapshot
|
||||
@ -1023,26 +1004,46 @@ public class CameraScreen: ViewController {
|
||||
self.previewBlurPromise.set(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateInFromEditor(toGallery: Bool) {
|
||||
if !toGallery {
|
||||
self.resumeCameraCapture()
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||
guard let layout = self.validLayout else {
|
||||
return
|
||||
}
|
||||
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: zoomControlTag) {
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View {
|
||||
view.animateInFromEditor(transition: transition)
|
||||
}
|
||||
let progress = 1.0 - value
|
||||
let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width
|
||||
let maxOffset = -56.0
|
||||
|
||||
let scale = 1.0 * progress + (1.0 - progress) * maxScale
|
||||
let offset = (1.0 - progress) * maxOffset
|
||||
transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true)
|
||||
}
|
||||
|
||||
func presentDraftTooltip() {
|
||||
@ -1067,8 +1068,22 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func requestUpdateLayout(hasAppeared: Bool, transition: Transition) {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout: layout, forceUpdate: true, hasAppeared: hasAppeared, transition: transition)
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: flashButtonTag) {
|
||||
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||
view.layer.shadowRadius = 4.0
|
||||
view.layer.shadowColor = UIColor.black.cgColor
|
||||
view.layer.shadowOpacity = 0.2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
|
||||
fileprivate var hasAppeared = false
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) {
|
||||
guard let _ = self.controller else {
|
||||
return
|
||||
}
|
||||
@ -1103,8 +1118,9 @@ public class CameraScreen: ViewController {
|
||||
var transition = transition
|
||||
if isFirstTime {
|
||||
transition = transition.withUserData(CameraScreenTransition.animateIn)
|
||||
} else if animateOut {
|
||||
transition = transition.withUserData(CameraScreenTransition.animateOut)
|
||||
} else if hasAppeared && !self.hasAppeared {
|
||||
self.hasAppeared = hasAppeared
|
||||
transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn)
|
||||
}
|
||||
|
||||
let componentSize = self.componentHost.update(
|
||||
@ -1114,7 +1130,7 @@ public class CameraScreen: ViewController {
|
||||
context: self.context,
|
||||
camera: self.camera,
|
||||
changeMode: self.changeMode,
|
||||
isDismissing: self.panTranslation != nil,
|
||||
hasAppeared: self.hasAppeared,
|
||||
present: { [weak self] c in
|
||||
self?.controller?.present(c, in: .window(.root))
|
||||
},
|
||||
@ -1127,34 +1143,48 @@ public class CameraScreen: ViewController {
|
||||
environment: {
|
||||
environment
|
||||
},
|
||||
forceUpdate: forceUpdate || animateOut,
|
||||
forceUpdate: forceUpdate,
|
||||
containerSize: layout.size
|
||||
)
|
||||
if let componentView = self.componentHost.view {
|
||||
if componentView.superview == nil {
|
||||
self.view.insertSubview(componentView, at: 3)
|
||||
self.containerView.insertSubview(componentView, belowSubview: transitionDimView)
|
||||
componentView.clipsToBounds = true
|
||||
}
|
||||
|
||||
if self.panTranslation == nil {
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
transition.setFrame(view: componentView, frame: componentFrame)
|
||||
}
|
||||
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
transition.setFrame(view: componentView, frame: componentFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
transition.setPosition(view: self.backgroundView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0))
|
||||
transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
if self.panTranslation == nil {
|
||||
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
||||
transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
}
|
||||
transition.setPosition(view: self.containerView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0))
|
||||
transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size))
|
||||
|
||||
|
||||
let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize)
|
||||
transition.setFrame(view: self.previewContainerView, frame: previewFrame)
|
||||
transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
|
||||
let screenCornerRadius = layout.deviceMetrics.screenCornerRadius
|
||||
if screenCornerRadius > 0.0, self.transitionCornersView.image == nil {
|
||||
self.transitionCornersView.image = generateImage(CGSize(width: screenCornerRadius, height: screenCornerRadius * 3.0), rotatedContext: { size, context in
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
context.setBlendMode(.clear)
|
||||
|
||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height)), cornerRadius: size.width)
|
||||
context.addPath(path.cgPath)
|
||||
context.fillPath()
|
||||
})?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(screenCornerRadius))
|
||||
}
|
||||
|
||||
transition.setPosition(view: self.transitionCornersView, position: CGPoint(x: layout.size.width + screenCornerRadius / 2.0, y: layout.size.height / 2.0))
|
||||
transition.setBounds(view: self.transitionCornersView, bounds: CGRect(origin: .zero, size: CGSize(width: screenCornerRadius, height: layout.size.height)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -1167,15 +1197,36 @@ public class CameraScreen: ViewController {
|
||||
fileprivate let holder: CameraHolder?
|
||||
fileprivate let transitionIn: TransitionIn?
|
||||
fileprivate let transitionOut: (Bool) -> TransitionOut?
|
||||
fileprivate let completion: (Signal<CameraScreen.Result, NoError>) -> Void
|
||||
|
||||
public final class ResultTransition {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceImage: UIImage?
|
||||
public let transitionOut: () -> (UIView, CGRect)?
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceImage: UIImage?,
|
||||
transitionOut: @escaping () -> (UIView, CGRect)?
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceImage = sourceImage
|
||||
self.transitionOut = transitionOut
|
||||
}
|
||||
}
|
||||
fileprivate let completion: (Signal<CameraScreen.Result, NoError>, ResultTransition?) -> Void
|
||||
|
||||
private var audioSessionDisposable: Disposable?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
mode: Mode,
|
||||
holder: CameraHolder? = nil,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool) -> TransitionOut?,
|
||||
completion: @escaping (Signal<CameraScreen.Result, NoError>) -> Void
|
||||
completion: @escaping (Signal<CameraScreen.Result, NoError>, ResultTransition?) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mode = mode
|
||||
@ -1186,15 +1237,21 @@ public class CameraScreen: ViewController {
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.statusBar.statusBarStyle = .White
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
self.navigationPresentation = .flatModal
|
||||
|
||||
self.requestAudioSession()
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.audioSessionDisposable?.dispose()
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self)
|
||||
@ -1202,31 +1259,61 @@ public class CameraScreen: ViewController {
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
public func returnFromEditor() {
|
||||
self.node.animateInFromEditor()
|
||||
private func requestAudioSession() {
|
||||
self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in }, deactivate: { _ in
|
||||
return .single(Void())
|
||||
})
|
||||
}
|
||||
|
||||
public func commitTransitionToEditor() {
|
||||
self.node.commitTransitionToEditor()
|
||||
private weak var galleryController: ViewController?
|
||||
public func returnFromEditor() {
|
||||
self.node.animateInFromEditor(toGallery: self.galleryController != nil)
|
||||
}
|
||||
|
||||
func presentGallery() {
|
||||
var dismissGalleryControllerImpl: (() -> Void)?
|
||||
let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result in
|
||||
dismissGalleryControllerImpl?()
|
||||
var didStopCameraCapture = false
|
||||
let stopCameraCapture = { [weak self] in
|
||||
guard !didStopCameraCapture, let self else {
|
||||
return
|
||||
}
|
||||
didStopCameraCapture = true
|
||||
|
||||
self.node.pauseCameraCapture()
|
||||
}
|
||||
|
||||
let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut in
|
||||
if let self {
|
||||
self.node.animateOutToEditor()
|
||||
stopCameraCapture()
|
||||
|
||||
let resultTransition = ResultTransition(
|
||||
sourceView: transitionView,
|
||||
sourceRect: transitionRect,
|
||||
sourceImage: transitionImage,
|
||||
transitionOut: transitionOut
|
||||
)
|
||||
if let asset = result as? PHAsset {
|
||||
self.completion(.single(.asset(asset)))
|
||||
self.completion(.single(.asset(asset)), resultTransition)
|
||||
} else if let draft = result as? MediaEditorDraft {
|
||||
self.completion(.single(.draft(draft)))
|
||||
self.completion(.single(.draft(draft)), resultTransition)
|
||||
}
|
||||
}
|
||||
}, dismissed: { [weak self] in
|
||||
if let self {
|
||||
self.node.resumeCameraCapture()
|
||||
}
|
||||
})
|
||||
dismissGalleryControllerImpl = { [weak controller] in
|
||||
controller?.dismiss(animated: true)
|
||||
controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in
|
||||
if let self, let controller {
|
||||
let transitionFactor = controller.modalStyleOverlayTransitionFactor
|
||||
if transitionFactor > 0.1 {
|
||||
stopCameraCapture()
|
||||
}
|
||||
self.node.updateModalTransitionFactor(transitionFactor, transition: transition)
|
||||
}
|
||||
}
|
||||
push(controller)
|
||||
|
||||
self.galleryController = controller
|
||||
self.push(controller)
|
||||
}
|
||||
|
||||
public func presentDraftTooltip() {
|
||||
@ -1234,21 +1321,82 @@ public class CameraScreen: ViewController {
|
||||
}
|
||||
|
||||
private var isDismissed = false
|
||||
fileprivate func requestDismiss(animated: Bool) {
|
||||
fileprivate func requestDismiss(animated: Bool, interactive: Bool = false) {
|
||||
guard !self.isDismissed else {
|
||||
return
|
||||
}
|
||||
self.node.camera.stopCapture(invalidate: true)
|
||||
self.isDismissed = true
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
if animated {
|
||||
self.node.animateOut(completion: {
|
||||
self.dismiss(animated: false)
|
||||
if !interactive {
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate)
|
||||
}
|
||||
}
|
||||
self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
self?.dismiss(animated: false)
|
||||
})
|
||||
} else {
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
private var isTransitioning = false
|
||||
public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) {
|
||||
self.isTransitioning = true
|
||||
let offsetX = (1.0 - transitionFraction) * self.node.frame.width * -1.0
|
||||
transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
let scale = max(0.8, min(1.0, 0.8 + 0.2 * transitionFraction))
|
||||
transition.updateSublayerTransformScaleAndOffset(layer: self.node.containerView.layer, scale: scale, offset: CGPoint(x: -offsetX * 1.0 / scale * 0.5, y: 0.0), completion: { _ in
|
||||
completion()
|
||||
})
|
||||
|
||||
let dimAlpha = 0.6 * (1.0 - transitionFraction)
|
||||
transition.updateAlpha(layer: self.node.transitionDimView.layer, alpha: dimAlpha)
|
||||
transition.updateTransform(layer: self.node.transitionCornersView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0))
|
||||
|
||||
self.statusBar.updateStatusBarStyle(transitionFraction > 0.45 ? .White : .Ignore, animated: true)
|
||||
|
||||
if let navigationController = self.navigationController as? NavigationController {
|
||||
let offsetX = transitionFraction * self.node.frame.width
|
||||
navigationController.updateRootContainerTransitionOffset(offsetX, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) {
|
||||
self.isTransitioning = false
|
||||
if dismissing {
|
||||
if transitionFraction < 0.7 || velocity > 1000.0 {
|
||||
self.requestDismiss(animated: true, interactive: true)
|
||||
} else {
|
||||
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
if transitionFraction > 0.33 || velocity > 1000.0 {
|
||||
self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in
|
||||
if let self, let navigationController = self.navigationController as? NavigationController {
|
||||
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
||||
self.node.requestUpdateLayout(hasAppeared: true, transition: .immediate)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
self.requestDismiss(animated: true, interactive: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) {
|
||||
if !flag {
|
||||
self.galleryController?.dismiss(animated: false)
|
||||
}
|
||||
super.dismiss(animated: flag, completion: completion)
|
||||
}
|
||||
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
|
@ -108,7 +108,7 @@ private final class ShutterButtonContentComponent: Component {
|
||||
}
|
||||
self.blobView.updatePrimaryOffset(bandedOffset, transition: transition)
|
||||
} else {
|
||||
self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.15))
|
||||
self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.2))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -411,11 +411,11 @@ final class CaptureControlsComponent: Component {
|
||||
if let galleryButton = self.galleryButtonView.view {
|
||||
blobOffset = galleryButton.center.x - self.frame.width / 2.0
|
||||
}
|
||||
self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.5)))
|
||||
self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.35)))
|
||||
} else {
|
||||
self.hapticFeedback.impact(.light)
|
||||
self.component?.shutterReleased()
|
||||
self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.3)))
|
||||
self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.25)))
|
||||
}
|
||||
default:
|
||||
break
|
||||
@ -450,7 +450,7 @@ final class CaptureControlsComponent: Component {
|
||||
self.component?.zoomUpdated(1.0)
|
||||
}
|
||||
|
||||
if location.x < self.frame.width / 2.0 - 20.0 {
|
||||
if location.x < self.frame.width / 2.0 - 30.0 {
|
||||
if location.x < self.frame.width / 2.0 - 60.0 {
|
||||
self.component?.swipeHintUpdated(.releaseLock)
|
||||
if location.x < 75.0 {
|
||||
@ -464,7 +464,7 @@ final class CaptureControlsComponent: Component {
|
||||
blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0)
|
||||
isBanding = true
|
||||
}
|
||||
} else if location.x > self.frame.width / 2.0 + 20.0 {
|
||||
} else if location.x > self.frame.width / 2.0 + 30.0 {
|
||||
self.component?.swipeHintUpdated(.flip)
|
||||
if location.x > self.frame.width / 2.0 + 60.0 {
|
||||
self.panBlobState = .transientToFlip
|
||||
@ -488,8 +488,8 @@ final class CaptureControlsComponent: Component {
|
||||
}
|
||||
var transition: Transition = .immediate
|
||||
if let wasBanding = self.wasBanding, wasBanding != isBanding {
|
||||
self.hapticFeedback.impact(.light)
|
||||
transition = .spring(duration: 0.3)
|
||||
//self.hapticFeedback.impact(.light)
|
||||
transition = .spring(duration: 0.35)
|
||||
}
|
||||
self.wasBanding = isBanding
|
||||
self.shutterUpdateOffset.invoke((blobOffset, transition))
|
||||
@ -574,11 +574,14 @@ final class CaptureControlsComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponent(
|
||||
Image(
|
||||
image: state.cachedAssetImage?.1,
|
||||
size: CGSize(width: 50.0, height: 50.0),
|
||||
contentMode: .scaleAspectFill
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "gallery",
|
||||
component: AnyComponent(
|
||||
Image(
|
||||
image: state.cachedAssetImage?.1,
|
||||
size: CGSize(width: 50.0, height: 50.0),
|
||||
contentMode: .scaleAspectFill
|
||||
)
|
||||
)
|
||||
),
|
||||
tag: component.galleryButtonTag,
|
||||
@ -632,8 +635,11 @@ final class CaptureControlsComponent: Component {
|
||||
transition: .immediate,
|
||||
component: AnyComponent(
|
||||
CameraButton(
|
||||
content: AnyComponent(
|
||||
FlipButtonContentComponent(action: flipAnimationAction)
|
||||
content: AnyComponentWithIdentity(
|
||||
id: "flip",
|
||||
component: AnyComponent(
|
||||
FlipButtonContentComponent(action: flipAnimationAction)
|
||||
)
|
||||
),
|
||||
minSize: CGSize(width: 44.0, height: 44.0),
|
||||
action: {
|
||||
|
@ -0,0 +1,22 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
|
||||
final class FocusCrosshairsView: UIView {
|
||||
private let indicatorView: UIImageView
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.indicatorView = UIImageView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.indicatorView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(pointOfInterest: CGPoint) {
|
||||
|
||||
}
|
||||
}
|
@ -41,15 +41,15 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
}
|
||||
|
||||
func update(value: T, transition: Transition = .immediate) {
|
||||
let currentTimestamp = CACurrentMediaTime()
|
||||
if case .none = transition.animation {
|
||||
if let animation = self.animation, case let .curve(duration, curve) = animation.animation {
|
||||
self.value = value
|
||||
let elapsed = duration - (CACurrentMediaTime() - animation.startTimestamp)
|
||||
if elapsed < 0.1 {
|
||||
self.presentationValue = value
|
||||
self.animation = nil
|
||||
let elapsed = duration - (currentTimestamp - animation.startTimestamp)
|
||||
if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 {
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp)
|
||||
} else {
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: CACurrentMediaTime())
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp)
|
||||
}
|
||||
} else {
|
||||
self.value = value
|
||||
@ -58,7 +58,7 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
}
|
||||
} else {
|
||||
self.value = value
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: CACurrentMediaTime())
|
||||
self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,13 +73,18 @@ private final class AnimatableProperty<T: Interpolatable> {
|
||||
case .easeInOut:
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
case .spring:
|
||||
t = listViewAnimationCurveSystem(t)
|
||||
t = listViewAnimationCurveEaseInOut(t)
|
||||
//t = listViewAnimationCurveSystem(t)
|
||||
case let .custom(x1, y1, x2, y2):
|
||||
t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t)
|
||||
}
|
||||
self.presentationValue = animation.valueAt(t) as! T
|
||||
|
||||
return timeFromStart <= duration
|
||||
if timeFromStart <= duration {
|
||||
return true
|
||||
}
|
||||
self.animation = nil
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -204,26 +209,21 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
super.init(frame: CGRect(), device: device)
|
||||
|
||||
self.delegate = self
|
||||
|
||||
self.isOpaque = false
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.framebufferOnly = true
|
||||
self.colorPixelFormat = .bgra8Unorm
|
||||
self.framebufferOnly = true
|
||||
self.presentsWithTransaction = true
|
||||
self.isPaused = true
|
||||
self.delegate = self
|
||||
|
||||
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||
self?.tick()
|
||||
}
|
||||
self.displayLink?.isPaused = true
|
||||
|
||||
self.isPaused = true
|
||||
}
|
||||
|
||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||
self.viewportDimensions = size
|
||||
}
|
||||
|
||||
required public init(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
@ -232,6 +232,10 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
self.displayLink?.invalidate()
|
||||
}
|
||||
|
||||
public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) {
|
||||
self.viewportDimensions = size
|
||||
}
|
||||
|
||||
func updateState(_ state: BlobState, transition: Transition = .immediate) {
|
||||
guard self.state != state else {
|
||||
return
|
||||
@ -253,6 +257,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
}
|
||||
let mappedOffset = offset / self.frame.height * 2.0
|
||||
self.primaryOffset.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
@ -262,6 +267,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
}
|
||||
let mappedOffset = offset / self.frame.height * 2.0
|
||||
self.secondaryOffset.update(value: mappedOffset, transition: transition)
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
@ -288,47 +294,51 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
|
||||
private func tick() {
|
||||
self.updateAnimations()
|
||||
self.draw()
|
||||
autoreleasepool {
|
||||
self.draw(in: self)
|
||||
}
|
||||
}
|
||||
|
||||
override public func draw(_ rect: CGRect) {
|
||||
self.redraw(drawable: self.currentDrawable!)
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
private func redraw(drawable: MTLDrawable) {
|
||||
|
||||
func draw(in view: MTKView) {
|
||||
guard let commandBuffer = self.commandQueue.makeCommandBuffer() else {
|
||||
return
|
||||
}
|
||||
|
||||
let renderPassDescriptor = self.currentRenderPassDescriptor!
|
||||
|
||||
guard let renderPassDescriptor = self.currentRenderPassDescriptor else {
|
||||
return
|
||||
}
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .clear
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0)
|
||||
|
||||
|
||||
guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let viewportDimensions = self.viewportDimensions
|
||||
renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0))
|
||||
|
||||
renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState)
|
||||
|
||||
|
||||
let w = Float(1)
|
||||
let h = Float(1)
|
||||
|
||||
var vertices: [Float] = [
|
||||
w, -h,
|
||||
w, -h,
|
||||
-w, -h,
|
||||
-w, h,
|
||||
w, -h,
|
||||
w, -h,
|
||||
-w, h,
|
||||
w, h
|
||||
w, h
|
||||
]
|
||||
renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0)
|
||||
|
||||
|
||||
var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height))
|
||||
renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout<simd_uint2>.size * 2, index: 0)
|
||||
|
||||
|
||||
var primaryParameters = simd_float4(
|
||||
Float(self.primarySize.presentationValue),
|
||||
Float(self.primaryOffset.presentationValue),
|
||||
@ -343,23 +353,15 @@ final class ShutterBlobView: MTKView, MTKViewDelegate {
|
||||
Float(self.secondaryRedness.presentationValue)
|
||||
)
|
||||
renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout<simd_float3>.size, index: 2)
|
||||
|
||||
renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1)
|
||||
|
||||
renderEncoder.endEncoding()
|
||||
drawable.present()
|
||||
//commandBuffer.present(drawable)
|
||||
|
||||
if let currentDrawable = self.currentDrawable {
|
||||
commandBuffer.present(currentDrawable)
|
||||
}
|
||||
commandBuffer.commit()
|
||||
commandBuffer.waitUntilScheduled()
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
self.tick()
|
||||
}
|
||||
|
||||
func draw(in view: MTKView) {
|
||||
//commandBuffer.waitUntilScheduled()
|
||||
|
||||
//self.currentDrawable?.present()
|
||||
}
|
||||
}
|
||||
|
@ -120,15 +120,6 @@ final class ZoomComponent: Component {
|
||||
let spacing: CGFloat = 3.0
|
||||
let buttonSize = CGSize(width: 37.0, height: 37.0)
|
||||
let size: CGSize = CGSize(width: buttonSize.width * CGFloat(component.availableValues.count) + spacing * CGFloat(component.availableValues.count - 1) + sideInset * 2.0, height: 43.0)
|
||||
|
||||
if let screenTransition = transition.userData(CameraScreenTransition.self) {
|
||||
switch screenTransition {
|
||||
case .animateIn:
|
||||
self.animateIn()
|
||||
case .animateOut:
|
||||
self.animateOut()
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 3.0), size: buttonSize)
|
||||
|
@ -78,7 +78,7 @@ fragment half4 enhanceColorLookupFragmentShader(RasterizerData in [[stage_in]],
|
||||
constexpr sampler lutSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero);
|
||||
|
||||
float2 sourceCoord = in.texCoord;
|
||||
half4 color = sourceTexture.sample(colorSampler,sourceCoord);
|
||||
half4 color = sourceTexture.sample(colorSampler, sourceCoord);
|
||||
half3 hslColor = rgbToHsl(color.rgb);
|
||||
|
||||
float txf = sourceCoord.x * tileGridSize.x - 0.5;
|
||||
|
@ -0,0 +1,42 @@
|
||||
#include <metal_stdlib>
|
||||
#include "EditorCommon.h"
|
||||
#include "EditorUtils.h"
|
||||
|
||||
using namespace metal;
|
||||
|
||||
static inline float4 BT709_decode(const float Y, const float Cb, const float Cr) {
|
||||
float Yn = Y;
|
||||
|
||||
float Cbn = (Cb - (128.0f/255.0f));
|
||||
float Crn = (Cr - (128.0f/255.0f));
|
||||
|
||||
float3 YCbCr = float3(Yn, Cbn, Crn);
|
||||
|
||||
const float3x3 kColorConversion709 = float3x3(float3(1.0, 1.0, 1.0),
|
||||
float3(0.0f, -0.1873, 1.8556),
|
||||
float3(1.5748, -0.4681, 0.0));
|
||||
|
||||
float3 rgb = kColorConversion709 * YCbCr;
|
||||
|
||||
rgb = saturate(rgb);
|
||||
|
||||
return float4(rgb.r, rgb.g, rgb.b, 1.0f);
|
||||
}
|
||||
|
||||
|
||||
fragment float4 bt709ToRGBFragmentShader(RasterizerData in [[stage_in]],
|
||||
texture2d<half, access::sample> inYTexture [[texture(0)]],
|
||||
texture2d<half, access::sample> inUVTexture [[texture(1)]]
|
||||
)
|
||||
{
|
||||
constexpr sampler textureSampler (mag_filter::nearest, min_filter::nearest);
|
||||
|
||||
float Y = float(inYTexture.sample(textureSampler, in.texCoord).r);
|
||||
half2 uvSamples = inUVTexture.sample(textureSampler, in.texCoord).rg;
|
||||
|
||||
float Cb = float(uvSamples[0]);
|
||||
float Cr = float(uvSamples[1]);
|
||||
|
||||
float4 pixel = BT709_decode(Y, Cb, Cr);
|
||||
return pixel;
|
||||
}
|
@ -18,6 +18,45 @@ struct MediaEditorAdjustments {
|
||||
var warmth: simd_float1
|
||||
var grain: simd_float1
|
||||
var vignette: simd_float1
|
||||
|
||||
var hasValues: Bool {
|
||||
let epsilon: simd_float1 = 0.005
|
||||
|
||||
if abs(self.shadows) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.highlights) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.contrast) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.fade) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.saturation) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.shadowsTintIntensity) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.highlightsTintIntensity) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.exposure) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.warmth) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.grain) > epsilon {
|
||||
return true
|
||||
}
|
||||
if abs(self.vignette) > epsilon {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
final class AdjustmentsRenderPass: DefaultRenderPass {
|
||||
@ -50,10 +89,14 @@ final class AdjustmentsRenderPass: DefaultRenderPass {
|
||||
return "adjustmentsFragmentShader"
|
||||
}
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard self.adjustments.hasValues else {
|
||||
return input
|
||||
}
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
self.adjustments.dimensions = simd_float2(Float(width), Float(height))
|
||||
|
@ -29,11 +29,11 @@ private final class BlurGaussianPass: RenderPass {
|
||||
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return nil
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, intensity: Float, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, intensity: Float, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
let radius = round(4.0 + intensity * 26.0)
|
||||
if self.blur?.sigma != radius {
|
||||
self.blur = MPSImageGaussianBlur(device: device, sigma: radius)
|
||||
@ -67,10 +67,8 @@ private final class BlurLinearPass: DefaultRenderPass {
|
||||
return "blurLinearFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = output
|
||||
@ -83,7 +81,7 @@ private final class BlurLinearPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
width: Double(input.width), height: Double(input.height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
@ -105,10 +103,8 @@ private final class BlurRadialPass: DefaultRenderPass {
|
||||
return "blurRadialFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = output
|
||||
@ -121,7 +117,7 @@ private final class BlurRadialPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
width: Double(input.width), height: Double(input.height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
@ -145,11 +141,9 @@ private final class BlurPortraitPass: DefaultRenderPass {
|
||||
return "blurPortraitFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = output
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
@ -161,7 +155,7 @@ private final class BlurPortraitPass: DefaultRenderPass {
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
width: Double(input.width), height: Double(input.height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
@ -208,17 +202,18 @@ final class BlurRenderPass: RenderPass {
|
||||
self.portraitPass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.process(input: input, maskTexture: self.maskTexture, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.process(input: input, maskTexture: self.maskTexture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, maskTexture: MTLTexture?, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, maskTexture: MTLTexture?, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard self.intensity > 0.005 && self.mode != .off else {
|
||||
return input
|
||||
}
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil {
|
||||
self.value.aspectRatio = Float(height) / Float(width)
|
||||
|
||||
@ -235,18 +230,18 @@ final class BlurRenderPass: RenderPass {
|
||||
self.cachedTexture = texture
|
||||
}
|
||||
|
||||
guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, rotation: rotation, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else {
|
||||
guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else {
|
||||
return input
|
||||
}
|
||||
|
||||
switch self.mode {
|
||||
case .linear:
|
||||
return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer)
|
||||
case .radial:
|
||||
return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer)
|
||||
case .portrait:
|
||||
if let maskTexture {
|
||||
return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer)
|
||||
} else {
|
||||
return input
|
||||
}
|
||||
|
@ -21,20 +21,12 @@ private final class EnhanceLightnessPass: DefaultRenderPass {
|
||||
return .r8Unorm
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, size: TextureSize, scale: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, size: TextureSize, scale: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let width: Int
|
||||
let height: Int
|
||||
switch rotation {
|
||||
case .rotate90Degrees, .rotate270Degrees:
|
||||
width = size.height
|
||||
height = size.width
|
||||
default:
|
||||
width = size.width
|
||||
height = size.height
|
||||
}
|
||||
|
||||
let width = size.width
|
||||
let height = size.height
|
||||
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
@ -130,7 +122,7 @@ private final class EnhanceLUTGeneratorPass: RenderPass {
|
||||
}
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -204,10 +196,11 @@ private final class EnhanceLookupPass: DefaultRenderPass {
|
||||
return "enhanceColorLookupFragmentShader"
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
@ -269,7 +262,7 @@ final class EnhanceRenderPass: RenderPass {
|
||||
self.lookupPass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard self.value > 0.005 else {
|
||||
return input
|
||||
}
|
||||
@ -279,12 +272,12 @@ final class EnhanceRenderPass: RenderPass {
|
||||
let lightnessSize = TextureSize(width: input.width + dX, height: input.height + dY)
|
||||
let lightnessScale = simd_float2(Float(input.width + dX) / Float(input.width), Float(input.height + dY) / Float(input.height))
|
||||
|
||||
let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
let lookupTexture = self.lutGeneratorPass.process(input: lightness!, gridSize: self.tileGridSize, clipLimit: self.clipLimit, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
let gridSize = simd_float2(Float(self.tileGridSize.width), Float(self.tileGridSize.height))
|
||||
let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, device: device, commandBuffer: commandBuffer)
|
||||
|
||||
return output
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ final class HistogramCalculationPass: DefaultRenderPass {
|
||||
fileprivate var histogramBuffer: MTLBuffer?
|
||||
fileprivate var calculation: MPSImageHistogram?
|
||||
|
||||
var isEnabled = false
|
||||
var updated: ((Data) -> Void)?
|
||||
|
||||
override var fragmentShaderFunctionName: String {
|
||||
@ -40,58 +41,61 @@ final class HistogramCalculationPass: DefaultRenderPass {
|
||||
super.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = .r8Unorm
|
||||
textureDescriptor.storageMode = .shared
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
if self.isEnabled {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = width
|
||||
textureDescriptor.height = height
|
||||
textureDescriptor.pixelFormat = .r8Unorm
|
||||
textureDescriptor.storageMode = .shared
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
guard let texture = device.makeTexture(descriptor: textureDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "lumaTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
self.cachedTexture = texture
|
||||
texture.label = "lumaTexture"
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return input
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
var texCoordScales = simd_float2(x: 1.0, y: 1.0)
|
||||
renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout<simd_float2>.stride, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation {
|
||||
calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0)
|
||||
|
||||
let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm)
|
||||
calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength)
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(width), height: Double(height),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length)
|
||||
self.updated?(histogramData)
|
||||
renderCommandEncoder.setFragmentTexture(input, index: 0)
|
||||
|
||||
var texCoordScales = simd_float2(x: 1.0, y: 1.0)
|
||||
renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout<simd_float2>.stride, index: 0)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation {
|
||||
calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0)
|
||||
|
||||
let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm)
|
||||
calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength)
|
||||
|
||||
let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length)
|
||||
self.updated?(histogramData)
|
||||
}
|
||||
}
|
||||
|
||||
return input
|
||||
|
@ -3,45 +3,57 @@ import AVFoundation
|
||||
import Metal
|
||||
import MetalKit
|
||||
import Display
|
||||
import Accelerate
|
||||
|
||||
func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? {
|
||||
func dataForImage(_ image: UIImage) -> UnsafeMutablePointer<UInt8> {
|
||||
let imageRef = image.cgImage
|
||||
let width = Int(image.size.width)
|
||||
let height = Int(image.size.height)
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
let rawData = UnsafeMutablePointer<UInt8>.allocate(capacity: width * height * 4)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * Int(width)
|
||||
let bitsPerComponent = 8
|
||||
let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue
|
||||
let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo)
|
||||
context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height))
|
||||
|
||||
return rawData
|
||||
}
|
||||
|
||||
let width = Int(image.size.width * image.scale)
|
||||
let height = Int(image.size.height * image.scale)
|
||||
let bytePerPixel = 4
|
||||
let bytesPerRow = bytePerPixel * width
|
||||
|
||||
var texture : MTLTexture?
|
||||
let region = MTLRegionMake2D(0, 0, Int(width), Int(height))
|
||||
let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false)
|
||||
texture = device.makeTexture(descriptor: textureDescriptor)
|
||||
|
||||
let data = dataForImage(image)
|
||||
texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow)
|
||||
|
||||
return texture
|
||||
}
|
||||
|
||||
final class ImageTextureSource: TextureSource {
|
||||
weak var output: TextureConsumer?
|
||||
|
||||
var textureLoader: MTKTextureLoader?
|
||||
var texture: MTLTexture?
|
||||
|
||||
init(image: UIImage, renderTarget: RenderTarget) {
|
||||
guard let device = renderTarget.mtlDevice, var cgImage = image.cgImage else {
|
||||
return
|
||||
if let device = renderTarget.mtlDevice {
|
||||
self.texture = loadTexture(image: image, device: device)
|
||||
}
|
||||
let textureLoader = MTKTextureLoader(device: device)
|
||||
self.textureLoader = textureLoader
|
||||
|
||||
if let bitsPerPixel = image.cgImage?.bitsPerPixel, bitsPerPixel > 32 {
|
||||
let updatedImage = generateImage(image.size, contextGenerator: { size, context in
|
||||
context.setFillColor(UIColor.black.cgColor)
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
context.draw(cgImage, in: CGRect(origin: .zero, size: size))
|
||||
}, opaque: false)
|
||||
cgImage = updatedImage?.cgImage ?? cgImage
|
||||
}
|
||||
|
||||
self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false])
|
||||
}
|
||||
|
||||
func start() {
|
||||
|
||||
}
|
||||
|
||||
func pause() {
|
||||
|
||||
}
|
||||
|
||||
func connect(to consumer: TextureConsumer) {
|
||||
self.output = consumer
|
||||
|
||||
if let texture = self.texture {
|
||||
self.output?.consumeTexture(texture, rotation: .rotate0Degrees)
|
||||
self.output?.consumeTexture(texture)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -59,3 +71,17 @@ func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTe
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getTextureImage(device: MTLDevice, texture: MTLTexture) -> UIImage? {
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let context = CIContext(mtlDevice: device, options: [:])
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else {
|
||||
return nil
|
||||
}
|
||||
let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
|
@ -76,7 +76,15 @@ public final class MediaEditor {
|
||||
public var histogram: Signal<Data, NoError> {
|
||||
return self.histogramPromise.get()
|
||||
}
|
||||
|
||||
public var isHistogramEnabled: Bool {
|
||||
get {
|
||||
return self.histogramCalculationPass.isEnabled
|
||||
}
|
||||
set {
|
||||
self.histogramCalculationPass.isEnabled = newValue
|
||||
}
|
||||
}
|
||||
|
||||
private var textureCache: CVMetalTextureCache!
|
||||
|
||||
public var hasPortraitMask: Bool {
|
||||
@ -90,7 +98,7 @@ public final class MediaEditor {
|
||||
public var resultImage: UIImage? {
|
||||
return self.renderer.finalRenderedImage()
|
||||
}
|
||||
|
||||
|
||||
private let playerPromise = Promise<AVPlayer?>()
|
||||
private var playerPlaybackState: (Double, Double, Bool) = (0.0, 0.0, false) {
|
||||
didSet {
|
||||
@ -250,6 +258,7 @@ public final class MediaEditor {
|
||||
}
|
||||
}
|
||||
|
||||
private var volumeFade: SwiftSignalKit.Timer?
|
||||
private func setupSource() {
|
||||
guard let renderTarget = self.previewView else {
|
||||
return
|
||||
@ -367,14 +376,19 @@ public final class MediaEditor {
|
||||
|> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in
|
||||
if let self {
|
||||
let (source, image, player, topColor, bottomColor) = sourceAndColors
|
||||
self.renderer.onNextRender = { [weak self] in
|
||||
self?.previewView?.removeTransitionImage()
|
||||
}
|
||||
self.renderer.textureSource = source
|
||||
self.player = player
|
||||
self.playerPromise.set(.single(player))
|
||||
self.gradientColorsValue = (topColor, bottomColor)
|
||||
self.setGradientColors([topColor, bottomColor])
|
||||
|
||||
self.maybeGeneratePersonSegmentation(image)
|
||||
|
||||
if player == nil {
|
||||
self.maybeGeneratePersonSegmentation(image)
|
||||
}
|
||||
|
||||
if let player {
|
||||
self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in
|
||||
guard let self, let duration = player.currentItem?.duration.seconds else {
|
||||
@ -394,6 +408,7 @@ public final class MediaEditor {
|
||||
}
|
||||
})
|
||||
self.player?.play()
|
||||
self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4)
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -431,16 +446,45 @@ public final class MediaEditor {
|
||||
self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted)
|
||||
}
|
||||
|
||||
public func seek(_ position: Double, andPlay: Bool) {
|
||||
if !andPlay {
|
||||
private var targetTimePosition: (CMTime, Bool)?
|
||||
private var updatingTimePosition = false
|
||||
public func seek(_ position: Double, andPlay play: Bool) {
|
||||
if !play {
|
||||
self.player?.pause()
|
||||
}
|
||||
self.player?.seek(to: CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in })
|
||||
if andPlay {
|
||||
let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0))
|
||||
if self.targetTimePosition?.0 != targetPosition {
|
||||
self.targetTimePosition = (targetPosition, play)
|
||||
if !self.updatingTimePosition {
|
||||
self.updateVideoTimePosition()
|
||||
}
|
||||
}
|
||||
if play {
|
||||
self.player?.play()
|
||||
}
|
||||
}
|
||||
|
||||
public func stop() {
|
||||
self.player?.pause()
|
||||
}
|
||||
|
||||
private func updateVideoTimePosition() {
|
||||
guard let (targetPosition, _) = self.targetTimePosition else {
|
||||
return
|
||||
}
|
||||
self.updatingTimePosition = true
|
||||
self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in
|
||||
if let self {
|
||||
if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition {
|
||||
self.updatingTimePosition = false
|
||||
self.targetTimePosition = nil
|
||||
} else {
|
||||
self.updateVideoTimePosition()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func setVideoTrimStart(_ trimStart: Double) {
|
||||
let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0
|
||||
let trimRange = trimStart ..< trimEnd
|
||||
@ -583,15 +627,23 @@ final class MediaEditorRenderChain {
|
||||
}
|
||||
case .shadowsTint:
|
||||
if let value = value as? TintValue {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity
|
||||
if value.color != .clear {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity
|
||||
} else {
|
||||
self.adjustmentsPass.adjustments.shadowsTintIntensity = 0.0
|
||||
}
|
||||
}
|
||||
case .highlightsTint:
|
||||
if let value = value as? TintValue {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity
|
||||
if value.color != .clear {
|
||||
let (red, green, blue, _) = value.color.components
|
||||
self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue))
|
||||
self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity
|
||||
} else {
|
||||
self.adjustmentsPass.adjustments.highlightsTintIntensity = 0.0
|
||||
}
|
||||
}
|
||||
case .blur:
|
||||
if let value = value as? BlurValue {
|
||||
@ -626,3 +678,11 @@ final class MediaEditorRenderChain {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func debugSaveImage(_ image: UIImage, name: String) {
|
||||
let path = NSTemporaryDirectory() + "debug_\(name)_\(Int64.random(in: .min ... .max)).png"
|
||||
print(path)
|
||||
if let data = image.pngData() {
|
||||
try? data.write(to: URL(fileURLWithPath: path))
|
||||
}
|
||||
}
|
||||
|
@ -12,8 +12,26 @@ import TelegramAnimatedStickerNode
|
||||
import YuvConversion
|
||||
import StickerResources
|
||||
|
||||
func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? {
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, 1.0)
|
||||
if let context = UIGraphicsGetCurrentContext() {
|
||||
let gradientColors = colors.map { $0.cgColor } as CFArray
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
|
||||
var locations: [CGFloat] = [0.0, 1.0]
|
||||
let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)!
|
||||
context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions())
|
||||
}
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()!
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
final class MediaEditorComposer {
|
||||
let device: MTLDevice?
|
||||
private let colorSpace: CGColorSpace
|
||||
|
||||
private let values: MediaEditorValues
|
||||
private let dimensions: CGSize
|
||||
@ -34,27 +52,29 @@ final class MediaEditorComposer {
|
||||
self.dimensions = dimensions
|
||||
self.outputDimensions = outputDimensions
|
||||
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
self.colorSpace = colorSpace
|
||||
|
||||
self.renderer.addRenderChain(self.renderChain)
|
||||
self.renderer.addRenderPass(ComposerRenderPass())
|
||||
|
||||
if let gradientColors = values.gradientColors {
|
||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||
self.gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) {
|
||||
self.gradientImage = CIImage(image: image, options: [.colorSpace: self.colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
self.gradientImage = CIImage(color: .black)
|
||||
}
|
||||
|
||||
if let drawing = values.drawing, let drawingImage = CIImage(image: drawing) {
|
||||
if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) {
|
||||
self.drawingImage = drawingImage.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
self.drawingImage = nil
|
||||
}
|
||||
|
||||
self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0) }
|
||||
self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) }
|
||||
|
||||
self.device = MTLCreateSystemDefaultDevice()
|
||||
if let device = self.device {
|
||||
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()])
|
||||
self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : self.colorSpace])
|
||||
|
||||
CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &self.textureCache)
|
||||
} else {
|
||||
@ -82,10 +102,10 @@ final class MediaEditorComposer {
|
||||
texture = CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
if let texture {
|
||||
self.renderer.consumeTexture(texture, rotation: .rotate90Degrees)
|
||||
self.renderer.consumeTexture(texture)
|
||||
self.renderer.renderFrame()
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) {
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
|
||||
var pixelBuffer: CVPixelBuffer?
|
||||
@ -116,13 +136,12 @@ final class MediaEditorComposer {
|
||||
completion(nil, time)
|
||||
return
|
||||
}
|
||||
if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage {
|
||||
let textureLoader = MTKTextureLoader(device: device)
|
||||
if let texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) {
|
||||
self.renderer.consumeTexture(texture, rotation: .rotate0Degrees)
|
||||
if self.filteredImage == nil, let device = self.device {
|
||||
if let texture = loadTexture(image: inputImage, device: device) {
|
||||
self.renderer.consumeTexture(texture)
|
||||
self.renderer.renderFrame()
|
||||
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) {
|
||||
if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) {
|
||||
ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height))
|
||||
self.filteredImage = ciImage
|
||||
}
|
||||
@ -137,7 +156,7 @@ final class MediaEditorComposer {
|
||||
makeEditorImageFrameComposition(inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in
|
||||
if var compositedImage {
|
||||
let scale = self.outputDimensions.width / self.dimensions.width
|
||||
compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale))
|
||||
|
||||
self.ciContext?.render(compositedImage, to: pixelBuffer)
|
||||
completion(pixelBuffer, time)
|
||||
@ -157,21 +176,21 @@ final class MediaEditorComposer {
|
||||
}
|
||||
|
||||
public func makeEditorImageComposition(account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) {
|
||||
let inputImage = CIImage(image: inputImage)!
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])!
|
||||
let gradientImage: CIImage
|
||||
var drawingImage: CIImage?
|
||||
if let gradientColors = values.gradientColors {
|
||||
let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])!
|
||||
gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) {
|
||||
gradientImage = CIImage(image: image, options: [.colorSpace: colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
} else {
|
||||
gradientImage = CIImage(color: .black)
|
||||
}
|
||||
|
||||
if let drawing = values.drawing, let image = CIImage(image: drawing) {
|
||||
if let drawing = values.drawing, let image = CIImage(image: drawing, options: [.colorSpace: colorSpace]) {
|
||||
drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
}
|
||||
|
||||
let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0) }
|
||||
let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) }
|
||||
makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in
|
||||
if let ciImage {
|
||||
let context = CIContext(options: [.workingColorSpace : NSNull()])
|
||||
@ -190,7 +209,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0))
|
||||
resultImage = gradientImage.composited(over: resultImage)
|
||||
|
||||
var mediaImage = inputImage.transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
|
||||
var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY))
|
||||
|
||||
var initialScale: CGFloat
|
||||
if mediaImage.extent.height > mediaImage.extent.width {
|
||||
@ -206,7 +225,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
resultImage = mediaImage.composited(over: resultImage)
|
||||
|
||||
if let drawingImage {
|
||||
resultImage = drawingImage.composited(over: resultImage)
|
||||
resultImage = drawingImage.samplingLinear().composited(over: resultImage)
|
||||
}
|
||||
|
||||
let frameRate: Float = 30.0
|
||||
@ -235,7 +254,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
}
|
||||
let index = i
|
||||
entity.image(for: time, frameRate: frameRate, completion: { image in
|
||||
if var image = image {
|
||||
if var image = image?.samplingLinear() {
|
||||
let resetTransform = CGAffineTransform(translationX: -image.extent.width / 2.0, y: -image.extent.height / 2.0)
|
||||
image = image.transformed(by: resetTransform)
|
||||
|
||||
@ -266,7 +285,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage:
|
||||
maybeFinalize()
|
||||
}
|
||||
|
||||
private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity) -> MediaEditorComposerEntity? {
|
||||
private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? {
|
||||
if let entity = entity as? DrawingStickerEntity {
|
||||
let content: MediaEditorComposerStickerEntity.Content
|
||||
switch entity.content {
|
||||
@ -275,8 +294,8 @@ private func composerEntityForDrawingEntity(account: Account, entity: DrawingEnt
|
||||
case let .image(image):
|
||||
content = .image(image)
|
||||
}
|
||||
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored)
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) {
|
||||
return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace)
|
||||
} else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) {
|
||||
if let entity = entity as? DrawingBubbleEntity {
|
||||
return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false)
|
||||
} else if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
@ -331,6 +350,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let rotation: CGFloat
|
||||
let baseSize: CGSize?
|
||||
let mirrored: Bool
|
||||
let colorSpace: CGColorSpace
|
||||
|
||||
var isAnimated: Bool
|
||||
var source: AnimatedStickerNodeSource?
|
||||
@ -349,13 +369,14 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
var imagePixelBuffer: CVPixelBuffer?
|
||||
let imagePromise = Promise<UIImage>()
|
||||
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool) {
|
||||
init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) {
|
||||
self.content = content
|
||||
self.position = position
|
||||
self.scale = scale
|
||||
self.rotation = rotation
|
||||
self.baseSize = baseSize
|
||||
self.mirrored = mirrored
|
||||
self.colorSpace = colorSpace
|
||||
|
||||
switch content {
|
||||
case let .file(file):
|
||||
@ -373,7 +394,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let queue = strongSelf.queue
|
||||
let frameSource = QueueLocalObject<AnimatedStickerDirectFrameSource>(queue: queue, generate: {
|
||||
return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)!
|
||||
//return AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})!
|
||||
})
|
||||
frameSource.syncWith { frameSource in
|
||||
strongSelf.frameCount = frameSource.frameCount
|
||||
@ -391,13 +411,13 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
} else {
|
||||
self.isAnimated = false
|
||||
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false)
|
||||
self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] generator in
|
||||
if let strongSelf = self {
|
||||
if let self {
|
||||
let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets()))
|
||||
let image = context?.generateImage()
|
||||
if let image = image {
|
||||
strongSelf.imagePromise.set(.single(image))
|
||||
let image = context?.generateImage(colorSpace: self.colorSpace)
|
||||
if let image {
|
||||
self.imagePromise.set(.single(image))
|
||||
}
|
||||
}
|
||||
}))
|
||||
@ -488,7 +508,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
}
|
||||
|
||||
if let imagePixelBuffer {
|
||||
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor)
|
||||
let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, colorSpace: strongSelf.colorSpace, tintColor: tintColor)
|
||||
strongSelf.image = image
|
||||
}
|
||||
completion(strongSelf.image)
|
||||
@ -508,9 +528,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity {
|
||||
let _ = (self.imagePromise.get()
|
||||
|> take(1)
|
||||
|> deliverOn(self.queue)).start(next: { [weak self] image in
|
||||
if let strongSelf = self {
|
||||
strongSelf.image = CIImage(image: image)
|
||||
completion(strongSelf.image)
|
||||
if let self {
|
||||
self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace])
|
||||
completion(self.image)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -528,7 +548,7 @@ protocol MediaEditorComposerEntity {
|
||||
func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void)
|
||||
}
|
||||
|
||||
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? {
|
||||
private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, colorSpace: CGColorSpace, tintColor: UIColor?) -> CIImage? {
|
||||
//let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31)
|
||||
//assert(bytesPerRow == calculatedBytesPerRow)
|
||||
|
||||
@ -557,16 +577,17 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type:
|
||||
|
||||
CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0))
|
||||
|
||||
return CIImage(cvPixelBuffer: pixelBuffer)
|
||||
return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace])
|
||||
}
|
||||
|
||||
final class ComposerRenderPass: DefaultRenderPass {
|
||||
fileprivate var cachedTexture: MTLTexture?
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation)
|
||||
let width = input.width
|
||||
let height = input.height
|
||||
|
||||
if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
|
@ -67,4 +67,33 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge
|
||||
}
|
||||
self.renderer?.renderFrame()
|
||||
}
|
||||
|
||||
private var transitionView: UIImageView?
|
||||
public func setTransitionImage(_ image: UIImage) {
|
||||
self.transitionView?.removeFromSuperview()
|
||||
|
||||
let transitionView = UIImageView(image: image)
|
||||
transitionView.frame = self.bounds
|
||||
self.addSubview(transitionView)
|
||||
|
||||
self.transitionView = transitionView
|
||||
}
|
||||
|
||||
public func removeTransitionImage() {
|
||||
if let transitionView = self.transitionView {
|
||||
// transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak transitionView] _ in
|
||||
//
|
||||
// })
|
||||
transitionView.removeFromSuperview()
|
||||
self.transitionView = nil
|
||||
}
|
||||
}
|
||||
|
||||
public override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
|
||||
if let transitionView = self.transitionView {
|
||||
transitionView.frame = self.bounds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ import Photos
|
||||
import SwiftSignalKit
|
||||
|
||||
protocol TextureConsumer: AnyObject {
|
||||
func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation)
|
||||
func consumeTexture(_ texture: MTLTexture)
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation)
|
||||
}
|
||||
|
||||
final class RenderingContext {
|
||||
@ -24,7 +25,7 @@ final class RenderingContext {
|
||||
|
||||
protocol RenderPass: AnyObject {
|
||||
func setup(device: MTLDevice, library: MTLLibrary)
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture?
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture?
|
||||
}
|
||||
|
||||
protocol TextureSource {
|
||||
@ -51,7 +52,9 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
|
||||
var semaphore = DispatchSemaphore(value: 3)
|
||||
private var renderPasses: [RenderPass] = []
|
||||
private var outputRenderPass = OutputRenderPass()
|
||||
|
||||
private let videoInputPass = VideoInputPass()
|
||||
private let outputRenderPass = OutputRenderPass()
|
||||
private weak var renderTarget: RenderTarget? {
|
||||
didSet {
|
||||
self.outputRenderPass.renderTarget = self.renderTarget
|
||||
@ -59,10 +62,14 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
|
||||
private var device: MTLDevice?
|
||||
private var commandQueue: MTLCommandQueue?
|
||||
private var currentTexture: MTLTexture?
|
||||
private var currentRotation: TextureRotation = .rotate0Degrees
|
||||
private var library: MTLLibrary?
|
||||
private var commandQueue: MTLCommandQueue?
|
||||
private var textureCache: CVMetalTextureCache?
|
||||
|
||||
private var currentTexture: MTLTexture?
|
||||
private var currentPixelBuffer: (CVPixelBuffer, TextureRotation)?
|
||||
|
||||
public var onNextRender: (() -> Void)?
|
||||
|
||||
var finalTexture: MTLTexture?
|
||||
|
||||
@ -94,6 +101,8 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||
|
||||
let mainBundle = Bundle(for: MediaEditorRenderer.self)
|
||||
guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else {
|
||||
return
|
||||
@ -102,15 +111,16 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
return
|
||||
}
|
||||
self.library = defaultLibrary
|
||||
self.library = library
|
||||
|
||||
self.commandQueue = device.makeCommandQueue()
|
||||
self.commandQueue?.label = "Media Editor Command Queue"
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) }
|
||||
self.outputRenderPass.setup(device: device, library: defaultLibrary)
|
||||
self.videoInputPass.setup(device: device, library: library)
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
self.outputRenderPass.setup(device: device, library: library)
|
||||
}
|
||||
|
||||
func setupForComposer(composer: MediaEditorComposer) {
|
||||
@ -118,6 +128,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
self.device = device
|
||||
CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache)
|
||||
|
||||
let mainBundle = Bundle(for: MediaEditorRenderer.self)
|
||||
guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else {
|
||||
@ -127,17 +138,17 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
guard let library = try? device.makeDefaultLibrary(bundle: bundle) else {
|
||||
return
|
||||
}
|
||||
self.library = defaultLibrary
|
||||
self.library = library
|
||||
|
||||
self.commandQueue = device.makeCommandQueue()
|
||||
self.commandQueue?.label = "Media Editor Command Queue"
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) }
|
||||
self.videoInputPass.setup(device: device, library: library)
|
||||
self.renderPasses.forEach { $0.setup(device: device, library: library) }
|
||||
}
|
||||
|
||||
private var currentCommandBuffer: MTLCommandBuffer?
|
||||
func renderFrame() {
|
||||
let device: MTLDevice?
|
||||
if let renderTarget = self.renderTarget {
|
||||
@ -149,7 +160,7 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
guard let device = device,
|
||||
let commandQueue = self.commandQueue,
|
||||
var texture = self.currentTexture else {
|
||||
let textureCache = self.textureCache else {
|
||||
return
|
||||
}
|
||||
|
||||
@ -157,22 +168,36 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
return
|
||||
}
|
||||
|
||||
var rotation: TextureRotation = self.currentRotation
|
||||
var texture: MTLTexture
|
||||
if let currentTexture = self.currentTexture {
|
||||
texture = currentTexture
|
||||
} else if let (currentPixelBuffer, textureRotation) = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, rotation: textureRotation, textureCache: textureCache, device: device, commandBuffer: commandBuffer) {
|
||||
texture = videoTexture
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
for renderPass in self.renderPasses {
|
||||
if let nextTexture = renderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) {
|
||||
if nextTexture !== texture {
|
||||
rotation = .rotate0Degrees
|
||||
}
|
||||
if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) {
|
||||
texture = nextTexture
|
||||
}
|
||||
}
|
||||
if self.renderTarget != nil {
|
||||
let _ = self.outputRenderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
let _ = self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
self.finalTexture = texture
|
||||
|
||||
commandBuffer.addCompletedHandler { [weak self] _ in
|
||||
self?.semaphore.signal()
|
||||
if let self {
|
||||
self.semaphore.signal()
|
||||
|
||||
if let onNextRender = self.onNextRender {
|
||||
self.onNextRender = nil
|
||||
Queue.mainQueue().async {
|
||||
onNextRender()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let _ = self.renderTarget {
|
||||
@ -184,18 +209,17 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
}
|
||||
|
||||
func commit() {
|
||||
if let commandBuffer = self.currentCommandBuffer {
|
||||
commandBuffer.commit()
|
||||
self.currentCommandBuffer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) {
|
||||
func consumeTexture(_ texture: MTLTexture) {
|
||||
self.semaphore.wait()
|
||||
|
||||
self.currentTexture = texture
|
||||
self.currentRotation = rotation
|
||||
self.renderTarget?.scheduleFrame()
|
||||
}
|
||||
|
||||
func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) {
|
||||
self.semaphore.wait()
|
||||
|
||||
self.currentPixelBuffer = (pixelBuffer, rotation)
|
||||
self.renderTarget?.scheduleFrame()
|
||||
}
|
||||
|
||||
@ -209,27 +233,10 @@ final class MediaEditorRenderer: TextureConsumer {
|
||||
}
|
||||
|
||||
func finalRenderedImage() -> UIImage? {
|
||||
if let finalTexture = self.finalTexture {
|
||||
return getTextureImage(finalTexture)
|
||||
if let finalTexture = self.finalTexture, let device = self.renderTarget?.mtlDevice {
|
||||
return getTextureImage(device: device, texture: finalTexture)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func getTextureImage(_ texture: MTLTexture) -> UIImage? {
|
||||
guard let device = self.renderTarget?.mtlDevice else {
|
||||
return nil
|
||||
}
|
||||
let options = [CIImageOption.colorSpace: CGColorSpaceCreateDeviceRGB()]
|
||||
let context = CIContext(mtlDevice: device)
|
||||
guard var ciImage = CIImage(mtlTexture: texture, options: options) else {
|
||||
return nil
|
||||
}
|
||||
let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height)
|
||||
ciImage = ciImage.transformed(by: transform)
|
||||
guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else {
|
||||
return nil
|
||||
}
|
||||
return UIImage(cgImage: cgImage)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,41 @@
|
||||
import Foundation
|
||||
import AVFoundation
|
||||
import SwiftSignalKit
|
||||
|
||||
extension AVPlayer {
|
||||
func fadeVolume(from: Float, to: Float, duration: Float, completion: (() -> Void)? = nil) -> SwiftSignalKit.Timer? {
|
||||
self.volume = from
|
||||
guard from != to else { return nil }
|
||||
|
||||
let interval: Float = 0.1
|
||||
let range = to - from
|
||||
let step = (range * interval) / duration
|
||||
|
||||
func reachedTarget() -> Bool {
|
||||
guard self.volume >= 0, self.volume <= 1 else {
|
||||
self.volume = to
|
||||
return true
|
||||
}
|
||||
|
||||
if to > from {
|
||||
return self.volume >= to
|
||||
}
|
||||
return self.volume <= to
|
||||
}
|
||||
|
||||
var invalidateImpl: (() -> Void)?
|
||||
let timer = SwiftSignalKit.Timer(timeout: Double(interval), repeat: true, completion: { [weak self] in
|
||||
if let self, !reachedTarget() {
|
||||
self.volume += step
|
||||
} else {
|
||||
invalidateImpl?()
|
||||
completion?()
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
invalidateImpl = { [weak timer] in
|
||||
timer?.invalidate()
|
||||
}
|
||||
timer.start()
|
||||
return timer
|
||||
}
|
||||
}
|
@ -102,7 +102,7 @@ class DefaultRenderPass: RenderPass {
|
||||
}
|
||||
}
|
||||
|
||||
func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation) {
|
||||
func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation = .rotate0Degrees) {
|
||||
if self.verticesBuffer == nil || rotation != self.textureRotation {
|
||||
self.textureRotation = rotation
|
||||
let vertices = verticesDataForRotation(rotation)
|
||||
@ -113,8 +113,8 @@ class DefaultRenderPass: RenderPass {
|
||||
}
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -131,11 +131,11 @@ class DefaultRenderPass: RenderPass {
|
||||
final class OutputRenderPass: DefaultRenderPass {
|
||||
weak var renderTarget: RenderTarget?
|
||||
|
||||
override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else {
|
||||
return nil
|
||||
}
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
self.setupVerticesBuffer(device: device)
|
||||
|
||||
let drawableSize = renderTarget.drawableSize
|
||||
|
||||
|
@ -11,7 +11,7 @@ final class SharpenRenderPass: RenderPass {
|
||||
|
||||
}
|
||||
|
||||
func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
return input
|
||||
}
|
||||
}
|
||||
|
@ -13,22 +13,19 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
private let device: MTLDevice?
|
||||
private var textureRotation: TextureRotation = .rotate0Degrees
|
||||
|
||||
|
||||
private var forceUpdate: Bool = false
|
||||
|
||||
weak var output: TextureConsumer?
|
||||
var textureCache: CVMetalTextureCache!
|
||||
var queue: DispatchQueue!
|
||||
var started: Bool = false
|
||||
|
||||
init(player: AVPlayer, renderTarget: RenderTarget) {
|
||||
self.player = player
|
||||
|
||||
if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess {
|
||||
print("error")
|
||||
}
|
||||
|
||||
self.device = renderTarget.mtlDevice!
|
||||
|
||||
self.queue = DispatchQueue(
|
||||
label: "VideoTextureSource Queue",
|
||||
qos: .userInteractive,
|
||||
@ -47,7 +44,8 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
}
|
||||
|
||||
deinit {
|
||||
print()
|
||||
self.playerItemObservation?.invalidate()
|
||||
self.playerItemStatusObservation?.invalidate()
|
||||
}
|
||||
|
||||
private func updatePlayerItem(_ playerItem: AVPlayerItem?) {
|
||||
@ -63,7 +61,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
self.playerItemStatusObservation = nil
|
||||
|
||||
self.playerItem = playerItem
|
||||
self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial,.new], changeHandler: { [weak self] item, change in
|
||||
self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial, .new], changeHandler: { [weak self] item, change in
|
||||
guard let strongSelf = self else {
|
||||
return
|
||||
}
|
||||
@ -91,21 +89,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
} else if t.b == -1.0 && t.c == 1.0 {
|
||||
self.textureRotation = .rotate270Degrees
|
||||
} else if t.a == -1.0 && t.d == 1.0 {
|
||||
// if (mirrored != NULL) {
|
||||
// *mirrored = true;
|
||||
// }
|
||||
self.textureRotation = .rotate270Degrees
|
||||
} else if t.a == 1.0 && t.d == -1.0 {
|
||||
// if (mirrored != NULL) {
|
||||
// *mirrored = true;
|
||||
// }
|
||||
self.textureRotation = .rotate180Degrees
|
||||
} else {
|
||||
// if (t.c == 1) {
|
||||
// if (mirrored != NULL) {
|
||||
// *mirrored = true;
|
||||
// }
|
||||
// }
|
||||
self.textureRotation = .rotate90Degrees
|
||||
}
|
||||
}
|
||||
@ -115,7 +102,20 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
return
|
||||
}
|
||||
|
||||
let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as NSString as String: kCVPixelFormatType_32BGRA])
|
||||
let colorProperties: [String: Any] = [
|
||||
AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2,
|
||||
AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2,
|
||||
AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2
|
||||
]
|
||||
|
||||
let outputSettings: [String: Any] = [
|
||||
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange,
|
||||
kCVPixelBufferMetalCompatibilityKey as String: true,
|
||||
AVVideoColorPropertiesKey: colorProperties
|
||||
]
|
||||
|
||||
let output = AVPlayerItemVideoOutput(outputSettings: outputSettings)
|
||||
output.suppressesPlayerRendering = true
|
||||
output.setDelegate(self, queue: self.queue)
|
||||
playerItem.add(output)
|
||||
self.playerItemOutput = output
|
||||
@ -174,12 +174,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
|
||||
var presentationTime: CMTime = .zero
|
||||
if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) {
|
||||
if let texture = self.pixelBufferToMTLTexture(pixelBuffer: pixelBuffer) {
|
||||
self.output?.consumeTexture(texture, rotation: self.textureRotation)
|
||||
}
|
||||
self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func setNeedsUpdate() {
|
||||
self.displayLink?.isPaused = false
|
||||
self.forceUpdate = true
|
||||
@ -196,19 +194,88 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD
|
||||
self.output = consumer
|
||||
}
|
||||
|
||||
private func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer) -> MTLTexture? {
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
let format: MTLPixelFormat = .bgra8Unorm
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef)
|
||||
if status == kCVReturnSuccess {
|
||||
return CVMetalTextureGetTexture(textureRef!)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
public func outputMediaDataWillChange(_ sender: AVPlayerItemOutput) {
|
||||
self.displayLink?.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
final class VideoInputPass: DefaultRenderPass {
|
||||
private var cachedTexture: MTLTexture?
|
||||
|
||||
override var fragmentShaderFunctionName: String {
|
||||
return "bt709ToRGBFragmentShader"
|
||||
}
|
||||
|
||||
func processPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? {
|
||||
var textureRef : CVMetalTexture?
|
||||
let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, plane, &textureRef)
|
||||
if status == kCVReturnSuccess, let textureRef {
|
||||
return CVMetalTextureGetTexture(textureRef)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
let width = CVPixelBufferGetWidth(pixelBuffer)
|
||||
let height = CVPixelBufferGetHeight(pixelBuffer)
|
||||
guard let inputYTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .r8Unorm, width: width, height: height, plane: 0),
|
||||
let inputCbCrTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .rg8Unorm, width: width >> 1, height: height >> 1, plane: 1) else {
|
||||
return nil
|
||||
}
|
||||
return self.process(yTexture: inputYTexture, cbcrTexture: inputCbCrTexture, width: width, height: height, rotation: rotation, device: device, commandBuffer: commandBuffer)
|
||||
}
|
||||
|
||||
func process(yTexture: MTLTexture, cbcrTexture: MTLTexture, width: Int, height: Int, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? {
|
||||
self.setupVerticesBuffer(device: device, rotation: rotation)
|
||||
|
||||
func textureDimensionsForRotation(width: Int, height: Int, rotation: TextureRotation) -> (width: Int, height: Int) {
|
||||
switch rotation {
|
||||
case .rotate90Degrees, .rotate270Degrees:
|
||||
return (height, width)
|
||||
default:
|
||||
return (width, height)
|
||||
}
|
||||
}
|
||||
|
||||
let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation)
|
||||
// let outputSize = CGSize(width: outputWidth, height: outputHeight).fitted(CGSize(width: 1920.0, height: 1920.0))
|
||||
// outputWidth = Int(outputSize.width)
|
||||
// outputHeight = Int(outputSize.height)
|
||||
if self.cachedTexture == nil {
|
||||
let textureDescriptor = MTLTextureDescriptor()
|
||||
textureDescriptor.textureType = .type2D
|
||||
textureDescriptor.width = outputWidth
|
||||
textureDescriptor.height = outputHeight
|
||||
textureDescriptor.pixelFormat = .bgra8Unorm
|
||||
textureDescriptor.storageMode = .private
|
||||
textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget]
|
||||
if let texture = device.makeTexture(descriptor: textureDescriptor) {
|
||||
self.cachedTexture = texture
|
||||
}
|
||||
}
|
||||
|
||||
let renderPassDescriptor = MTLRenderPassDescriptor()
|
||||
renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture!
|
||||
renderPassDescriptor.colorAttachments[0].loadAction = .dontCare
|
||||
renderPassDescriptor.colorAttachments[0].storeAction = .store
|
||||
renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0)
|
||||
guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
renderCommandEncoder.setViewport(MTLViewport(
|
||||
originX: 0, originY: 0,
|
||||
width: Double(outputWidth), height: Double(outputHeight),
|
||||
znear: -1.0, zfar: 1.0)
|
||||
)
|
||||
|
||||
renderCommandEncoder.setFragmentTexture(yTexture, index: 0)
|
||||
renderCommandEncoder.setFragmentTexture(cbcrTexture, index: 1)
|
||||
|
||||
self.encodeDefaultCommands(using: renderCommandEncoder)
|
||||
|
||||
renderCommandEncoder.endEncoding()
|
||||
|
||||
return self.cachedTexture
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import BlurredBackgroundComponent
|
||||
import AvatarNode
|
||||
import ShareWithPeersScreen
|
||||
import PresentationDataUtils
|
||||
import ContextUI
|
||||
|
||||
enum DrawingScreenType {
|
||||
case drawing
|
||||
@ -37,23 +38,20 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
let context: AccountContext
|
||||
let mediaEditor: MediaEditor?
|
||||
let privacy: EngineStoryPrivacy
|
||||
let timeout: Bool
|
||||
let privacy: MediaEditorResultPrivacy
|
||||
let openDrawing: (DrawingScreenType) -> Void
|
||||
let openTools: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
mediaEditor: MediaEditor?,
|
||||
privacy: EngineStoryPrivacy,
|
||||
timeout: Bool,
|
||||
privacy: MediaEditorResultPrivacy,
|
||||
openDrawing: @escaping (DrawingScreenType) -> Void,
|
||||
openTools: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.mediaEditor = mediaEditor
|
||||
self.privacy = privacy
|
||||
self.timeout = timeout
|
||||
self.openDrawing = openDrawing
|
||||
self.openTools = openTools
|
||||
}
|
||||
@ -65,9 +63,6 @@ final class MediaEditorScreenComponent: Component {
|
||||
if lhs.privacy != rhs.privacy {
|
||||
return false
|
||||
}
|
||||
if lhs.timeout != rhs.timeout {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -186,7 +181,11 @@ final class MediaEditorScreenComponent: Component {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func animateInFromCamera() {
|
||||
enum TransitionAnimationSource {
|
||||
case camera
|
||||
case gallery
|
||||
}
|
||||
func animateIn(from source: TransitionAnimationSource) {
|
||||
if let view = self.cancelButton.view {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
@ -217,7 +216,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
if let view = self.inputPanel.view {
|
||||
view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
|
||||
view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.saveButton.view {
|
||||
@ -236,7 +235,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
func animateOutToCamera() {
|
||||
func animateOut(to source: TransitionAnimationSource) {
|
||||
let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
if let view = self.cancelButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
@ -334,7 +333,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
if let view = self.privacyButton.view {
|
||||
transition.setAlpha(view: view, alpha: 0.0)
|
||||
transition.setScale(view: view, scale: 0.1)
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.scrubber.view {
|
||||
@ -386,7 +385,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
if let view = self.privacyButton.view {
|
||||
transition.setAlpha(view: view, alpha: 1.0)
|
||||
transition.setScale(view: view, scale: 1.0)
|
||||
view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
|
||||
if let view = self.scrubber.view {
|
||||
@ -637,6 +636,17 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
}
|
||||
|
||||
let timeoutValue: Int32
|
||||
let timeoutSelected: Bool
|
||||
switch component.privacy {
|
||||
case let .story(_, archive):
|
||||
timeoutValue = 24
|
||||
timeoutSelected = archive
|
||||
case let .message(_, timeout):
|
||||
timeoutValue = timeout ?? 1
|
||||
timeoutSelected = timeout != nil
|
||||
}
|
||||
|
||||
self.inputPanel.parentState = state
|
||||
let inputPanelSize = self.inputPanel.update(
|
||||
transition: transition,
|
||||
@ -665,16 +675,25 @@ final class MediaEditorScreenComponent: Component {
|
||||
discardMediaRecordingPreview: nil,
|
||||
attachmentAction: nil,
|
||||
reactionAction: nil,
|
||||
timeoutAction: { view in
|
||||
|
||||
timeoutAction: { [weak self] view in
|
||||
guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else {
|
||||
return
|
||||
}
|
||||
switch controller.state.privacy {
|
||||
case let .story(privacy, archive):
|
||||
controller.state.privacy = .story(privacy: privacy, archive: !archive)
|
||||
controller.node.presentStoryArchiveTooltip(sourceView: view)
|
||||
case .message:
|
||||
controller.presentTimeoutSetup(sourceView: view)
|
||||
}
|
||||
},
|
||||
audioRecorder: nil,
|
||||
videoRecordingStatus: nil,
|
||||
isRecordingLocked: false,
|
||||
recordedAudioPreview: nil,
|
||||
wasRecordingDismissed: false,
|
||||
timeoutValue: 24,
|
||||
timeoutSelected: component.timeout,
|
||||
timeoutValue: timeoutValue,
|
||||
timeoutSelected: timeoutSelected,
|
||||
displayGradient: false,//component.inputHeight != 0.0,
|
||||
bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset
|
||||
)),
|
||||
@ -697,18 +716,26 @@ final class MediaEditorScreenComponent: Component {
|
||||
}
|
||||
|
||||
let privacyText: String
|
||||
switch component.privacy.base {
|
||||
case .everyone:
|
||||
privacyText = "Everyone"
|
||||
case .closeFriends:
|
||||
privacyText = "Close Friends"
|
||||
case .contacts:
|
||||
privacyText = "Contacts"
|
||||
case .nobody:
|
||||
privacyText = "Selected Contacts"
|
||||
switch component.privacy {
|
||||
case let .story(privacy, _):
|
||||
switch privacy.base {
|
||||
case .everyone:
|
||||
privacyText = "Everyone"
|
||||
case .closeFriends:
|
||||
privacyText = "Close Friends"
|
||||
case .contacts:
|
||||
privacyText = "Contacts"
|
||||
case .nobody:
|
||||
privacyText = "Selected Contacts"
|
||||
}
|
||||
case let .message(peerIds, _):
|
||||
if peerIds.count == 1 {
|
||||
privacyText = "User Test"
|
||||
} else {
|
||||
privacyText = "\(peerIds.count) Recipients"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let privacyButtonSize = self.privacyButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
@ -845,21 +872,31 @@ final class MediaEditorScreenComponent: Component {
|
||||
|
||||
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
|
||||
|
||||
public enum MediaEditorResultPrivacy: Equatable {
|
||||
case story(privacy: EngineStoryPrivacy, archive: Bool)
|
||||
case message(peers: [EnginePeer.Id], timeout: Int32?)
|
||||
}
|
||||
|
||||
public final class MediaEditorScreen: ViewController {
|
||||
public final class TransitionIn {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceCornerRadius: CGFloat
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceCornerRadius: CGFloat
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceCornerRadius = sourceCornerRadius
|
||||
public enum TransitionIn {
|
||||
public final class GalleryTransitionIn {
|
||||
public weak var sourceView: UIView?
|
||||
public let sourceRect: CGRect
|
||||
public let sourceImage: UIImage?
|
||||
|
||||
public init(
|
||||
sourceView: UIView,
|
||||
sourceRect: CGRect,
|
||||
sourceImage: UIImage?
|
||||
) {
|
||||
self.sourceView = sourceView
|
||||
self.sourceRect = sourceRect
|
||||
self.sourceImage = sourceImage
|
||||
}
|
||||
}
|
||||
|
||||
case camera
|
||||
case gallery(GalleryTransitionIn)
|
||||
}
|
||||
|
||||
public final class TransitionOut {
|
||||
@ -878,6 +915,16 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
struct State {
|
||||
var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: true)
|
||||
}
|
||||
|
||||
var state = State() {
|
||||
didSet {
|
||||
self.node.requestUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
|
||||
private weak var controller: MediaEditorScreen?
|
||||
private let context: AccountContext
|
||||
@ -885,8 +932,6 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
fileprivate var subject: MediaEditorScreen.Subject?
|
||||
private var subjectDisposable: Disposable?
|
||||
fileprivate var storyPrivacy: EngineStoryPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
|
||||
fileprivate var timeout: Bool = true
|
||||
|
||||
private let backgroundDimView: UIView
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
@ -898,6 +943,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
|
||||
fileprivate let entitiesContainerView: UIView
|
||||
fileprivate let entitiesView: DrawingEntitiesView
|
||||
fileprivate let selectionContainerView: DrawingSelectionContainerView
|
||||
fileprivate let drawingView: DrawingView
|
||||
fileprivate let previewView: MediaEditorPreviewView
|
||||
fileprivate var mediaEditor: MediaEditor?
|
||||
@ -910,7 +956,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
init(controller: MediaEditorScreen) {
|
||||
self.controller = controller
|
||||
self.context = controller.context
|
||||
|
||||
|
||||
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
self.backgroundDimView = UIView()
|
||||
@ -938,16 +984,20 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.drawingView = DrawingView(size: storyDimensions)
|
||||
self.drawingView.isUserInteractionEnabled = false
|
||||
|
||||
self.selectionContainerView = DrawingSelectionContainerView(frame: .zero)
|
||||
self.entitiesView.selectionContainerView = self.selectionContainerView
|
||||
|
||||
super.init()
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
//self.view.addSubview(self.backgroundDimView)
|
||||
self.view.addSubview(self.backgroundDimView)
|
||||
self.view.addSubview(self.previewContainerView)
|
||||
self.previewContainerView.addSubview(self.gradientView)
|
||||
self.previewContainerView.addSubview(self.entitiesContainerView)
|
||||
self.entitiesContainerView.addSubview(self.entitiesView)
|
||||
self.previewContainerView.addSubview(self.drawingView)
|
||||
self.previewContainerView.addSubview(self.selectionContainerView)
|
||||
|
||||
self.subjectDisposable = (
|
||||
controller.subject
|
||||
@ -1035,7 +1085,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize)
|
||||
mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0)
|
||||
if fittedSize.height > fittedSize.width {
|
||||
mediaEntity.scale = storyDimensions.height / fittedSize.height
|
||||
mediaEntity.scale = storyDimensions.height / fittedSize.height
|
||||
} else {
|
||||
mediaEntity.scale = storyDimensions.width / fittedSize.width
|
||||
}
|
||||
@ -1082,10 +1132,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
|
||||
self.previewContainerView.layer.allowsGroupOpacity = false
|
||||
self.controller?.onReady()
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
})
|
||||
} else {
|
||||
self.controller?.onReady()
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1131,18 +1181,47 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
if let sourceHint = self.controller?.sourceHint {
|
||||
switch sourceHint {
|
||||
if let transitionIn = self.controller?.transitionIn {
|
||||
switch transitionIn {
|
||||
case .camera:
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateInFromCamera()
|
||||
view.animateIn(from: .camera)
|
||||
}
|
||||
case let .gallery(transitionIn):
|
||||
if let transitionImage = transitionIn.sourceImage {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
self.previewView.setTransitionImage(transitionImage)
|
||||
}
|
||||
if let sourceView = transitionIn.sourceView {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .gallery)
|
||||
}
|
||||
|
||||
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
|
||||
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
|
||||
let sourceAspectRatio = sourceLocalFrame.height / sourceLocalFrame.width
|
||||
|
||||
let duration: Double = 0.5
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
|
||||
self.backgroundDimView.alpha = 1.0
|
||||
self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
|
||||
if let componentView = self.componentHost.view {
|
||||
componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Queue.mainQueue().after(0.5) {
|
||||
// self.presentPrivacyTooltip()
|
||||
// }
|
||||
// Queue.mainQueue().after(0.5) {
|
||||
// self.presentPrivacyTooltip()
|
||||
// }
|
||||
}
|
||||
|
||||
func animateOut(finished: Bool, completion: @escaping () -> Void) {
|
||||
@ -1151,18 +1230,68 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
controller.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView {
|
||||
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
|
||||
|
||||
let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width
|
||||
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
self.backgroundDimView.alpha = 0.0
|
||||
self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25)
|
||||
|
||||
if finished, case .message = controller.state.privacy {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOut(to: .camera)
|
||||
}
|
||||
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
|
||||
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
} else if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView {
|
||||
if !finished, let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage {
|
||||
let transitionOutView = UIImageView(image: sourceImage)
|
||||
var initialScale: CGFloat
|
||||
if sourceImage.size.height > sourceImage.size.width {
|
||||
initialScale = self.previewContainerView.bounds.height / sourceImage.size.height
|
||||
} else {
|
||||
initialScale = self.previewContainerView.bounds.width / sourceImage.size.width
|
||||
}
|
||||
transitionOutView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0)
|
||||
transitionOutView.transform = CGAffineTransformMakeScale(initialScale, initialScale)
|
||||
transitionOutView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.previewContainerView.addSubview(transitionOutView)
|
||||
}
|
||||
view.animateOut(to: .gallery)
|
||||
}
|
||||
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
|
||||
let destinatinoScale = destinationLocalFrame.width / self.previewContainerView.frame.width
|
||||
let destinationAspectRatio = destinationLocalFrame.height / destinationLocalFrame.width
|
||||
|
||||
var destinationSnapshotView: UIView?
|
||||
if let destinationNode = destinationView.asyncdisplaykit_node, destinationNode is AvatarNode, let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) {
|
||||
destinationView.isHidden = true
|
||||
|
||||
let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width
|
||||
snapshotView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0)
|
||||
snapshotView.transform = CGAffineTransform(scaleX: snapshotScale, y: snapshotScale)
|
||||
|
||||
self.previewContainerView.addSubview(snapshotView)
|
||||
destinationSnapshotView = snapshotView
|
||||
}
|
||||
|
||||
self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in
|
||||
destinationView.isHidden = false
|
||||
destinationSnapshotView?.removeFromSuperview()
|
||||
completion()
|
||||
})
|
||||
self.previewContainerView.layer.animateScale(from: 1.0, to: destinatinoScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
|
||||
let targetCornerRadius: CGFloat
|
||||
if transitionOut.destinationCornerRadius > 0.0 {
|
||||
targetCornerRadius = self.previewContainerView.bounds.width
|
||||
} else {
|
||||
targetCornerRadius = 0.0
|
||||
}
|
||||
|
||||
self.previewContainerView.layer.animate(
|
||||
from: self.previewContainerView.layer.cornerRadius as NSNumber,
|
||||
to: self.previewContainerView.bounds.width / 2.0 as NSNumber,
|
||||
to: targetCornerRadius / 2.0 as NSNumber,
|
||||
keyPath: "cornerRadius",
|
||||
timingFunction: kCAMediaTimingFunctionSpring,
|
||||
duration: 0.4,
|
||||
@ -1172,7 +1301,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
if let componentView = self.componentHost.view {
|
||||
componentView.clipsToBounds = true
|
||||
componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateScale(from: 1.0, to: destinatinoScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.bounds.height - componentView.bounds.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
componentView.layer.animate(
|
||||
@ -1184,17 +1313,14 @@ public final class MediaEditorScreen: ViewController {
|
||||
removeOnCompletion: false
|
||||
)
|
||||
}
|
||||
} else if let sourceHint = controller.sourceHint {
|
||||
switch sourceHint {
|
||||
case .camera:
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOutToCamera()
|
||||
}
|
||||
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
|
||||
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else if let transitionIn = controller.transitionIn, case .camera = transitionIn {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOut(to: .camera)
|
||||
}
|
||||
let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
|
||||
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
completion()
|
||||
}
|
||||
@ -1211,7 +1337,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
view.animateInFromTool()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func presentPrivacyTooltip() {
|
||||
guard let sourceView = self.componentHost.findTaggedView(tag: privacyButtonTag) else {
|
||||
return
|
||||
@ -1221,10 +1347,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize())
|
||||
|
||||
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in
|
||||
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in
|
||||
return .ignore
|
||||
})
|
||||
self.controller?.present(controller, in: .current)
|
||||
self.controller?.present(tooltipController, in: .current)
|
||||
}
|
||||
|
||||
func presentSaveTooltip() {
|
||||
@ -1243,11 +1369,40 @@ public final class MediaEditorScreen: ViewController {
|
||||
} else {
|
||||
text = "Image saved to Photos"
|
||||
}
|
||||
|
||||
let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in
|
||||
|
||||
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in
|
||||
return .ignore
|
||||
})
|
||||
self.controller?.present(controller, in: .current)
|
||||
self.controller?.present(tooltipController, in: .current)
|
||||
}
|
||||
|
||||
private weak var storyArchiveTooltip: ViewController?
|
||||
func presentStoryArchiveTooltip(sourceView: UIView) {
|
||||
guard let controller = self.controller, case let .story(_, archive) = controller.state.privacy else {
|
||||
return
|
||||
}
|
||||
|
||||
if let storyArchiveTooltip = self.storyArchiveTooltip {
|
||||
storyArchiveTooltip.dismiss(animated: true)
|
||||
self.storyArchiveTooltip = nil
|
||||
}
|
||||
|
||||
let parentFrame = self.view.convert(self.bounds, to: nil)
|
||||
let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0)
|
||||
let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 3.0), size: CGSize())
|
||||
|
||||
let text: String
|
||||
if archive {
|
||||
text = "Story will be kept on your page."
|
||||
} else {
|
||||
text = "Story will disappear in 24 hours."
|
||||
}
|
||||
|
||||
let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .bottom), displayDuration: .default, inset: 7.0, shouldDismissOnTouch: { _ in
|
||||
return .ignore
|
||||
})
|
||||
self.storyArchiveTooltip = tooltipController
|
||||
self.controller?.present(tooltipController, in: .current)
|
||||
}
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
@ -1266,9 +1421,24 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
private func insertDrawingEntity(_ entity: DrawingEntity) {
|
||||
self.entitiesView.prepareNewEntity(entity)
|
||||
self.entitiesView.add(entity)
|
||||
self.entitiesView.selectEntity(entity)
|
||||
|
||||
if let entityView = entitiesView.getView(for: entity.uuid) {
|
||||
entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2)
|
||||
|
||||
if let selectionView = entityView.selectionView {
|
||||
selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var drawingScreen: DrawingScreen?
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) {
|
||||
guard let _ = self.controller else {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
@ -1305,19 +1475,31 @@ public final class MediaEditorScreen: ViewController {
|
||||
MediaEditorScreenComponent(
|
||||
context: self.context,
|
||||
mediaEditor: self.mediaEditor,
|
||||
privacy: self.storyPrivacy,
|
||||
timeout: self.timeout,
|
||||
privacy: controller.state.privacy,
|
||||
openDrawing: { [weak self] mode in
|
||||
if let self {
|
||||
let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, existingStickerPickerInputData: self.stickerPickerInputData)
|
||||
switch mode {
|
||||
case .sticker:
|
||||
let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get())
|
||||
controller.completion = { [weak self] file in
|
||||
if let self, let file {
|
||||
let stickerEntity = DrawingStickerEntity(content: .file(file))
|
||||
self.insertDrawingEntity(stickerEntity)
|
||||
}
|
||||
}
|
||||
self.controller?.present(controller, in: .current)
|
||||
return
|
||||
case .text:
|
||||
break
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData)
|
||||
self.drawingScreen = controller
|
||||
self.drawingView.isUserInteractionEnabled = true
|
||||
|
||||
let selectionContainerView = controller.selectionContainerView
|
||||
selectionContainerView.frame = self.previewContainerView.bounds
|
||||
self.previewContainerView.addSubview(selectionContainerView)
|
||||
|
||||
controller.requestDismiss = { [weak controller, weak self, weak selectionContainerView] in
|
||||
|
||||
controller.requestDismiss = { [weak controller, weak self] in
|
||||
self?.drawingScreen = nil
|
||||
controller?.animateOut({
|
||||
controller?.dismiss()
|
||||
@ -1325,9 +1507,9 @@ public final class MediaEditorScreen: ViewController {
|
||||
self?.drawingView.isUserInteractionEnabled = false
|
||||
self?.animateInFromTool()
|
||||
|
||||
selectionContainerView?.removeFromSuperview()
|
||||
self?.entitiesView.selectEntity(nil)
|
||||
}
|
||||
controller.requestApply = { [weak controller, weak self, weak selectionContainerView] in
|
||||
controller.requestApply = { [weak controller, weak self] in
|
||||
self?.drawingScreen = nil
|
||||
controller?.animateOut({
|
||||
controller?.dismiss()
|
||||
@ -1341,7 +1523,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: [])
|
||||
}
|
||||
|
||||
selectionContainerView?.removeFromSuperview()
|
||||
self?.entitiesView.selectEntity(nil)
|
||||
}
|
||||
self.controller?.present(controller, in: .current)
|
||||
|
||||
@ -1407,6 +1589,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
|
||||
transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size))
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
@ -1475,22 +1659,16 @@ public final class MediaEditorScreen: ViewController {
|
||||
fileprivate let subject: Signal<Subject?, NoError>
|
||||
fileprivate let transitionIn: TransitionIn?
|
||||
fileprivate let transitionOut: (Bool) -> TransitionOut?
|
||||
|
||||
public enum SourceHint {
|
||||
case camera
|
||||
}
|
||||
public var sourceHint: SourceHint?
|
||||
|
||||
|
||||
public var cancelled: (Bool) -> Void = { _ in }
|
||||
public var completion: (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void = { _, _, _ in }
|
||||
public var onReady: () -> Void = {}
|
||||
public var completion: (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void = { _, _, _ in }
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: Signal<Subject?, NoError>,
|
||||
transitionIn: TransitionIn?,
|
||||
transitionOut: @escaping (Bool) -> TransitionOut?,
|
||||
completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void
|
||||
completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.subject = subject
|
||||
@ -1518,22 +1696,175 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
|
||||
func presentPrivacySettings() {
|
||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context)
|
||||
if case .message(_, _) = self.state.privacy {
|
||||
self.presentSendAsMessage()
|
||||
} else {
|
||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories)
|
||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
let initialPrivacy: EngineStoryPrivacy
|
||||
if case let .story(privacy, _) = self.state.privacy {
|
||||
initialPrivacy = privacy
|
||||
} else {
|
||||
initialPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
|
||||
}
|
||||
|
||||
self.push(
|
||||
ShareWithPeersScreen(
|
||||
context: self.context,
|
||||
initialPrivacy: initialPrivacy,
|
||||
stateContext: stateContext,
|
||||
completion: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state.privacy = .story(privacy: privacy, archive: true)
|
||||
},
|
||||
editCategory: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentEditCategory(privacy: privacy, completion: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state.privacy = .story(privacy: privacy, archive: true)
|
||||
self.presentPrivacySettings()
|
||||
})
|
||||
},
|
||||
secondaryAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentSendAsMessage()
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private func presentEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base))
|
||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.push(ShareWithPeersScreen(context: self.context, initialPrivacy: self.node.storyPrivacy, stateContext: stateContext, completion: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.node.storyPrivacy = privacy
|
||||
self.node.requestUpdate()
|
||||
}))
|
||||
self.push(
|
||||
ShareWithPeersScreen(
|
||||
context: self.context,
|
||||
initialPrivacy: privacy,
|
||||
stateContext: stateContext,
|
||||
completion: { [weak self] result in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case .closeFriends = privacy.base {
|
||||
let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start()
|
||||
}
|
||||
completion(result)
|
||||
},
|
||||
editCategory: { _ in },
|
||||
secondaryAction: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.presentSendAsMessage()
|
||||
}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private func presentSendAsMessage() {
|
||||
var initialPeerIds = Set<EnginePeer.Id>()
|
||||
if case let .message(peers, _) = self.state.privacy {
|
||||
initialPeerIds = Set(peers)
|
||||
}
|
||||
let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .chats, initialPeerIds: initialPeerIds)
|
||||
let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
self.push(
|
||||
ShareWithPeersScreen(
|
||||
context: self.context,
|
||||
initialPrivacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []),
|
||||
stateContext: stateContext,
|
||||
completion: { [weak self] privacy in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.state.privacy = .message(peers: privacy.additionallyIncludePeers, timeout: nil)
|
||||
},
|
||||
editCategory: { _ in },
|
||||
secondaryAction: {}
|
||||
)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func presentTimeoutSetup(sourceView: UIView) {
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
let updateTimeout: (Int32?) -> Void = { [weak self] timeout in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if case let .message(peers, _) = self.state.privacy {
|
||||
self.state.privacy = .message(peers: peers, timeout: timeout)
|
||||
}
|
||||
}
|
||||
|
||||
let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil
|
||||
items.append(.action(ContextMenuActionItem(text: "Choose how long the media will be kept after opening.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction)))
|
||||
|
||||
items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(1)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(3)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(10)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(60)
|
||||
})))
|
||||
items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in
|
||||
return nil
|
||||
}, action: { _, a in
|
||||
a(.default)
|
||||
|
||||
updateTimeout(nil)
|
||||
})))
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil)
|
||||
self.present(contextController, in: .window(.root))
|
||||
}
|
||||
|
||||
func maybePresentDiscardAlert() {
|
||||
if let subject = self.node.subject, case .asset = subject {
|
||||
self.requestDismiss(saveDraft: false, animated: true)
|
||||
@ -1606,6 +1937,10 @@ public final class MediaEditorScreen: ViewController {
|
||||
}
|
||||
}
|
||||
|
||||
if let mediaEditor = self.node.mediaEditor {
|
||||
mediaEditor.stop()
|
||||
}
|
||||
|
||||
self.cancelled(saveDraft)
|
||||
|
||||
self.node.animateOut(finished: false, completion: { [weak self] in
|
||||
@ -1617,6 +1952,8 @@ public final class MediaEditorScreen: ViewController {
|
||||
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
|
||||
return
|
||||
}
|
||||
|
||||
mediaEditor.stop()
|
||||
|
||||
if mediaEditor.resultIsVideo {
|
||||
let videoResult: Result.VideoResult
|
||||
@ -1664,7 +2001,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
}, self.node.storyPrivacy)
|
||||
}, self.state.privacy)
|
||||
|
||||
if case let .draft(draft) = subject {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
@ -1677,7 +2014,7 @@ public final class MediaEditorScreen: ViewController {
|
||||
self?.node.animateOut(finished: true, completion: { [weak self] in
|
||||
self?.dismiss()
|
||||
})
|
||||
}, self.node.storyPrivacy)
|
||||
}, self.state.privacy)
|
||||
if case let .draft(draft) = subject {
|
||||
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
||||
}
|
||||
@ -1866,3 +2203,20 @@ final class PrivacyButtonComponent: CombinedComponent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let controller: ViewController
|
||||
private let sourceView: UIView
|
||||
var keepInPlace: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
init(controller: ViewController, sourceView: UIView) {
|
||||
self.controller = controller
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
|
||||
}
|
||||
}
|
||||
|
@ -520,6 +520,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
})
|
||||
}
|
||||
|
||||
var needsHistogram = false
|
||||
let screenSize: CGSize
|
||||
let optionsSize: CGSize
|
||||
let optionsTransition: Transition = sectionChanged ? .immediate : transition
|
||||
@ -711,6 +712,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
|
||||
)
|
||||
case .curves:
|
||||
needsHistogram = true
|
||||
let internalState: CurvesInternalState
|
||||
if let current = self.curvesState {
|
||||
internalState = current
|
||||
@ -755,6 +757,7 @@ private final class MediaToolsScreenComponent: Component {
|
||||
containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height)
|
||||
)
|
||||
}
|
||||
component.mediaEditor.isHistogramEnabled = needsHistogram
|
||||
|
||||
let optionsFrame = CGRect(origin: .zero, size: optionsSize)
|
||||
if let optionsView = self.toolOptions.view {
|
||||
|
@ -239,7 +239,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
}
|
||||
|
||||
func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0)
|
||||
var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 41.0)
|
||||
|
||||
if let _ = component.attachmentAction {
|
||||
insets.left = 41.0
|
||||
@ -321,8 +321,8 @@ public final class MessageInputPanelComponent: Component {
|
||||
environment: {},
|
||||
containerSize: availableTextFieldSize
|
||||
)
|
||||
if self.textFieldExternalState.isEditing {
|
||||
insets.right = 41.0
|
||||
if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil {
|
||||
insets.right = insets.left
|
||||
}
|
||||
|
||||
let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height))
|
||||
@ -707,7 +707,7 @@ public final class MessageInputPanelComponent: Component {
|
||||
if timeoutButtonView.superview == nil {
|
||||
self.addSubview(timeoutButtonView)
|
||||
}
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - timeoutButtonSize.height) * 0.5)), size: timeoutButtonSize)
|
||||
let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 3.0 - timeoutButtonSize.height), size: timeoutButtonSize)
|
||||
transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center)
|
||||
transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size))
|
||||
|
||||
|
@ -29,9 +29,11 @@ swift_library(
|
||||
"//submodules/TelegramUI/Components/PlainButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedCounterComponent",
|
||||
"//submodules/TelegramUI/Components/TokenListTextField",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/CheckNode",
|
||||
"//submodules/PeerPresenceStatusManager",
|
||||
"//submodules/LocalizedPeerData"
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -10,6 +10,7 @@ import MultilineTextComponent
|
||||
import AvatarNode
|
||||
import TelegramPresentationData
|
||||
import CheckNode
|
||||
import BundleIconComponent
|
||||
|
||||
final class CategoryListItemComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
@ -27,6 +28,7 @@ final class CategoryListItemComponent: Component {
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: () -> Void
|
||||
let secondaryAction: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -38,7 +40,8 @@ final class CategoryListItemComponent: Component {
|
||||
subtitle: String?,
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping () -> Void
|
||||
action: @escaping () -> Void,
|
||||
secondaryAction: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -50,6 +53,7 @@ final class CategoryListItemComponent: Component {
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
self.secondaryAction = secondaryAction
|
||||
}
|
||||
|
||||
static func ==(lhs: CategoryListItemComponent, rhs: CategoryListItemComponent) -> Bool {
|
||||
@ -88,6 +92,7 @@ final class CategoryListItemComponent: Component {
|
||||
|
||||
private let title = ComponentView<Empty>()
|
||||
private let label = ComponentView<Empty>()
|
||||
private let labelArrow = ComponentView<Empty>()
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let iconView: UIImageView
|
||||
|
||||
@ -120,7 +125,11 @@ final class CategoryListItemComponent: Component {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.action()
|
||||
if case .editing(true, _) = component.selectionState {
|
||||
component.secondaryAction()
|
||||
} else {
|
||||
component.action()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
@ -223,7 +232,12 @@ final class CategoryListItemComponent: Component {
|
||||
|
||||
transition.setFrame(view: self.iconView, frame: avatarFrame)
|
||||
|
||||
let labelData: (String, Bool) = ("", false)
|
||||
let labelData: (String, Bool, Bool)
|
||||
if let subtitle = component.subtitle {
|
||||
labelData = (subtitle, true, true)
|
||||
} else {
|
||||
labelData = ("", false, false)
|
||||
}
|
||||
|
||||
let labelSize = self.label.update(
|
||||
transition: .immediate,
|
||||
@ -234,6 +248,13 @@ final class CategoryListItemComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let labelArrowSize = self.labelArrow.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(BundleIconComponent(name: "Contact List/SubtitleArrow", tintColor: component.theme.list.itemAccentColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated && !"".isEmpty {
|
||||
@ -284,6 +305,13 @@ final class CategoryListItemComponent: Component {
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize))
|
||||
}
|
||||
if let labelArrowView = self.labelArrow.view, !labelData.0.isEmpty {
|
||||
if labelArrowView.superview == nil {
|
||||
labelArrowView.isUserInteractionEnabled = false
|
||||
self.containerButton.addSubview(labelArrowView)
|
||||
}
|
||||
transition.setFrame(view: labelArrowView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + labelSize.width + 5.0, y: titleFrame.maxY + titleSpacing + floorToScreenPixels(labelSize.height / 2.0 - labelArrowSize.height / 2.0)), size: labelArrowSize))
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
|
||||
|
@ -17,6 +17,7 @@ import PlainButtonComponent
|
||||
import AnimatedCounterComponent
|
||||
import TokenListTextField
|
||||
import AvatarNode
|
||||
import LocalizedPeerData
|
||||
|
||||
final class ShareWithPeersScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
@ -26,19 +27,25 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
let initialPrivacy: EngineStoryPrivacy
|
||||
let categoryItems: [CategoryItem]
|
||||
let completion: (EngineStoryPrivacy) -> Void
|
||||
let editCategory: (EngineStoryPrivacy) -> Void
|
||||
let secondaryAction: () -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
stateContext: ShareWithPeersScreen.StateContext,
|
||||
initialPrivacy: EngineStoryPrivacy,
|
||||
categoryItems: [CategoryItem],
|
||||
completion: @escaping (EngineStoryPrivacy) -> Void
|
||||
completion: @escaping (EngineStoryPrivacy) -> Void,
|
||||
editCategory: @escaping (EngineStoryPrivacy) -> Void,
|
||||
secondaryAction: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.stateContext = stateContext
|
||||
self.initialPrivacy = initialPrivacy
|
||||
self.categoryItems = categoryItems
|
||||
self.completion = completion
|
||||
self.editCategory = editCategory
|
||||
self.secondaryAction = secondaryAction
|
||||
}
|
||||
|
||||
static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool {
|
||||
@ -204,6 +211,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
private let navigationBackgroundView: BlurredBackgroundView
|
||||
private let navigationTitle = ComponentView<Empty>()
|
||||
private let navigationLeftButton = ComponentView<Empty>()
|
||||
private let navigationRightButton = ComponentView<Empty>()
|
||||
private let navigationSeparatorLayer: SimpleLayer
|
||||
private let navigationTextFieldState = TokenListTextField.ExternalState()
|
||||
private let navigationTextField = ComponentView<Empty>()
|
||||
@ -440,7 +448,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
if section.id == 0 {
|
||||
sectionTitle = "WHO CAN VIEW FOR 24 HOURS"
|
||||
} else {
|
||||
sectionTitle = "CONTACTS"
|
||||
if case .chats = component.stateContext.subject {
|
||||
sectionTitle = "CHATS"
|
||||
} else {
|
||||
sectionTitle = "CONTACTS"
|
||||
}
|
||||
}
|
||||
|
||||
let _ = sectionHeader.update(
|
||||
@ -521,6 +533,26 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
},
|
||||
secondaryAction: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
let base: EngineStoryPrivacy.Base?
|
||||
switch categoryId {
|
||||
case .everyone:
|
||||
base = nil
|
||||
case .contacts:
|
||||
base = .contacts
|
||||
case .closeFriends:
|
||||
base = .closeFriends
|
||||
case .selectedContacts:
|
||||
base = .nobody
|
||||
}
|
||||
if let base {
|
||||
component.editCategory(EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers))
|
||||
controller.dismiss()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -700,6 +732,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
var applyState = false
|
||||
self.defaultStateValue = component.stateContext.stateValue
|
||||
self.selectedPeers = Array(component.stateContext.initialPeerIds)
|
||||
|
||||
self.stateDisposable = (component.stateContext.state
|
||||
|> deliverOnMainQueue).start(next: { [weak self] stateValue in
|
||||
guard let self else {
|
||||
@ -738,97 +772,85 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor
|
||||
}
|
||||
|
||||
var tokens: [TokenListTextField.Token] = []
|
||||
for categoryId in self.selectedCategories.sorted(by: { $0.rawValue < $1.rawValue }) {
|
||||
let categoryTitle: String
|
||||
var categoryImage: UIImage?
|
||||
switch categoryId {
|
||||
case .everyone:
|
||||
categoryTitle = "Everyone"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue)
|
||||
case .contacts:
|
||||
categoryTitle = "Contacts"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.9, cornerRadius: 6.0, circleCorners: true, color: .yellow)
|
||||
case .closeFriends:
|
||||
categoryTitle = "Close Friends"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .green)
|
||||
case .selectedContacts:
|
||||
categoryTitle = "Selected Contacts"
|
||||
categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .purple)
|
||||
}
|
||||
tokens.append(TokenListTextField.Token(
|
||||
id: AnyHashable(categoryId),
|
||||
title: categoryTitle,
|
||||
fixedPosition: categoryId.rawValue,
|
||||
content: .category(categoryImage)
|
||||
))
|
||||
}
|
||||
for peerId in self.selectedPeers {
|
||||
guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else {
|
||||
continue
|
||||
}
|
||||
tokens.append(TokenListTextField.Token(
|
||||
id: AnyHashable(peerId),
|
||||
title: peer.compactDisplayTitle,
|
||||
fixedPosition: nil,
|
||||
content: .peer(peer)
|
||||
))
|
||||
}
|
||||
|
||||
self.navigationTextField.parentState = state
|
||||
let navigationTextFieldSize = self.navigationTextField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TokenListTextField(
|
||||
externalState: self.navigationTextFieldState,
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
placeholder: "Search Contacts",
|
||||
tokens: tokens,
|
||||
sideInset: sideInset,
|
||||
deleteToken: { [weak self] tokenId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let categoryId = tokenId.base as? CategoryId {
|
||||
self.selectedCategories.remove(categoryId)
|
||||
} else if let peerId = tokenId.base as? EnginePeer.Id {
|
||||
self.selectedPeers.removeAll(where: { $0 == peerId })
|
||||
}
|
||||
if self.selectedCategories.isEmpty {
|
||||
self.selectedCategories.insert(.everyone)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
let navigationTextFieldSize: CGSize
|
||||
if case .stories = component.stateContext.subject {
|
||||
navigationTextFieldSize = .zero
|
||||
} else {
|
||||
var tokens: [TokenListTextField.Token] = []
|
||||
for peerId in self.selectedPeers {
|
||||
guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else {
|
||||
continue
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
if !self.navigationTextFieldState.text.isEmpty {
|
||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) {
|
||||
} else {
|
||||
self.searchStateDisposable?.dispose()
|
||||
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text))
|
||||
var applyState = false
|
||||
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.searchStateContext = searchStateContext
|
||||
if applyState {
|
||||
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true)))
|
||||
}
|
||||
})
|
||||
applyState = true
|
||||
tokens.append(TokenListTextField.Token(
|
||||
id: AnyHashable(peerId),
|
||||
title: peer.compactDisplayTitle,
|
||||
fixedPosition: nil,
|
||||
content: .peer(peer)
|
||||
))
|
||||
}
|
||||
} else if let _ = self.searchStateContext {
|
||||
self.searchStateContext = nil
|
||||
self.searchStateDisposable?.dispose()
|
||||
self.searchStateDisposable = nil
|
||||
|
||||
contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true))
|
||||
let placeholder: String
|
||||
switch component.stateContext.subject {
|
||||
case .chats:
|
||||
placeholder = "Search Chats"
|
||||
default:
|
||||
placeholder = "Search Contacts"
|
||||
}
|
||||
self.navigationTextField.parentState = state
|
||||
navigationTextFieldSize = self.navigationTextField.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(TokenListTextField(
|
||||
externalState: self.navigationTextFieldState,
|
||||
context: component.context,
|
||||
theme: environment.theme,
|
||||
placeholder: placeholder,
|
||||
tokens: tokens,
|
||||
sideInset: sideInset,
|
||||
deleteToken: { [weak self] tokenId in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if let categoryId = tokenId.base as? CategoryId {
|
||||
self.selectedCategories.remove(categoryId)
|
||||
} else if let peerId = tokenId.base as? EnginePeer.Id {
|
||||
self.selectedPeers.removeAll(where: { $0 == peerId })
|
||||
}
|
||||
if self.selectedCategories.isEmpty {
|
||||
self.selectedCategories.insert(.everyone)
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
)
|
||||
|
||||
if !self.navigationTextFieldState.text.isEmpty {
|
||||
if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) {
|
||||
} else {
|
||||
self.searchStateDisposable?.dispose()
|
||||
let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text))
|
||||
var applyState = false
|
||||
self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.searchStateContext = searchStateContext
|
||||
if applyState {
|
||||
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true)))
|
||||
}
|
||||
})
|
||||
applyState = true
|
||||
}
|
||||
} else if let _ = self.searchStateContext {
|
||||
self.searchStateContext = nil
|
||||
self.searchStateDisposable?.dispose()
|
||||
self.searchStateDisposable = nil
|
||||
|
||||
contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
|
||||
let categoryItemSize = self.categoryTemplateItem.update(
|
||||
@ -843,7 +865,8 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
subtitle: nil,
|
||||
selectionState: .editing(isSelected: false, isTinted: false),
|
||||
hasNext: true,
|
||||
action: {}
|
||||
action: {},
|
||||
secondaryAction: {}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
@ -870,54 +893,121 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
var sections: [ItemLayout.Section] = []
|
||||
if let stateValue = self.effectiveStateValue {
|
||||
if self.searchStateContext == nil {
|
||||
if case .stories = component.stateContext.subject {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 0,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0),
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: categoryItemSize.height,
|
||||
itemCount: component.categoryItems.count
|
||||
))
|
||||
} else {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 1,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0),
|
||||
itemHeight: peerItemSize.height,
|
||||
itemCount: stateValue.peers.count
|
||||
))
|
||||
}
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 1,
|
||||
insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0),
|
||||
itemHeight: peerItemSize.height,
|
||||
itemCount: stateValue.peers.count
|
||||
))
|
||||
}
|
||||
|
||||
let containerInset: CGFloat = environment.statusBarHeight + 10.0
|
||||
|
||||
var navigationHeight: CGFloat = 56.0
|
||||
|
||||
let navigationSideInset: CGFloat = 16.0
|
||||
let navigationLeftButtonSize = self.navigationLeftButton.update(
|
||||
var navigationButtonsWidth: CGFloat = 0.0
|
||||
|
||||
if case .stories = component.stateContext.subject {
|
||||
} else {
|
||||
let navigationLeftButtonSize = self.navigationLeftButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() else {
|
||||
return
|
||||
}
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: navigationHeight, height: navigationHeight))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
|
||||
)
|
||||
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize)
|
||||
if let navigationLeftButtonView = self.navigationLeftButton.view {
|
||||
if navigationLeftButtonView.superview == nil {
|
||||
self.navigationContainerView.addSubview(navigationLeftButtonView)
|
||||
}
|
||||
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
|
||||
}
|
||||
navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset
|
||||
}
|
||||
|
||||
let navigationRightButtonSize = self.navigationRightButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
content: AnyComponent(Text(text: "Done", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)),
|
||||
action: { [weak self] in
|
||||
guard let self, let environment = self.environment, let controller = environment.controller() else {
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
let base: EngineStoryPrivacy.Base
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
base = .everyone
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
base = .closeFriends
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
base = .contacts
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
base = .nobody
|
||||
} else {
|
||||
base = .nobody
|
||||
}
|
||||
|
||||
component.completion(EngineStoryPrivacy(
|
||||
base: base,
|
||||
additionallyIncludePeers: self.selectedPeers
|
||||
))
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: navigationHeight, height: navigationHeight))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: navigationHeight)
|
||||
)
|
||||
let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize)
|
||||
if let navigationLeftButtonView = self.navigationLeftButton.view {
|
||||
if navigationLeftButtonView.superview == nil {
|
||||
self.navigationContainerView.addSubview(navigationLeftButtonView)
|
||||
let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - navigationSideInset - navigationRightButtonSize.width, y: floor((navigationHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize)
|
||||
if let navigationRightButtonView = self.navigationRightButton.view {
|
||||
if navigationRightButtonView.superview == nil {
|
||||
self.navigationContainerView.addSubview(navigationRightButtonView)
|
||||
}
|
||||
transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame)
|
||||
transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame)
|
||||
}
|
||||
navigationButtonsWidth += navigationRightButtonSize.width + navigationSideInset
|
||||
|
||||
let title: String
|
||||
switch component.stateContext.subject {
|
||||
case .stories:
|
||||
title = "Share Story"
|
||||
case .chats:
|
||||
title = "Send as a Message"
|
||||
case let .contacts(category):
|
||||
switch category {
|
||||
case .closeFriends:
|
||||
title = "Close Friends"
|
||||
case .contacts:
|
||||
title = "Excluded People"
|
||||
case .nobody:
|
||||
title = "Selected Contacts"
|
||||
case .everyone:
|
||||
title = ""
|
||||
}
|
||||
case .search:
|
||||
title = ""
|
||||
}
|
||||
let navigationTitleSize = self.navigationTitle.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(Text(text: "Share Story", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationSideInset - navigationLeftButtonFrame.maxX, height: navigationHeight)
|
||||
containerSize: CGSize(width: availableSize.width - navigationButtonsWidth, height: navigationHeight)
|
||||
)
|
||||
let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: floor((navigationHeight - navigationTitleSize.height) * 0.5)), size: navigationTitleSize)
|
||||
if let navigationTitleView = self.navigationTitle.view {
|
||||
@ -943,7 +1033,11 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty {
|
||||
topInset = 0.0
|
||||
} else {
|
||||
topInset = max(0.0, availableSize.height - containerInset - 600.0)
|
||||
if case .stories = component.stateContext.subject {
|
||||
topInset = max(0.0, availableSize.height - containerInset - 410.0)
|
||||
} else {
|
||||
topInset = max(0.0, availableSize.height - containerInset - 600.0)
|
||||
}
|
||||
}
|
||||
|
||||
self.navigationBackgroundView.update(size: CGSize(width: availableSize.width, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition)
|
||||
@ -951,84 +1045,47 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
|
||||
transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
var actionButtonTitle: String = "Post Story"
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
actionButtonTitle = "Post Story"
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
actionButtonTitle = "Send to Close Friends"
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
actionButtonTitle = "Send to Contacts"
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
actionButtonTitle = "Send to Selected Contacts"
|
||||
}
|
||||
|
||||
let actionButtonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
color: environment.theme.list.itemCheckColors.fillColor,
|
||||
foreground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: actionButtonTitle,
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: actionButtonTitle,
|
||||
badge: 0,
|
||||
textColor: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: environment.theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: environment.theme.list.itemCheckColors.fillColor
|
||||
))
|
||||
),
|
||||
isEnabled: true,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
let base: EngineStoryPrivacy.Base
|
||||
if self.selectedCategories.contains(.everyone) {
|
||||
base = .everyone
|
||||
} else if self.selectedCategories.contains(.closeFriends) {
|
||||
base = .closeFriends
|
||||
} else if self.selectedCategories.contains(.contacts) {
|
||||
base = .contacts
|
||||
} else if self.selectedCategories.contains(.selectedContacts) {
|
||||
base = .nobody
|
||||
} else {
|
||||
base = .nobody
|
||||
}
|
||||
|
||||
component.completion(EngineStoryPrivacy(
|
||||
base: base,
|
||||
additionallyIncludePeers: self.selectedPeers
|
||||
))
|
||||
controller.dismiss()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 50.0)
|
||||
)
|
||||
|
||||
var bottomPanelHeight: CGFloat = 0.0
|
||||
if environment.inputHeight != 0.0 {
|
||||
bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height
|
||||
} else {
|
||||
bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height
|
||||
}
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||
if let actionButtonView = self.actionButton.view {
|
||||
if actionButtonView.superview == nil {
|
||||
self.addSubview(actionButtonView)
|
||||
if case .stories = component.stateContext.subject {
|
||||
let actionButtonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Button(
|
||||
content: AnyComponent(Text(
|
||||
text: "Send as a Message",
|
||||
font: Font.regular(17.0),
|
||||
color: environment.theme.list.itemAccentColor
|
||||
)),
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let controller = self.environment?.controller() else {
|
||||
return
|
||||
}
|
||||
|
||||
component.secondaryAction()
|
||||
controller.dismiss()
|
||||
}
|
||||
).minSize(CGSize(width: 200.0, height: 44.0))),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 44.0)
|
||||
)
|
||||
|
||||
if environment.inputHeight != 0.0 {
|
||||
bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height
|
||||
} else {
|
||||
bottomPanelHeight += environment.safeInsets.bottom + actionButtonSize.height
|
||||
}
|
||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||
let actionButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - actionButtonSize.width) / 2.0, y: availableSize.height - bottomPanelHeight), size: actionButtonSize)
|
||||
if let actionButtonView = self.actionButton.view {
|
||||
if actionButtonView.superview == nil {
|
||||
self.addSubview(actionButtonView)
|
||||
}
|
||||
transition.setFrame(view: actionButtonView, frame: actionButtonFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0)))
|
||||
self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0)))
|
||||
self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition)
|
||||
transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
let itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: bottomPanelHeight + environment.safeInsets.bottom, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections)
|
||||
let previousItemLayout = self.itemLayout
|
||||
self.itemLayout = itemLayout
|
||||
@ -1099,13 +1156,16 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
public final class StateContext {
|
||||
public enum Subject: Equatable {
|
||||
case contacts
|
||||
case stories
|
||||
case chats
|
||||
case contacts(EngineStoryPrivacy.Base)
|
||||
case search(String)
|
||||
}
|
||||
|
||||
fileprivate var stateValue: State?
|
||||
|
||||
public let subject: Subject
|
||||
public private(set) var initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||
|
||||
private var stateDisposable: Disposable?
|
||||
private let stateSubject = Promise<State>()
|
||||
@ -1119,12 +1179,51 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
subject: Subject = .contacts
|
||||
subject: Subject = .chats,
|
||||
initialPeerIds: Set<EnginePeer.Id> = Set()
|
||||
) {
|
||||
self.subject = subject
|
||||
self.initialPeerIds = initialPeerIds
|
||||
|
||||
switch subject {
|
||||
case .contacts:
|
||||
case .stories:
|
||||
let state = State(peers: [], presences: [:])
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
self.readySubject.set(true)
|
||||
case .chats:
|
||||
self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200)
|
||||
|> deliverOnMainQueue).start(next: { [weak self] chatList in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
var selectedPeers: [EnginePeer] = []
|
||||
for item in chatList.items.reversed() {
|
||||
if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer {
|
||||
selectedPeers.append(peer)
|
||||
}
|
||||
}
|
||||
|
||||
var presences: [EnginePeer.Id: EnginePeer.Presence] = [:]
|
||||
for item in chatList.items {
|
||||
presences[item.renderedPeer.peerId] = item.presence
|
||||
}
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
peers = chatList.items.filter { !self.initialPeerIds.contains($0.renderedPeer.peerId) }.reversed().compactMap { $0.renderedPeer.peer }
|
||||
peers.insert(contentsOf: selectedPeers, at: 0)
|
||||
|
||||
let state = State(
|
||||
peers: peers,
|
||||
presences: presences
|
||||
)
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
self.readySubject.set(true)
|
||||
})
|
||||
case let .contacts(base):
|
||||
self.stateDisposable = (context.engine.data.subscribe(
|
||||
TelegramEngine.EngineData.Item.Contacts.List(includePresences: true)
|
||||
)
|
||||
@ -1133,23 +1232,40 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
return
|
||||
}
|
||||
|
||||
let state = State(
|
||||
peers: contactList.peers.sorted(by: { lhs, rhs in
|
||||
let lhsPresence = contactList.presences[lhs.id]
|
||||
let rhsPresence = contactList.presences[rhs.id]
|
||||
|
||||
if let lhsPresence, let rhsPresence {
|
||||
return lhsPresence.status > rhsPresence.status
|
||||
} else if lhsPresence != nil {
|
||||
return true
|
||||
} else if rhsPresence != nil {
|
||||
return false
|
||||
} else {
|
||||
return lhs.id < rhs.id
|
||||
var selectedPeers: [EnginePeer] = []
|
||||
if case .closeFriends = base {
|
||||
for peer in contactList.peers {
|
||||
if case let .user(user) = peer, user.flags.contains(.isCloseFriend) {
|
||||
selectedPeers.append(peer)
|
||||
}
|
||||
}),
|
||||
}
|
||||
selectedPeers = selectedPeers.sorted(by: { lhs, rhs in
|
||||
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
|
||||
if result == .orderedSame {
|
||||
return lhs.id < rhs.id
|
||||
} else {
|
||||
return result == .orderedAscending
|
||||
}
|
||||
})
|
||||
self.initialPeerIds = Set(selectedPeers.map { $0.id })
|
||||
}
|
||||
|
||||
var peers: [EnginePeer] = []
|
||||
peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) }.sorted(by: { lhs, rhs in
|
||||
let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast)
|
||||
if result == .orderedSame {
|
||||
return lhs.id < rhs.id
|
||||
} else {
|
||||
return result == .orderedAscending
|
||||
}
|
||||
})
|
||||
peers.insert(contentsOf: selectedPeers, at: 0)
|
||||
|
||||
let state = State(
|
||||
peers: peers,
|
||||
presences: contactList.presences
|
||||
)
|
||||
|
||||
self.stateValue = state
|
||||
self.stateSubject.set(.single(state))
|
||||
|
||||
@ -1183,45 +1299,49 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
|
||||
private var isDismissed: Bool = false
|
||||
|
||||
public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void) {
|
||||
public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> Void) {
|
||||
self.context = context
|
||||
|
||||
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .everyone,
|
||||
title: "Everyone",
|
||||
icon: "Chat List/Filters/Channel",
|
||||
iconColor: .blue,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .contacts,
|
||||
title: "Contacts",
|
||||
icon: "Chat List/Tabs/IconContacts",
|
||||
iconColor: .yellow,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .closeFriends,
|
||||
title: "Close Friends",
|
||||
icon: "Call/StarHighlighted",
|
||||
iconColor: .green,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .selectedContacts,
|
||||
title: "Selected Contacts",
|
||||
icon: "Chat List/Filters/Group",
|
||||
iconColor: .purple,
|
||||
actionTitle: nil
|
||||
))
|
||||
if case .stories = stateContext.subject {
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .everyone,
|
||||
title: "Everyone",
|
||||
icon: "Chat List/Filters/Channel",
|
||||
iconColor: .blue,
|
||||
actionTitle: nil
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .contacts,
|
||||
title: "Contacts",
|
||||
icon: "Chat List/Tabs/IconContacts",
|
||||
iconColor: .yellow,
|
||||
actionTitle: "exclude people"
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .closeFriends,
|
||||
title: "Close Friends",
|
||||
icon: "Call/StarHighlighted",
|
||||
iconColor: .green,
|
||||
actionTitle: "edit list"
|
||||
))
|
||||
categoryItems.append(ShareWithPeersScreenComponent.CategoryItem(
|
||||
id: .selectedContacts,
|
||||
title: "Selected Contacts",
|
||||
icon: "Chat List/Filters/Group",
|
||||
iconColor: .violet,
|
||||
actionTitle: "edit list"
|
||||
))
|
||||
}
|
||||
|
||||
super.init(context: context, component: ShareWithPeersScreenComponent(
|
||||
context: context,
|
||||
stateContext: stateContext,
|
||||
initialPrivacy: initialPrivacy,
|
||||
categoryItems: categoryItems,
|
||||
completion: completion
|
||||
completion: completion,
|
||||
editCategory: editCategory,
|
||||
secondaryAction: secondaryAction
|
||||
), navigationBarAppearance: .none, theme: .dark)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
@ -134,7 +134,7 @@ public final class TextFieldComponent: Component {
|
||||
self.layoutManager.ensureLayout(for: self.textContainer)
|
||||
|
||||
let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer)
|
||||
let size = CGSize(width: availableSize.width, height: min(100.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
let size = CGSize(width: availableSize.width, height: min(200.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom))
|
||||
|
||||
let refreshScrolling = self.textView.bounds.size != size
|
||||
self.textView.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
12
submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "off shadow.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
1366
submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf
vendored
Normal file
1366
submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf
vendored
Normal file
File diff suppressed because it is too large
Load Diff
15
submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json
vendored
Normal file
15
submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "SubtitleArrow.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
92
submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf
vendored
Normal file
92
submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< >>
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
<< /Length 3 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
-1.000000 0.000000 -0.000000 -1.000000 4.664993 10.125000 cm
|
||||
0.000000 0.478431 1.000000 scn
|
||||
4.470226 8.989735 m
|
||||
4.729925 9.249434 4.729925 9.670488 4.470226 9.930187 c
|
||||
4.210527 10.189886 3.789473 10.189886 3.529774 9.930187 c
|
||||
4.470226 8.989735 l
|
||||
h
|
||||
0.000000 5.459961 m
|
||||
-0.470226 5.930187 l
|
||||
-0.729925 5.670488 -0.729925 5.249434 -0.470226 4.989735 c
|
||||
0.000000 5.459961 l
|
||||
h
|
||||
3.529774 0.989735 m
|
||||
3.789473 0.730036 4.210527 0.730036 4.470226 0.989735 c
|
||||
4.729925 1.249434 4.729925 1.670488 4.470226 1.930187 c
|
||||
3.529774 0.989735 l
|
||||
h
|
||||
3.529774 9.930187 m
|
||||
-0.470226 5.930187 l
|
||||
0.470226 4.989735 l
|
||||
4.470226 8.989735 l
|
||||
3.529774 9.930187 l
|
||||
h
|
||||
-0.470226 4.989735 m
|
||||
3.529774 0.989735 l
|
||||
4.470226 1.930187 l
|
||||
0.470226 5.930187 l
|
||||
-0.470226 4.989735 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
767
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 5.329987 9.330078 ]
|
||||
/Resources 1 0 R
|
||||
/Contents 2 0 R
|
||||
/Parent 5 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /Kids [ 4 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Pages 5 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 7
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000034 00000 n
|
||||
0000000857 00000 n
|
||||
0000000879 00000 n
|
||||
0000001050 00000 n
|
||||
0000001124 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 6 0 R
|
||||
/Size 7
|
||||
>>
|
||||
startxref
|
||||
1183
|
||||
%%EOF
|
12
submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json
vendored
Normal file
12
submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "check.pdf",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
150
submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf
vendored
Normal file
150
submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf
vendored
Normal file
@ -0,0 +1,150 @@
|
||||
%PDF-1.7
|
||||
|
||||
1 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 2 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << >>
|
||||
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
16.557842 15.662290 m
|
||||
17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c
|
||||
8.443170 0.821279 l
|
||||
8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c
|
||||
7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c
|
||||
0.389548 6.608668 l
|
||||
-0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c
|
||||
0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c
|
||||
7.112782 3.647242 l
|
||||
14.716830 15.276961 l
|
||||
15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c
|
||||
h
|
||||
f*
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
2 0 obj
|
||||
584
|
||||
endobj
|
||||
|
||||
3 0 obj
|
||||
<< /Type /XObject
|
||||
/Length 4 0 R
|
||||
/Group << /Type /Group
|
||||
/S /Transparency
|
||||
>>
|
||||
/Subtype /Form
|
||||
/Resources << >>
|
||||
/BBox [ 0.000000 0.000000 40.000000 40.000000 ]
|
||||
>>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm
|
||||
0.000000 0.000000 0.000000 scn
|
||||
0.000000 20.000000 m
|
||||
0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c
|
||||
20.000000 40.000000 l
|
||||
31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c
|
||||
40.000000 20.000000 l
|
||||
40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c
|
||||
20.000000 0.000000 l
|
||||
8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c
|
||||
0.000000 20.000000 l
|
||||
h
|
||||
f
|
||||
n
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
4 0 obj
|
||||
472
|
||||
endobj
|
||||
|
||||
5 0 obj
|
||||
<< /XObject << /X1 1 0 R >>
|
||||
/ExtGState << /E1 << /SMask << /Type /Mask
|
||||
/G 3 0 R
|
||||
/S /Alpha
|
||||
>>
|
||||
/Type /ExtGState
|
||||
>> >>
|
||||
>>
|
||||
endobj
|
||||
|
||||
6 0 obj
|
||||
<< /Length 7 0 R >>
|
||||
stream
|
||||
/DeviceRGB CS
|
||||
/DeviceRGB cs
|
||||
q
|
||||
/E1 gs
|
||||
/X1 Do
|
||||
Q
|
||||
|
||||
endstream
|
||||
endobj
|
||||
|
||||
7 0 obj
|
||||
46
|
||||
endobj
|
||||
|
||||
8 0 obj
|
||||
<< /Annots []
|
||||
/Type /Page
|
||||
/MediaBox [ 0.000000 0.000000 40.000000 40.000000 ]
|
||||
/Resources 5 0 R
|
||||
/Contents 6 0 R
|
||||
/Parent 9 0 R
|
||||
>>
|
||||
endobj
|
||||
|
||||
9 0 obj
|
||||
<< /Kids [ 8 0 R ]
|
||||
/Count 1
|
||||
/Type /Pages
|
||||
>>
|
||||
endobj
|
||||
|
||||
10 0 obj
|
||||
<< /Pages 9 0 R
|
||||
/Type /Catalog
|
||||
>>
|
||||
endobj
|
||||
|
||||
xref
|
||||
0 11
|
||||
0000000000 65535 f
|
||||
0000000010 00000 n
|
||||
0000000842 00000 n
|
||||
0000000864 00000 n
|
||||
0000001584 00000 n
|
||||
0000001606 00000 n
|
||||
0000001904 00000 n
|
||||
0000002006 00000 n
|
||||
0000002027 00000 n
|
||||
0000002200 00000 n
|
||||
0000002274 00000 n
|
||||
trailer
|
||||
<< /ID [ (some) (id) ]
|
||||
/Root 10 0 R
|
||||
/Size 11
|
||||
>>
|
||||
startxref
|
||||
2334
|
||||
%%EOF
|
@ -1828,8 +1828,8 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker)
|
||||
}
|
||||
|
||||
public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, completion: completion)
|
||||
public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed)
|
||||
}
|
||||
|
||||
public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController {
|
||||
|
@ -26,6 +26,7 @@ import AvatarNode
|
||||
import LocalMediaResources
|
||||
import ShareWithPeersScreen
|
||||
import ImageCompression
|
||||
import TextFormat
|
||||
|
||||
private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode {
|
||||
private var presentationData: PresentationData
|
||||
@ -196,16 +197,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
var transitionIn: StoryCameraTransitionIn?
|
||||
if let cameraItemView = self.rootTabController?.viewForCameraItem() {
|
||||
transitionIn = StoryCameraTransitionIn(
|
||||
sourceView: cameraItemView,
|
||||
sourceRect: cameraItemView.bounds,
|
||||
sourceCornerRadius: cameraItemView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
self.openStoryCamera(
|
||||
transitionIn: transitionIn,
|
||||
let coordinator = self.openStoryCamera(
|
||||
transitionIn: nil,
|
||||
transitionOut: { [weak self] finished in
|
||||
guard let self else {
|
||||
return nil
|
||||
@ -218,18 +211,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
destinationCornerRadius: transitionView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if let cameraItemView = self.rootTabController?.viewForCameraItem() {
|
||||
return StoryCameraTransitionOut(
|
||||
destinationView: cameraItemView,
|
||||
destinationRect: cameraItemView.bounds,
|
||||
destinationCornerRadius: cameraItemView.bounds.height / 2.0
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
coordinator?.animateIn()
|
||||
}
|
||||
)
|
||||
|
||||
@ -288,9 +274,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
presentedLegacyShortcutCamera(context: self.context, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller)
|
||||
}
|
||||
|
||||
public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) {
|
||||
@discardableResult
|
||||
public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? {
|
||||
guard let controller = self.viewControllers.last as? ViewController else {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
controller.view.endEditing(true)
|
||||
|
||||
@ -299,7 +286,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
var presentImpl: ((ViewController) -> Void)?
|
||||
var returnToCameraImpl: (() -> Void)?
|
||||
var dismissCameraImpl: (() -> Void)?
|
||||
var hideCameraImpl: (() -> Void)?
|
||||
var showDraftTooltipImpl: (() -> Void)?
|
||||
let cameraController = CameraScreen(
|
||||
context: context,
|
||||
@ -326,7 +312,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return nil
|
||||
}
|
||||
},
|
||||
completion: { result in
|
||||
completion: { result, resultTransition in
|
||||
let subject: Signal<MediaEditorScreen.Subject?, NoError> = result
|
||||
|> map { value -> MediaEditorScreen.Subject? in
|
||||
switch value {
|
||||
@ -342,17 +328,37 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return .draft(draft)
|
||||
}
|
||||
}
|
||||
|
||||
var transitionIn: MediaEditorScreen.TransitionIn?
|
||||
if let resultTransition, let sourceView = resultTransition.sourceView {
|
||||
transitionIn = .gallery(
|
||||
MediaEditorScreen.TransitionIn.GalleryTransitionIn(
|
||||
sourceView: sourceView,
|
||||
sourceRect: resultTransition.sourceRect,
|
||||
sourceImage: resultTransition.sourceImage
|
||||
)
|
||||
)
|
||||
} else {
|
||||
transitionIn = .camera
|
||||
}
|
||||
|
||||
let controller = MediaEditorScreen(
|
||||
context: context,
|
||||
subject: subject,
|
||||
transitionIn: nil,
|
||||
transitionIn: transitionIn,
|
||||
transitionOut: { finished in
|
||||
if finished, let transitionOut = transitionOut(true), let destinationView = transitionOut.destinationView {
|
||||
if finished, let transitionOut = transitionOut(finished), let destinationView = transitionOut.destinationView {
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: transitionOut.destinationRect,
|
||||
destinationCornerRadius: transitionOut.destinationCornerRadius
|
||||
)
|
||||
} else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut() {
|
||||
return MediaEditorScreen.TransitionOut(
|
||||
destinationView: destinationView,
|
||||
destinationRect: destinationRect,
|
||||
destinationCornerRadius: 0.0
|
||||
)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
@ -367,10 +373,71 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
switch mediaResult {
|
||||
case let .image(image, dimensions, caption):
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.6) {
|
||||
storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy)
|
||||
Queue.mainQueue().after(0.2, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
switch privacy {
|
||||
case let .story(storyPrivacy, _):
|
||||
storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy)
|
||||
Queue.mainQueue().after(0.2, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
case let .message(peerIds, timeout):
|
||||
var randomId: Int64 = 0
|
||||
arc4random_buf(&randomId, 8)
|
||||
let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpg"
|
||||
let _ = try? imageData.write(to: URL(fileURLWithPath: tempFilePath))
|
||||
|
||||
var representations: [TelegramMediaImageRepresentation] = []
|
||||
let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId)
|
||||
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false))
|
||||
|
||||
var attributes: [MessageAttribute] = []
|
||||
let imageFlags: TelegramMediaImageFlags = []
|
||||
// var stickerFiles: [TelegramMediaFile] = []
|
||||
// if !stickers.isEmpty {
|
||||
// for fileReference in stickers {
|
||||
// stickerFiles.append(fileReference.media)
|
||||
// }
|
||||
// }
|
||||
// if !stickerFiles.isEmpty {
|
||||
// attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles))
|
||||
// imageFlags.insert(.hasStickers)
|
||||
// }
|
||||
|
||||
let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags)
|
||||
if let timeout, timeout > 0 && timeout <= 60 {
|
||||
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: nil))
|
||||
}
|
||||
|
||||
let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString()))
|
||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||
if !entities.isEmpty {
|
||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||
}
|
||||
var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:]
|
||||
text.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: text.length), using: { value, _, _ in
|
||||
if let value = value as? ChatTextInputTextCustomEmojiAttribute {
|
||||
if let file = value.file {
|
||||
if let packId = value.interactivelySelectedFromPackId {
|
||||
bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
var bubbleUpEmojiOrStickersets: [ItemCollectionId] = []
|
||||
for entity in entities {
|
||||
if case let .CustomEmoji(_, fileId) = entity.type {
|
||||
if let packId = bubbleUpEmojiOrStickersetsById[fileId] {
|
||||
if !bubbleUpEmojiOrStickersets.contains(packId) {
|
||||
bubbleUpEmojiOrStickersets.append(packId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = enqueueMessagesToMultiplePeers(
|
||||
account: self.context.account,
|
||||
peerIds: peerIds, threadIds: [:],
|
||||
messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start()
|
||||
}
|
||||
}
|
||||
case let .video(content, _, values, duration, dimensions, caption):
|
||||
let adjustments: VideoMediaResourceAdjustments
|
||||
@ -388,10 +455,14 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
case let .asset(localIdentifier):
|
||||
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
|
||||
}
|
||||
storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy)
|
||||
Queue.mainQueue().after(0.2, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
if case let .story(storyPrivacy, _) = privacy {
|
||||
storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy)
|
||||
Queue.mainQueue().after(0.2, { [weak chatListController] in
|
||||
chatListController?.animateStoryUploadRipple()
|
||||
})
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -400,16 +471,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
commit()
|
||||
}
|
||||
)
|
||||
controller.sourceHint = .camera
|
||||
controller.cancelled = { showDraftTooltip in
|
||||
if showDraftTooltip {
|
||||
showDraftTooltipImpl?()
|
||||
}
|
||||
returnToCameraImpl?()
|
||||
}
|
||||
controller.onReady = {
|
||||
hideCameraImpl?()
|
||||
}
|
||||
presentImpl?(controller)
|
||||
}
|
||||
)
|
||||
@ -429,16 +496,28 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
cameraController.returnFromEditor()
|
||||
}
|
||||
}
|
||||
hideCameraImpl = { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.commitTransitionToEditor()
|
||||
}
|
||||
}
|
||||
showDraftTooltipImpl = { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.presentDraftTooltip()
|
||||
}
|
||||
}
|
||||
return StoryCameraTransitionInCoordinator(
|
||||
animateIn: { [weak cameraController] in
|
||||
if let cameraController {
|
||||
cameraController.updateTransitionProgress(0.0, transition: .immediate)
|
||||
cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false)
|
||||
}
|
||||
},
|
||||
updateTransitionProgress: { [weak cameraController] transitionFraction in
|
||||
if let cameraController {
|
||||
cameraController.updateTransitionProgress(transitionFraction, transition: .immediate)
|
||||
}
|
||||
},
|
||||
completeWithTransitionProgressAndVelocity: { [weak cameraController] transitionFraction, velocity in
|
||||
if let cameraController {
|
||||
cameraController.completeWithTransitionProgress(transitionFraction, velocity: velocity, dismissing: false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public func openSettings() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user