Camera and editor improvements

This commit is contained in:
Ilya Laktyushin 2023-05-16 15:38:36 +04:00
parent b7e25ec8e7
commit 340001788e
5 changed files with 172 additions and 155 deletions

View File

@ -2533,6 +2533,15 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
}
}
public func transitionViewForOwnStoryItem() -> UIView? {
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {
return transitionView
}
}
return nil
}
public func animateStoryUploadRipple() {
if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View {
if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) {

View File

@ -602,6 +602,9 @@ public extension MediaEditorValues {
}
var requiresComposing: Bool {
if self.originalDimensions.width > self.originalDimensions.height {
return true
}
if abs(1.0 - self.cropScale) > 0.0 {
return true
}

View File

@ -519,21 +519,7 @@ public final class MediaEditorVideoExport {
return false
}
}
// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds
// if self.videoOutput === output {
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) }
// }
// if self.audioOutput === output {
// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: progress) }
// }
} else {
// if self.videoOutput === output {
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: 1) }
// }
// if self.audioOutput === output {
// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: 1) }
// }
writer.markVideoAsFinished()
return false
}
@ -553,24 +539,11 @@ public final class MediaEditorVideoExport {
}
self.pauseDispatchGroup.wait()
if let buffer = output.copyNextSampleBuffer() {
// let progress = (CMSampleBufferGetPresentationTimeStamp(buffer) - self.configuration.timeRange.start).seconds/self.duration.seconds
// if self.videoOutput === output {
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: progress) }
// }
// if self.audioOutput === output {
// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: progress) }
// }
if !writer.appendVideoBuffer(buffer) {
if !writer.appendAudioBuffer(buffer) {
writer.markAudioAsFinished()
return false
}
} else {
// if self.videoOutput === output {
// self.dispatchProgressCallback { $0.updateVideoEncodingProgress(fractionCompleted: 1) }
// }
// if self.audioOutput === output {
// self.dispatchProgressCallback { $0.updateAudioEncodingProgress(fractionCompleted: 1) }
// }
writer.markAudioAsFinished()
return false
}

View File

