mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
[WIP] Multiple story upload
This commit is contained in:
parent
746239cf69
commit
d8dd96e39e
@ -14188,3 +14188,12 @@ Sorry for the inconvenience.";
|
||||
"SendInviteLink.TextCallsRestrictedMultipleUsers_1" = "{user_list}, and **%d** more person do not accept calls.";
|
||||
"SendInviteLink.TextCallsRestrictedMultipleUsers_any" = "{user_list}, and **%d** more people do not accept calls.";
|
||||
"SendInviteLink.TextCallsRestrictedSendInviteLink" = "You can try to send an invite link instead.";
|
||||
|
||||
"Story.Privacy.ShareStories" = "Share Stories";
|
||||
"Story.Privacy.PostStoriesAsHeader" = "POST STORIES AS";
|
||||
"Story.Privacy.WhoCanViewStoriesHeader" = "WHO CAN VIEW THIS STORIES";
|
||||
"Story.Privacy.PostStories_1" = "Post %@ Story";
|
||||
"Story.Privacy.PostStories_any" = "Post %@ Stories";
|
||||
"Story.Privacy.KeepOnMyPageManyInfo" = "Keep these stories on your profile even after they expire in %@. Privacy settings will apply.";
|
||||
"Story.Privacy.KeepOnChannelPageManyInfo" = "Keep these stories on the channel profile even after they expire in %@.";
|
||||
"Story.Privacy.KeepOnGroupPageManyInfo" = "Keep these stories on the group page even after they expire in %@.";
|
||||
|
@ -809,7 +809,7 @@ public protocol MediaEditorScreenResult {
|
||||
public protocol TelegramRootControllerInterface: NavigationController {
|
||||
@discardableResult
|
||||
func openStoryCamera(customTarget: Stories.PendingTarget?, transitionIn: StoryCameraTransitionIn?, transitionedIn: @escaping () -> Void, transitionOut: @escaping (Stories.PendingTarget?, Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator?
|
||||
func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
|
||||
func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void)
|
||||
|
||||
func getContactsController() -> ViewController?
|
||||
func getChatsController() -> ViewController?
|
||||
@ -1152,7 +1152,7 @@ public protocol SharedAccountContext: AnyObject {
|
||||
|
||||
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
|
||||
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
|
||||
|
||||
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
|
||||
|
||||
|
@ -42,6 +42,7 @@ public struct AttachmentMainButtonState {
|
||||
public let isEnabled: Bool
|
||||
public let hasShimmer: Bool
|
||||
public let iconName: String?
|
||||
public let smallSpacing: Bool
|
||||
public let position: Position?
|
||||
|
||||
public init(
|
||||
@ -55,6 +56,7 @@ public struct AttachmentMainButtonState {
|
||||
isEnabled: Bool,
|
||||
hasShimmer: Bool,
|
||||
iconName: String? = nil,
|
||||
smallSpacing: Bool = false,
|
||||
position: Position? = nil
|
||||
) {
|
||||
self.text = text
|
||||
@ -67,6 +69,7 @@ public struct AttachmentMainButtonState {
|
||||
self.isEnabled = isEnabled
|
||||
self.hasShimmer = hasShimmer
|
||||
self.iconName = iconName
|
||||
self.smallSpacing = smallSpacing
|
||||
self.position = position
|
||||
}
|
||||
|
||||
|
@ -790,6 +790,7 @@ private final class MainButtonNode: HighlightTrackingButtonNode {
|
||||
iconNode = ASImageNode()
|
||||
iconNode.displaysAsynchronously = false
|
||||
iconNode.image = generateTintedImage(image: UIImage(bundleImageName: iconName), color: state.textColor)
|
||||
self.iconNode = iconNode
|
||||
self.addSubnode(iconNode)
|
||||
}
|
||||
if let iconSize = iconNode.image?.size {
|
||||
@ -1806,7 +1807,9 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
} else {
|
||||
height = bounds.height + 8.0
|
||||
}
|
||||
if !isNarrowButton {
|
||||
if isTwoVerticalButtons && self.secondaryButtonState.smallSpacing {
|
||||
|
||||
} else if !isNarrowButton {
|
||||
height += 9.0
|
||||
}
|
||||
if isTwoVerticalButtons {
|
||||
@ -1896,7 +1899,8 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate {
|
||||
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize)
|
||||
case .bottom:
|
||||
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize)
|
||||
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + sideInset + buttonSize.height), size: buttonSize)
|
||||
let buttonSpacing = self.secondaryButtonState.smallSpacing ? 8.0 : sideInset
|
||||
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY + buttonSpacing + buttonSize.height), size: buttonSize)
|
||||
case .left:
|
||||
secondaryButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX, y: buttonOriginY), size: buttonSize)
|
||||
mainButtonFrame = CGRect(origin: CGPoint(x: buttonOriginX + buttonSize.width + sideInset, y: buttonOriginY), size: buttonSize)
|
||||
|
@ -566,6 +566,14 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
||||
self.hasSelectionChanged(false)
|
||||
}
|
||||
|
||||
public func clearAll() {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
view.reset()
|
||||
view.selectionView?.removeFromSuperview()
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
private func clear(animated: Bool = false) {
|
||||
if animated {
|
||||
for case let view as DrawingEntityView in self.subviews {
|
||||
|
@ -184,7 +184,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
private let bannedSendVideos: (Int32, Bool)?
|
||||
private let canBoostToUnrestrict: Bool
|
||||
fileprivate let paidMediaAllowed: Bool
|
||||
private let subject: Subject
|
||||
fileprivate let subject: Subject
|
||||
fileprivate let forCollage: Bool
|
||||
private let saveEditedPhotos: Bool
|
||||
|
||||
@ -1826,6 +1826,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
fileprivate let secondaryButtonStatePromise = Promise<AttachmentMainButtonState?>(nil)
|
||||
|
||||
private let mainButtonAction: (() -> Void)?
|
||||
private let secondaryButtonAction: (() -> Void)?
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
@ -1845,7 +1846,8 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
selectionContext: TGMediaSelectionContext? = nil,
|
||||
saveEditedPhotos: Bool = false,
|
||||
mainButtonState: AttachmentMainButtonState? = nil,
|
||||
mainButtonAction: (() -> Void)? = nil
|
||||
mainButtonAction: (() -> Void)? = nil,
|
||||
secondaryButtonAction: (() -> Void)? = nil
|
||||
) {
|
||||
self.context = context
|
||||
|
||||
@ -1865,6 +1867,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
self.saveEditedPhotos = saveEditedPhotos
|
||||
self.mainButtonStatePromise.set(.single(mainButtonState))
|
||||
self.mainButtonAction = mainButtonAction
|
||||
self.secondaryButtonAction = secondaryButtonAction
|
||||
|
||||
let selectionContext = selectionContext ?? TGMediaSelectionContext()
|
||||
|
||||
@ -1998,7 +2001,14 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
} else if collection == nil {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
|
||||
var hasSelect = false
|
||||
if forCollage {
|
||||
hasSelect = true
|
||||
} else if case .story = mode {
|
||||
hasSelect = true
|
||||
}
|
||||
|
||||
if hasSelect {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Select, target: self, action: #selector(self.selectPressed))
|
||||
} else {
|
||||
if [.createSticker].contains(mode) {
|
||||
@ -2338,6 +2348,9 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
let transition = ContainedViewLayoutTransition.animated(duration: 0.25, curve: .easeInOut)
|
||||
var moreIsVisible = false
|
||||
if case let .assets(_, mode) = self.subject, [.story, .createSticker].contains(mode) {
|
||||
if count == 1 {
|
||||
self.requestAttachmentMenuExpansion()
|
||||
}
|
||||
moreIsVisible = true
|
||||
} else if case let .media(media) = self.subject {
|
||||
self.titleView.title = media.count == 1 ? self.presentationData.strings.Attachment_Pasteboard : self.presentationData.strings.Attachment_SelectedMedia(count)
|
||||
@ -2381,7 +2394,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
transition.updateAlpha(node: self.moreButtonNode.iconNode, alpha: moreIsVisible ? 1.0 : 0.0)
|
||||
transition.updateTransformScale(node: self.moreButtonNode.iconNode, scale: moreIsVisible ? 1.0 : 0.1)
|
||||
|
||||
if self.selectionCount > 0 {
|
||||
if case .assets(_, .story) = self.subject, self.selectionCount > 0 {
|
||||
//TODO:localize
|
||||
var text = "Create 1 Story"
|
||||
if self.selectionCount > 1 {
|
||||
@ -2390,7 +2403,7 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
self.mainButtonStatePromise.set(.single(AttachmentMainButtonState(text: text, badge: nil, font: .bold, background: .color(self.presentationData.theme.actionSheet.controlAccentColor), textColor: self.presentationData.theme.list.itemCheckColors.foregroundColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, position: .top)))
|
||||
|
||||
if self.selectionCount > 1 && self.selectionCount <= 6 {
|
||||
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", position: .bottom)))
|
||||
self.secondaryButtonStatePromise.set(.single(AttachmentMainButtonState(text: "Combine into Collage", badge: nil, font: .regular, background: .color(.clear), textColor: self.presentationData.theme.actionSheet.controlAccentColor, isVisible: true, progress: .none, isEnabled: true, hasShimmer: false, iconName: "Media Editor/Collage", smallSpacing: true, position: .bottom)))
|
||||
} else {
|
||||
self.secondaryButtonStatePromise.set(.single(nil))
|
||||
}
|
||||
@ -2427,6 +2440,10 @@ public final class MediaPickerScreenImpl: ViewController, MediaPickerScreen, Att
|
||||
self.mainButtonAction?()
|
||||
}
|
||||
|
||||
func secondaryButtonPressed() {
|
||||
self.secondaryButtonAction?()
|
||||
}
|
||||
|
||||
func dismissAllTooltips() {
|
||||
self.undoOverlayController?.dismissWithCommitAction()
|
||||
}
|
||||
@ -2810,7 +2827,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||
private weak var controller: MediaPickerScreenImpl?
|
||||
|
||||
var selectionCount: Signal<Int, NoError> {
|
||||
if self.controller?.forCollage == true {
|
||||
if let controller = self.controller, case .assets(_, .story) = controller.subject {
|
||||
return .single(0)
|
||||
} else {
|
||||
return Signal { [weak self] subscriber in
|
||||
@ -2973,7 +2990,7 @@ final class MediaPickerContext: AttachmentMediaPickerContext {
|
||||
}
|
||||
|
||||
func secondaryButtonAction() {
|
||||
self.controller?.mainButtonPressed()
|
||||
self.controller?.secondaryButtonPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@ -3162,7 +3179,7 @@ public func storyMediaPickerController(
|
||||
selectionLimit: Int?,
|
||||
getSourceRect: @escaping () -> CGRect,
|
||||
completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void,
|
||||
multipleCompletion: @escaping ([Any]) -> Void,
|
||||
multipleCompletion: @escaping ([Any], Bool) -> Void,
|
||||
dismissed: @escaping () -> Void,
|
||||
groupsPresented: @escaping () -> Void
|
||||
) -> ViewController {
|
||||
@ -3181,9 +3198,18 @@ public func storyMediaPickerController(
|
||||
}
|
||||
}
|
||||
|
||||
let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: {
|
||||
return nil
|
||||
})
|
||||
let controller = AttachmentController(
|
||||
context: context,
|
||||
updatedPresentationData: updatedPresentationData,
|
||||
chatLocation: nil,
|
||||
buttons: [.standalone],
|
||||
initialButton: .standalone,
|
||||
fromMenu: false,
|
||||
hasTextInput: false,
|
||||
makeEntityInputView: {
|
||||
return nil
|
||||
}
|
||||
)
|
||||
controller.forceSourceRect = true
|
||||
controller.getSourceRect = getSourceRect
|
||||
controller.requestController = { _, present in
|
||||
@ -3207,7 +3233,18 @@ public func storyMediaPickerController(
|
||||
results.append(asset)
|
||||
}
|
||||
}
|
||||
multipleCompletion(results)
|
||||
multipleCompletion(results, false)
|
||||
}
|
||||
},
|
||||
secondaryButtonAction: { [weak selectionContext] in
|
||||
if let selectionContext, let selectedItems = selectionContext.selectedItems() {
|
||||
var results: [Any] = []
|
||||
for item in selectedItems {
|
||||
if let item = item as? TGMediaAsset, let asset = item.backingAsset {
|
||||
results.append(asset)
|
||||
}
|
||||
}
|
||||
multipleCompletion(results, true)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -1651,6 +1651,7 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
case videoCollage(VideoCollage)
|
||||
case asset(PHAsset)
|
||||
case draft(MediaEditorDraft)
|
||||
case assets([PHAsset])
|
||||
|
||||
func withPIPPosition(_ position: CameraScreenImpl.PIPPosition) -> Result {
|
||||
switch self {
|
||||
@ -3637,11 +3638,10 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
selectionLimit = 10
|
||||
}
|
||||
}
|
||||
//TODO:unmock
|
||||
controller = self.context.sharedContext.makeStoryMediaPickerScreen(
|
||||
context: self.context,
|
||||
isDark: true,
|
||||
forCollage: self.cameraState.isCollageEnabled || "".isEmpty,
|
||||
forCollage: self.cameraState.isCollageEnabled,
|
||||
selectionLimit: selectionLimit,
|
||||
getSourceRect: { [weak self] in
|
||||
if let self {
|
||||
@ -3707,44 +3707,52 @@ public class CameraScreenImpl: ViewController, CameraScreen {
|
||||
}
|
||||
}
|
||||
}
|
||||
}, multipleCompletion: { [weak self] results in
|
||||
}, multipleCompletion: { [weak self] results, collage in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
if !self.cameraState.isCollageEnabled {
|
||||
var selectedGrid: Camera.CollageGrid = collageGrids.first!
|
||||
for grid in collageGrids {
|
||||
if grid.count == results.count {
|
||||
selectedGrid = grid
|
||||
break
|
||||
if collage {
|
||||
if !self.cameraState.isCollageEnabled {
|
||||
var selectedGrid: Camera.CollageGrid = collageGrids.first!
|
||||
for grid in collageGrids {
|
||||
if grid.count == results.count {
|
||||
selectedGrid = grid
|
||||
break
|
||||
}
|
||||
}
|
||||
self.updateCameraState({
|
||||
$0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid)
|
||||
}, transition: .spring(duration: 0.3))
|
||||
}
|
||||
self.updateCameraState({
|
||||
$0.updatedIsCollageEnabled(true).updatedCollageProgress(0.0).updatedIsDualCameraEnabled(false).updatedCollageGrid(selectedGrid)
|
||||
}, transition: .spring(duration: 0.3))
|
||||
}
|
||||
|
||||
if let assets = results as? [PHAsset] {
|
||||
var results: [Signal<CameraScreenImpl.Result, NoError>] = []
|
||||
for asset in assets {
|
||||
if asset.mediaType == .video && asset.duration > 1.0 {
|
||||
results.append(.single(.asset(asset)))
|
||||
} else {
|
||||
results.append(
|
||||
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
|
||||
|> runOn(Queue.concurrentDefaultQueue())
|
||||
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
|
||||
if let image {
|
||||
return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
|
||||
} else {
|
||||
return .complete()
|
||||
if let assets = results as? [PHAsset] {
|
||||
var results: [Signal<CameraScreenImpl.Result, NoError>] = []
|
||||
for asset in assets {
|
||||
if asset.mediaType == .video && asset.duration > 1.0 {
|
||||
results.append(.single(.asset(asset)))
|
||||
} else {
|
||||
results.append(
|
||||
assetImage(asset: asset, targetSize: CGSize(width: 1080, height: 1080), exact: false, deliveryMode: .highQualityFormat)
|
||||
|> runOn(Queue.concurrentDefaultQueue())
|
||||
|> mapToSignal { image -> Signal<CameraScreenImpl.Result, NoError> in
|
||||
if let image {
|
||||
return .single(.image(Result.Image(image: image, additionalImage: nil, additionalImagePosition: .topLeft)))
|
||||
} else {
|
||||
return .complete()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
self.node.collage?.addResults(signals: results)
|
||||
}
|
||||
} else {
|
||||
if let assets = results as? [PHAsset] {
|
||||
self.completion(.single(.assets(assets)), nil, {
|
||||
|
||||
})
|
||||
}
|
||||
self.node.collage?.addResults(signals: results)
|
||||
}
|
||||
self.galleryController = nil
|
||||
|
||||
|
@ -65,6 +65,8 @@ swift_library(
|
||||
"//submodules/UrlEscaping",
|
||||
"//submodules/DeviceLocationManager",
|
||||
"//submodules/TelegramUI/Components/SaveProgressScreen",
|
||||
"//submodules/TelegramUI/Components/MediaAssetsContext",
|
||||
"//submodules/CheckNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -160,7 +160,7 @@ public extension MediaEditorScreenImpl {
|
||||
} else {
|
||||
existingMedia = storyItem.media
|
||||
}
|
||||
rootController.proceedWithStoryUpload(target: target, result: result as! MediaEditorScreenResult, existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
|
||||
rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
|
@ -327,6 +327,7 @@ final class MediaEditorScreenComponent: Component {
|
||||
private let switchCameraButton = ComponentView<Empty>()
|
||||
|
||||
private let selectionButton = ComponentView<Empty>()
|
||||
private let selectionPanel = ComponentView<Empty>()
|
||||
|
||||
private let textCancelButton = ComponentView<Empty>()
|
||||
private let textDoneButton = ComponentView<Empty>()
|
||||
@ -741,6 +742,13 @@ final class MediaEditorScreenComponent: Component {
|
||||
return inputText
|
||||
}
|
||||
|
||||
func setInputText(_ text: NSAttributedString) {
|
||||
guard let inputPanelView = self.inputPanel.view as? MessageInputPanelComponent.View else {
|
||||
return
|
||||
}
|
||||
inputPanelView.setSendMessageInput(value: .text(text), updateState: true)
|
||||
}
|
||||
|
||||
private func updateCoverPosition() {
|
||||
guard let controller = self.environment?.controller() as? MediaEditorScreenImpl, let mediaEditor = controller.node.mediaEditor else {
|
||||
return
|
||||
@ -1993,39 +2001,118 @@ final class MediaEditorScreenComponent: Component {
|
||||
transition.setAlpha(view: switchCameraButtonView, alpha: isRecordingAdditionalVideo ? 1.0 : 0.0)
|
||||
}
|
||||
|
||||
if controller.node.items.count > 1 {
|
||||
let selectionButtonSize = self.selectionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
SelectionPanelButtonContentComponent(
|
||||
count: Int32(controller.node.items.count(where: { $0.isEnabled })),
|
||||
isSelected: self.isSelectionPanelOpen,
|
||||
tag: nil
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
|
||||
self.state?.updated()
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
let selectionButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0),
|
||||
size: selectionButtonSize
|
||||
)
|
||||
if let selectionButtonView = self.selectionButton.view as? PlainButtonComponent.View {
|
||||
if selectionButtonView.superview == nil {
|
||||
self.addSubview(selectionButtonView)
|
||||
}
|
||||
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
|
||||
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
|
||||
transition.setScale(view: selectionButtonView, scale: displayTopButtons ? 1.0 : 0.01)
|
||||
transition.setAlpha(view: selectionButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0)
|
||||
|
||||
let selectionButtonSize = self.selectionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(PlainButtonComponent(
|
||||
content: AnyComponent(
|
||||
SelectionPanelButtonContentComponent(
|
||||
count: 1,
|
||||
isSelected: self.isSelectionPanelOpen,
|
||||
tag: nil
|
||||
if self.isSelectionPanelOpen {
|
||||
let selectionPanelFrame = CGRect(
|
||||
origin: CGPoint(x: 12.0, y: inputPanelFrame.minY - selectionButtonSize.height - 3.0 - 130.0),
|
||||
size: CGSize(width: availableSize.width - 24.0, height: 120.0)
|
||||
)
|
||||
),
|
||||
effectAlignment: .center,
|
||||
action: { [weak self] in
|
||||
if let self {
|
||||
self.isSelectionPanelOpen = !self.isSelectionPanelOpen
|
||||
self.state?.updated()
|
||||
|
||||
var selectedItemId = ""
|
||||
if case let .asset(asset) = controller.node.subject {
|
||||
selectedItemId = asset.localIdentifier
|
||||
}
|
||||
},
|
||||
animateAlpha: false
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
let selectionButtonFrame = CGRect(
|
||||
origin: CGPoint(x: availableSize.width - selectionButtonSize.width - 12.0, y: max(environment.statusBarHeight + 10.0, inputPanelFrame.minY - selectionButtonSize.height - 3.0)),
|
||||
size: selectionButtonSize
|
||||
)
|
||||
if let selectionButtonView = self.selectionButton.view {
|
||||
if selectionButtonView.superview == nil {
|
||||
self.addSubview(selectionButtonView)
|
||||
|
||||
let _ = self.selectionPanel.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
SelectionPanelComponent(
|
||||
previewContainerView: controller.node.previewContentContainerView,
|
||||
frame: selectionPanelFrame,
|
||||
items: controller.node.items,
|
||||
selectedItemId: selectedItemId,
|
||||
itemTapped: { [weak self, weak controller] id in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
self.isSelectionPanelOpen = false
|
||||
self.state?.updated()
|
||||
|
||||
if let id {
|
||||
controller.node.switchToItem(id)
|
||||
}
|
||||
},
|
||||
itemSelectionToggled: { [weak self, weak controller] id in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
if let itemIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == id }) {
|
||||
controller.node.items[itemIndex].isEnabled = !controller.node.items[itemIndex].isEnabled
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
},
|
||||
itemReordered: { [weak self, weak controller] fromId, toId in
|
||||
guard let self, let controller else {
|
||||
return
|
||||
}
|
||||
guard let fromIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == fromId }), let toIndex = controller.node.items.firstIndex(where: { $0.asset.localIdentifier == toId }), toIndex < controller.node.items.count else {
|
||||
return
|
||||
}
|
||||
let fromItem = controller.node.items[fromIndex]
|
||||
let toItem = controller.node.items[toIndex]
|
||||
controller.node.items[fromIndex] = toItem
|
||||
controller.node.items[toIndex] = fromItem
|
||||
self.state?.updated(transition: .spring(duration: 0.3))
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if selectionPanelView.superview == nil {
|
||||
self.insertSubview(selectionPanelView, belowSubview: selectionButtonView)
|
||||
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateIn(from: buttonView)
|
||||
}
|
||||
}
|
||||
selectionPanelView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
}
|
||||
} else if let selectionPanelView = self.selectionPanel.view as? SelectionPanelComponent.View {
|
||||
if let buttonView = selectionButtonView.contentView as? SelectionPanelButtonContentComponent.View {
|
||||
selectionPanelView.animateOut(to: buttonView, completion: { [weak selectionPanelView] in
|
||||
selectionPanelView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
selectionPanelView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
transition.setPosition(view: selectionButtonView, position: selectionButtonFrame.center)
|
||||
transition.setBounds(view: selectionButtonView, bounds: CGRect(origin: .zero, size: selectionButtonFrame.size))
|
||||
}
|
||||
} else {
|
||||
inputPanelSize = CGSize(width: 0.0, height: 12.0)
|
||||
@ -2795,6 +2882,38 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
}
|
||||
|
||||
struct EditingItem: Equatable {
|
||||
let asset: PHAsset
|
||||
var values: MediaEditorValues?
|
||||
var caption = NSAttributedString()
|
||||
var thumbnail: UIImage?
|
||||
var isEnabled = true
|
||||
var version: Int = 0
|
||||
|
||||
init(asset: PHAsset) {
|
||||
self.asset = asset
|
||||
}
|
||||
|
||||
public static func ==(lhs: EditingItem, rhs: EditingItem) -> Bool {
|
||||
if lhs.asset.localIdentifier != rhs.asset.localIdentifier {
|
||||
return false
|
||||
}
|
||||
if lhs.values != rhs.values {
|
||||
return false
|
||||
}
|
||||
if lhs.caption != rhs.caption {
|
||||
return false
|
||||
}
|
||||
if lhs.thumbnail != rhs.thumbnail {
|
||||
return false
|
||||
}
|
||||
if lhs.version != rhs.version {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate, UIScrollViewDelegate {
|
||||
private weak var controller: MediaEditorScreenImpl?
|
||||
private let context: AccountContext
|
||||
@ -2803,6 +2922,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
var subject: MediaEditorScreenImpl.Subject?
|
||||
var actualSubject: MediaEditorScreenImpl.Subject?
|
||||
var items: [EditingItem] = []
|
||||
|
||||
private var subjectDisposable: Disposable?
|
||||
private var appInForegroundDisposable: Disposable?
|
||||
@ -2891,6 +3011,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
private let readyValue = Promise<Bool>()
|
||||
|
||||
var componentHostView: MediaEditorScreenComponent.View? {
|
||||
return self.componentHost.view as? MediaEditorScreenComponent.View
|
||||
}
|
||||
|
||||
init(controller: MediaEditorScreenImpl) {
|
||||
self.controller = controller
|
||||
self.context = controller.context
|
||||
@ -3062,7 +3186,46 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|> deliverOnMainQueue
|
||||
).start(next: { [weak self] subject in
|
||||
if let self, let subject {
|
||||
self.setup(with: subject)
|
||||
self.actualSubject = subject
|
||||
|
||||
var effectiveSubject = subject
|
||||
switch subject {
|
||||
case let .assets(assets):
|
||||
effectiveSubject = .asset(assets.first!)
|
||||
self.items = assets.map { EditingItem(asset: $0) }
|
||||
case let .draft(draft, _):
|
||||
for entity in draft.values.entities {
|
||||
if case let .sticker(sticker) = entity {
|
||||
switch sticker.content {
|
||||
case let .message(ids, _, _, _, _):
|
||||
effectiveSubject = .message(ids)
|
||||
case let .gift(gift, _):
|
||||
effectiveSubject = .gift(gift)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
var privacy: MediaEditorResultPrivacy?
|
||||
var values: MediaEditorValues?
|
||||
var isDraft = false
|
||||
if case let .draft(draft, _) = subject {
|
||||
privacy = draft.privacy
|
||||
values = draft.values
|
||||
isDraft = true
|
||||
}
|
||||
|
||||
self.setup(
|
||||
subject: effectiveSubject,
|
||||
privacy: privacy,
|
||||
values: values,
|
||||
caption: nil,
|
||||
isDraft: isDraft
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
@ -3183,35 +3346,24 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.stickerCutoutStatusDisposable?.dispose()
|
||||
}
|
||||
|
||||
private func setup(with subject: MediaEditorScreenImpl.Subject) {
|
||||
func setup(
|
||||
subject: MediaEditorScreenImpl.Subject,
|
||||
privacy: MediaEditorResultPrivacy? = nil,
|
||||
values: MediaEditorValues?,
|
||||
caption: NSAttributedString?,
|
||||
isDraft: Bool = false
|
||||
) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
|
||||
self.actualSubject = subject
|
||||
|
||||
var effectiveSubject = subject
|
||||
if case let .draft(draft, _ ) = subject {
|
||||
for entity in draft.values.entities {
|
||||
if case let .sticker(sticker) = entity {
|
||||
switch sticker.content {
|
||||
case let .message(ids, _, _, _, _):
|
||||
effectiveSubject = .message(ids)
|
||||
case let .gift(gift, _):
|
||||
effectiveSubject = .gift(gift)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.subject = effectiveSubject
|
||||
self.subject = subject
|
||||
|
||||
Queue.mainQueue().justDispatch {
|
||||
controller.setupAudioSessionIfNeeded()
|
||||
}
|
||||
|
||||
if case let .draft(draft, _) = subject, let privacy = draft.privacy {
|
||||
if let privacy {
|
||||
controller.state.privacy = privacy
|
||||
}
|
||||
|
||||
@ -3229,7 +3381,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
controller.isSavingAvailable = isSavingAvailable
|
||||
controller.requestLayout(transition: .immediate)
|
||||
|
||||
let mediaDimensions = effectiveSubject.dimensions
|
||||
let mediaDimensions = subject.dimensions
|
||||
let maxSide: CGFloat = 1920.0 / UIScreen.main.scale
|
||||
let fittedSize = mediaDimensions.cgSize.fitted(CGSize(width: maxSide, height: maxSide))
|
||||
let mediaEntity = DrawingMediaEntity(size: fittedSize)
|
||||
@ -3268,27 +3420,28 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
|
||||
let initialValues: MediaEditorValues?
|
||||
if case let .draft(draft, _) = subject {
|
||||
initialValues = draft.values
|
||||
if let values {
|
||||
initialValues = values
|
||||
|
||||
for entity in draft.values.entities {
|
||||
for entity in values.entities {
|
||||
self.entitiesView.add(entity.entity.duplicate(copy: true), announce: false)
|
||||
}
|
||||
|
||||
if let drawingData = initialValues?.drawing?.pngData() {
|
||||
if let drawingData = values.drawing?.pngData() {
|
||||
self.drawingView.setup(withDrawing: drawingData)
|
||||
}
|
||||
} else {
|
||||
initialValues = nil
|
||||
}
|
||||
|
||||
var mediaEditorMode: MediaEditor.Mode = .default
|
||||
if case .stickerEditor = controller.mode {
|
||||
let mediaEditorMode: MediaEditor.Mode
|
||||
switch controller.mode {
|
||||
case .stickerEditor:
|
||||
mediaEditorMode = .sticker
|
||||
} else if case .avatarEditor = controller.mode {
|
||||
mediaEditorMode = .avatar
|
||||
} else if case .coverEditor = controller.mode {
|
||||
case .avatarEditor, .coverEditor:
|
||||
mediaEditorMode = .avatar
|
||||
default:
|
||||
mediaEditorMode = .default
|
||||
}
|
||||
|
||||
if let mediaEntityView = self.entitiesView.add(mediaEntity, announce: false) as? DrawingMediaEntityView {
|
||||
@ -3314,7 +3467,13 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
}
|
||||
|
||||
let mediaEditor = MediaEditor(context: self.context, mode: mediaEditorMode, subject: effectiveSubject.editorSubject, values: initialValues, hasHistogram: true)
|
||||
let mediaEditor = MediaEditor(
|
||||
context: self.context,
|
||||
mode: mediaEditorMode,
|
||||
subject: subject.editorSubject,
|
||||
values: initialValues,
|
||||
hasHistogram: true
|
||||
)
|
||||
if case .avatarEditor = controller.mode {
|
||||
mediaEditor.setVideoIsMuted(true)
|
||||
} else if case let .coverEditor(dimensions) = controller.mode {
|
||||
@ -3327,7 +3486,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
mediaEditor.seek(initialVideoPosition, andPlay: true)
|
||||
}
|
||||
}
|
||||
if self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
|
||||
if !isDraft, self.context.sharedContext.currentPresentationData.with({$0}).autoNightModeTriggered {
|
||||
switch subject {
|
||||
case .message, .gift:
|
||||
mediaEditor.setNightTheme(true)
|
||||
@ -3347,46 +3506,48 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
controller.requestLayout(transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
}
|
||||
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
|
||||
guard let self else {
|
||||
return
|
||||
if case .stickerEditor = controller.mode {
|
||||
self.stickerCutoutStatusDisposable = (mediaEditor.cutoutStatus
|
||||
|> deliverOnMainQueue).start(next: { [weak self] cutoutStatus in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.stickerCutoutStatus = cutoutStatus
|
||||
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
|
||||
})
|
||||
mediaEditor.maskUpdated = { [weak self] mask, apply in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if self.stickerMaskDrawingView == nil {
|
||||
self.setupMaskDrawingView(size: mask.size)
|
||||
}
|
||||
if apply, let maskData = mask.pngData() {
|
||||
self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true)
|
||||
}
|
||||
}
|
||||
self.stickerCutoutStatus = cutoutStatus
|
||||
self.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.25))
|
||||
})
|
||||
mediaEditor.maskUpdated = { [weak self] mask, apply in
|
||||
guard let self else {
|
||||
return
|
||||
mediaEditor.classificationUpdated = { [weak self] classes in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
|
||||
}
|
||||
if self.stickerMaskDrawingView == nil {
|
||||
self.setupMaskDrawingView(size: mask.size)
|
||||
}
|
||||
if apply, let maskData = mask.pngData() {
|
||||
self.stickerMaskDrawingView?.setup(withDrawing: maskData, storeAsClear: true)
|
||||
}
|
||||
}
|
||||
mediaEditor.classificationUpdated = { [weak self] classes in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.controller?.stickerRecommendedEmoji = emojiForClasses(classes.map { $0.0 })
|
||||
}
|
||||
mediaEditor.attachPreviewView(self.previewView, andPlay: !(self.controller?.isEditingStoryCover ?? false))
|
||||
|
||||
if case .empty = effectiveSubject {
|
||||
if case .empty = subject {
|
||||
self.stickerMaskDrawingView?.emptyColor = .black
|
||||
self.stickerMaskDrawingView?.clearWithEmptyColor()
|
||||
}
|
||||
|
||||
switch effectiveSubject {
|
||||
switch subject {
|
||||
case .message, .gift:
|
||||
break
|
||||
default:
|
||||
self.readyValue.set(.single(true))
|
||||
}
|
||||
|
||||
switch effectiveSubject {
|
||||
switch subject {
|
||||
case let .image(_, _, additionalImage, position):
|
||||
if let additionalImage {
|
||||
let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in
|
||||
@ -3431,7 +3592,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
case .message, .gift:
|
||||
var isGift = false
|
||||
let messages: Signal<[Message], NoError>
|
||||
if case let .message(messageIds) = effectiveSubject {
|
||||
if case let .message(messageIds) = subject {
|
||||
messages = self.context.engine.data.get(
|
||||
EngineDataMap(messageIds.map(TelegramEngine.EngineData.Item.Messages.Message.init(id:)))
|
||||
)
|
||||
@ -3444,7 +3605,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
return messages
|
||||
}
|
||||
} else if case let .gift(gift) = effectiveSubject {
|
||||
} else if case let .gift(gift) = subject {
|
||||
isGift = true
|
||||
let media: [Media] = [TelegramMediaAction(action: .starGiftUnique(gift: .unique(gift), isUpgrade: false, isTransferred: false, savedToProfile: false, canExportDate: nil, transferStars: nil, isRefunded: false, peerId: nil, senderId: nil, savedId: nil, resaleStars: nil))]
|
||||
let message = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: self.context.account.peerId, namespace: Namespaces.Message.Cloud, id: -1), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], customTags: [], forwardInfo: nil, author: nil, text: "", attributes: [], media: media, peers: SimpleDictionary(), associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
|
||||
@ -3468,7 +3629,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
|
||||
let wallpaperColors: Signal<(UIColor?, UIColor?), NoError>
|
||||
if let subject = self.subject, case .gift = subject {
|
||||
if case .gift = subject {
|
||||
wallpaperColors = self.mediaEditorPromise.get()
|
||||
|> mapToSignal { mediaEditor in
|
||||
if let mediaEditor {
|
||||
@ -3498,7 +3659,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
let renderer = DrawingMessageRenderer(context: self.context, messages: messages, parentView: self.view, isGift: isGift, wallpaperDayColor: wallpaperColors.0, wallpaperNightColor: wallpaperColors.1)
|
||||
renderer.render(completion: { result in
|
||||
if case .draft = subject, let existingEntityView = self.entitiesView.getView(where: { entityView in
|
||||
if isDraft, let existingEntityView = self.entitiesView.getView(where: { entityView in
|
||||
if let stickerEntityView = entityView as? DrawingStickerEntityView {
|
||||
if case .message = (stickerEntityView.entity as! DrawingStickerEntity).content {
|
||||
return true
|
||||
@ -3508,13 +3669,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
return false
|
||||
}) as? DrawingStickerEntityView {
|
||||
#if DEBUG
|
||||
if let data = result.dayImage.pngData() {
|
||||
let path = NSTemporaryDirectory() + "\(Int(Date().timeIntervalSince1970)).png"
|
||||
try? data.write(to: URL(fileURLWithPath: path))
|
||||
}
|
||||
#endif
|
||||
|
||||
existingEntityView.isNightTheme = isNightTheme
|
||||
let messageEntity = existingEntityView.entity as! DrawingStickerEntity
|
||||
messageEntity.renderImage = result.dayImage
|
||||
@ -3524,7 +3678,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
} else {
|
||||
var content: DrawingStickerEntity.Content
|
||||
var position: CGPoint
|
||||
switch effectiveSubject {
|
||||
switch subject {
|
||||
case let .message(messageIds):
|
||||
content = .message(messageIds, result.size, messageFile, result.mediaFrame?.rect, result.mediaFrame?.cornerRadius)
|
||||
position = CGPoint(x: storyDimensions.width / 2.0 - 54.0, y: storyDimensions.height / 2.0)
|
||||
@ -3579,7 +3733,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.backgroundDimView.isHidden = false
|
||||
})
|
||||
}
|
||||
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject {
|
||||
} else if CACurrentMediaTime() - self.initializationTimestamp > 0.2, case .image = subject {
|
||||
self.previewContainerView.alpha = 1.0
|
||||
self.previewContainerView.layer.allowsGroupOpacity = true
|
||||
self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
|
||||
@ -3608,7 +3762,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
})
|
||||
}
|
||||
|
||||
if effectiveSubject.isPhoto {
|
||||
if subject.isPhoto {
|
||||
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
|
||||
@ -3621,6 +3775,12 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let caption {
|
||||
mediaEditor.onFirstDisplay = { [weak self] in
|
||||
self?.componentHostView?.setInputText(caption)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mediaEditor.onPlaybackAction = { [weak self] action in
|
||||
@ -3815,7 +3975,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return nil
|
||||
}
|
||||
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
||||
let imageSize = CGSize(width: 1080, height: 1920)
|
||||
let imageSize = storyDimensions
|
||||
if let context = DrawingContext(size: imageSize, scale: 1.0, opaque: true, colorSpace: colorSpace) {
|
||||
context.withFlippedContext { context in
|
||||
if let image = mediaEditor.resultImage?.cgImage {
|
||||
@ -4214,9 +4374,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
completion()
|
||||
case .camera:
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .camera, completion: completion)
|
||||
}
|
||||
self.componentHostView?.animateIn(from: .camera, completion: completion)
|
||||
|
||||
if let subject = self.subject, case let .video(_, mainTransitionImage, _, _, additionalTransitionImage, _, _, positionChangeTimestamps, pipPosition) = subject, let mainTransitionImage {
|
||||
var transitionImage = mainTransitionImage
|
||||
@ -4227,7 +4385,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
backgroundImage = additionalTransitionImage
|
||||
foregroundImage = mainTransitionImage
|
||||
}
|
||||
if let combinedTransitionImage = generateImage(CGSize(width: 1080, height: 1920), scale: 1.0, rotatedContext: { size, context in
|
||||
if let combinedTransitionImage = generateImage(storyDimensions, scale: 1.0, rotatedContext: { size, context in
|
||||
UIGraphicsPushContext(context)
|
||||
backgroundImage.draw(in: CGRect(origin: CGPoint(x: (size.width - backgroundImage.size.width) / 2.0, y: (size.height - backgroundImage.size.height) / 2.0), size: backgroundImage.size))
|
||||
|
||||
@ -4253,9 +4411,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.setupTransitionImage(sourceImage)
|
||||
}
|
||||
if let sourceView = transitionIn.sourceView {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .gallery)
|
||||
}
|
||||
self.componentHostView?.animateIn(from: .gallery)
|
||||
|
||||
let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view)
|
||||
let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width
|
||||
@ -4273,7 +4429,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.backgroundDimView.isHidden = false
|
||||
self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.35)
|
||||
|
||||
if let componentView = self.componentHost.view {
|
||||
if let componentView = self.componentHostView {
|
||||
componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: timingFunction)
|
||||
componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
@ -4293,8 +4449,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
if animateIn, let layout = self.validLayout {
|
||||
self.layer.animatePosition(from: CGPoint(x: 0.0, y: layout.size.height), to: .zero, duration: 0.35, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
|
||||
completion()
|
||||
} else if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateIn(from: .camera, completion: completion)
|
||||
} else {
|
||||
self.componentHostView?.animateIn(from: .camera, completion: completion)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -4346,9 +4502,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
destinationTransitionView = destinationTransitionOutView
|
||||
destinationTransitionRect = galleryTransitionIn.sourceRect
|
||||
}
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOut(to: .gallery)
|
||||
}
|
||||
self.componentHostView?.animateOut(to: .gallery)
|
||||
}
|
||||
let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view)
|
||||
let destinationScale = destinationLocalFrame.width / self.previewContainerView.frame.width
|
||||
@ -4446,7 +4600,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
removeOnCompletion: false
|
||||
)
|
||||
|
||||
if let componentView = self.componentHost.view {
|
||||
if let componentView = self.componentHostView {
|
||||
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: destinationScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
|
||||
@ -4464,18 +4618,14 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
}
|
||||
} else if let transitionIn = controller.transitionIn, case .camera = transitionIn {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOut(to: .camera)
|
||||
}
|
||||
self.componentHostView?.animateOut(to: .camera)
|
||||
let transition = ComponentTransition(animation: .curve(duration: 0.25, curve: .easeInOut))
|
||||
transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
} else {
|
||||
if controller.isEmbeddedEditor {
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOut(to: .gallery)
|
||||
}
|
||||
self.componentHostView?.animateOut(to: .gallery)
|
||||
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, removeOnCompletion: false, completion: { _ in
|
||||
@ -4495,9 +4645,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.isDisplayingTool = tool
|
||||
|
||||
let transition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateOutToTool(inPlace: inPlace, transition: transition)
|
||||
}
|
||||
self.componentHostView?.animateOutToTool(inPlace: inPlace, transition: transition)
|
||||
self.requestUpdate(transition: transition)
|
||||
}
|
||||
|
||||
@ -4505,9 +4653,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
self.isDisplayingTool = nil
|
||||
|
||||
let transition: ComponentTransition = .easeInOut(duration: 0.2)
|
||||
if let view = self.componentHost.view as? MediaEditorScreenComponent.View {
|
||||
view.animateInFromTool(inPlace: inPlace, transition: transition)
|
||||
}
|
||||
self.componentHostView?.animateInFromTool(inPlace: inPlace, transition: transition)
|
||||
self.requestUpdate(transition: transition)
|
||||
}
|
||||
|
||||
@ -4721,12 +4867,10 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
|
||||
var location: CLLocationCoordinate2D?
|
||||
if let subject = self.actualSubject {
|
||||
if case let .asset(asset) = subject {
|
||||
location = asset.location?.coordinate
|
||||
} else if case let .draft(draft, _) = subject {
|
||||
location = draft.location
|
||||
}
|
||||
if case let .draft(draft, _) = self.actualSubject {
|
||||
location = draft.location
|
||||
} else if case let .asset(asset) = self.subject {
|
||||
location = asset.location?.coordinate
|
||||
}
|
||||
let locationController = storyLocationPickerController(
|
||||
context: self.context,
|
||||
@ -5195,7 +5339,6 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
func addWeather(_ weather: StickerPickerScreen.Weather.LoadedWeather?) {
|
||||
guard let weather else {
|
||||
|
||||
return
|
||||
}
|
||||
let maxWeatherCount = 1
|
||||
@ -5222,6 +5365,58 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
)
|
||||
}
|
||||
|
||||
func getCaption() -> NSAttributedString {
|
||||
return self.componentHostView?.getInputText() ?? NSAttributedString()
|
||||
}
|
||||
|
||||
func switchToItem(_ identifier: String) {
|
||||
guard let controller = self.controller, let mediaEditor = self.mediaEditor, let itemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == identifier }), case let .asset(asset) = self.subject, let currentItemIndex = self.items.firstIndex(where: { $0.asset.localIdentifier == asset.localIdentifier }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let entities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
var updatedCurrentItem = self.items[currentItemIndex]
|
||||
updatedCurrentItem.caption = self.getCaption()
|
||||
|
||||
if mediaEditor.values.hasChanges && updatedCurrentItem.values != mediaEditor.values {
|
||||
updatedCurrentItem.values = mediaEditor.values
|
||||
updatedCurrentItem.version += 1
|
||||
|
||||
if let resultImage = mediaEditor.resultImage {
|
||||
mediaEditor.seek(0.0, andPlay: false)
|
||||
makeEditorImageComposition(
|
||||
context: self.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: mediaEditor.values,
|
||||
time: .zero,
|
||||
textScale: 2.0,
|
||||
completion: { [weak self] resultImage in
|
||||
updatedCurrentItem.version += 1
|
||||
updatedCurrentItem.thumbnail = resultImage
|
||||
self?.items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
updatedCurrentItem.version += 1
|
||||
self.items[currentItemIndex] = updatedCurrentItem
|
||||
}
|
||||
|
||||
self.entitiesView.clearAll()
|
||||
|
||||
let targetItem = self.items[itemIndex]
|
||||
controller.node.setup(
|
||||
subject: .asset(targetItem.asset),
|
||||
values: targetItem.values,
|
||||
caption: targetItem.caption
|
||||
)
|
||||
}
|
||||
|
||||
func requestCompletion(playHaptic: Bool = true) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
@ -5323,7 +5518,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||
let result = super.hitTest(point, with: event)
|
||||
if result == self.componentHost.view {
|
||||
if result == self.componentHostView {
|
||||
let point = self.view.convert(point, to: self.previewContainerView)
|
||||
if let previewResult = self.previewContainerView.hitTest(point, with: event) {
|
||||
return previewResult
|
||||
@ -6181,6 +6376,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
case message([MessageId])
|
||||
case gift(StarGift.UniqueGift)
|
||||
case sticker(TelegramMediaFile, [String])
|
||||
case assets([PHAsset])
|
||||
|
||||
var dimensions: PixelDimensions {
|
||||
switch self {
|
||||
@ -6192,8 +6388,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight))
|
||||
case let .draft(draft, _):
|
||||
return draft.dimensions
|
||||
case .message, .gift, .sticker, .videoCollage:
|
||||
return PixelDimensions(width: 1080, height: 1920)
|
||||
case .message, .gift, .sticker, .videoCollage, .assets:
|
||||
return PixelDimensions(storyDimensions)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6220,6 +6416,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return .gift(gift)
|
||||
case let .sticker(sticker, _):
|
||||
return .sticker(sticker)
|
||||
case let .assets(assets):
|
||||
return .asset(assets.first!)
|
||||
}
|
||||
}
|
||||
|
||||
@ -6247,6 +6445,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
return false
|
||||
case .sticker:
|
||||
return false
|
||||
case .assets:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -6555,7 +6755,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
let privacy = privacy ?? self.state.privacy
|
||||
|
||||
let text = self.getCaption().string
|
||||
let text = self.node.getCaption().string
|
||||
let mentions = generateTextEntities(text, enabledTypes: [.mention], currentEntities: []).map { (text as NSString).substring(with: NSRange(location: $0.range.lowerBound + 1, length: $0.range.upperBound - $0.range.lowerBound - 1)) }
|
||||
|
||||
let coverImage: UIImage?
|
||||
@ -6567,7 +6767,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
|
||||
let stateContext = ShareWithPeersScreen.StateContext(
|
||||
context: self.context,
|
||||
subject: .stories(editing: false),
|
||||
subject: .stories(editing: false, count: Int32(self.node.items.count(where: { $0.isEnabled }))),
|
||||
editing: false,
|
||||
initialPeerIds: Set(privacy.privacy.additionallyIncludePeers),
|
||||
closeFriends: self.closeFriends.get(),
|
||||
@ -7106,12 +7306,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
})
|
||||
}
|
||||
|
||||
func getCaption() -> NSAttributedString {
|
||||
return (self.node.componentHost.view as? MediaEditorScreenComponent.View)?.getInputText() ?? NSAttributedString()
|
||||
}
|
||||
|
||||
fileprivate func checkCaptionLimit() -> Bool {
|
||||
let caption = self.getCaption()
|
||||
let caption = self.node.getCaption()
|
||||
if caption.length > self.context.userLimits.maxStoryCaptionLength {
|
||||
let _ = (self.context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: self.context.account.peerId))
|
||||
|> deliverOnMainQueue).start(next: { [weak self] peer in
|
||||
@ -7160,7 +7356,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
var caption = self.getCaption()
|
||||
var caption = self.node.getCaption()
|
||||
caption = convertMarkdownToAttributes(caption)
|
||||
|
||||
var hasEntityChanges = false
|
||||
@ -7209,7 +7405,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
|
||||
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
|
||||
self.saveDraft(id: randomId, edit: true)
|
||||
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
|
||||
@ -7498,7 +7694,7 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
duration = 5.0
|
||||
case .sticker:
|
||||
let image = generateImage(CGSize(width: 1080, height: 1920), contextGenerator: { size, context in
|
||||
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"
|
||||
@ -7509,6 +7705,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
duration = 3.0
|
||||
|
||||
firstFrame = .single((image, nil))
|
||||
case .assets:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
|
||||
@ -8399,6 +8597,8 @@ public final class MediaEditorScreenImpl: ViewController, MediaEditorScreen, UID
|
||||
}
|
||||
case let .sticker(file, _):
|
||||
exportSubject = .single(.sticker(file: file))
|
||||
case .assets:
|
||||
fatalError()
|
||||
}
|
||||
|
||||
let _ = (exportSubject
|
||||
|
@ -11,7 +11,16 @@ import DrawingUI
|
||||
|
||||
extension MediaEditorScreenImpl {
|
||||
func isEligibleForDraft() -> Bool {
|
||||
if self.isEditingStory {
|
||||
guard !self.isEditingStory else {
|
||||
return false
|
||||
}
|
||||
if case .avatarEditor = self.mode {
|
||||
return false
|
||||
}
|
||||
if case .coverEditor = self.mode {
|
||||
return false
|
||||
}
|
||||
if case .assets = self.node.actualSubject {
|
||||
return false
|
||||
}
|
||||
guard let mediaEditor = self.node.mediaEditor else {
|
||||
@ -21,13 +30,6 @@ extension MediaEditorScreenImpl {
|
||||
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
|
||||
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
|
||||
|
||||
if case .avatarEditor = self.mode {
|
||||
return false
|
||||
}
|
||||
if case .coverEditor = self.mode {
|
||||
return false
|
||||
}
|
||||
|
||||
let filteredEntities = self.node.entitiesView.entities.filter { entity in
|
||||
if entity is DrawingMediaEntity {
|
||||
return false
|
||||
@ -44,34 +46,42 @@ extension MediaEditorScreenImpl {
|
||||
|
||||
let values = mediaEditor.values
|
||||
let filteredValues = values.withUpdatedEntities([])
|
||||
let caption = self.node.getCaption()
|
||||
|
||||
let caption = self.getCaption()
|
||||
if let subject = self.node.subject {
|
||||
if case .asset = subject, !values.hasChanges && caption.string.isEmpty {
|
||||
return false
|
||||
} else if case .message = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
|
||||
return false
|
||||
} else if case .gift = subject, !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
|
||||
return false
|
||||
} else if case .empty = subject, !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
|
||||
return false
|
||||
} else if case .videoCollage = subject {
|
||||
switch subject {
|
||||
case .asset:
|
||||
if !values.hasChanges && caption.string.isEmpty {
|
||||
return false
|
||||
}
|
||||
case .message, .gift:
|
||||
if !filteredValues.hasChanges && filteredEntities.isEmpty && caption.string.isEmpty {
|
||||
return false
|
||||
}
|
||||
case .empty:
|
||||
if !self.node.hasAnyChanges && !self.node.drawingView.internalState.canUndo {
|
||||
return false
|
||||
}
|
||||
case .videoCollage:
|
||||
return false
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func saveDraft(id: Int64?, edit: Bool = false) {
|
||||
func saveDraft(id: Int64?, isEdit: Bool = false, completion: ((MediaEditorDraft) -> Void)? = nil) {
|
||||
guard case .storyEditor = self.mode, let subject = self.node.subject, let actualSubject = self.node.actualSubject, let mediaEditor = self.node.mediaEditor else {
|
||||
return
|
||||
}
|
||||
|
||||
try? FileManager.default.createDirectory(atPath: draftPath(engine: self.context.engine), withIntermediateDirectories: true)
|
||||
|
||||
let values = mediaEditor.values
|
||||
let privacy = self.state.privacy
|
||||
let forwardSource = self.forwardSource
|
||||
let caption = self.getCaption()
|
||||
let caption = self.node.getCaption()
|
||||
let duration = mediaEditor.duration ?? 0.0
|
||||
|
||||
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
|
||||
@ -99,10 +109,19 @@ extension MediaEditorScreenImpl {
|
||||
}
|
||||
|
||||
if let resultImage = mediaEditor.resultImage {
|
||||
if !edit {
|
||||
if !isEdit {
|
||||
mediaEditor.seek(0.0, andPlay: false)
|
||||
}
|
||||
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: resultImage, dimensions: storyDimensions, values: values, time: .zero, textScale: 2.0, completion: { resultImage in
|
||||
|
||||
makeEditorImageComposition(
|
||||
context: self.node.ciContext,
|
||||
postbox: self.context.account.postbox,
|
||||
inputImage: resultImage,
|
||||
dimensions: storyDimensions,
|
||||
values: values,
|
||||
time: .zero,
|
||||
textScale: 2.0,
|
||||
completion: { resultImage in
|
||||
guard let resultImage else {
|
||||
return
|
||||
}
|
||||
@ -148,54 +167,64 @@ extension MediaEditorScreenImpl {
|
||||
}
|
||||
|
||||
let context = self.context
|
||||
func innerSaveDraft(media: MediaInput) {
|
||||
func innerSaveDraft(media: MediaInput, save: Bool = true) -> MediaEditorDraft? {
|
||||
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
|
||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
|
||||
let draft = MediaEditorDraft(
|
||||
path: path,
|
||||
isVideo: media.isVideo,
|
||||
thumbnail: thumbnailImage,
|
||||
dimensions: media.dimensions,
|
||||
duration: media.duration,
|
||||
values: values,
|
||||
caption: caption,
|
||||
privacy: privacy,
|
||||
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
|
||||
timestamp: timestamp,
|
||||
location: location,
|
||||
expiresOn: expiresOn
|
||||
)
|
||||
switch media {
|
||||
case let .image(image, _):
|
||||
if let data = image.jpegData(compressionQuality: 0.87) {
|
||||
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
|
||||
}
|
||||
case let .video(path, _, _):
|
||||
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
|
||||
guard let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) else {
|
||||
return nil
|
||||
}
|
||||
let path = "\(Int64.random(in: .min ... .max)).\(media.fileExtension)"
|
||||
let draft = MediaEditorDraft(
|
||||
path: path,
|
||||
isVideo: media.isVideo,
|
||||
thumbnail: thumbnailImage,
|
||||
dimensions: media.dimensions,
|
||||
duration: media.duration,
|
||||
values: values,
|
||||
caption: caption,
|
||||
privacy: privacy,
|
||||
forwardInfo: forwardSource.flatMap { StoryId(peerId: $0.0.id, id: $0.1.id) },
|
||||
timestamp: timestamp,
|
||||
location: location,
|
||||
expiresOn: expiresOn
|
||||
)
|
||||
switch media {
|
||||
case let .image(image, _):
|
||||
if let data = image.jpegData(compressionQuality: 0.87) {
|
||||
try? data.write(to: URL(fileURLWithPath: draft.fullPath(engine: context.engine)))
|
||||
}
|
||||
case let .video(path, _, _):
|
||||
try? FileManager.default.copyItem(atPath: path, toPath: draft.fullPath(engine: context.engine))
|
||||
}
|
||||
if save {
|
||||
if let id {
|
||||
saveStorySource(engine: context.engine, item: draft, peerId: context.account.peerId, id: id)
|
||||
} else {
|
||||
addStoryDraft(engine: context.engine, item: draft)
|
||||
}
|
||||
}
|
||||
return draft
|
||||
}
|
||||
|
||||
switch subject {
|
||||
case .empty:
|
||||
break
|
||||
case let .image(image, dimensions, _, _):
|
||||
innerSaveDraft(media: .image(image: image, dimensions: dimensions))
|
||||
if let draft = innerSaveDraft(media: .image(image: image, dimensions: dimensions)) {
|
||||
completion?(draft)
|
||||
}
|
||||
case let .video(path, _, _, _, _, dimensions, _, _, _):
|
||||
innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration))
|
||||
if let draft = innerSaveDraft(media: .video(path: path, dimensions: dimensions, duration: duration)) {
|
||||
completion?(draft)
|
||||
}
|
||||
case let .videoCollage(items):
|
||||
let _ = items
|
||||
case let .asset(asset):
|
||||
if asset.mediaType == .video {
|
||||
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
|
||||
if let urlAsset = avAsset as? AVURLAsset {
|
||||
innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration))
|
||||
if let draft = innerSaveDraft(media: .video(path: urlAsset.url.relativePath, dimensions: PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)), duration: duration)) {
|
||||
completion?(draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@ -203,22 +232,32 @@ extension MediaEditorScreenImpl {
|
||||
options.deliveryMode = .highQualityFormat
|
||||
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
|
||||
if let image {
|
||||
innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size)))
|
||||
if let draft = innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) {
|
||||
completion?(draft)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case let .draft(draft, _):
|
||||
if draft.isVideo {
|
||||
innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0))
|
||||
if let draft = innerSaveDraft(media: .video(path: draft.fullPath(engine: context.engine), dimensions: draft.dimensions, duration: draft.duration ?? 0.0)) {
|
||||
completion?(draft)
|
||||
}
|
||||
} else if let image = UIImage(contentsOfFile: draft.fullPath(engine: context.engine)) {
|
||||
innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions))
|
||||
if let draft = innerSaveDraft(media: .image(image: image, dimensions: draft.dimensions)) {
|
||||
completion?(draft)
|
||||
}
|
||||
}
|
||||
case .message, .gift:
|
||||
if let pixel = generateSingleColorImage(size: CGSize(width: 1, height: 1), color: .black) {
|
||||
innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920)))
|
||||
if let draft = innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) {
|
||||
completion?(draft)
|
||||
}
|
||||
}
|
||||
case .sticker:
|
||||
break
|
||||
case .assets:
|
||||
break
|
||||
}
|
||||
|
||||
if case let .draft(draft, _) = actualSubject {
|
@ -34,7 +34,7 @@ final class SelectionPanelButtonContentComponent: Component {
|
||||
return false
|
||||
}
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
let backgroundView: BlurredBackgroundView
|
||||
private let outline = SimpleLayer()
|
||||
private let icon = SimpleLayer()
|
||||
private let label = ComponentView<Empty>()
|
||||
|
@ -1,7 +1,670 @@
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import AccountContext
|
||||
import MediaEditor
|
||||
import MediaAssetsContext
|
||||
import CheckNode
|
||||
import TelegramPresentationData
|
||||
|
||||
final class SelectionPanelComponent: Component {
|
||||
let previewContainerView: PortalSourceView
|
||||
let frame: CGRect
|
||||
let items: [MediaEditorScreenImpl.EditingItem]
|
||||
let selectedItemId: String
|
||||
let itemTapped: (String?) -> Void
|
||||
let itemSelectionToggled: (String) -> Void
|
||||
let itemReordered: (String, String) -> Void
|
||||
|
||||
init(
|
||||
previewContainerView: PortalSourceView,
|
||||
frame: CGRect,
|
||||
items: [MediaEditorScreenImpl.EditingItem],
|
||||
selectedItemId: String,
|
||||
itemTapped: @escaping (String?) -> Void,
|
||||
itemSelectionToggled: @escaping (String) -> Void,
|
||||
itemReordered: @escaping (String, String) -> Void
|
||||
) {
|
||||
self.previewContainerView = previewContainerView
|
||||
self.frame = frame
|
||||
self.items = items
|
||||
self.selectedItemId = selectedItemId
|
||||
self.itemTapped = itemTapped
|
||||
self.itemSelectionToggled = itemSelectionToggled
|
||||
self.itemReordered = itemReordered
|
||||
}
|
||||
|
||||
static func ==(lhs: SelectionPanelComponent, rhs: SelectionPanelComponent) -> Bool {
|
||||
return lhs.frame == rhs.frame && lhs.items == rhs.items && lhs.selectedItemId == rhs.selectedItemId
|
||||
}
|
||||
|
||||
final class View: UIView, UIGestureRecognizerDelegate {
|
||||
final class ItemView: UIView {
|
||||
private let backgroundNode: ASImageNode
|
||||
private let imageNode: ImageNode
|
||||
private let checkNode: InteractiveCheckNode
|
||||
private var selectionLayer: SimpleShapeLayer?
|
||||
|
||||
var toggleSelection: () -> Void = {}
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
|
||||
self.imageNode = ImageNode()
|
||||
self.imageNode.contentMode = .scaleAspectFill
|
||||
|
||||
self.checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: defaultDarkColorPresentationTheme, style: .overlay))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.clipsToBounds = true
|
||||
self.layer.cornerRadius = 6.0
|
||||
|
||||
self.addSubview(self.backgroundNode.view)
|
||||
self.addSubview(self.imageNode.view)
|
||||
self.addSubview(self.checkNode.view)
|
||||
|
||||
self.checkNode.valueChanged = { [weak self] value in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.toggleSelection()
|
||||
}
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
fileprivate var item: MediaEditorScreenImpl.EditingItem?
|
||||
func update(item: MediaEditorScreenImpl.EditingItem, number: Int, isSelected: Bool, isEnabled: Bool, size: CGSize, portalView: PortalView?, transition: ComponentTransition) {
|
||||
let previousItem = self.item
|
||||
self.item = item
|
||||
|
||||
if previousItem?.asset.localIdentifier != item.asset.localIdentifier || previousItem?.version != item.version {
|
||||
let imageSignal: Signal<UIImage?, NoError>
|
||||
if let thumbnail = item.thumbnail {
|
||||
imageSignal = .single(thumbnail)
|
||||
self.imageNode.contentMode = .scaleAspectFill
|
||||
} else {
|
||||
imageSignal = assetImage(asset: item.asset, targetSize:CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale), exact: false, synchronous: true)
|
||||
self.imageNode.contentUpdated = { [weak self] image in
|
||||
if let self {
|
||||
if self.backgroundNode.image == nil {
|
||||
if let image, image.size.width > image.size.height {
|
||||
self.imageNode.contentMode = .scaleAspectFit
|
||||
Queue.concurrentDefaultQueue().async {
|
||||
let colors = mediaEditorGetGradientColors(from: image)
|
||||
let gradientImage = mediaEditorGenerateGradientImage(size: CGSize(width: 3.0, height: 128.0), colors: colors.array)
|
||||
Queue.mainQueue().async {
|
||||
self.backgroundNode.image = gradientImage
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.imageNode.contentMode = .scaleAspectFill
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.imageNode.setSignal(imageSignal)
|
||||
}
|
||||
|
||||
let backgroundSize = CGSize(width: size.width, height: floorToScreenPixels(size.width / 9.0 * 16.0))
|
||||
self.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: floorToScreenPixels((size.height - backgroundSize.height) / 2.0)), size: backgroundSize)
|
||||
|
||||
self.imageNode.frame = CGRect(origin: .zero, size: size)
|
||||
|
||||
//self.checkNode.content = .counter(number)
|
||||
self.checkNode.setSelected(isEnabled, animated: previousItem != nil)
|
||||
|
||||
let checkSize = CGSize(width: 29.0, height: 29.0)
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: size.width - checkSize.width - 4.0, y: 4.0), size: checkSize)
|
||||
|
||||
if isSelected, let portalView {
|
||||
portalView.view.frame = CGRect(origin: .zero, size: size)
|
||||
self.insertSubview(portalView.view, aboveSubview: self.imageNode.view)
|
||||
}
|
||||
|
||||
let lineWidth: CGFloat = 2.0 - UIScreenPixel
|
||||
let selectionFrame = CGRect(origin: .zero, size: size)
|
||||
if isSelected {
|
||||
let selectionLayer: SimpleShapeLayer
|
||||
if let current = self.selectionLayer {
|
||||
selectionLayer = current
|
||||
} else {
|
||||
selectionLayer = SimpleShapeLayer()
|
||||
self.selectionLayer = selectionLayer
|
||||
self.layer.addSublayer(selectionLayer)
|
||||
|
||||
selectionLayer.fillColor = UIColor.clear.cgColor
|
||||
selectionLayer.strokeColor = UIColor.white.cgColor
|
||||
selectionLayer.lineWidth = lineWidth
|
||||
selectionLayer.frame = selectionFrame
|
||||
selectionLayer.path = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
||||
|
||||
// if !transition.animation.isImmediate {
|
||||
// let initialPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
||||
// selectionLayer.animate(from: initialPath, to: selectionLayer.path as AnyObject, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2)
|
||||
// selectionLayer.animateShapeLineWidth(from: 0.0, to: lineWidth, duration: 0.2)
|
||||
// }
|
||||
}
|
||||
|
||||
} else if let selectionLayer = self.selectionLayer {
|
||||
self.selectionLayer = nil
|
||||
selectionLayer.removeFromSuperlayer()
|
||||
|
||||
// let targetPath = CGPath(roundedRect: CGRect(origin: .zero, size: selectionFrame.size).insetBy(dx: 0.0, dy: 0.0), cornerWidth: 6.0, cornerHeight: 6.0, transform: nil)
|
||||
// selectionLayer.animate(from: selectionLayer.path, to: targetPath, keyPath: "path", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, duration: 0.2, removeOnCompletion: false)
|
||||
// selectionLayer.animateShapeLineWidth(from: selectionLayer.lineWidth, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
|
||||
// selectionLayer.removeFromSuperlayer()
|
||||
// })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let backgroundMaskView: UIView
|
||||
private let backgroundMaskPanelView: UIView
|
||||
|
||||
private let scrollView: UIScrollView
|
||||
private var itemViews: [AnyHashable: ItemView] = [:]
|
||||
private var portalView: PortalView?
|
||||
|
||||
private var reorderRecognizer: ReorderGestureRecognizer?
|
||||
private var reorderingItem: (id: AnyHashable, initialPosition: CGPoint, position: CGPoint, snapshotView: UIView)?
|
||||
|
||||
private var tapRecognizer: UITapGestureRecognizer?
|
||||
|
||||
private var component: SelectionPanelComponent?
|
||||
private var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
|
||||
self.backgroundMaskView = UIView(frame: .zero)
|
||||
|
||||
self.backgroundMaskPanelView = UIView(frame: .zero)
|
||||
self.backgroundMaskPanelView.backgroundColor = UIColor.white
|
||||
self.backgroundMaskPanelView.clipsToBounds = true
|
||||
self.backgroundMaskPanelView.layer.cornerRadius = 10.0
|
||||
|
||||
self.scrollView = UIScrollView(frame: .zero)
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.layer.cornerRadius = 10.0
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundView.mask = self.backgroundMaskView
|
||||
|
||||
let reorderRecognizer = ReorderGestureRecognizer(
|
||||
shouldBegin: { [weak self] point in
|
||||
guard let self, let item = self.item(at: point) else {
|
||||
return (allowed: false, requiresLongPress: false, item: nil)
|
||||
}
|
||||
|
||||
return (allowed: true, requiresLongPress: true, item: item)
|
||||
},
|
||||
willBegin: { point in
|
||||
},
|
||||
began: { [weak self] item in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setReorderingItem(item: item)
|
||||
},
|
||||
ended: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.setReorderingItem(item: nil)
|
||||
},
|
||||
moved: { [weak self] distance in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.moveReorderingItem(distance: distance)
|
||||
},
|
||||
isActiveUpdated: { _ in
|
||||
}
|
||||
)
|
||||
reorderRecognizer.delegate = self
|
||||
self.reorderRecognizer = reorderRecognizer
|
||||
self.addGestureRecognizer(reorderRecognizer)
|
||||
|
||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap))
|
||||
self.tapRecognizer = tapRecognizer
|
||||
self.addGestureRecognizer(tapRecognizer)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.addSubview(self.scrollView)
|
||||
|
||||
self.backgroundMaskView.addSubview(self.backgroundMaskPanelView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
deinit {
|
||||
}
|
||||
|
||||
@objc private func handleTap(_ gestureRecognizer: UITapGestureRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
self.reorderRecognizer?.isEnabled = false
|
||||
self.reorderRecognizer?.isEnabled = true
|
||||
|
||||
let location = gestureRecognizer.location(in: self)
|
||||
if let itemView = self.item(at: location), let item = itemView.item, item.asset.localIdentifier != component.selectedItemId {
|
||||
component.itemTapped(item.asset.localIdentifier)
|
||||
} else {
|
||||
component.itemTapped(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if otherGestureRecognizer is UITapGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
if otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
if gestureRecognizer === self.reorderRecognizer, ![.began, .changed].contains(gestureRecognizer.state) {
|
||||
gestureRecognizer.isEnabled = false
|
||||
gestureRecognizer.isEnabled = true
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func item(at point: CGPoint) -> ItemView? {
|
||||
let point = self.convert(point, to: self.scrollView)
|
||||
for (_, itemView) in self.itemViews {
|
||||
if itemView.frame.contains(point) {
|
||||
return itemView
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setReorderingItem(item: ItemView?) {
|
||||
self.tapRecognizer?.isEnabled = false
|
||||
self.tapRecognizer?.isEnabled = true
|
||||
|
||||
var mappedItem: (AnyHashable, ItemView)?
|
||||
if let item {
|
||||
for (id, visibleItem) in self.itemViews {
|
||||
if visibleItem === item {
|
||||
mappedItem = (id, visibleItem)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.reorderingItem?.id != mappedItem?.0 {
|
||||
let transition: ComponentTransition = .spring(duration: 0.4)
|
||||
if let (id, itemView) = mappedItem, let snapshotView = itemView.snapshotView(afterScreenUpdates: false) {
|
||||
itemView.isHidden = true
|
||||
|
||||
let position = self.scrollView.convert(itemView.center, to: self)
|
||||
snapshotView.center = position
|
||||
transition.setScale(view: snapshotView, scale: 0.9)
|
||||
self.addSubview(snapshotView)
|
||||
|
||||
self.reorderingItem = (id, position, position, snapshotView)
|
||||
} else {
|
||||
if let (id, _, _, snapshotView) = self.reorderingItem {
|
||||
if let itemView = self.itemViews[id] {
|
||||
if let innerSnapshotView = snapshotView.snapshotView(afterScreenUpdates: false) {
|
||||
innerSnapshotView.center = self.convert(snapshotView.center, to: self.scrollView)
|
||||
innerSnapshotView.transform = CGAffineTransformMakeScale(0.9, 0.9)
|
||||
self.scrollView.addSubview(innerSnapshotView)
|
||||
|
||||
transition.setPosition(view: innerSnapshotView, position: itemView.center, completion: { [weak innerSnapshotView] _ in
|
||||
innerSnapshotView?.removeFromSuperview()
|
||||
itemView.isHidden = false
|
||||
})
|
||||
transition.setScale(view: innerSnapshotView, scale: 1.0)
|
||||
}
|
||||
|
||||
transition.setPosition(view: snapshotView, position: self.scrollView.convert(itemView.center, to: self), completion: { [weak snapshotView] _ in
|
||||
snapshotView?.removeFromSuperview()
|
||||
})
|
||||
transition.setScale(view: snapshotView, scale: 1.0)
|
||||
transition.setAlpha(view: snapshotView, alpha: 0.0)
|
||||
}
|
||||
}
|
||||
self.reorderingItem = nil
|
||||
}
|
||||
self.state?.updated(transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
func moveReorderingItem(distance: CGPoint) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
if let (id, initialPosition, _, snapshotView) = self.reorderingItem {
|
||||
let targetPosition = CGPoint(x: initialPosition.x + distance.x, y: initialPosition.y + distance.y)
|
||||
self.reorderingItem = (id, initialPosition, targetPosition, snapshotView)
|
||||
snapshotView.center = targetPosition
|
||||
|
||||
let mappedPosition = self.convert(targetPosition, to: self.scrollView)
|
||||
|
||||
if let visibleReorderingItem = self.itemViews[id], let fromId = self.itemViews[id]?.item?.asset.localIdentifier {
|
||||
for (_, visibleItem) in self.itemViews {
|
||||
if visibleItem === visibleReorderingItem {
|
||||
continue
|
||||
}
|
||||
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.asset.localIdentifier {
|
||||
component.itemReordered(fromId, toId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
|
||||
|
||||
}
|
||||
|
||||
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func update(component: SelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if self.portalView == nil {
|
||||
if let portalView = PortalView(matchPosition: false) {
|
||||
portalView.view.layer.rasterizationScale = UIScreenScale
|
||||
|
||||
let scale = 95.0 / component.previewContainerView.frame.width
|
||||
portalView.view.transform = CGAffineTransformMakeScale(scale, scale)
|
||||
|
||||
component.previewContainerView.addPortal(view: portalView)
|
||||
self.portalView = portalView
|
||||
}
|
||||
}
|
||||
|
||||
var validIds = Set<AnyHashable>()
|
||||
|
||||
let itemSize = CGSize(width: 95.0, height: 112.0)
|
||||
let spacing: CGFloat = 4.0
|
||||
|
||||
var itemFrame: CGRect = CGRect(origin: CGPoint(x: spacing, y: spacing), size: itemSize)
|
||||
|
||||
var index = 1
|
||||
for item in component.items {
|
||||
let id = item.asset.localIdentifier
|
||||
validIds.insert(id)
|
||||
|
||||
var itemTransition = transition
|
||||
let itemView: ItemView
|
||||
if let current = self.itemViews[id] {
|
||||
itemView = current
|
||||
} else {
|
||||
itemView = ItemView(frame: itemFrame)
|
||||
self.scrollView.addSubview(itemView)
|
||||
self.itemViews[id] = itemView
|
||||
|
||||
itemTransition = .immediate
|
||||
}
|
||||
itemView.toggleSelection = { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.itemSelectionToggled(id)
|
||||
}
|
||||
itemView.update(item: item, number: index, isSelected: item.asset.localIdentifier == component.selectedItemId, isEnabled: item.isEnabled, size: itemFrame.size, portalView: self.portalView, transition: itemTransition)
|
||||
|
||||
itemTransition.setBounds(view: itemView, bounds: CGRect(origin: .zero, size: itemFrame.size))
|
||||
itemTransition.setPosition(view: itemView, position: itemFrame.center)
|
||||
|
||||
itemFrame.origin.x += itemSize.width + spacing
|
||||
index += 1
|
||||
}
|
||||
|
||||
var removeIds: [AnyHashable] = []
|
||||
for (id, itemView) in self.itemViews {
|
||||
if !validIds.contains(id) {
|
||||
removeIds.append(id)
|
||||
transition.setAlpha(view: itemView, alpha: 0.0, completion: { [weak itemView] _ in
|
||||
itemView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
for id in removeIds {
|
||||
self.itemViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
let contentSize = CGSize(width: itemFrame.minX, height: itemSize.height + spacing * 2.0)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
|
||||
let backgroundSize = CGSize(width: min(availableSize.width - 24.0, contentSize.width), height: contentSize.height)
|
||||
self.backgroundView.frame = CGRect(origin: .zero, size: availableSize)
|
||||
self.backgroundView.update(size: availableSize, transition: .immediate)
|
||||
|
||||
let contentFrame = CGRect(origin: CGPoint(x: availableSize.width - 12.0 - backgroundSize.width, y: component.frame.minY), size: backgroundSize)
|
||||
self.backgroundMaskPanelView.frame = contentFrame
|
||||
self.scrollView.frame = contentFrame
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class ReorderGestureRecognizer: UIGestureRecognizer {
|
||||
private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?)
|
||||
private let willBegin: (CGPoint) -> Void
|
||||
private let began: (SelectionPanelComponent.View.ItemView) -> Void
|
||||
private let ended: () -> Void
|
||||
private let moved: (CGPoint) -> Void
|
||||
private let isActiveUpdated: (Bool) -> Void
|
||||
|
||||
private var initialLocation: CGPoint?
|
||||
private var longTapTimer: SwiftSignalKit.Timer?
|
||||
private var longPressTimer: SwiftSignalKit.Timer?
|
||||
|
||||
private var itemView: SelectionPanelComponent.View.ItemView?
|
||||
|
||||
public init(shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, item: SelectionPanelComponent.View.ItemView?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (SelectionPanelComponent.View.ItemView) -> Void, ended: @escaping () -> Void, moved: @escaping (CGPoint) -> Void, isActiveUpdated: @escaping (Bool) -> Void) {
|
||||
self.shouldBegin = shouldBegin
|
||||
self.willBegin = willBegin
|
||||
self.began = began
|
||||
self.ended = ended
|
||||
self.moved = moved
|
||||
self.isActiveUpdated = isActiveUpdated
|
||||
|
||||
super.init(target: nil, action: nil)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longPressTimer?.invalidate()
|
||||
}
|
||||
|
||||
private func startLongTapTimer() {
|
||||
self.longTapTimer?.invalidate()
|
||||
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
|
||||
self?.longTapTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longTapTimer = longTapTimer
|
||||
longTapTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongTapTimer() {
|
||||
self.itemView = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
}
|
||||
|
||||
private func startLongPressTimer() {
|
||||
self.longPressTimer?.invalidate()
|
||||
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
|
||||
self?.longPressTimerFired()
|
||||
}, queue: Queue.mainQueue())
|
||||
self.longPressTimer = longPressTimer
|
||||
longPressTimer.start()
|
||||
}
|
||||
|
||||
private func stopLongPressTimer() {
|
||||
self.itemView = nil
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
}
|
||||
|
||||
override public func reset() {
|
||||
super.reset()
|
||||
|
||||
self.itemView = nil
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
|
||||
self.isActiveUpdated(false)
|
||||
}
|
||||
|
||||
private func longTapTimerFired() {
|
||||
guard let location = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
|
||||
self.willBegin(location)
|
||||
}
|
||||
|
||||
private func longPressTimerFired() {
|
||||
guard let _ = self.initialLocation else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isActiveUpdated(true)
|
||||
self.state = .began
|
||||
self.longPressTimer?.invalidate()
|
||||
self.longPressTimer = nil
|
||||
self.longTapTimer?.invalidate()
|
||||
self.longTapTimer = nil
|
||||
if let itemView = self.itemView {
|
||||
self.began(itemView)
|
||||
}
|
||||
self.isActiveUpdated(true)
|
||||
}
|
||||
|
||||
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesBegan(touches, with: event)
|
||||
|
||||
if self.numberOfTouches > 1 {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
self.ended()
|
||||
return
|
||||
}
|
||||
|
||||
if self.state == .possible {
|
||||
if let location = touches.first?.location(in: self.view) {
|
||||
let (allowed, requiresLongPress, itemView) = self.shouldBegin(location)
|
||||
if allowed {
|
||||
self.isActiveUpdated(true)
|
||||
|
||||
self.itemView = itemView
|
||||
self.initialLocation = location
|
||||
if requiresLongPress {
|
||||
self.startLongTapTimer()
|
||||
self.startLongPressTimer()
|
||||
} else {
|
||||
self.state = .began
|
||||
if let itemView = self.itemView {
|
||||
self.began(itemView)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
} else {
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesEnded(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.stopLongPressTimer()
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesCancelled(touches, with: event)
|
||||
|
||||
self.initialLocation = nil
|
||||
|
||||
self.stopLongTapTimer()
|
||||
if self.longPressTimer != nil {
|
||||
self.isActiveUpdated(false)
|
||||
self.stopLongPressTimer()
|
||||
self.state = .failed
|
||||
}
|
||||
if self.state == .began || self.state == .changed {
|
||||
self.isActiveUpdated(false)
|
||||
self.ended()
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
|
||||
super.touchesMoved(touches, with: event)
|
||||
|
||||
if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
|
||||
self.state = .changed
|
||||
let offset = CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y)
|
||||
self.moved(offset)
|
||||
} else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
|
||||
let touchLocation = touch.location(in: self.view)
|
||||
let dX = touchLocation.x - initialTapLocation.x
|
||||
let dY = touchLocation.y - initialTapLocation.y
|
||||
|
||||
if dX * dX + dY * dY > 3.0 * 3.0 {
|
||||
self.stopLongTapTimer()
|
||||
self.stopLongPressTimer()
|
||||
self.initialLocation = nil
|
||||
self.isActiveUpdated(false)
|
||||
self.state = .failed
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10018,7 +10018,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
viewControllers = viewControllers.filter { !($0 is AttachmentController)}
|
||||
rootController.setViewControllers(viewControllers, animated: false)
|
||||
|
||||
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
}
|
||||
},
|
||||
cancelled: {}
|
||||
@ -10053,7 +10053,7 @@ final class PeerInfoScreenNode: ViewControllerTracingNode, PeerInfoScreenNodePro
|
||||
|
||||
self.openBotPreviewEditor(target: .botPreview(id: self.peerId, language: pane.currentBotPreviewLanguage?.id), source: result, transitionIn: (transitionView, transitionRect, transitionImage))
|
||||
},
|
||||
multipleCompletion: { _ in },
|
||||
multipleCompletion: { _, _ in },
|
||||
dismissed: {},
|
||||
groupsPresented: {}
|
||||
)
|
||||
|
@ -991,10 +991,22 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
let sectionTitle: String
|
||||
if section.id == 0, case .stories = component.stateContext.subject {
|
||||
sectionTitle = component.coverItem == nil ? environment.strings.Story_Privacy_PostStoryAsHeader : ""
|
||||
if section.id == 0, case let .stories(_, count) = component.stateContext.subject {
|
||||
if component.coverItem == nil {
|
||||
if count > 1 {
|
||||
sectionTitle = environment.strings.Story_Privacy_PostStoriesAsHeader
|
||||
} else {
|
||||
sectionTitle = environment.strings.Story_Privacy_PostStoryAsHeader
|
||||
}
|
||||
} else {
|
||||
sectionTitle = ""
|
||||
}
|
||||
} else if section.id == 2 {
|
||||
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
|
||||
if case let .stories(_, count) = component.stateContext.subject, count > 1 {
|
||||
sectionTitle = environment.strings.Story_Privacy_WhoCanViewStoriesHeader
|
||||
} else {
|
||||
sectionTitle = environment.strings.Story_Privacy_WhoCanViewHeader
|
||||
}
|
||||
} else if section.id == 1 {
|
||||
if case let .members(isGroup, _, _) = component.stateContext.subject {
|
||||
sectionTitle = isGroup ? environment.strings.BoostGift_Members_SectionTitle : environment.strings.BoostGift_Subscribers_SectionTitle
|
||||
@ -1637,10 +1649,19 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
}
|
||||
|
||||
let footerValue = environment.strings.Story_Privacy_KeepOnMyPageHours(Int32(component.timeout / 3600))
|
||||
var footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
|
||||
|
||||
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
|
||||
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string
|
||||
var footerText: String
|
||||
if case let .stories(_, count) = component.stateContext.subject, count > 1 {
|
||||
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
|
||||
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageManyInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageManyInfo(footerValue).string
|
||||
} else {
|
||||
footerText = environment.strings.Story_Privacy_KeepOnMyPageManyInfo(footerValue).string
|
||||
}
|
||||
} else {
|
||||
if let sendAsPeerId = self.sendAsPeerId, sendAsPeerId.isGroupOrChannel == true {
|
||||
footerText = isSendAsGroup ? environment.strings.Story_Privacy_KeepOnGroupPageInfo(footerValue).string : environment.strings.Story_Privacy_KeepOnChannelPageInfo(footerValue).string
|
||||
} else {
|
||||
footerText = environment.strings.Story_Privacy_KeepOnMyPageInfo(footerValue).string
|
||||
}
|
||||
}
|
||||
|
||||
let footerSize = sectionFooter.update(
|
||||
@ -2371,7 +2392,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
)
|
||||
|
||||
var footersTotalHeight: CGFloat = 0.0
|
||||
if case let .stories(editing) = component.stateContext.subject {
|
||||
if case let .stories(editing, _) = component.stateContext.subject {
|
||||
let body = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.freeTextColor)
|
||||
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
|
||||
let link = MarkdownAttributeSet(font: Font.regular(13.0), textColor: environment.theme.list.itemAccentColor)
|
||||
@ -2451,7 +2472,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
itemHeight: peerItemSize.height,
|
||||
itemCount: peers.count
|
||||
))
|
||||
} else if case let .stories(editing) = component.stateContext.subject {
|
||||
} else if case let .stories(editing, _) = component.stateContext.subject {
|
||||
if !editing && hasChannels {
|
||||
sections.append(ItemLayout.Section(
|
||||
id: 0,
|
||||
@ -2533,12 +2554,17 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
switch component.stateContext.subject {
|
||||
case .peers:
|
||||
title = environment.strings.Story_Privacy_PostStoryAs
|
||||
case let .stories(editing):
|
||||
case let .stories(editing, count):
|
||||
if editing {
|
||||
title = environment.strings.Story_Privacy_EditStory
|
||||
} else {
|
||||
title = environment.strings.Story_Privacy_ShareStory
|
||||
actionButtonTitle = environment.strings.Story_Privacy_PostStory
|
||||
if count > 1 {
|
||||
title = environment.strings.Story_Privacy_ShareStories
|
||||
actionButtonTitle = environment.strings.Story_Privacy_PostStories(count)
|
||||
} else {
|
||||
title = environment.strings.Story_Privacy_ShareStory
|
||||
actionButtonTitle = environment.strings.Story_Privacy_PostStory
|
||||
}
|
||||
}
|
||||
case let .chats(grayList):
|
||||
if grayList {
|
||||
@ -2627,7 +2653,7 @@ final class ShareWithPeersScreenComponent: Component {
|
||||
inset = 1000.0
|
||||
} else if case .channels = component.stateContext.subject {
|
||||
inset = 1000.0
|
||||
} else if case let .stories(editing) = component.stateContext.subject {
|
||||
} else if case let .stories(editing, _) = component.stateContext.subject {
|
||||
if editing {
|
||||
inset = 351.0
|
||||
inset += 10.0 + environment.safeInsets.bottom + 50.0 + footersTotalHeight
|
||||
@ -3026,7 +3052,7 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer {
|
||||
var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = []
|
||||
var optionItems: [ShareWithPeersScreenComponent.OptionItem] = []
|
||||
var coverItem: ShareWithPeersScreenComponent.CoverItem?
|
||||
if case let .stories(editing) = stateContext.subject {
|
||||
if case let .stories(editing, _) = stateContext.subject {
|
||||
var everyoneSubtitle = presentationData.strings.Story_Privacy_ExcludePeople
|
||||
if (stateContext.stateValue?.savedSelectedPeers[.everyone]?.count ?? 0) > 0 {
|
||||
var peerNamesArray: [String] = []
|
||||
|
@ -44,7 +44,7 @@ public extension ShareWithPeersScreen {
|
||||
final class StateContext {
|
||||
public enum Subject: Equatable {
|
||||
case peers(peers: [EnginePeer], peerId: EnginePeer.Id?)
|
||||
case stories(editing: Bool)
|
||||
case stories(editing: Bool, count: Int32)
|
||||
case chats(blocked: Bool)
|
||||
case contacts(base: EngineStoryPrivacy.Base)
|
||||
case contactsSearch(query: String, onlyContacts: Bool)
|
||||
|
@ -5068,7 +5068,7 @@ public final class StoryItemSetContainerComponent: Component {
|
||||
|
||||
let stateContext = ShareWithPeersScreen.StateContext(
|
||||
context: context,
|
||||
subject: .stories(editing: true),
|
||||
subject: .stories(editing: true, count: 1),
|
||||
editing: true,
|
||||
initialSelectedPeers: selectedPeers,
|
||||
closeFriends: component.closeFriends.get(),
|
||||
|
@ -3572,7 +3572,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
return mediaPickerController(context: context, hasSearch: hasSearch, completion: completion)
|
||||
}
|
||||
|
||||
public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any]) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
|
||||
public func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController {
|
||||
return storyMediaPickerController(context: context, isDark: isDark, forCollage: forCollage, selectionLimit: selectionLimit, getSourceRect: getSourceRect, completion: completion, multipleCompletion: multipleCompletion, dismissed: dismissed, groupsPresented: groupsPresented)
|
||||
}
|
||||
|
||||
@ -3737,7 +3737,7 @@ public final class SharedAccountContextImpl: SharedAccountContext {
|
||||
externalState.storyTarget = target
|
||||
|
||||
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
}
|
||||
|
||||
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|
||||
|
@ -391,6 +391,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
return .asset(asset)
|
||||
case let .draft(draft):
|
||||
return .draft(draft, nil)
|
||||
case let .assets(assets):
|
||||
return .assets(assets)
|
||||
}
|
||||
}
|
||||
|
||||
@ -451,7 +453,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
|
||||
if let customTarget, case .botPreview = customTarget {
|
||||
externalState.storyTarget = customTarget
|
||||
self.proceedWithStoryUpload(target: customTarget, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
self.proceedWithStoryUpload(target: customTarget, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
|
||||
dismissCameraImpl?()
|
||||
return
|
||||
@ -484,7 +486,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
externalState.isPeerArchived = channel.storiesHidden ?? false
|
||||
}
|
||||
|
||||
self.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
self.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
|
||||
dismissCameraImpl?()
|
||||
})
|
||||
@ -548,8 +550,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
})
|
||||
}
|
||||
|
||||
public func proceedWithStoryUpload(target: Stories.PendingTarget, result: MediaEditorScreenResult, existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) {
|
||||
guard let result = result as? MediaEditorScreenImpl.Result else {
|
||||
public func proceedWithStoryUpload(target: Stories.PendingTarget, results: [MediaEditorScreenResult], existingMedia: EngineMedia?, forwardInfo: Stories.PendingForwardInfo?, externalState: MediaEditorTransitionOutExternalState, commit: @escaping (@escaping () -> Void) -> Void) {
|
||||
guard let results = results as? [MediaEditorScreenImpl.Result] else {
|
||||
return
|
||||
}
|
||||
let context = self.context
|
||||
@ -657,83 +659,85 @@ public final class TelegramRootController: NavigationController, TelegramRootCon
|
||||
}
|
||||
|
||||
if let _ = self.chatListController as? ChatListControllerImpl {
|
||||
var media: EngineStoryInputMedia?
|
||||
for result in results {
|
||||
var media: EngineStoryInputMedia?
|
||||
|
||||
if let mediaResult = result.media {
|
||||
switch mediaResult {
|
||||
case let .image(image, dimensions):
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||
defer {
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
|
||||
media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
|
||||
}
|
||||
case let .video(content, firstFrameImage, 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)
|
||||
|
||||
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))
|
||||
}
|
||||
if let mediaResult = result.media {
|
||||
switch mediaResult {
|
||||
case let .image(image, dimensions):
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||
defer {
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
|
||||
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
|
||||
let file = TempBox.shared.tempFile(fileName: "image.jpg")
|
||||
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
|
||||
return file
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
|
||||
media = .image(dimensions: dimensions, data: imageData, stickers: result.stickers)
|
||||
}
|
||||
case let .video(content, firstFrameImage, 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)
|
||||
|
||||
var coverTime: Double?
|
||||
if let coverImageTimestamp = values.coverImageTimestamp {
|
||||
if let trimRange = values.videoTrimRange {
|
||||
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound)
|
||||
} else {
|
||||
coverTime = min(duration, coverImageTimestamp)
|
||||
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))
|
||||
}
|
||||
let tempFile = TempBox.shared.tempFile(fileName: "file")
|
||||
defer {
|
||||
TempBox.shared.dispose(tempFile)
|
||||
}
|
||||
let imageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
|
||||
let firstFrameFile = imageData.flatMap { data -> TempBoxFile? in
|
||||
let file = TempBox.shared.tempFile(fileName: "image.jpg")
|
||||
if let _ = try? data.write(to: URL(fileURLWithPath: file.path)) {
|
||||
return file
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime)
|
||||
var coverTime: Double?
|
||||
if let coverImageTimestamp = values.coverImageTimestamp {
|
||||
if let trimRange = values.videoTrimRange {
|
||||
coverTime = min(duration, coverImageTimestamp - trimRange.lowerBound)
|
||||
} else {
|
||||
coverTime = min(duration, coverImageTimestamp)
|
||||
}
|
||||
}
|
||||
|
||||
media = .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: coverTime)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
default:
|
||||
break
|
||||
} else if let existingMedia {
|
||||
media = .existing(media: existingMedia._asMedia())
|
||||
}
|
||||
} else if let existingMedia {
|
||||
media = .existing(media: existingMedia._asMedia())
|
||||
}
|
||||
|
||||
if let media {
|
||||
let _ = (context.engine.messages.uploadStory(
|
||||
target: target,
|
||||
media: media,
|
||||
mediaAreas: result.mediaAreas,
|
||||
text: result.caption.string,
|
||||
entities: generateChatInputTextEntities(result.caption),
|
||||
pin: result.options.pin,
|
||||
privacy: result.options.privacy,
|
||||
isForwardingDisabled: result.options.isForwardingDisabled,
|
||||
period: result.options.timeout,
|
||||
randomId: result.randomId,
|
||||
forwardInfo: forwardInfo
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { stableId in
|
||||
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId))
|
||||
})
|
||||
if let media {
|
||||
let _ = (context.engine.messages.uploadStory(
|
||||
target: target,
|
||||
media: media,
|
||||
mediaAreas: result.mediaAreas,
|
||||
text: result.caption.string,
|
||||
entities: generateChatInputTextEntities(result.caption),
|
||||
pin: result.options.pin,
|
||||
privacy: result.options.privacy,
|
||||
isForwardingDisabled: result.options.isForwardingDisabled,
|
||||
period: result.options.timeout,
|
||||
randomId: result.randomId,
|
||||
forwardInfo: forwardInfo
|
||||
)
|
||||
|> deliverOnMainQueue).startStandalone(next: { stableId in
|
||||
moveStorySource(engine: context.engine, peerId: context.account.peerId, from: result.randomId, to: Int64(stableId))
|
||||
})
|
||||
}
|
||||
}
|
||||
completionImpl()
|
||||
}
|
||||
|
@ -1483,7 +1483,7 @@ public final class WebAppController: ViewController, AttachmentContainable {
|
||||
externalState.storyTarget = target
|
||||
|
||||
if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
|
||||
rootController.proceedWithStoryUpload(target: target, result: result, existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
rootController.proceedWithStoryUpload(target: target, results: [result], existingMedia: nil, forwardInfo: nil, externalState: externalState, commit: commit)
|
||||
}
|
||||
})
|
||||
if let navigationController = self.controller?.getNavigationController() {
|
||||
|
Loading…
x
Reference in New Issue
Block a user