mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
843 lines
40 KiB
Swift
843 lines
40 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import Display
|
|
import AVFoundation
|
|
import SwiftSignalKit
|
|
import TelegramCore
|
|
import TextFormat
|
|
import Photos
|
|
import MediaEditor
|
|
import DrawingUI
|
|
|
|
extension MediaEditorScreenImpl {
|
|
func requestStoryCompletion(animated: Bool) {
|
|
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
|
|
return
|
|
}
|
|
|
|
self.didComplete = true
|
|
|
|
self.updateMediaEditorEntities()
|
|
|
|
mediaEditor.stop()
|
|
mediaEditor.invalidate()
|
|
self.node.entitiesView.invalidate()
|
|
|
|
if let navigationController = self.navigationController as? NavigationController {
|
|
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
|
|
}
|
|
|
|
var multipleItems: [EditingItem] = []
|
|
var isLongVideo = false
|
|
if self.node.items.count > 1 {
|
|
multipleItems = self.node.items.filter({ $0.isEnabled })
|
|
} else if case let .asset(asset) = self.node.subject {
|
|
let duration: Double
|
|
if let playerDuration = mediaEditor.duration {
|
|
duration = playerDuration
|
|
} else {
|
|
duration = asset.duration
|
|
}
|
|
if duration > storyMaxVideoDuration {
|
|
let originalDuration = mediaEditor.originalDuration ?? asset.duration
|
|
let values = mediaEditor.values
|
|
|
|
let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))
|
|
var start = values.videoTrimRange?.lowerBound ?? 0
|
|
let end = values.videoTrimRange?.upperBound ?? (min(originalDuration, start + storyMaxCombinedVideoDuration))
|
|
|
|
for i in 0 ..< storyCount {
|
|
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(end, start + storyMaxVideoDuration))
|
|
|
|
var editingItem = EditingItem(asset: asset)
|
|
if i == 0 {
|
|
editingItem.caption = self.node.getCaption()
|
|
}
|
|
editingItem.values = trimmedValues
|
|
multipleItems.append(editingItem)
|
|
|
|
start += storyMaxVideoDuration
|
|
}
|
|
isLongVideo = true
|
|
}
|
|
}
|
|
|
|
if multipleItems.count > 1 {
|
|
self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo)
|
|
} else {
|
|
self.processSingleItem()
|
|
}
|
|
|
|
self.dismissAllTooltips()
|
|
}
|
|
|
|
private func processSingleItem() {
|
|
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else {
|
|
return
|
|
}
|
|
|
|
var caption = self.node.getCaption()
|
|
caption = convertMarkdownToAttributes(caption)
|
|
|
|
var hasEntityChanges = false
|
|
let randomId: Int64
|
|
if case let .draft(_, id) = actualSubject, let id {
|
|
randomId = id
|
|
} else {
|
|
randomId = Int64.random(in: .min ... .max)
|
|
}
|
|
|
|
let codableEntities = mediaEditor.values.entities
|
|
var mediaAreas: [MediaArea] = []
|
|
if case let .draft(draft, _) = actualSubject {
|
|
if draft.values.entities != codableEntities {
|
|
hasEntityChanges = true
|
|
}
|
|
} else {
|
|
mediaAreas = self.initialMediaAreas ?? []
|
|
}
|
|
|
|
var stickers: [TelegramMediaFile] = []
|
|
for entity in codableEntities {
|
|
switch entity {
|
|
case let .sticker(stickerEntity):
|
|
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
|
stickers.append(file.media)
|
|
}
|
|
case let .text(textEntity):
|
|
if let subEntities = textEntity.renderSubEntities {
|
|
for entity in subEntities {
|
|
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
|
stickers.append(file.media)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
if let mediaArea = entity.mediaArea {
|
|
mediaAreas.append(mediaArea)
|
|
}
|
|
}
|
|
|
|
var hasAnyChanges = self.node.hasAnyChanges
|
|
if self.isEditingStoryCover {
|
|
hasAnyChanges = false
|
|
}
|
|
|
|
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
|
|
self.saveDraft(id: randomId, isEdit: true)
|
|
|
|
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
|
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
|
self?.dismiss()
|
|
Queue.mainQueue().justDispatch {
|
|
finished()
|
|
}
|
|
})
|
|
})
|
|
return
|
|
}
|
|
|
|
if !(self.isEditingStory || self.isEditingStoryCover) {
|
|
let privacy = self.state.privacy
|
|
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
|
if let current {
|
|
return current.withUpdatedPrivacy(privacy)
|
|
} else {
|
|
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
|
}
|
|
}).start()
|
|
}
|
|
|
|
if mediaEditor.resultIsVideo {
|
|
self.saveDraft(id: randomId)
|
|
|
|
var firstFrame: Signal<(UIImage?, UIImage?), NoError>
|
|
let firstFrameTime: CMTime
|
|
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
|
|
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
|
} else {
|
|
firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
|
}
|
|
let videoResult: Signal<MediaResult.VideoResult, NoError>
|
|
var videoIsMirrored = false
|
|
let duration: Double
|
|
switch subject {
|
|
case let .empty(dimensions):
|
|
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
})!
|
|
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
|
if let data = image.jpegData(compressionQuality: 0.85) {
|
|
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
|
}
|
|
videoResult = .single(.imageFile(path: tempImagePath))
|
|
duration = 3.0
|
|
|
|
firstFrame = .single((image, nil))
|
|
case let .image(image, _, _, _):
|
|
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
|
if let data = image.jpegData(compressionQuality: 0.85) {
|
|
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
|
}
|
|
videoResult = .single(.imageFile(path: tempImagePath))
|
|
duration = 5.0
|
|
|
|
firstFrame = .single((image, nil))
|
|
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _):
|
|
videoIsMirrored = mirror
|
|
videoResult = .single(.videoFile(path: path))
|
|
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
|
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
|
} else {
|
|
duration = durationValue
|
|
}
|
|
|
|
var additionalPath = additionalPath
|
|
if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
|
additionalPath = valuesAdditionalPath
|
|
}
|
|
|
|
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
|
let avAsset = AVURLAsset(url: URL(fileURLWithPath: path))
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
|
if let cgImage {
|
|
if let additionalPath {
|
|
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
|
if let additionalCGImage {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
|
subscriber.putCompletion()
|
|
} else {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
})
|
|
} else {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
}
|
|
})
|
|
return ActionDisposable {
|
|
avAssetGenerator.cancelAllCGImageGeneration()
|
|
}
|
|
}
|
|
case let .videoCollage(items):
|
|
var maxDurationItem: (Double, Subject.VideoCollageItem)?
|
|
for item in items {
|
|
switch item.content {
|
|
case .image:
|
|
break
|
|
case let .video(_, duration):
|
|
if let (maxDuration, _) = maxDurationItem {
|
|
if duration > maxDuration {
|
|
maxDurationItem = (duration, item)
|
|
}
|
|
} else {
|
|
maxDurationItem = (duration, item)
|
|
}
|
|
case let .asset(asset):
|
|
if let (maxDuration, _) = maxDurationItem {
|
|
if asset.duration > maxDuration {
|
|
maxDurationItem = (asset.duration, item)
|
|
}
|
|
} else {
|
|
maxDurationItem = (asset.duration, item)
|
|
}
|
|
}
|
|
}
|
|
guard let (maxDuration, mainItem) = maxDurationItem else {
|
|
fatalError()
|
|
}
|
|
switch mainItem.content {
|
|
case let .video(path, _):
|
|
videoResult = .single(.videoFile(path: path))
|
|
case let .asset(asset):
|
|
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
|
default:
|
|
fatalError()
|
|
}
|
|
let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
})!
|
|
firstFrame = .single((image, nil))
|
|
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
|
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
|
} else {
|
|
duration = min(maxDuration, storyMaxVideoDuration)
|
|
}
|
|
case let .asset(asset):
|
|
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
|
|
if asset.mediaType == .video {
|
|
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
|
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
|
} else {
|
|
duration = min(asset.duration, storyMaxVideoDuration)
|
|
}
|
|
} else {
|
|
duration = 5.0
|
|
}
|
|
|
|
var additionalPath: String?
|
|
if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
|
|
additionalPath = valuesAdditionalPath
|
|
}
|
|
|
|
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
|
if asset.mediaType == .video {
|
|
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
|
if let avAsset {
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
|
if let cgImage {
|
|
if let additionalPath {
|
|
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
|
if let additionalCGImage {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
|
|
subscriber.putCompletion()
|
|
} else {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
})
|
|
} else {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
} else {
|
|
let options = PHImageRequestOptions()
|
|
options.deliveryMode = .highQualityFormat
|
|
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
|
if let image {
|
|
if let additionalPath {
|
|
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
|
|
if let additionalCGImage {
|
|
subscriber.putNext((image, UIImage(cgImage: additionalCGImage)))
|
|
subscriber.putCompletion()
|
|
} else {
|
|
subscriber.putNext((image, nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
})
|
|
} else {
|
|
subscriber.putNext((image, nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return EmptyDisposable
|
|
}
|
|
case let .draft(draft, _):
|
|
let draftPath = draft.fullPath(engine: context.engine)
|
|
if draft.isVideo {
|
|
videoResult = .single(.videoFile(path: draftPath))
|
|
if let videoTrimRange = mediaEditor.values.videoTrimRange {
|
|
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
|
} else {
|
|
duration = min(draft.duration ?? 5.0, storyMaxVideoDuration)
|
|
}
|
|
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
|
|
let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath))
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
|
|
if let cgImage {
|
|
subscriber.putNext((UIImage(cgImage: cgImage), nil))
|
|
subscriber.putCompletion()
|
|
}
|
|
})
|
|
return ActionDisposable {
|
|
avAssetGenerator.cancelAllCGImageGeneration()
|
|
}
|
|
}
|
|
} else {
|
|
videoResult = .single(.imageFile(path: draftPath))
|
|
duration = 5.0
|
|
|
|
if let image = UIImage(contentsOfFile: draftPath) {
|
|
firstFrame = .single((image, nil))
|
|
} else {
|
|
firstFrame = .single((UIImage(), nil))
|
|
}
|
|
}
|
|
case .message, .gift:
|
|
let peerId: EnginePeer.Id
|
|
if case let .message(messageIds) = subject {
|
|
peerId = messageIds.first!.peerId
|
|
} else {
|
|
peerId = self.context.account.peerId
|
|
}
|
|
|
|
let isNightTheme = mediaEditor.values.nightTheme
|
|
let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId)
|
|
|> map { _, image, nightImage -> UIImage? in
|
|
if isNightTheme {
|
|
return nightImage ?? image
|
|
} else {
|
|
return image
|
|
}
|
|
}
|
|
|
|
videoResult = wallpaper
|
|
|> mapToSignal { image in
|
|
if let image {
|
|
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
|
|
if let data = image.jpegData(compressionQuality: 0.85) {
|
|
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
|
}
|
|
return .single(.imageFile(path: tempImagePath))
|
|
} else {
|
|
return .complete()
|
|
}
|
|
}
|
|
|
|
firstFrame = wallpaper
|
|
|> map { image in
|
|
return (image, nil)
|
|
}
|
|
duration = 5.0
|
|
case .sticker:
|
|
let image = generateImage(storyDimensions, contextGenerator: { size, context in
|
|
context.clear(CGRect(origin: .zero, size: size))
|
|
}, opaque: false, scale: 1.0)
|
|
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
|
|
if let data = image?.pngData() {
|
|
try? data.write(to: URL(fileURLWithPath: tempImagePath))
|
|
}
|
|
videoResult = .single(.imageFile(path: tempImagePath))
|
|
duration = 3.0
|
|
|
|
firstFrame = .single((image, nil))
|
|
case .assets:
|
|
fatalError()
|
|
}
|
|
|
|
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
|
|
.start(next: { [weak self] images, videoResult in
|
|
if let self {
|
|
let (image, additionalImage) = images
|
|
var currentImage = mediaEditor.resultImage
|
|
if let image {
|
|
mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true)
|
|
if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) {
|
|
currentImage = updatedImage
|
|
}
|
|
}
|
|
|
|
var inputImage: UIImage
|
|
if let currentImage {
|
|
inputImage = currentImage
|
|
} else if let image {
|
|
inputImage = image
|
|
} else {
|
|
inputImage = UIImage()
|
|
}
|
|
|
|
var values = mediaEditor.values
|
|
if case .avatarEditor = self.mode, values.videoTrimRange == nil && duration > avatarMaxVideoDuration {
|
|
values = values.withUpdatedVideoTrimRange(0 ..< avatarMaxVideoDuration)
|
|
}
|
|
|
|
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in
|
|
if let self {
|
|
self.willComplete(coverImage, true, { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
|
|
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
|
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
|
self?.dismiss()
|
|
Queue.mainQueue().justDispatch {
|
|
finished()
|
|
}
|
|
})
|
|
})
|
|
})
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
if case let .draft(draft, id) = actualSubject, id == nil {
|
|
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
|
|
}
|
|
} else if let image = mediaEditor.resultImage {
|
|
self.saveDraft(id: randomId)
|
|
|
|
var values = mediaEditor.values
|
|
var outputDimensions: CGSize?
|
|
if case .avatarEditor = self.mode {
|
|
outputDimensions = CGSize(width: 640.0, height: 640.0)
|
|
values = values.withUpdatedQualityPreset(.profile)
|
|
}
|
|
makeEditorImageComposition(
|
|
context: self.node.ciContext,
|
|
postbox: self.context.account.postbox,
|
|
inputImage: image,
|
|
dimensions: storyDimensions,
|
|
outputDimensions: outputDimensions,
|
|
values: values,
|
|
time: .zero,
|
|
textScale: 2.0,
|
|
completion: { [weak self] resultImage in
|
|
if let self, let resultImage {
|
|
self.willComplete(resultImage, false, { [weak self] in
|
|
guard let self else {
|
|
return
|
|
}
|
|
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
|
|
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
|
|
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
|
self?.dismiss()
|
|
Queue.mainQueue().justDispatch {
|
|
finished()
|
|
}
|
|
})
|
|
})
|
|
if case let .draft(draft, id) = actualSubject, id == nil {
|
|
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) {
|
|
guard !items.isEmpty else {
|
|
return
|
|
}
|
|
|
|
var items = items
|
|
if !isLongVideo, let mediaEditor = self.node.mediaEditor, case let .asset(asset) = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) {
|
|
var updatedCurrentItem = items[currentItemIndex]
|
|
updatedCurrentItem.caption = self.node.getCaption()
|
|
updatedCurrentItem.values = mediaEditor.values
|
|
items[currentItemIndex] = updatedCurrentItem
|
|
}
|
|
|
|
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
|
|
let totalItems = items.count
|
|
|
|
let dispatchGroup = DispatchGroup()
|
|
|
|
let privacy = self.state.privacy
|
|
|
|
if !(self.isEditingStory || self.isEditingStoryCover) {
|
|
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
|
|
if let current {
|
|
return current.withUpdatedPrivacy(privacy)
|
|
} else {
|
|
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
|
|
}
|
|
}).start()
|
|
}
|
|
|
|
var order: [Int64] = []
|
|
for (index, item) in items.enumerated() {
|
|
guard item.isEnabled else {
|
|
continue
|
|
}
|
|
|
|
dispatchGroup.enter()
|
|
|
|
let randomId = Int64.random(in: .min ... .max)
|
|
order.append(randomId)
|
|
|
|
if item.asset.mediaType == .video {
|
|
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
|
|
let _ = multipleResults.modify { results in
|
|
var updatedResults = results
|
|
updatedResults.append(result)
|
|
return updatedResults
|
|
}
|
|
|
|
dispatchGroup.leave()
|
|
}
|
|
} else if item.asset.mediaType == .image {
|
|
processImageItem(item: item, index: index, randomId: randomId) { result in
|
|
let _ = multipleResults.modify { results in
|
|
var updatedResults = results
|
|
updatedResults.append(result)
|
|
return updatedResults
|
|
}
|
|
|
|
dispatchGroup.leave()
|
|
}
|
|
} else {
|
|
dispatchGroup.leave()
|
|
}
|
|
}
|
|
|
|
dispatchGroup.notify(queue: .main) {
|
|
let results = multipleResults.with { $0 }
|
|
if results.count == totalItems {
|
|
var orderedResults: [MediaEditorScreenImpl.Result] = []
|
|
for id in order {
|
|
if let item = results.first(where: { $0.randomId == id }) {
|
|
orderedResults.append(item)
|
|
}
|
|
}
|
|
self.completion(orderedResults, { [weak self] finished in
|
|
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
|
|
self?.dismiss()
|
|
Queue.mainQueue().justDispatch {
|
|
finished()
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
|
let asset = item.asset
|
|
|
|
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
|
|
|
var caption = item.caption
|
|
caption = convertMarkdownToAttributes(caption)
|
|
|
|
var mediaAreas: [MediaArea] = []
|
|
var stickers: [TelegramMediaFile] = []
|
|
|
|
if let entities = item.values?.entities {
|
|
for entity in entities {
|
|
if let mediaArea = entity.mediaArea {
|
|
mediaAreas.append(mediaArea)
|
|
}
|
|
extractStickersFromEntity(entity, into: &stickers)
|
|
}
|
|
}
|
|
|
|
let firstFrameTime: CMTime
|
|
if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 {
|
|
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
|
|
} else {
|
|
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
|
|
}
|
|
|
|
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { [weak self] avAsset, _, _ in
|
|
guard let avAsset else {
|
|
DispatchQueue.main.async {
|
|
if let self {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
let duration: Double
|
|
if let videoTrimRange = item.values?.videoTrimRange {
|
|
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
|
|
} else {
|
|
duration = min(asset.duration, storyMaxVideoDuration)
|
|
}
|
|
|
|
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
|
|
avAssetGenerator.appliesPreferredTrackTransform = true
|
|
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
if let cgImage {
|
|
let image = UIImage(cgImage: cgImage)
|
|
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
|
|
|
|
if let resultImage = itemMediaEditor.resultImage {
|
|
makeEditorImageComposition(
|
|
context: self.node.ciContext,
|
|
postbox: self.context.account.postbox,
|
|
inputImage: resultImage,
|
|
dimensions: storyDimensions,
|
|
values: itemMediaEditor.values,
|
|
time: firstFrameTime,
|
|
textScale: 2.0
|
|
) { coverImage in
|
|
if let coverImage = coverImage {
|
|
let result = MediaEditorScreenImpl.Result(
|
|
media: .video(
|
|
video: .asset(localIdentifier: asset.localIdentifier),
|
|
coverImage: coverImage,
|
|
values: itemMediaEditor.values,
|
|
duration: duration,
|
|
dimensions: itemMediaEditor.values.resultDimensions
|
|
),
|
|
mediaAreas: mediaAreas,
|
|
caption: caption,
|
|
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
|
|
options: self.state.privacy,
|
|
stickers: stickers,
|
|
randomId: randomId
|
|
)
|
|
completion(result)
|
|
} else {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
}
|
|
} else {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
} else {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
|
|
let asset = item.asset
|
|
|
|
let itemMediaEditor = setupMediaEditorForItem(item: item)
|
|
|
|
var caption = item.caption
|
|
caption = convertMarkdownToAttributes(caption)
|
|
|
|
var mediaAreas: [MediaArea] = []
|
|
var stickers: [TelegramMediaFile] = []
|
|
|
|
if let entities = item.values?.entities {
|
|
for entity in entities {
|
|
if let mediaArea = entity.mediaArea {
|
|
mediaAreas.append(mediaArea)
|
|
}
|
|
extractStickersFromEntity(entity, into: &stickers)
|
|
}
|
|
}
|
|
|
|
let options = PHImageRequestOptions()
|
|
options.deliveryMode = .highQualityFormat
|
|
options.isNetworkAccessAllowed = true
|
|
|
|
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { [weak self] image, _ in
|
|
guard let self else {
|
|
return
|
|
}
|
|
DispatchQueue.main.async {
|
|
if let image {
|
|
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
|
|
if itemMediaEditor.values.gradientColors == nil {
|
|
itemMediaEditor.setGradientColors(mediaEditorGetGradientColors(from: image))
|
|
}
|
|
|
|
if let resultImage = itemMediaEditor.resultImage {
|
|
makeEditorImageComposition(
|
|
context: self.node.ciContext,
|
|
postbox: self.context.account.postbox,
|
|
inputImage: resultImage,
|
|
dimensions: storyDimensions,
|
|
values: itemMediaEditor.values,
|
|
time: .zero,
|
|
textScale: 2.0
|
|
) { resultImage in
|
|
if let resultImage = resultImage {
|
|
let result = MediaEditorScreenImpl.Result(
|
|
media: .image(
|
|
image: resultImage,
|
|
dimensions: PixelDimensions(resultImage.size)
|
|
),
|
|
mediaAreas: mediaAreas,
|
|
caption: caption,
|
|
coverTimestamp: nil,
|
|
options: self.state.privacy,
|
|
stickers: stickers,
|
|
randomId: randomId
|
|
)
|
|
completion(result)
|
|
} else {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
}
|
|
} else {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
} else {
|
|
completion(self.createEmptyResult(randomId: randomId))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor {
|
|
var values = item.values
|
|
if values?.videoTrimRange == nil {
|
|
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
|
|
}
|
|
return MediaEditor(
|
|
context: self.context,
|
|
mode: .default,
|
|
subject: .asset(item.asset),
|
|
values: values,
|
|
hasHistogram: false,
|
|
isStandalone: true
|
|
)
|
|
}
|
|
|
|
private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) {
|
|
switch entity {
|
|
case let .sticker(stickerEntity):
|
|
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
|
stickers.append(file.media)
|
|
}
|
|
case let .text(textEntity):
|
|
if let subEntities = textEntity.renderSubEntities {
|
|
for entity in subEntities {
|
|
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
|
|
stickers.append(file.media)
|
|
}
|
|
}
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result {
|
|
let emptyImage = UIImage()
|
|
return MediaEditorScreenImpl.Result(
|
|
media: .image(
|
|
image: emptyImage,
|
|
dimensions: PixelDimensions(emptyImage.size)
|
|
),
|
|
mediaAreas: [],
|
|
caption: NSAttributedString(),
|
|
coverTimestamp: nil,
|
|
options: self.state.privacy,
|
|
stickers: [],
|
|
randomId: randomId
|
|
)
|
|
}
|
|
|
|
|
|
|
|
func updateMediaEditorEntities() {
|
|
guard let mediaEditor = self.node.mediaEditor else {
|
|
return
|
|
}
|
|
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
|
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
|
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
|
}
|
|
}
|