@ -385,11 +385,20 @@ final class MediaEditorScreenComponent: Component {
image: state.image(.done),
size: CGSize(width: 33.0, height: 33.0)
)),
action: {
guard let controller = environment.controller() as? MediaEditorScreen else {
action: { [weak self] in
guard let self, let controller = environment.controller() as? MediaEditorScreen else {
return
}
controller.requestCompletion(animated: true)
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
return
}
var inputText = NSAttributedString(string: "")
switch inputPanelView.getSendMessageInput() {
case let .text(text):
inputText = NSAttributedString(string: text)
}
controller.requestCompletion(caption: inputText, animated: true)
}
)),
environment: {},
@ -687,6 +696,38 @@ final class MediaEditorScreenComponent: Component {
private let storyDimensions = CGSize(width: 1080.0, height: 1920.0)
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 final class TransitionOut {
public weak var destinationView: UIView?
public let destinationRect: CGRect
public let destinationCornerRadius: CGFloat
public init(
destinationView: UIView,
destinationRect: CGRect,
destinationCornerRadius: CGFloat
) {
self.destinationView = destinationView
self.destinationRect = destinationRect
self.destinationCornerRadius = destinationCornerRadius
}
}
fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate {
private weak var controller: MediaEditorScreen?
private let context: AccountContext
@ -935,11 +976,44 @@ public final class MediaEditorScreen: ViewController {
}
}
func animateOut(completion: @escaping () -> Void) {
func animateOut(finished: Bool, completion: @escaping () -> Void) {
guard let controller = self.controller else {
return
}
if let sourceHint = controller.sourceHint {
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
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.frame.height - self.previewContainerView.frame.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.previewContainerView.layer.animate(
from: self.previewContainerView.layer.cornerRadius as NSNumber,
to: self.previewContainerView.bounds.width / 2.0 as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.4,
removeOnCompletion: false
)
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.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.frame.height - componentView.frame.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(
from: componentView.layer.cornerRadius as NSNumber,
to: componentView.bounds.width / 2.0 as NSNumber,
keyPath: "cornerRadius",
timingFunction: kCAMediaTimingFunctionSpring,
duration: 0.4,
removeOnCompletion: false
)
}
} else if let sourceHint = controller.sourceHint {
switch sourceHint {
case .camera:
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
@ -1175,6 +1249,8 @@ public final class MediaEditorScreen: ViewController {
fileprivate let context: AccountContext
fileprivate let subject: Signal<Subject?, NoError>
fileprivate let transitionIn: TransitionIn?
fileprivate let transitionOut: (Bool) -> TransitionOut?
public enum SourceHint {
case camera
@ -1184,9 +1260,17 @@ public final class MediaEditorScreen: ViewController {
public var cancelled: () -> Void = {}
public var completion: (MediaEditorScreen.Result, @escaping () -> Void) -> Void = { _, _ in }
public init(context: AccountContext, subject: Signal<Subject?, NoError>, completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void) -> Void) {
public init(
context: AccountContext,
subject: Signal<Subject?, NoError>,
transitionIn: TransitionIn?,
transitionOut: @escaping (Bool) -> TransitionOut?,
completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void) -> Void
) {
self.context = context
self.subject = subject
self.transitionIn = transitionIn
self.transitionOut = transitionOut
self.completion = completion
super.init(navigationBarPresentationData: nil)
@ -1210,12 +1294,12 @@ public final class MediaEditorScreen: ViewController {
func requestDismiss(animated: Bool) {
self.cancelled()
self.node.animateOut(completion: { [weak self] in
self.node.animateOut(finished: false, completion: { [weak self] in
self?.dismiss()
})
}
func requestCompletion(animated: Bool) {
func requestCompletion(caption: NSAttributedString, animated: Bool) {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else {
return
}
@ -1250,8 +1334,8 @@ public final class MediaEditorScreen: ViewController {
duration = 5.0
}
}
self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: PixelDimensions(width: 1080, height: 1920), caption: nil), { [weak self] in
self?.node.animateOut(completion: { [weak self] in
self.completion(.video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: PixelDimensions(width: 1080, height: 1920), caption: caption), { [weak self] in
self?.node.animateOut(finished: true, completion: { [weak self] in
self?.dismiss()
})
})
@ -1259,8 +1343,8 @@ public final class MediaEditorScreen: ViewController {
if let image = mediaEditor.resultImage {
makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { resultImage in
if let resultImage {
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: nil), { [weak self] in
self?.node.animateOut(completion: { [weak self] in
self.completion(.image(image: resultImage, dimensions: PixelDimensions(resultImage.size), caption: caption), { [weak self] in
self?.node.animateOut(finished: true, completion: { [weak self] in
self?.dismiss()
})
})

View File

@ -210,7 +210,13 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return nil
}
if finished {
if let chatListController = self.chatListController as? ChatListControllerImpl, let transitionView = chatListController.transitionViewForOwnStoryItem() {
return StoryCameraTransitionOut(
destinationView: transitionView,
destinationRect: transitionView.bounds,
destinationCornerRadius: transitionView.bounds.height / 2.0
)
}
} else {
if let cameraItemView = self.rootTabController?.viewForCameraItem() {
return StoryCameraTransitionOut(
@ -221,18 +227,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
}
}
return nil
// if finished {
// return nil
// } else {
// if let self, let cameraItemView = self.rootTabController?.viewForCameraItem() {
// return StoryCameraTransitionOut(
// destinationView: cameraItemView,
// destinationRect: cameraItemView.bounds,
// destinationCornerRadius: cameraItemView.bound.height / 2.0
// )
// }
// }
// return nil
}
)
}
@ -344,114 +338,68 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
return .asset(asset)
}
}
let controller = MediaEditorScreen(context: context, subject: subject, completion: { mediaResult, commit in
enum AdditionalCategoryId: Int {
case everyone
case contacts
case closeFriends
}
let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 })
let additionalCategories: [ChatListNodeAdditionalCategory] = [
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.everyone.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), cornerRadius: nil, color: .blue),
smallIcon: 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),
title: "Everyone",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.contacts.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 1.0 * 0.8, cornerRadius: nil, color: .yellow),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.8, cornerRadius: 6.0, circleCorners: true, color: .yellow),
title: presentationData.strings.ChatListFolder_CategoryContacts,
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
),
ChatListNodeAdditionalCategory(
id: AdditionalCategoryId.closeFriends.rawValue,
icon: generateAvatarImage(size: CGSize(width: 40.0, height: 40.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 1.0 * 0.6, cornerRadius: nil, color: .green),
smallIcon: generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 0.6, cornerRadius: 6.0, circleCorners: true, color: .green),
title: "Close Friends",
appearance: .option(sectionTitle: "WHO CAN VIEW FOR 24 HOURS")
let controller = MediaEditorScreen(context: context, subject: subject, transitionIn: nil, transitionOut: { finished in
if finished, let transitionOut = transitionOut(true), let destinationView = transitionOut.destinationView {
return MediaEditorScreen.TransitionOut(
destinationView: destinationView,
destinationRect: transitionOut.destinationRect,
destinationCornerRadius: transitionOut.destinationCornerRadius
)
]
let selectionController = self.context.sharedContext.makeContactMultiselectionController(ContactMultiselectionControllerParams(context: self.context, mode: .chatSelection(ContactMultiselectionControllerMode.ChatSelection(
title: "Share Story",
searchPlaceholder: "Search contacts",
selectedChats: Set(),
additionalCategories: ContactMultiselectionControllerAdditionalCategories(categories: additionalCategories, selectedCategories: Set([AdditionalCategoryId.everyone.rawValue])),
chatListFilters: nil,
displayPresence: true
)), options: [], filters: [.excludeSelf], alwaysEnabled: true, limit: 1000, reachedLimit: { _ in
}))
selectionController.navigationPresentation = .modal
self.pushViewController(selectionController)
let _ = (selectionController.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak selectionController] result in
guard case let .result(peerIds, additionalCategoryIds) = result else {
selectionController?.dismiss()
return
}
var privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) {
privacy.base = .everyone
} else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) {
privacy.base = .contacts
} else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) {
privacy.base = .closeFriends
}
privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in
switch id {
case let .peer(peerId):
return peerId
default:
return nil
} else {
return nil
}
}, completion: { mediaResult, commit in
let privacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: [])
// if additionalCategoryIds.contains(AdditionalCategoryId.everyone.rawValue) {
// privacy.base = .everyone
// } else if additionalCategoryIds.contains(AdditionalCategoryId.contacts.rawValue) {
// privacy.base = .contacts
// } else if additionalCategoryIds.contains(AdditionalCategoryId.closeFriends.rawValue) {
// privacy.base = .closeFriends
// }
// privacy.additionallyIncludePeers = peerIds.compactMap { id -> EnginePeer.Id? in
// switch id {
// case let .peer(peerId):
// return peerId
// default:
// return nil
// }
// }
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
switch mediaResult {
case let .image(image, dimensions, caption):
if let data = image.jpegData(compressionQuality: 0.8) {
storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: caption?.string ?? "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.3, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
}
}
selectionController?.displayProgress = true
if let chatListController = self.chatListController as? ChatListControllerImpl, let storyListContext = chatListController.storyListContext {
switch mediaResult {
case let .image(image, dimensions, _):
if let data = image.jpegData(compressionQuality: 0.8) {
storyListContext.upload(media: .image(dimensions: dimensions, data: data), text: "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.3, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
}
case let .video(content, _, values, duration, dimensions, _):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
case let .video(content, _, values, duration, dimensions, caption):
let adjustments: VideoMediaResourceAdjustments
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
adjustments = VideoMediaResourceAdjustments(data: data, digest: digest, isStory: true)
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .asset(localIdentifier):
resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments))
}
storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: "", entities: [], privacy: privacy)
Queue.mainQueue().after(0.3, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
let resource: TelegramMediaResource
switch content {
case let .imageFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
case let .videoFile(path):
resource = LocalFileVideoMediaResource(randomId: Int64.random(in: .min ... .max), path: path, adjustments: adjustments)
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.3, { [weak chatListController] in
chatListController?.animateStoryUploadRipple()
})
}
}
dismissCameraImpl?()
commit()
selectionController?.dismiss()
})
}
dismissCameraImpl?()
commit()
})
controller.sourceHint = .camera
controller.cancelled = {