Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-05-26 17:32:02 +04:00
parent d086a8f674
commit eeb1c469f3
64 changed files with 4037 additions and 1108 deletions

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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))

View File

@ -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:

View File

@ -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 {

View File

@ -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

View File

@ -36,6 +36,7 @@ swift_library(
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
"//submodules/QrCodeUI:QrCodeUI",
"//submodules/LocalizedPeerData:LocalizedPeerData"
],
visibility = [
"//visibility:public",

View File

@ -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] = []

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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,

View File

@ -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))
}
}

View File

@ -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]) {
}

View File

@ -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 }

View File

@ -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)
}
}

View File

@ -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) })

View File

@ -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)

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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)
}
}
}

View File

@ -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",
],

View File

@ -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
)

View File

@ -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)

View File

@ -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: {

View File

@ -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) {
}
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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;
}

View File

@ -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))

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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)
}

View File

@ -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))
}
}

View File

@ -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()

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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))

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "off shadow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"filename" : "SubtitleArrow.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View 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

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "check.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View 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

View File

@ -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 {

View File

@ -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() {