Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,77 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
swift_library(
name = "MediaEditorScreen",
module_name = "MediaEditorScreen",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
],
deps = [
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
"//submodules/Display:Display",
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
"//submodules/ComponentFlow:ComponentFlow",
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
"//submodules/TelegramPresentationData:TelegramPresentationData",
"//submodules/AccountContext:AccountContext",
"//submodules/AppBundle:AppBundle",
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
"//submodules/TelegramUIPreferences:TelegramUIPreferences",
"//submodules/PresentationDataUtils:PresentationDataUtils",
"//submodules/ContextUI",
"//submodules/LegacyComponents:LegacyComponents",
"//submodules/LegacyMediaPickerUI",
"//submodules/TelegramUI/Components/MediaEditor",
"//submodules/DrawingUI:DrawingUI",
"//submodules/Components/LottieAnimationComponent:LottieAnimationComponent",
"//submodules/Components/BundleIconComponent:BundleIconComponent",
"//submodules/TelegramUI/Components/MessageInputPanelComponent",
"//submodules/TelegramUI/Components/TextFieldComponent",
"//submodules/TelegramUI/Components/ChatInputNode",
"//submodules/TelegramUI/Components/ChatEntityKeyboardInputNode",
"//submodules/TelegramUI/Components/PlainButtonComponent",
"//submodules/TooltipUI",
"//submodules/AvatarNode",
"//submodules/TelegramUI/Components/ShareWithPeersScreen",
"//submodules/TelegramUI/Components/CameraButtonComponent",
"//submodules/ChatPresentationInterfaceState",
"//submodules/DeviceAccess",
"//submodules/LocationUI",
"//submodules/TelegramUI/Components/AudioWaveformComponent",
"//submodules/ReactionSelectionNode",
"//submodules/TelegramUI/Components/VolumeSliderContextItem",
"//submodules/TelegramUI/Components/Stories/ForwardInfoPanelComponent",
"//submodules/TelegramUI/Components/ContextReferenceButtonComponent",
"//submodules/TelegramUI/Components/MediaScrubberComponent",
"//submodules/Components/BlurredBackgroundComponent",
"//submodules/TelegramUI/Components/DustEffect",
"//submodules/WebPBinding",
"//submodules/StickerResources",
"//submodules/StickerPeekUI",
"//submodules/TelegramUI/Components/Stickers/StickerPackEditTitleController",
"//submodules/TelegramUI/Components/StickerPickerScreen",
"//submodules/UIKitRuntimeUtils",
"//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation",
"//submodules/Components/HierarchyTrackingLayer",
"//submodules/TelegramUI/Components/ListSectionComponent",
"//submodules/WebsiteType",
"//submodules/UrlEscaping",
"//submodules/DeviceLocationManager",
"//submodules/TelegramUI/Components/SaveProgressScreen",
"//submodules/TelegramUI/Components/MediaAssetsContext",
"//submodules/CheckNode",
"//submodules/TelegramNotices",
"//submodules/TelegramUI/Components/AttachmentFileController",
"//submodules/SaveToCameraRoll",
],
visibility = [
"//visibility:public",
],
)
@@ -0,0 +1,458 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import LegacyComponents
import MediaEditor
final class AdjustmentSliderComponent: Component {
typealias EnvironmentType = Empty
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
let isEnabled: Bool
let trackColor: UIColor?
let displayValue: Bool
let valueUpdated: (Float) -> Void
let isTrackingUpdated: ((Bool) -> Void)?
init(
title: String,
value: Float,
minValue: Float,
maxValue: Float,
startValue: Float,
isEnabled: Bool,
trackColor: UIColor?,
displayValue: Bool,
valueUpdated: @escaping (Float) -> Void,
isTrackingUpdated: ((Bool) -> Void)? = nil
) {
self.title = title
self.value = value
self.minValue = minValue
self.maxValue = maxValue
self.startValue = startValue
self.isEnabled = isEnabled
self.trackColor = trackColor
self.displayValue = displayValue
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: AdjustmentSliderComponent, rhs: AdjustmentSliderComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.minValue != rhs.minValue {
return false
}
if lhs.maxValue != rhs.maxValue {
return false
}
if lhs.startValue != rhs.startValue {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.trackColor != rhs.trackColor {
return false
}
if lhs.displayValue != rhs.displayValue {
return false
}
return true
}
final class View: UIView, UITextFieldDelegate {
private let title = ComponentView<Empty>()
private let value = ComponentView<Empty>()
private var sliderView: TGPhotoEditorSliderView?
private var component: AdjustmentSliderComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AdjustmentSliderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
var internalIsTrackingUpdated: ((Bool) -> Void)?
if let isTrackingUpdated = component.isTrackingUpdated {
internalIsTrackingUpdated = { [weak self] isTracking in
if let self {
if isTracking {
self.sliderView?.bordered = true
} else {
Queue.mainQueue().after(0.1) {
self.sliderView?.bordered = false
}
}
isTrackingUpdated(isTracking)
let transition: ComponentTransition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
if let titleView = self.title.view {
transition.setAlpha(view: titleView, alpha: isTracking ? 0.0 : 1.0)
}
if let valueView = self.value.view {
transition.setAlpha(view: valueView, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
let sliderView: TGPhotoEditorSliderView
if let current = self.sliderView {
sliderView = current
sliderView.value = CGFloat(component.value)
} else {
sliderView = TGPhotoEditorSliderView()
sliderView.backgroundColor = .clear
sliderView.startColor = UIColor(rgb: 0xffffff)
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.minimumValue = CGFloat(component.minValue)
sliderView.maximumValue = CGFloat(component.maxValue)
sliderView.startValue = CGFloat(component.startValue)
sliderView.value = CGFloat(component.value)
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
sliderView.layer.allowsGroupOpacity = true
self.sliderView = sliderView
self.addSubview(sliderView)
}
sliderView.interactionBegan = {
internalIsTrackingUpdated?(true)
}
sliderView.interactionEnded = {
internalIsTrackingUpdated?(false)
}
if component.isEnabled {
sliderView.alpha = 1.3
sliderView.trackColor = component.trackColor ?? UIColor(rgb: 0xffffff)
sliderView.isUserInteractionEnabled = true
} else {
sliderView.trackColor = UIColor(rgb: 0xffffff)
sliderView.alpha = 0.3
sliderView.isUserInteractionEnabled = false
}
transition.setFrame(view: sliderView, frame: CGRect(origin: CGPoint(x: 22.0, y: 7.0), size: CGSize(width: availableSize.width - 22.0 * 2.0, height: 44.0)))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(
Text(text: component.title, font: Font.regular(14.0), color: UIColor(rgb: 0x808080))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: 21.0, y: 0.0), size: titleSize))
}
let valueText: String
if component.displayValue {
if component.value > 0.005 {
valueText = String(format: "+%.2f", component.value)
} else if component.value < -0.005 {
valueText = String(format: "%.2f", component.value)
} else {
valueText = ""
}
} else {
valueText = ""
}
let valueSize = self.value.update(
transition: .immediate,
component: AnyComponent(
Text(text: valueText, font: Font.with(size: 14.0, traits: .monospacedNumbers), color: UIColor(rgb: 0xffd300))
),
environment: {},
containerSize: CGSize(width: 100.0, height: 100.0)
)
if let valueView = self.value.view {
if valueView.superview == nil {
self.addSubview(valueView)
}
transition.setFrame(view: valueView, frame: CGRect(origin: CGPoint(x: availableSize.width - 21.0 - valueSize.width, y: 0.0), size: valueSize))
}
return CGSize(width: availableSize.width, height: 52.0)
}
@objc private func sliderValueChanged() {
guard let component = self.component, let sliderView = self.sliderView else {
return
}
component.valueUpdated(Float(sliderView.value))
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
struct AdjustmentTool: Equatable {
let key: EditorToolKey
let title: String
let value: Float
let minValue: Float
let maxValue: Float
let startValue: Float
}
final class AdjustmentsComponent: Component {
typealias EnvironmentType = Empty
let tools: [AdjustmentTool]
let valueUpdated: (EditorToolKey, Float) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
tools: [AdjustmentTool],
valueUpdated: @escaping (EditorToolKey, Float) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.tools = tools
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: AdjustmentsComponent, rhs: AdjustmentsComponent) -> Bool {
if lhs.tools != rhs.tools {
return false
}
return true
}
final class View: UIView {
private let scrollView = UIScrollView()
private var toolViews: [ComponentView<Empty>] = []
private var component: AdjustmentsComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.scrollView.showsVerticalScrollIndicator = false
super.init(frame: frame)
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AdjustmentsComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let valueUpdated = component.valueUpdated
let isTrackingUpdated: (EditorToolKey, Bool) -> Void = { [weak self] trackingTool, isTracking in
component.isTrackingUpdated(isTracking)
if let self {
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
if tool.key != trackingTool && i < self.toolViews.count {
if let view = self.toolViews[i].view {
let transition: ComponentTransition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
transition.setAlpha(view: view, alpha: isTracking ? 0.0 : 1.0)
}
}
}
}
}
var sizes: [CGSize] = []
for i in 0 ..< component.tools.count {
let tool = component.tools[i]
let componentView: ComponentView<Empty>
if i >= self.toolViews.count {
componentView = ComponentView<Empty>()
self.toolViews.append(componentView)
} else {
componentView = self.toolViews[i]
}
var valueIsNegative = false
var value = tool.value
if case .enhance = tool.key {
if value < 0.0 {
valueIsNegative = true
}
value = abs(value)
}
let size = componentView.update(
transition: transition,
component: AnyComponent(
AdjustmentSliderComponent(
title: tool.title,
value: value,
minValue: tool.minValue,
maxValue: tool.maxValue,
startValue: tool.startValue,
isEnabled: true,
trackColor: nil,
displayValue: true,
valueUpdated: { value in
var updatedValue = value
if valueIsNegative {
updatedValue *= -1.0
}
valueUpdated(tool.key, updatedValue)
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(tool.key, isTracking)
}
)
),
environment: {},
containerSize: availableSize
)
sizes.append(size)
}
var origin: CGPoint = CGPoint(x: 0.0, y: 11.0)
for i in 0 ..< component.tools.count {
let size = sizes[i]
let componentView = self.toolViews[i]
if let view = componentView.view {
if view.superview == nil {
self.scrollView.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
}
origin = origin.offsetBy(dx: 0.0, dy: size.height)
}
let size = CGSize(width: availableSize.width, height: 180.0)
let contentSize = CGSize(width: availableSize.width, height: origin.y)
if contentSize != self.scrollView.contentSize {
self.scrollView.contentSize = contentSize
}
transition.setFrame(view: self.scrollView, frame: CGRect(origin: .zero, size: size))
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class AdjustmentsScreenComponent: Component {
typealias EnvironmentType = Empty
let toggleUneditedPreview: (Bool) -> Void
init(
toggleUneditedPreview: @escaping (Bool) -> Void
) {
self.toggleUneditedPreview = toggleUneditedPreview
}
static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool {
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var component: AdjustmentsScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:)))
longPressGestureRecognizer.minimumPressDuration = 0.05
self.addGestureRecognizer(longPressGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.toggleUneditedPreview(true)
case .ended, .cancelled:
component.toggleUneditedPreview(false)
default:
break
}
}
func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,866 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import MediaEditor
import TelegramPresentationData
private final class BlurModeComponent: Component {
typealias EnvironmentType = Empty
let title: String
let icon: UIImage?
let isSelected: Bool
init(
title: String,
icon: UIImage?,
isSelected: Bool
) {
self.title = title
self.icon = icon
self.isSelected = isSelected
}
static func ==(lhs: BlurModeComponent, rhs: BlurModeComponent) -> Bool {
if lhs.title != rhs.title {
return false
}
if lhs.icon !== rhs.icon {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
return true
}
final class View: UIView {
private let icon = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private var component: BlurModeComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BlurModeComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let iconSize = self.icon.update(
transition: transition,
component: AnyComponent(
Image(
image: component.icon,
tintColor: component.isSelected ? UIColor(rgb: 0xffd300) : .white,
size: CGSize(width: 30.0, height: 30.0)
)
),
environment: {},
containerSize: availableSize
)
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
Text(
text: component.title,
font: Font.regular(14.0),
color: component.isSelected ? UIColor(rgb: 0xffd300) : UIColor(rgb: 0x808080)
)
),
environment: {},
containerSize: availableSize
)
let spacing: CGFloat = 3.0
let size = CGSize(width: 66.0, height: iconSize.height + spacing + titleSize.height)
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - iconSize.width) / 2.0), y: 0.0), size: iconSize)
if let view = self.icon.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: iconFrame)
}
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - titleSize.width) / 2.0), y: iconSize.height + spacing), size: titleSize)
if let view = self.title.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: titleFrame)
}
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class BlurComponent: Component {
typealias EnvironmentType = Empty
let strings: PresentationStrings
let value: BlurValue
let hasPortrait: Bool
let valueUpdated: (BlurValue) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
strings: PresentationStrings,
value: BlurValue,
hasPortrait: Bool,
valueUpdated: @escaping (BlurValue) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.strings = strings
self.value = value
self.hasPortrait = hasPortrait
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: BlurComponent, rhs: BlurComponent) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.value != rhs.value {
return false
}
if lhs.hasPortrait != rhs.hasPortrait {
return false
}
return true
}
func makeState() -> State {
return State(value: self.value)
}
final class State: ComponentState {
var value: BlurValue
init(value: BlurValue) {
self.value = value
}
}
final class View: UIView {
private let title = ComponentView<Empty>()
private let offButton = ComponentView<Empty>()
private let radialButton = ComponentView<Empty>()
private let linearButton = ComponentView<Empty>()
private let portraitButton = ComponentView<Empty>()
private let slider = ComponentView<Empty>()
private var component: BlurComponent?
private weak var state: State?
private let offImage = UIImage(bundleImageName: "Media Editor/BlurOff")
private let radialImage = UIImage(bundleImageName: "Media Editor/BlurRadial")
private let linearImage = UIImage(bundleImageName: "Media Editor/BlurLinear")
private let portraitImage = UIImage(bundleImageName: "Media Editor/BlurPortrait")
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: BlurComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
state.value = component.value
let valueUpdated = component.valueUpdated
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(
Text(
text: component.strings.Story_Editor_Blur_Title,
font: Font.regular(14.0),
color: UIColor(rgb: 0x808080)
)
),
environment: {},
containerSize: availableSize
)
let titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - titleSize.width) / 2.0), y: 11.0), size: titleSize)
if let view = self.title.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: titleFrame)
}
let offButtonSize = self.offButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
BlurModeComponent(
title: component.strings.Story_Editor_Blur_Off,
icon: self.offImage,
isSelected: state.value.mode == .off
)
),
action: { [weak state] in
if let state {
valueUpdated(state.value.withUpdatedMode(.off))
}
}
)
),
environment: {},
containerSize: availableSize
)
let _ = self.radialButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
BlurModeComponent(
title: component.strings.Story_Editor_Blur_Radial,
icon: self.radialImage,
isSelected: state.value.mode == .radial
)
),
action: { [weak state] in
if let state {
valueUpdated(state.value.withUpdatedMode(.radial))
}
}
)
),
environment: {},
containerSize: availableSize
)
let _ = self.linearButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
BlurModeComponent(
title: component.strings.Story_Editor_Blur_Linear,
icon: self.linearImage,
isSelected: state.value.mode == .linear
)
),
action: { [weak state] in
if let state {
valueUpdated(state.value.withUpdatedMode(.linear))
}
}
)
),
environment: {},
containerSize: availableSize
)
let _ = self.portraitButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
BlurModeComponent(
title: component.strings.Story_Editor_Blur_Portrait,
icon: self.portraitImage,
isSelected: state.value.mode == .portrait
)
),
action: { [weak state] in
if let state {
valueUpdated(state.value.withUpdatedMode(.portrait))
}
}
)
),
environment: {},
containerSize: availableSize
)
let isTrackingUpdated: (Bool) -> Void = { [weak self] isTracking in
component.isTrackingUpdated(isTracking)
if let self {
let transition: ComponentTransition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
let alpha: CGFloat = isTracking ? 0.0 : 1.0
if let view = self.title.view {
transition.setAlpha(view: view, alpha: alpha)
}
if let view = self.offButton.view {
transition.setAlpha(view: view, alpha: alpha)
}
if let view = self.radialButton.view {
transition.setAlpha(view: view, alpha: alpha)
}
if let view = self.linearButton.view {
transition.setAlpha(view: view, alpha: alpha)
}
if let view = self.portraitButton.view {
transition.setAlpha(view: view, alpha: alpha)
}
}
}
let sliderSize = self.slider.update(
transition: transition,
component: AnyComponent(
AdjustmentSliderComponent(
title: "",
value: state.value.intensity,
minValue: 0.0,
maxValue: 1.0,
startValue: 0.0,
isEnabled: state.value.mode != .off,
trackColor: nil,
displayValue: false,
valueUpdated: { [weak state] value in
if let state {
valueUpdated(state.value.withUpdatedIntensity(value))
}
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(isTracking)
}
)
),
environment: {},
containerSize: availableSize
)
var buttons = [self.offButton, self.radialButton, self.linearButton]
if component.hasPortrait {
buttons.append(self.portraitButton)
}
let topInset: CGFloat = 34.0
let horizontalSpacing: CGFloat = 24.0
let width: CGFloat = CGFloat(buttons.count) * offButtonSize.width + (CGFloat(buttons.count - 1) * horizontalSpacing)
let commonX = floorToScreenPixels((availableSize.width - width) / 2.0)
var offsetX: CGFloat = commonX
for button in buttons {
if let view = button.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: CGPoint(x: offsetX, y: topInset), size: offButtonSize))
}
offsetX += offButtonSize.width + horizontalSpacing
}
let verticalSpacing: CGFloat = -5.0
let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + offButtonSize.height + verticalSpacing), size: sliderSize)
if let view = self.slider.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: sliderFrame)
}
return CGSize(width: availableSize.width, height: topInset + offButtonSize.height + verticalSpacing + sliderSize.height + 6.0)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private let blurInsetProximity: CGFloat = 20.0
private let blurMinimumFalloff: Float = 0.1
private let blurMinimumDifference: Float = 0.02
private let blurViewCenterInset: CGFloat = 30.0
private let blurViewRadiusInset: CGFloat = 30.0
final class BlurScreenComponent: Component {
typealias EnvironmentType = Empty
let value: BlurValue
let valueUpdated: (BlurValue) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
value: BlurValue,
valueUpdated: @escaping (BlurValue) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.value = value
self.valueUpdated = valueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: BlurScreenComponent, rhs: BlurScreenComponent) -> Bool {
if lhs.value != rhs.value {
return false
}
return true
}
final class View: UIView, UIGestureRecognizerDelegate {
enum Control {
case center
case innerRadius
case outerRadius
case rotation
case wholeArea
}
private var component: BlurScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.contentMode = .redraw
self.isOpaque = false
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
panGestureRecognizer.delegate = self
self.addGestureRecognizer(panGestureRecognizer)
let pinchGestureRecognizer = UIPinchGestureRecognizer(target: self, action: #selector(self.handlePinch(_:)))
pinchGestureRecognizer.delegate = self
self.addGestureRecognizer(pinchGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var activeControl: Control?
private var startCenterPoint: CGPoint?
private var startDistance: CGFloat?
private var startRadius: CGFloat?
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
let location = gestureRecognizer.location(in: gestureRecognizer.view)
let centerPoint = CGPoint(
x: component.value.position.x * self.frame.width,
y: component.value.position.y * self.frame.height
)
let delta = CGPoint(x: location.x - centerPoint.x, y: location.y - centerPoint.y)
let shorterSide = min(self.frame.width, self.frame.height)
let innerRadius = shorterSide * CGFloat(component.value.falloff)
let outerRadius = shorterSide * CGFloat(component.value.size)
switch gestureRecognizer.state {
case .began:
switch component.value.mode {
case .radial:
component.isTrackingUpdated(true)
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
let close = abs(outerRadius - innerRadius) < blurInsetProximity
let innerRadiusOuterInset = close ? 0 : blurViewRadiusInset
let outerRadiusInnerInset = close ? 0 : blurViewRadiusInset
if distance < blurViewCenterInset {
self.activeControl = .center
self.startCenterPoint = centerPoint
}
else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
self.activeControl = .innerRadius
self.startDistance = distance
self.startRadius = innerRadius
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
self.activeControl = .outerRadius
self.startDistance = distance
self.startRadius = outerRadius
}
case .linear:
component.isTrackingUpdated(true)
let radialDistance = sqrt(delta.x * delta.x + delta.y * delta.y)
let distance = abs(delta.x * cos(CGFloat(component.value.rotation) + .pi / 2.0) + delta.y * sin(CGFloat(component.value.rotation) + .pi / 2.0))
let close = abs(outerRadius - innerRadius) < blurInsetProximity
let innerRadiusOuterInset = close ? 0 : blurViewRadiusInset
let outerRadiusInnerInset = close ? 0 : blurViewRadiusInset
if radialDistance < blurViewCenterInset {
self.activeControl = .center
self.startCenterPoint = centerPoint
} else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
self.activeControl = .innerRadius
self.startDistance = distance
self.startRadius = innerRadius
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
self.activeControl = .outerRadius;
self.startDistance = distance
self.startRadius = outerRadius
} else if distance <= innerRadius - blurViewRadiusInset || distance >= outerRadius + blurViewRadiusInset {
self.activeControl = .rotation
}
default:
break
}
case .changed:
switch component.value.mode {
case .radial:
guard let activeControl = self.activeControl else {
return
}
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
switch activeControl {
case .center:
guard let startCenterPoint = self.startCenterPoint else {
return
}
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
let centerPoint = CGPoint(
x: max(0.0, min(self.frame.width, startCenterPoint.x + translation.x)),
y: max(0.0, min(self.frame.height, startCenterPoint.y + translation.y))
)
let position = CGPoint(
x: centerPoint.x / self.frame.width,
y: centerPoint.y / self.frame.height
)
component.valueUpdated(component.value.withUpdatedPosition(position))
case .innerRadius:
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
return
}
let delta = distance - startDistance
let falloff = min(max(blurMinimumFalloff, Float((startRadius + delta) / shorterSide)), component.value.size - blurMinimumDifference)
component.valueUpdated(component.value.withUpdatedFalloff(falloff))
case .outerRadius:
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
return
}
let delta = distance - startDistance
let size = max(component.value.falloff + blurMinimumDifference, Float((startRadius + delta) / shorterSide))
component.valueUpdated(component.value.withUpdatedSize(size))
default:
break
}
case .linear:
guard let activeControl = self.activeControl else {
return
}
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
switch activeControl {
case .center:
guard let startCenterPoint = self.startCenterPoint else {
return
}
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
let centerPoint = CGPoint(
x: max(0.0, min(self.frame.width, startCenterPoint.x + translation.x)),
y: max(0.0, min(self.frame.height, startCenterPoint.y + translation.y))
)
let position = CGPoint(
x: centerPoint.x / self.frame.width,
y: centerPoint.y / self.frame.height
)
component.valueUpdated(component.value.withUpdatedPosition(position))
case .innerRadius:
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
return
}
let delta = distance - startDistance
let falloff = min(max(blurMinimumFalloff, Float((startRadius + delta) / shorterSide)), component.value.size - blurMinimumDifference)
component.valueUpdated(component.value.withUpdatedFalloff(falloff))
case .outerRadius:
guard let startDistance = self.startDistance, let startRadius = self.startRadius else {
return
}
let delta = distance - startDistance
let size = max(component.value.falloff + blurMinimumDifference, Float((startRadius + delta) / shorterSide))
component.valueUpdated(component.value.withUpdatedSize(size))
case .rotation:
let translation = gestureRecognizer.translation(in: gestureRecognizer.view)
var clockwise = false
let right = location.x > centerPoint.x
let bottom = location.y > centerPoint.y
if !right && !bottom {
if abs(translation.y) > abs(translation.x) {
if translation.y < 0.0 {
clockwise = true
}
} else {
if translation.x > 0.0 {
clockwise = true
}
}
} else if right && !bottom {
if abs(translation.y) > abs(translation.x) {
if translation.y > 0.0 {
clockwise = true
}
} else
{
if translation.x > 0.0 {
clockwise = true
}
}
} else if right && bottom {
if abs(translation.y) > abs(translation.x) {
if translation.y > 0 {
clockwise = true
}
} else {
if translation.x < 0 {
clockwise = true
}
}
} else {
if abs(translation.y) > abs(translation.x) {
if translation.y < 0 {
clockwise = true
}
} else {
if translation.x < 0 {
clockwise = true
}
}
}
let delta = sqrt(translation.x * translation.x + translation.y * translation.y)
let angleInDegress = radiansToDegrees(radians: CGFloat(component.value.rotation))
let updatedAngle = angleInDegress + delta * (clockwise ? 1.0 : -1.0) / .pi / 1.15
component.valueUpdated(component.value.withUpdatedRotation(Float(degreesToRadians(degrees: updatedAngle))))
gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view)
default:
break
}
default:
break
}
default:
component.isTrackingUpdated(false)
self.activeControl = nil
self.startCenterPoint = nil
self.startDistance = nil
self.startRadius = nil
}
}
@objc func handlePinch(_ gestureRecognizer: UIPinchGestureRecognizer) {
guard let component = self.component else {
return
}
switch gestureRecognizer.state {
case .began:
component.isTrackingUpdated(true)
self.activeControl = .wholeArea
case .changed:
let scale = Float(gestureRecognizer.scale)
let size = max(component.value.falloff + blurMinimumDifference, component.value.size * scale)
let falloff = max(blurMinimumFalloff, component.value.falloff * scale)
component.valueUpdated(component.value.withUpdatedSize(size).withUpdatedFalloff(falloff))
gestureRecognizer.scale = 1.0
default:
component.isTrackingUpdated(false)
self.activeControl = nil
}
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
guard let component = self.component else {
return false
}
let location = gestureRecognizer.location(in: gestureRecognizer.view)
let centerPoint = CGPoint(
x: component.value.position.x * self.frame.width,
y: component.value.position.y * self.frame.height
)
let delta = CGPoint(x: location.x - centerPoint.x, y: location.y - centerPoint.y)
let innerRadius = min(self.frame.width, self.frame.height) * CGFloat(component.value.falloff)
let outerRadius = min(self.frame.width, self.frame.height) * CGFloat(component.value.size)
switch component.value.mode {
case .radial:
let distance = sqrt(delta.x * delta.x + delta.y * delta.y)
let close = abs(outerRadius - innerRadius) < blurInsetProximity
let innerRadiusOuterInset = close ? 0.0 : blurViewRadiusInset
let outerRadiusInnerInset = close ? 0.0 : blurViewRadiusInset
if distance < blurViewCenterInset && gestureRecognizer is UIPanGestureRecognizer {
return true
} else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
return true
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
return true
}
case .linear:
let radialDistance = sqrt(delta.x * delta.x + delta.y * delta.y)
let distance = abs(delta.x * cos(CGFloat(component.value.rotation) + .pi / 2.0) + delta.y * sin(CGFloat(component.value.rotation) + .pi / 2.0))
let close = abs(outerRadius - innerRadius) < blurInsetProximity
let innerRadiusOuterInset = close ? 0.0 : blurViewRadiusInset
let outerRadiusInnerInset = close ? 0.0 : blurViewRadiusInset
if radialDistance < blurViewCenterInset && gestureRecognizer is UIPanGestureRecognizer {
return true
} else if distance > innerRadius - blurViewRadiusInset && distance < innerRadius + innerRadiusOuterInset {
return true
} else if distance > outerRadius - outerRadiusInnerInset && distance < outerRadius + blurViewRadiusInset {
return true
} else if distance <= innerRadius - blurViewRadiusInset || distance >= outerRadius + blurViewRadiusInset {
return true
}
default:
break
}
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func update(component: BlurScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
self.setNeedsDisplay()
return availableSize
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext(), let component = self.component else {
return
}
guard ![.off, .portrait].contains(component.value.mode) else {
return
}
let centerPoint = CGPoint(
x: component.value.position.x * rect.size.width,
y: component.value.position.y * rect.size.height
)
let innerRadius = min(rect.size.width, rect.size.height) * CGFloat(component.value.falloff)
let outerRadius = min(rect.size.width, rect.size.height) * CGFloat(component.value.size)
context.setFillColor(UIColor.white.cgColor)
context.setShadow(offset: .zero, blur: 2.5, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
let knobSize = CGSize(width: 16.0, height: 16.0)
switch component.value.mode {
case .radial:
var radSpace = degreesToRadians(degrees: 6.15)
var radLen = degreesToRadians(degrees: 10.2)
for i in 0 ..< 22 {
let cgPath = CGMutablePath()
cgPath.addArc(
center: centerPoint,
radius: innerRadius,
startAngle: CGFloat(i) * (radSpace + radLen),
endAngle: CGFloat(i) * (radSpace + radLen) + radLen,
clockwise: false
)
let strokedArc = cgPath.copy(strokingWithWidth: 1.5, lineCap: .butt, lineJoin: .miter, miterLimit: 10.0)
context.addPath(strokedArc)
}
radSpace = degreesToRadians(degrees: 2.02)
radLen = degreesToRadians(degrees: 3.6)
for i in 0 ..< 64 {
let cgPath = CGMutablePath()
cgPath.addArc(
center: centerPoint,
radius: outerRadius,
startAngle: CGFloat(i) * (radSpace + radLen),
endAngle: CGFloat(i) * (radSpace + radLen) + radLen,
clockwise: false
)
let strokedArc = cgPath.copy(strokingWithWidth: 1.5, lineCap: .butt, lineJoin: .miter, miterLimit: 10.0)
context.addPath(strokedArc)
}
context.fillPath()
context.fillEllipse(in: CGRect(origin: CGPoint(x: centerPoint.x - knobSize.width / 2.0, y: centerPoint.y - knobSize.height / 2.0), size: knobSize))
case .linear:
context.translateBy(x: centerPoint.x, y: centerPoint.y)
context.rotate(by: CGFloat(component.value.rotation))
let space: CGFloat = 6.0
var length: CGFloat = 12.0
let thickness: CGFloat = 1.5
for i in 0 ..< 30 {
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: -innerRadius, width: length, height: thickness))
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: -innerRadius, width: length, height: thickness))
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: innerRadius, width: length, height: thickness))
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: innerRadius, width: length, height: thickness))
}
length = 6.0
for i in 0 ..< 64 {
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: -outerRadius, width: length, height: thickness))
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: -outerRadius, width: length, height: thickness))
context.addRect(CGRect(x: CGFloat(i) * (length + space), y: outerRadius, width: length, height: thickness))
context.addRect(CGRect(x: CGFloat(-i) * (length + space) - space - length, y: outerRadius, width: length, height: thickness))
}
context.fillPath()
context.fillEllipse(in: CGRect(origin: CGPoint(x: -knobSize.width / 2.0, y: -knobSize.height / 2.0), size: knobSize))
default:
break
}
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
private func degreesToRadians(degrees: CGFloat) -> CGFloat {
return degrees * .pi / 180.0
}
private func radiansToDegrees(radians: CGFloat) -> CGFloat {
return radians * 180.0 / .pi
}
@@ -0,0 +1,44 @@
import Foundation
import UIKit
import Display
final class CollageHighlightView: UIView {
private let borderLayer = SimpleLayer()
private let gradientView = UIImageView()
override public init(frame: CGRect) {
super.init(frame: frame)
self.borderLayer.cornerRadius = 12.0
self.borderLayer.borderWidth = 4.0
self.borderLayer.borderColor = UIColor.white.cgColor
self.layer.mask = self.borderLayer
self.addSubview(self.gradientView)
}
required public init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(size: CGSize, corners: CACornerMask, completion: @escaping () -> Void) {
self.borderLayer.maskedCorners = corners
self.borderLayer.frame = CGRect(origin: .zero, size: size)
let color = UIColor.white.withAlphaComponent(0.7)
let gradientWidth = size.width * 3.0
self.gradientView.image = generateGradientImage(
size: CGSize(width: gradientWidth, height: 24.0),
colors: [UIColor.white.withAlphaComponent(0.0), color, color, color, UIColor.white.withAlphaComponent(0.0)],
locations: [0.0, 0.2, 0.5, 0.8, 1.0],
direction: .horizontal
)
self.gradientView.frame = CGRect(origin: CGPoint(x: -gradientWidth, y: 0.0), size: CGSize(width: gradientWidth, height: size.height))
self.gradientView.layer.animatePosition(from: .zero, to: CGPoint(x: gradientWidth * 2.0, y: 0.0), duration: 1.4, additive: true, completion: { _ in
completion()
})
}
}
@@ -0,0 +1,235 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import ContextUI
import ChatPresentationInterfaceState
import AccountContext
import TelegramPresentationData
import WebsiteType
private enum OptionsId: Hashable {
case link
}
func presentLinkOptionsController(context: AccountContext, selfController: CreateLinkScreen, snapshotImage: UIImage?, isDark: Bool, sourceNode: ASDisplayNode, url: String, name: String, positionBelowText: Bool, largeMedia: Bool?, webPage: TelegramMediaWebpage, completion: @escaping (Bool, Bool?) -> Void, remove: @escaping () -> Void) {
var sources: [ContextController.Source] = []
if let source = linkOptions(context: context, selfController: selfController, snapshotImage: snapshotImage, isDark: isDark, sourceNode: sourceNode, url: url, text: name, positionBelowText: positionBelowText, largeMedia: largeMedia, webPage: webPage, completion: completion, remove: remove) {
sources.append(source)
}
if sources.isEmpty {
return
}
let contextController = ContextController(
presentationData: context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme),
configuration: ContextController.Configuration(
sources: sources,
initialId: AnyHashable(OptionsId.link)
)
)
selfController.presentInGlobalOverlay(contextController)
}
private func linkOptions(context: AccountContext, selfController: CreateLinkScreen, snapshotImage: UIImage?, isDark: Bool, sourceNode: ASDisplayNode, url: String, text: String, positionBelowText: Bool, largeMedia: Bool?, webPage: TelegramMediaWebpage, completion: @escaping (Bool, Bool?) -> Void, remove: @escaping () -> Void) -> ContextController.Source? {
let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
let initialUrlPreview = ChatPresentationInterfaceState.UrlPreview(url: url, webPage: webPage, positionBelowText: positionBelowText, largeMedia: largeMedia)
let urlPreview = ValuePromise<ChatPresentationInterfaceState.UrlPreview>(initialUrlPreview)
let linkOptions = urlPreview.get()
|> deliverOnMainQueue
|> map { urlPreview -> ChatControllerSubject.LinkOptions in
var webpageHasLargeMedia = false
if case let .Loaded(content) = webPage.content {
if let isMediaLargeByDefault = content.isMediaLargeByDefault {
if isMediaLargeByDefault {
webpageHasLargeMedia = true
}
} else {
webpageHasLargeMedia = true
}
}
let entities = [MessageTextEntity(range: 0 ..< (text as NSString).length, type: .Url)]
var largeMedia = false
if webpageHasLargeMedia {
if let value = urlPreview.largeMedia {
largeMedia = value
} else if case .Loaded = webPage.content {
largeMedia = false //!defaultWebpageImageSizeIsSmall(webpage: content)
} else {
largeMedia = true
}
} else {
largeMedia = false
}
return ChatControllerSubject.LinkOptions(
messageText: text,
messageEntities: entities,
hasAlternativeLinks: false,
replyMessageId: nil,
replyQuote: nil,
url: urlPreview.url,
webpage: urlPreview.webPage,
linkBelowText: urlPreview.positionBelowText,
largeMedia: largeMedia
)
}
|> distinctUntilChanged
let wallpaper: TelegramWallpaper?
if let image = snapshotImage {
let wallpaperResource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
if let wallpaperData = image.jpegData(compressionQuality: 0.87) {
context.account.postbox.mediaBox.storeResourceData(wallpaperResource.id, data: wallpaperData, synchronous: true)
}
let wallpaperRepresentation = TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: wallpaperResource, progressiveSizes: [], immediateThumbnailData: nil)
wallpaper = .image([wallpaperRepresentation], WallpaperSettings())
} else {
wallpaper = nil
}
let chatController = context.sharedContext.makeChatController(
context: context,
chatLocation: .peer(id: peerId),
subject: .messageOptions(peerIds: [peerId], ids: [], info: .link(ChatControllerSubject.MessageOptionsInfo.Link(options: linkOptions, isCentered: true))),
botStart: nil,
mode: .standard(.previewing),
params: ChatControllerParams(
forcedTheme: isDark ? defaultDarkColorPresentationTheme : defaultPresentationTheme,
forcedNavigationBarTheme: defaultDarkColorPresentationTheme,
forcedWallpaper: wallpaper
)
)
chatController.canReadHistory.set(false)
let items = linkOptions
|> deliverOnMainQueue
|> map { linkOptions -> ContextController.Items in
var items: [ContextMenuItem] = []
do {
items.append(.action(ContextMenuActionItem(text: linkOptions.linkBelowText ? presentationData.strings.Conversation_MessageOptionsLinkMoveUp : presentationData.strings.Conversation_MessageOptionsLinkMoveDown, icon: { theme in
return nil
}, iconAnimation: ContextMenuActionItem.IconAnimation(
name: linkOptions.linkBelowText ? "message_preview_sort_above" : "message_preview_sort_below"
), action: { _, f in
let _ = (urlPreview.get()
|> take(1)).start(next: { current in
var updatedUrlPreview = current
updatedUrlPreview.positionBelowText = !current.positionBelowText
urlPreview.set(updatedUrlPreview)
})
})))
}
if case let .Loaded(content) = linkOptions.webpage.content, let isMediaLargeByDefault = content.isMediaLargeByDefault, isMediaLargeByDefault {
let shrinkTitle: String
let enlargeTitle: String
if let file = content.file, file.isVideo {
shrinkTitle = presentationData.strings.Conversation_MessageOptionsShrinkVideo
enlargeTitle = presentationData.strings.Conversation_MessageOptionsEnlargeVideo
} else {
shrinkTitle = presentationData.strings.Conversation_MessageOptionsShrinkImage
enlargeTitle = presentationData.strings.Conversation_MessageOptionsEnlargeImage
}
items.append(.action(ContextMenuActionItem(text: linkOptions.largeMedia ? shrinkTitle : enlargeTitle, icon: { _ in
return nil
}, iconAnimation: ContextMenuActionItem.IconAnimation(
name: !linkOptions.largeMedia ? "message_preview_media_large" : "message_preview_media_small"
), action: { _, f in
let _ = (urlPreview.get()
|> take(1)).start(next: { current in
var updatedUrlPreview = current
if let largeMedia = current.largeMedia {
updatedUrlPreview.largeMedia = !largeMedia
} else {
updatedUrlPreview.largeMedia = false
}
urlPreview.set(updatedUrlPreview)
})
})))
}
if !items.isEmpty {
items.append(.separator)
}
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_MessageOptionsApplyChanges, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Select"), color: theme.contextMenu.primaryColor) }, action: { _, f in
f(.default)
let _ = (urlPreview.get()
|> take(1)).start(next: { current in
completion(current.positionBelowText, current.largeMedia)
})
})))
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_LinkOptionsCancel, textColor: .destructive, icon: { theme in return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Delete"), color: theme.contextMenu.destructiveColor) }, action: { _, f in
remove()
f(.default)
})))
return ContextController.Items(id: AnyHashable(linkOptions.url), content: .list(items))
}
return ContextController.Source(
id: AnyHashable(OptionsId.link),
title: presentationData.strings.Conversation_MessageOptionsTabLink,
source: .controller(ChatContextControllerContentSourceImpl(controller: chatController, sourceNode: sourceNode, passthroughTouches: true)),
items: items
)
}
final class ChatContextControllerContentSourceImpl: ContextControllerContentSource {
let controller: ViewController
weak var sourceNode: ASDisplayNode?
weak var sourceView: UIView?
let sourceRect: CGRect?
let navigationController: NavigationController? = nil
let passthroughTouches: Bool
init(controller: ViewController, sourceNode: ASDisplayNode?, sourceRect: CGRect? = nil, passthroughTouches: Bool) {
self.controller = controller
self.sourceNode = sourceNode
self.sourceRect = sourceRect
self.passthroughTouches = passthroughTouches
}
init(controller: ViewController, sourceView: UIView?, sourceRect: CGRect? = nil, passthroughTouches: Bool) {
self.controller = controller
self.sourceView = sourceView
self.sourceRect = sourceRect
self.passthroughTouches = passthroughTouches
}
func transitionInfo() -> ContextControllerTakeControllerInfo? {
let sourceView = self.sourceView
let sourceNode = self.sourceNode
let sourceRect = self.sourceRect
return ContextControllerTakeControllerInfo(contentAreaInScreenSpace: CGRect(origin: CGPoint(), size: CGSize(width: 10.0, height: 10.0)), sourceNode: { [weak sourceNode] in
if let sourceView = sourceView {
return (sourceView, sourceRect ?? sourceView.bounds)
} else if let sourceNode = sourceNode {
return (sourceNode.view, sourceRect ?? sourceNode.bounds)
} else {
return nil
}
})
}
func animatedIn() {
}
}
@@ -0,0 +1,105 @@
import Foundation
import UIKit
final class CropScrollView: UIScrollView, UIScrollViewDelegate {
private var contentView: UIView?
public var updated: (CGPoint, CGFloat) -> Void = { _, _ in }
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
self.showsVerticalScrollIndicator = false
self.showsHorizontalScrollIndicator = false
self.contentInsetAdjustmentBehavior = .never
self.clipsToBounds = false
self.bouncesZoom = true
self.delegate = self
self.decelerationRate = .fast
let transparentView = UIView(frame: bounds)
transparentView.backgroundColor = .clear
transparentView.isUserInteractionEnabled = false
self.addSubview(transparentView)
self.contentView = transparentView
self.minimumZoomScale = 1.0
self.maximumZoomScale = 4.0
}
required init?(coder: NSCoder) {
preconditionFailure()
}
override func layoutSubviews() {
super.layoutSubviews()
guard let contentView = self.contentView else {
return
}
let boundsSize = bounds.size
var frameToCenter = contentView.frame
if frameToCenter.size.width < boundsSize.width {
frameToCenter.origin.x = (boundsSize.width - frameToCenter.size.width) / 2
} else {
frameToCenter.origin.x = 0
}
if frameToCenter.size.height < boundsSize.height {
frameToCenter.origin.y = (boundsSize.height - frameToCenter.size.height) / 2
} else {
frameToCenter.origin.y = 0
}
contentView.frame = frameToCenter
}
func setContentSize(_ size: CGSize) {
self.contentView?.frame = CGRect(origin: .zero, size: size)
self.contentSize = size
self.zoom(to: CGRect(origin: CGPoint(x: floor((size.width - self.bounds.width) / 2.0), y: floor((size.height - self.bounds.height) / 2.0)), size: self.bounds.size), animated: false)
}
private func notify() {
let currentScale = self.zoomScale
let contentOffset = self.contentOffset
let centerOffset = CGPoint(
x: -1.0 * (contentOffset.x + self.bounds.width / 2.0 - self.contentSize.width / 2.0),
y: -1.0 * (contentOffset.y + self.bounds.height / 2.0 - self.contentSize.height / 2.0)
)
self.updated(centerOffset, currentScale)
}
func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return self.contentView
}
func scrollViewDidZoom(_ scrollView: UIScrollView) {
self.setNeedsLayout()
self.layoutIfNeeded()
self.notify()
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
self.notify()
}
func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
self.notify()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
self.notify()
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
self.notify()
}
}
@@ -0,0 +1,737 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import MediaEditor
import MultilineTextComponent
import TelegramPresentationData
private class HistogramView: UIView {
private var size: CGSize?
private var histogramBins: MediaEditorHistogram.HistogramBins?
private var color: UIColor?
private let shapeLayer = SimpleShapeLayer()
var dataPointsUpdated: (([Float]) -> Void)?
init() {
super.init(frame: .zero)
self.layer.addSublayer(self.shapeLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateSize(size: CGSize, histogramBins: MediaEditorHistogram.HistogramBins?, color: UIColor, transition: ComponentTransition) {
guard self.size != size || self.color != color || self.histogramBins != histogramBins else {
return
}
self.size = size
self.histogramBins = histogramBins
self.color = color
self.update(transition: transition)
}
func update(transition: ComponentTransition) {
guard let size = self.size, let histogramBins = self.histogramBins, histogramBins.count > 0, let color = self.color else {
self.shapeLayer.path = nil
return
}
transition.setShapeLayerFillColor(layer: self.shapeLayer, color: color)
let (path, _) = curveThroughPoints(
count: histogramBins.count,
valueAtIndex: { index in
return histogramBins.valueAtIndex(index, mirrored: true)
},
positionAtIndex: { index, step in
return CGFloat(index) * step
},
size: size,
type: .filled,
granularity: 200,
floor: true
)
transition.setShapeLayerPath(layer: self.shapeLayer, path: path.cgPath)
}
}
enum CurvesSection {
case all
case red
case green
case blue
}
class CurvesInternalState {
var section: CurvesSection = .all
}
final class CurvesComponent: Component {
typealias EnvironmentType = Empty
let strings: PresentationStrings
let histogram: MediaEditorHistogram?
let internalState: CurvesInternalState
init(
strings: PresentationStrings,
histogram: MediaEditorHistogram?,
internalState: CurvesInternalState
) {
self.strings = strings
self.histogram = histogram
self.internalState = internalState
}
static func ==(lhs: CurvesComponent, rhs: CurvesComponent) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.histogram != rhs.histogram {
return false
}
return true
}
final class State: ComponentState {
let internalState: CurvesInternalState
init(internalState: CurvesInternalState) {
self.internalState = internalState
}
var section: CurvesSection {
get {
return self.internalState.section
}
set {
self.internalState.section = newValue
}
}
}
func makeState() -> State {
return State(internalState: self.internalState)
}
final class View: UIView {
private var allButton = ComponentView<Empty>()
private var redButton = ComponentView<Empty>()
private var greenButton = ComponentView<Empty>()
private var blueButton = ComponentView<Empty>()
private let histogramView = HistogramView()
private var component: CurvesComponent?
private weak var state: State?
override init(frame: CGRect) {
super.init(frame: frame)
self.addSubview(self.histogramView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: CurvesComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let topInset: CGFloat = 11.0
let allButtonSize = self.allButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: component.strings.Story_Editor_Curves_All,
font: Font.regular(14.0),
color: state.section == .all ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .all
state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let allButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 - allButtonSize.width / 2.0), y: topInset), size: allButtonSize)
if let view = self.allButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: allButtonFrame)
}
let redButtonSize = self.redButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: component.strings.Story_Editor_Curves_Red,
font: Font.regular(14.0),
color: state.section == .red ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .red
state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let redButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 2.0 - redButtonSize.width / 2.0), y: topInset), size: redButtonSize)
if let view = self.redButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: redButtonFrame)
}
let greenButtonSize = self.greenButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: component.strings.Story_Editor_Curves_Green,
font: Font.regular(14.0),
color: state.section == .green ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .green
state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let greenButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 3.0 - greenButtonSize.width / 2.0), y: topInset), size: greenButtonSize)
if let view = self.greenButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: greenButtonFrame)
}
let blueButtonSize = self.blueButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: component.strings.Story_Editor_Curves_Blue,
font: Font.regular(14.0),
color: state.section == .blue ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .blue
state?.updated(transition: ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut)))
}
)
),
environment: {},
containerSize: availableSize
)
let blueButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 5.0 * 4.0 - blueButtonSize.width / 2.0), y: topInset), size: blueButtonSize)
if let view = self.blueButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: blueButtonFrame)
}
let histogramHeight: CGFloat = 85.0
let histogramColor: UIColor
let histogramBins: MediaEditorHistogram.HistogramBins?
switch state.section {
case .all:
histogramColor = .white
histogramBins = component.histogram?.luminance
case .red:
histogramColor = UIColor(rgb: 0xed3d4c)
histogramBins = component.histogram?.red
case .green:
histogramColor = UIColor(rgb: 0x10ee9d)
histogramBins = component.histogram?.green
case .blue:
histogramColor = UIColor(rgb: 0x3377fb)
histogramBins = component.histogram?.blue
}
let histogramSize = CGSize(width: availableSize.width, height: histogramHeight)
let verticalSpacing: CGFloat = 3.0
self.histogramView.updateSize(size: histogramSize, histogramBins: histogramBins, color: histogramColor, transition: transition)
transition.setFrame(view: self.histogramView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset + allButtonSize.height + verticalSpacing), size: histogramSize))
return CGSize(width: availableSize.width, height: topInset + allButtonSize.height + verticalSpacing + histogramHeight)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class CurvesScreenComponent: Component {
typealias EnvironmentType = Empty
let value: CurvesValue
let section: CurvesSection
let valueUpdated: (CurvesValue) -> Void
init(
value: CurvesValue,
section: CurvesSection,
valueUpdated: @escaping (CurvesValue) -> Void
) {
self.value = value
self.section = section
self.valueUpdated = valueUpdated
}
static func ==(lhs: CurvesScreenComponent, rhs: CurvesScreenComponent) -> Bool {
if lhs.value != rhs.value {
return false
}
if lhs.section != rhs.section {
return false
}
return true
}
final class View: UIView {
enum Field {
case blacks
case shadows
case midtones
case highlights
case whites
}
private var blacks = ComponentView<Empty>()
private var shadows = ComponentView<Empty>()
private var midtones = ComponentView<Empty>()
private var highlights = ComponentView<Empty>()
private let whites = ComponentView<Empty>()
private let line1 = SimpleLayer()
private let line2 = SimpleLayer()
private let line3 = SimpleLayer()
private let line4 = SimpleLayer()
private let curveContainer = SimpleLayer()
private let guideLayer = SimpleShapeLayer()
private let curveLayer = SimpleShapeLayer()
private var component: CurvesScreenComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
super.init(frame: frame)
self.layer.addSublayer(self.line1)
self.layer.addSublayer(self.line2)
self.layer.addSublayer(self.line3)
self.layer.addSublayer(self.line4)
self.layer.addSublayer(self.curveContainer)
self.curveContainer.addSublayer(self.guideLayer)
self.curveContainer.addSublayer(self.curveLayer)
self.curveContainer.masksToBounds = true
self.curveContainer.cornerRadius = 12.0
if #available(iOS 13.0, *) {
self.curveContainer.cornerCurve = .continuous
}
self.line1.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.line2.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.line3.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.line4.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.guideLayer.lineWidth = 1.5
self.guideLayer.lineDashPattern = [7, 4]
self.guideLayer.strokeColor = UIColor(rgb: 0xffffff, alpha: 0.5).cgColor
self.curveLayer.lineWidth = 2.0
self.curveLayer.fillColor = UIColor.clear.cgColor
let allLayers = [
self.line1,
self.line2,
self.line3,
self.line4,
self.guideLayer,
]
for layer in allLayers {
layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
layer.shadowRadius = 1.5
layer.shadowColor = UIColor.black.cgColor
layer.shadowOpacity = 0.3
}
self.curveLayer.shadowOffset = CGSize(width: 0.0, height: 0.0)
self.curveLayer.shadowRadius = 2.0
self.curveLayer.shadowColor = UIColor.black.cgColor
self.curveLayer.shadowOpacity = 0.16
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
self.addGestureRecognizer(panGestureRecognizer)
let doubleTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleDoubleTap(_:)))
doubleTapGestureRecognizer.numberOfTapsRequired = 2
self.addGestureRecognizer(doubleTapGestureRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private var selectedField: Field?
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
guard let component = self.component else {
return
}
let fieldWidth = self.frame.width / 5.0
switch gestureRecognizer.state {
case .began:
let location = gestureRecognizer.location(in: gestureRecognizer.view).x
let index = floor(location / fieldWidth)
switch index {
case 0:
self.selectedField = .blacks
case 1:
self.selectedField = .shadows
case 2:
self.selectedField = .midtones
case 3:
self.selectedField = .highlights
case 4:
self.selectedField = .whites
default:
break
}
case .changed:
guard let selectedField = self.selectedField else {
return
}
let translation = gestureRecognizer.translation(in: gestureRecognizer.view).y
let delta = Float(min(2.0, -1.0 * translation / 8.0) / 100.0)
var updatedValue = component.value
var curve: CurvesValue.CurveValue
switch component.section {
case .all:
curve = updatedValue.all
case .red:
curve = updatedValue.red
case .green:
curve = updatedValue.green
case .blue:
curve = updatedValue.blue
}
switch selectedField {
case .blacks:
curve = curve.withUpdatedBlacks(max(0.0, min(1.0, curve.blacks + delta)))
case .shadows:
curve = curve.withUpdatedShadows(max(0.0, min(1.0, curve.shadows + delta)))
case .midtones:
curve = curve.withUpdatedMidtones(max(0.0, min(1.0, curve.midtones + delta)))
case .highlights:
curve = curve.withUpdatedHighlights(max(0.0, min(1.0, curve.highlights + delta)))
case .whites:
curve = curve.withUpdatedWhites(max(0.0, min(1.0, curve.whites + delta)))
}
switch component.section {
case .all:
updatedValue = updatedValue.withUpdatedAll(curve)
case .red:
updatedValue = updatedValue.withUpdatedRed(curve)
case .green:
updatedValue = updatedValue.withUpdatedGreen(curve)
case .blue:
updatedValue = updatedValue.withUpdatedBlue(curve)
}
component.valueUpdated(updatedValue)
gestureRecognizer.setTranslation(.zero, in: gestureRecognizer.view)
default:
self.selectedField = nil
}
}
@objc func handleDoubleTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component else {
return
}
switch component.section {
case .all:
component.valueUpdated(component.value.withUpdatedAll(.initial))
case .red:
component.valueUpdated(component.value.withUpdatedRed(.initial))
case .green:
component.valueUpdated(component.value.withUpdatedGreen(.initial))
case .blue:
component.valueUpdated(component.value.withUpdatedBlue(.initial))
}
}
func update(component: CurvesScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let value: CurvesValue.CurveValue
let lineColor: UIColor
switch component.section {
case .all:
lineColor = UIColor.white
value = component.value.all
case .red:
lineColor = UIColor(rgb: 0xed3d4c)
value = component.value.red
case .green:
lineColor = UIColor(rgb: 0x10ee9d)
value = component.value.green
case .blue:
lineColor = UIColor(rgb: 0x3377fb)
value = component.value.blue
}
let fieldWidth = availableSize.width / 5.0
let bottomInset: CGFloat = 5.0
let blacksSize = self.blacks.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.blacks),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let blacksFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - blacksSize.height - bottomInset), size: blacksSize)
if let view = self.blacks.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: blacksFrame)
}
let shadowsSize = self.shadows.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.shadows),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let shadowsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - shadowsSize.height - bottomInset), size: shadowsSize)
if let view = self.shadows.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: shadowsFrame)
}
let midtonesSize = self.midtones.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.midtones),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let midtonesFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 2.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - midtonesSize.height - bottomInset), size: midtonesSize)
if let view = self.midtones.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: midtonesFrame)
}
let highlightsSize = self.highlights.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.highlights),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let highlightsFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 3.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - highlightsSize.height - bottomInset), size: highlightsSize)
if let view = self.highlights.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: highlightsFrame)
}
let whitesSize = self.whites.update(
transition: transition,
component: AnyComponent(
MultilineTextComponent(
text: .plain(
NSAttributedString(
string: String(format: "%.2f", value.whites),
font: Font.regular(14.0),
textColor: UIColor(rgb: 0xffffff)
)
),
textShadowColor: UIColor(rgb: 0x000000, alpha: 0.3),
textShadowBlur: 1.5
)
),
environment: {},
containerSize: availableSize
)
let whitesFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(fieldWidth * 4.0 + (fieldWidth - blacksSize.width) / 2.0), y: availableSize.height - whitesSize.height - bottomInset), size: whitesSize)
if let view = self.whites.view {
if view.superview == nil {
self.addSubview(view)
}
view.alpha = 0.75
transition.setFrame(view: view, frame: whitesFrame)
}
self.curveContainer.frame = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: availableSize.height + 12.0))
let lineWidth: CGFloat = 1.0 - UIScreenPixel
self.line1.frame = CGRect(x: fieldWidth, y: 0.0, width: lineWidth, height: availableSize.height)
self.line2.frame = CGRect(x: fieldWidth * 2.0, y: 0.0, width: lineWidth, height: availableSize.height)
self.line3.frame = CGRect(x: fieldWidth * 3.0, y: 0.0, width: lineWidth, height: availableSize.height)
self.line4.frame = CGRect(x: fieldWidth * 4.0, y: 0.0, width: lineWidth, height: availableSize.height)
let guidePath = UIBezierPath()
guidePath.move(to: CGPoint(x: 0.0, y: availableSize.height))
guidePath.addLine(to: CGPoint(x: availableSize.width, y: 0.0))
self.guideLayer.frame = CGRect(origin: .zero, size: availableSize)
self.guideLayer.path = guidePath.cgPath
self.curveLayer.strokeColor = lineColor.cgColor
self.curveLayer.frame = CGRect(origin: .zero, size: availableSize)
let points: [Float] = [
value.blacks,
value.blacks,
value.shadows,
value.midtones,
value.highlights,
value.whites,
value.whites
]
let (curvePath, _) = curveThroughPoints(
count: points.count,
valueAtIndex: { index in
return 1.0 - points[index]
},
positionAtIndex: { index, _ in
switch index {
case 0:
return -1.0
case 1:
return 0.0
case 2:
return 0.25 * availableSize.width
case 3:
return 0.5 * availableSize.width
case 4:
return 0.75 * availableSize.width
case 5:
return availableSize.width
default:
return availableSize.width + 1.0
}
},
size: availableSize,
type: .line,
granularity: 100,
floor: true
)
self.curveLayer.path = curvePath.cgPath
return availableSize
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,308 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TextFormat
import SaveToCameraRoll
import ImageCompression
import LocalMediaResources
public extension MediaEditorScreenImpl {
static func makeEditStoryController(
context: AccountContext,
peer: EnginePeer,
storyItem: EngineStoryItem,
videoPlaybackPosition: Double?,
cover: Bool,
repost: Bool,
transitionIn: MediaEditorScreenImpl.TransitionIn,
transitionOut: MediaEditorScreenImpl.TransitionOut?,
completed: @escaping () -> Void = {},
willDismiss: @escaping () -> Void = {},
update: @escaping (Disposable?) -> Void
) -> MediaEditorScreenImpl? {
guard let peerReference = PeerReference(peer._asPeer()) else {
return nil
}
let subject: Signal<MediaEditorScreenImpl.Subject?, NoError>
subject = getStorySource(engine: context.engine, peerId: peer.id, id: Int64(storyItem.id))
|> mapToSignal { source in
if !repost, let source {
return .single(.draft(source, Int64(storyItem.id)))
} else {
let media = storyItem.media._asMedia()
return fetchMediaData(context: context, postbox: context.account.postbox, userLocation: .peer(peerReference.id), customUserContentType: .story, mediaReference: .story(peer: peerReference, id: storyItem.id, media: media))
|> mapToSignal { (value, isImage) -> Signal<MediaEditorScreenImpl.Subject?, NoError> in
guard case let .data(data) = value, data.complete else {
return .complete()
}
if let image = UIImage(contentsOfFile: data.path) {
return .single(nil)
|> then(
.single(.image(image: image, dimensions: PixelDimensions(image.size), additionalImage: nil, additionalImagePosition: .bottomRight, fromCamera: false))
|> delay(0.1, queue: Queue.mainQueue())
)
} else {
var duration: Double?
if let file = media as? TelegramMediaFile {
duration = file.duration
}
let symlinkPath = data.path + ".mp4"
if fileSize(symlinkPath) == nil {
let _ = try? FileManager.default.linkItem(atPath: data.path, toPath: symlinkPath)
}
return .single(nil)
|> then(
.single(.video(videoPath: symlinkPath, thumbnail: nil, mirror: false, additionalVideoPath: nil, additionalThumbnail: nil, dimensions: PixelDimensions(width: 720, height: 1280), duration: duration ?? 0.0, videoPositionChanges: [], additionalVideoPosition: .bottomRight, fromCamera: false))
)
}
}
}
}
let initialCaption: NSAttributedString?
let initialPrivacy: EngineStoryPrivacy?
let initialMediaAreas: [MediaArea]
if repost {
initialCaption = nil
initialPrivacy = nil
initialMediaAreas = []
} else {
initialCaption = chatInputStateStringWithAppliedEntities(storyItem.text, entities: storyItem.entities)
initialPrivacy = storyItem.privacy
initialMediaAreas = storyItem.mediaAreas
}
let externalState = MediaEditorTransitionOutExternalState(
storyTarget: nil,
isForcedTarget: false,
isPeerArchived: false,
transitionOut: nil
)
var videoPlaybackPosition = videoPlaybackPosition
if cover, case let .file(file) = storyItem.media {
videoPlaybackPosition = 0.0
for attribute in file.attributes {
if case let .Video(_, _, _, _, coverTime, _) = attribute {
videoPlaybackPosition = coverTime
break
}
}
}
var updateProgressImpl: ((Float) -> Void)?
let controller = MediaEditorScreenImpl(
context: context,
mode: .storyEditor(remainingCount: 1),
subject: subject,
isEditing: !repost,
isEditingCover: cover,
forwardSource: repost ? (peer, storyItem) : nil,
initialCaption: initialCaption,
initialPrivacy: initialPrivacy,
initialMediaAreas: initialMediaAreas,
initialVideoPosition: videoPlaybackPosition,
transitionIn: transitionIn,
transitionOut: { finished, isNew in
if repost && finished {
if let transitionOut = externalState.transitionOut?(externalState.storyTarget, externalState.isPeerArchived), let destinationView = transitionOut.destinationView {
return MediaEditorScreenImpl.TransitionOut(
destinationView: destinationView,
destinationRect: transitionOut.destinationRect,
destinationCornerRadius: transitionOut.destinationCornerRadius
)
} else {
return nil
}
} else {
return transitionOut
}
},
completion: { results, commit in
guard let result = results.first else {
return
}
let entities = generateChatInputTextEntities(result.caption)
if repost {
let target: Stories.PendingTarget
let targetPeerId: EnginePeer.Id
if let sendAsPeerId = result.options.sendAsPeerId {
target = .peer(sendAsPeerId)
targetPeerId = sendAsPeerId
} else {
target = .myStories
targetPeerId = context.account.peerId
}
externalState.storyTarget = target
completed()
let _ = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: targetPeerId))
|> deliverOnMainQueue).startStandalone(next: { peer in
guard let peer else {
return
}
if case let .user(user) = peer {
externalState.isPeerArchived = user.storiesHidden ?? false
} else if case let .channel(channel) = peer {
externalState.isPeerArchived = channel.storiesHidden ?? false
}
let forwardInfo = Stories.PendingForwardInfo(peerId: peerReference.id, storyId: storyItem.id, isModified: result.media != nil)
if let rootController = context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface {
var existingMedia: EngineMedia?
if let _ = result.media {
} else {
existingMedia = storyItem.media
}
rootController.proceedWithStoryUpload(target: target, results: [result as! MediaEditorScreenResult], existingMedia: existingMedia, forwardInfo: forwardInfo, externalState: externalState, commit: commit)
}
})
} else {
var updatedText: String?
var updatedCoverTimestamp: Double?
var updatedEntities: [MessageTextEntity]?
if result.caption.string != storyItem.text || entities != storyItem.entities {
updatedText = result.caption.string
updatedEntities = entities
}
if let coverTimestamp = result.coverTimestamp {
updatedCoverTimestamp = coverTimestamp
}
if let mediaResult = result.media {
switch mediaResult {
case let .image(image, dimensions):
updateProgressImpl?(0.0)
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
if let imageData = compressImageToJPEG(image, quality: 0.7, tempFilePath: tempFile.path) {
update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .image(dimensions: dimensions, data: imageData, stickers: result.stickers), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|> deliverOnMainQueue).startStrict(next: { result in
switch result {
case let .progress(progress):
updateProgressImpl?(progress)
case .completed:
Queue.mainQueue().after(0.1) {
willDismiss()
HapticFeedback().success()
commit({})
}
}
}))
}
case let .video(content, firstFrameImage, values, duration, dimensions):
updateProgressImpl?(0.0)
if let valuesData = try? JSONEncoder().encode(values) {
let data = MemoryBuffer(data: valuesData)
let digest = MemoryBuffer(data: data.md5Digest())
let 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))
}
let tempFile = TempBox.shared.tempFile(fileName: "file")
defer {
TempBox.shared.dispose(tempFile)
}
let firstFrameImageData = firstFrameImage.flatMap { compressImageToJPEG($0, quality: 0.6, tempFilePath: tempFile.path) }
let firstFrameFile = firstFrameImageData.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
}
}
update((context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: .video(dimensions: dimensions, duration: duration, resource: resource, firstFrameFile: firstFrameFile, stickers: result.stickers, coverTime: nil), mediaAreas: result.mediaAreas, text: updatedText, entities: updatedEntities, privacy: nil)
|> deliverOnMainQueue).startStrict(next: { result in
switch result {
case let .progress(progress):
updateProgressImpl?(progress)
case .completed:
Queue.mainQueue().after(0.1) {
willDismiss()
HapticFeedback().success()
commit({})
}
}
}))
}
default:
break
}
} else if updatedText != nil || updatedCoverTimestamp != nil {
var media: EngineStoryInputMedia?
if let updatedCoverTimestamp {
if case let .file(file) = storyItem.media {
var updatedAttributes: [TelegramMediaFileAttribute] = []
for attribute in file.attributes {
if case let .Video(duration, size, flags, preloadSize, _, videoCodec) = attribute {
updatedAttributes.append(.Video(duration: duration, size: size, flags: flags, preloadSize: preloadSize, coverTime: min(duration, updatedCoverTimestamp), videoCodec: videoCodec))
} else {
updatedAttributes.append(attribute)
}
}
media = .existing(media: file.withUpdatedAttributes(updatedAttributes))
}
}
let _ = (context.engine.messages.editStory(peerId: peer.id, id: storyItem.id, media: media, mediaAreas: nil, text: updatedText, entities: updatedEntities, privacy: nil)
|> deliverOnMainQueue).startStandalone(next: { result in
switch result {
case .completed:
Queue.mainQueue().after(0.1) {
willDismiss()
HapticFeedback().success()
commit({})
}
default:
break
}
})
} else {
willDismiss()
HapticFeedback().success()
commit({})
}
}
}
)
controller.willDismiss = willDismiss
controller.navigationPresentation = .flatModal
updateProgressImpl = { [weak controller] progress in
controller?.updateEditProgress(progress, cancel: {
update(nil)
})
}
return controller
}
}
@@ -0,0 +1,88 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class FlipButtonContentComponent: Component {
let tag: AnyObject?
init(
tag: AnyObject?
) {
self.tag = tag
}
static func ==(lhs: FlipButtonContentComponent, rhs: FlipButtonContentComponent) -> Bool {
return lhs === rhs
}
final class View: UIView, ComponentTaggedView {
private var component: FlipButtonContentComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
private let backgroundView: BlurredBackgroundView
private let icon = SimpleLayer()
init() {
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
super.init(frame: CGRect())
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.icon)
self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.withRenderingMode(.alwaysTemplate).cgImage
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func playAnimation() {
let animation = CASpringAnimation(keyPath: "transform.rotation.z")
animation.fromValue = 0.0 as NSNumber
animation.toValue = CGFloat.pi as NSNumber
animation.mass = 5.0
animation.stiffness = 900.0
animation.damping = 90.0
animation.duration = animation.settlingDuration
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps)
}
self.icon.add(animation, forKey: "transform.rotation.z")
}
func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
self.component = component
let size = CGSize(width: 48.0, height: 48.0)
let backgroundFrame = CGRect(x: 4.0, y: 4.0, width: 40.0, height: 40.0)
self.icon.layerTintColor = UIColor.white.cgColor
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.icon.bounds = CGRect(origin: .zero, size: size)
self.backgroundView.frame = backgroundFrame
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.width / 2.0, transition: .immediate)
return size
}
}
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, transition: transition)
}
}
@@ -0,0 +1,610 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import MediaEditor
import MediaScrubberComponent
import ButtonComponent
private final class MediaCoverScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mediaEditor: Signal<MediaEditor?, NoError>
let exclusive: Bool
init(
context: AccountContext,
mediaEditor: Signal<MediaEditor?, NoError>,
exclusive: Bool
) {
self.context = context
self.mediaEditor = mediaEditor
self.exclusive = exclusive
}
static func ==(lhs: MediaCoverScreenComponent, rhs: MediaCoverScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.exclusive != rhs.exclusive {
return false
}
return true
}
final class State: ComponentState {
var playerStateDisposable: Disposable?
var playerState: MediaEditorPlayerState?
private(set) var mediaEditor: MediaEditor?
init(mediaEditor: Signal<MediaEditor?, NoError>) {
super.init()
let _ = (mediaEditor
|> take(1)
|> deliverOnMainQueue).start(next: { [weak self] mediaEditor in
if let self, let mediaEditor {
self.mediaEditor = mediaEditor
self.playerStateDisposable = (mediaEditor.playerState(framesCount: 16)
|> deliverOnMainQueue).start(next: { [weak self] playerState in
if let self {
if self.playerState != playerState {
self.playerState = playerState
self.updated()
}
}
})
}
})
}
deinit {
self.playerStateDisposable?.dispose()
}
}
func makeState() -> State {
return State(mediaEditor: self.mediaEditor)
}
public final class View: UIView {
private let buttonsContainerView = UIView()
private let buttonsBackgroundView = UIImageView()
private let previewContainerView = UIView()
private let cancelButton = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let doneButton = ComponentView<Empty>()
private let scrubber = ComponentView<Empty>()
private let fadeView = UIView()
private var component: MediaCoverScreenComponent?
private weak var state: State?
private var environment: ViewControllerComponentContainer.Environment?
override init(frame: CGRect) {
self.buttonsContainerView.clipsToBounds = true
self.fadeView.alpha = 0.0
self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7)
self.buttonsBackgroundView.image = generateImage(CGSize(width: 22.0, height: 22.0), rotatedContext: { size, context in
context.setFillColor(UIColor.black.cgColor)
context.fill(CGRect(origin: .zero, size: size))
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.addPath(CGPath(roundedRect: CGRect(x: 0.0, y: -11.0, width: size.width, height: 22.0), cornerWidth: 11.0, cornerHeight: 11.0, transform: nil))
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 11, topCapHeight: 11)
super.init(frame: frame)
self.backgroundColor = .clear
self.addSubview(self.buttonsContainerView)
self.buttonsContainerView.addSubview(self.buttonsBackgroundView)
self.addSubview(self.fadeView)
self.addSubview(self.previewContainerView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func animateInFromEditor() {
self.buttonsBackgroundView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.2, additive: true)
self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
}
private var animatingOut = false
func animateOutToEditor(completion: @escaping () -> Void) {
self.animatingOut = true
self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
self.buttonsBackgroundView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 44.0), duration: 0.2, removeOnCompletion: false, additive: true)
self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let view = self.scrubber.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
if let view = self.cancelButton.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
self.state?.updated()
}
func update(component: MediaCoverScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
guard let controller = environment.controller() as? MediaCoverScreen else {
return .zero
}
self.component = component
self.state = state
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let buttonSideInset: CGFloat = 16.0
var controlsBottomInset: CGFloat = 0.0
let previewSize: CGSize
var topInset: CGFloat = environment.statusBarHeight + 5.0
if isTablet {
let previewHeight = availableSize.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778))
if availableSize.height < previewSize.height + 30.0 {
topInset = 0.0
controlsBottomInset = -75.0
}
}
let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: topInset), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset))
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset))
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
MultilineTextComponent(text: .plain(NSAttributedString(string: environment.strings.Common_Cancel, font: Font.regular(17.0), textColor: .white)))
),
action: { [weak controller] in
controller?.requestDismiss(animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 120.0, height: 44.0)
)
let cancelButtonFrame = CGRect(
origin: CGPoint(x: 16.0, y: previewContainerFrame.minY + 28.0),
size: cancelButtonSize
)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
setupButtonShadow(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
let doneButtonSize = self.doneButton.update(
transition: transition,
component: AnyComponent(
ButtonComponent(
background: ButtonComponent.Background(
color: environment.theme.list.itemCheckColors.fillColor,
foreground: environment.theme.list.itemCheckColors.foregroundColor,
pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
),
content: AnyComponentWithIdentity(
id: AnyHashable(0),
component: AnyComponent(ButtonTextContentComponent(
text: environment.strings.Story_SaveCover,
badge: 0,
textColor: environment.theme.list.itemCheckColors.foregroundColor,
badgeBackground: .clear,
badgeForeground: .clear
))
),
isEnabled: true,
displaysProgress: false,
action: { [weak controller, weak self] in
guard let controller else {
return
}
if let playerState = self?.state?.playerState, let mediaEditor = self?.state?.mediaEditor, let image = mediaEditor.resultImage {
mediaEditor.setCoverImageTimestamp(playerState.position)
controller.completed(playerState.position, image)
}
if !controller.exclusive {
controller.requestDismiss(animated: true)
}
}
)
),
environment: {},
containerSize: CGSize(width: availableSize.width - buttonSideInset * 2.0, height: 50.0)
)
let doneButtonFrame = CGRect(
origin: CGPoint(x: floor((availableSize.width - doneButtonSize.width) / 2.0), y: min(buttonsContainerFrame.minY, availableSize.height - doneButtonSize.height - buttonSideInset)),
size: doneButtonSize
)
if let doneButtonView = self.doneButton.view {
if doneButtonView.superview == nil {
self.addSubview(doneButtonView)
}
transition.setFrame(view: doneButtonView, frame: doneButtonFrame)
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(Text(text: environment.strings.Story_Cover, font: Font.semibold(17.0), color: UIColor(rgb: 0xffffff))),
environment: {},
containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0)
)
let labelFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: previewContainerFrame.minY + 28.0),
size: labelSize
)
if let labelView = self.label.view {
if labelView.superview == nil {
self.addSubview(labelView)
setupButtonShadow(labelView)
}
if labelView.bounds.width > 0.0 && labelFrame.width != labelView.bounds.width {
if let snapshotView = labelView.snapshotView(afterScreenUpdates: false) {
snapshotView.center = labelView.center
self.buttonsContainerView.addSubview(snapshotView)
labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
}
}
labelView.bounds = CGRect(origin: .zero, size: labelFrame.size)
transition.setPosition(view: labelView, position: labelFrame.center)
}
let buttonCoverFrame = CGRect(origin: CGPoint(x: 0.0, y: doneButtonFrame.minY - buttonSideInset - 11.0), size: CGSize(width: previewContainerFrame.width, height: 100.0))
transition.setFrame(view: self.buttonsContainerView, frame: buttonCoverFrame)
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonCoverFrame.size))
transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame)
if let playerState = state.playerState {
let visibleTracks = playerState.tracks.filter { $0.id == 0 }.map { MediaScrubberComponent.Track($0) }
let scrubberInset: CGFloat = buttonSideInset
let scrubberSize = self.scrubber.update(
transition: transition,
component: AnyComponent(MediaScrubberComponent(
context: component.context,
style: .cover,
theme: environment.theme,
generationTimestamp: playerState.generationTimestamp,
position: playerState.position,
minDuration: 1.0,
maxDuration: storyMaxVideoDuration,
isPlaying: playerState.isPlaying,
tracks: visibleTracks,
isCollage: false,
portalView: controller.portalView,
positionUpdated: { [weak state] position, apply in
if let mediaEditor = state?.mediaEditor {
mediaEditor.seek(position, andPlay: false)
}
},
coverPositionUpdated: { [weak state] position, tap, commit in
if let mediaEditor = state?.mediaEditor {
if tap {
mediaEditor.setOnNextDisplay {
commit()
}
mediaEditor.seek(position, andPlay: false)
} else {
mediaEditor.seek(position, andPlay: false)
commit()
}
}
},
trackTrimUpdated: { _, _, _, _, _ in
},
trackOffsetUpdated: { _, _, _ in
},
trackLongPressed: { _, _ in
}
)),
environment: {},
containerSize: CGSize(width: previewSize.width - scrubberInset * 2.0, height: availableSize.height)
)
let scrubberFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - scrubberSize.width) / 2.0), y: min(previewContainerFrame.maxY, buttonCoverFrame.minY) - scrubberSize.height - 4.0), size: scrubberSize)
if let scrubberView = self.scrubber.view {
var animateIn = false
if scrubberView.superview == nil {
animateIn = true
self.addSubview(scrubberView)
}
if animateIn {
scrubberView.frame = scrubberFrame
} else {
transition.setFrame(view: scrubberView, frame: scrubberFrame)
}
if animateIn {
scrubberView.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
scrubberView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
scrubberView.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2)
}
}
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class MediaCoverScreen: ViewController {
fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate {
private weak var controller: MediaCoverScreen?
private let context: AccountContext
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
init(controller: MediaCoverScreen) {
self.controller = controller
self.context = controller.context
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
self.backgroundColor = .clear
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.view.disablesInteractiveKeyboardGestureRecognizer = true
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateInFromEditor() {
if let view = self.componentHost.view as? MediaCoverScreenComponent.View {
view.animateInFromEditor()
}
}
func animateOutToEditor(completion: @escaping () -> Void) {
if let view = self.componentHost.view as? MediaCoverScreenComponent.View {
view.animateOutToEditor(completion: completion)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.view {
return nil
}
return result
}
func requestLayout(transition: ComponentTransition) {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition)
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) {
guard let controller = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet = layout.metrics.isTablet
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
if isTablet {
let previewHeight = layout.size.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
}
let bottomInset = layout.size.height - previewSize.height - topInset
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0,
safeInsets: UIEdgeInsets(
top: topInset,
left: layout.safeInsets.left,
bottom: bottomInset,
right: layout.safeInsets.right
),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
isVisible: true,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(
MediaCoverScreenComponent(
context: self.context,
mediaEditor: controller.mediaEditor,
exclusive: controller.exclusive
)
),
environment: {
environment
},
forceUpdate: forceUpdate || animateOut,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3)
componentView.clipsToBounds = true
}
let componentFrame = CGRect(origin: .zero, size: componentSize)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
}
if isFirstTime {
self.animateInFromEditor()
}
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
fileprivate let context: AccountContext
fileprivate let mediaEditor: Signal<MediaEditor?, NoError>
fileprivate let previewView: MediaEditorPreviewView
fileprivate let portalView: PortalView
fileprivate let exclusive: Bool
func withMediaEditor(_ f: @escaping (MediaEditor) -> Void) {
let _ = (self.mediaEditor
|> take(1)
|> deliverOnMainQueue).start(next: { mediaEditor in
if let mediaEditor {
f(mediaEditor)
}
})
}
var completed: (Double, UIImage) -> Void = { _, _ in }
var dismissed: () -> Void = {}
init(
context: AccountContext,
mediaEditor: Signal<MediaEditor?, NoError>,
previewView: MediaEditorPreviewView,
portalView: PortalView,
exclusive: Bool
) {
self.context = context
self.mediaEditor = mediaEditor
self.previewView = previewView
self.portalView = portalView
self.exclusive = exclusive
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .White
self.withMediaEditor { mediaEditor in
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
mediaEditor.seek(coverImageTimestamp, andPlay: false)
} else {
mediaEditor.seek(mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, andPlay: false)
}
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
func requestDismiss(animated: Bool) {
self.dismissed()
self.node.animateOutToEditor(completion: {
if !self.exclusive {
self.dismiss()
}
})
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition))
}
}
private func setupButtonShadow(_ view: UIView, radius: CGFloat = 2.0) {
view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
view.layer.shadowRadius = radius
view.layer.shadowColor = UIColor.black.cgColor
view.layer.shadowOpacity = 0.35
}
@@ -0,0 +1,697 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import DrawingUI
import MediaEditor
import Photos
import LottieAnimationComponent
import MessageInputPanelComponent
import DustEffect
import PlainButtonComponent
import ImageObjectSeparation
private final class MediaCutoutScreenComponent: Component {
typealias EnvironmentType = ViewControllerComponentContainer.Environment
let context: AccountContext
let mediaEditor: MediaEditor
init(
context: AccountContext,
mediaEditor: MediaEditor
) {
self.context = context
self.mediaEditor = mediaEditor
}
static func ==(lhs: MediaCutoutScreenComponent, rhs: MediaCutoutScreenComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
return true
}
final class State: ComponentState {
enum ImageKey: Hashable {
case done
}
private var cachedImages: [ImageKey: UIImage] = [:]
func image(_ key: ImageKey) -> UIImage {
if let image = self.cachedImages[key] {
return image
} else {
var image: UIImage
switch key {
case .done:
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)!
}
cachedImages[key] = image
return image
}
}
}
func makeState() -> State {
return State()
}
public final class View: UIView {
private let buttonsContainerView = UIView()
private let buttonsBackgroundView = UIView()
private let previewContainerView = UIView()
private let cancelButton = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let doneButton = ComponentView<Empty>()
private let fadeView = UIView()
private var outlineViews: [StickerCutoutOutlineView] = []
private var component: MediaCutoutScreenComponent?
private weak var state: State?
private var environment: ViewControllerComponentContainer.Environment?
override init(frame: CGRect) {
self.buttonsContainerView.clipsToBounds = true
self.fadeView.alpha = 0.0
self.fadeView.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.7)
super.init(frame: frame)
self.backgroundColor = .clear
self.addSubview(self.buttonsContainerView)
self.buttonsContainerView.addSubview(self.buttonsBackgroundView)
self.addSubview(self.fadeView)
self.addSubview(self.previewContainerView)
self.previewContainerView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.previewTap(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func previewTap(_ gestureRecognizer: UITapGestureRecognizer) {
guard let component = self.component, let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let location = gestureRecognizer.location(in: controller.drawingView)
let point = CGPoint(
x: location.x / controller.drawingView.bounds.width,
y: location.y / controller.drawingView.bounds.height
)
let validRange: Range<CGFloat> = 0.0 ..< 1.0
guard validRange.contains(point.x) && validRange.contains(point.y) else {
return
}
component.mediaEditor.processImage { [weak self] originalImage, _ in
cutoutImage(from: originalImage, crop: nil, target: .point(point), includeExtracted: false, completion: { [weak self] results in
Queue.mainQueue().async {
if let self, let _ = self.component, let result = results.first, let maskImage = result.maskImage, let controller = self.environment?.controller() as? MediaCutoutScreen {
if case let .image(mask, _) = maskImage {
self.playDissolveAnimation()
component.mediaEditor.setSegmentationMask(mask)
if let maskData = mask.pngData() {
controller.drawingView.setup(withDrawing: maskData)
}
}
}
}
})
}
HapticFeedback().impact(.medium)
}
var initialOutlineValue: Float?
func animateInFromEditor() {
guard let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let mediaEditor = controller.mediaEditor
self.initialOutlineValue = mediaEditor.getToolValue(.stickerOutline) as? Float
mediaEditor.setToolValue(.stickerOutline, value: nil)
mediaEditor.isSegmentationMaskEnabled = false
controller.previewView.mask = controller.maskWrapperView
self.buttonsBackgroundView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
self.label.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2)
}
guard [.erase, .restore].contains(controller.mode) else {
return
}
controller.drawingView.isUserInteractionEnabled = true
self.updateBackgroundViews()
}
func updateBackgroundViews() {
guard let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let overlayView = controller.overlayView
let backgroundView = controller.backgroundView
let overlayAlpha: CGFloat
let backgroundAlpha: CGFloat
switch controller.mode {
case .restore:
overlayAlpha = 1.0
backgroundAlpha = 0.0
default:
overlayAlpha = 0.0
backgroundAlpha = 1.0
}
let transition = ComponentTransition(animation: .curve(duration: 0.2, curve: .easeInOut))
transition.setAlpha(view: overlayView, alpha: overlayAlpha)
transition.setAlpha(view: backgroundView, alpha: backgroundAlpha)
}
private var animatingOut = false
func animateOutToEditor(completion: @escaping () -> Void) {
guard let controller = self.environment?.controller() as? MediaCutoutScreen else {
return
}
let mediaEditor = controller.mediaEditor
if let drawingImage = controller.drawingView.drawingImage {
mediaEditor.setSegmentationMask(drawingImage, andEnable: true)
}
let initialOutlineValue = self.initialOutlineValue
mediaEditor.setOnNextDisplay { [weak controller, weak mediaEditor] in
controller?.previewView.mask = nil
if let initialOutlineValue {
mediaEditor?.setToolValue(.stickerOutline, value: initialOutlineValue)
}
controller?.completed()
}
self.animatingOut = true
self.cancelButton.view?.isHidden = true
self.fadeView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
for outlineView in self.outlineViews {
outlineView.layer.animateAlpha(from: self.fadeView.alpha, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
self.buttonsBackgroundView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { _ in
completion()
})
self.label.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
if let view = self.doneButton.view {
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2)
}
self.state?.updated()
guard [.erase, .restore].contains(controller.mode) else {
return
}
controller.drawingView.isUserInteractionEnabled = false
controller.overlayView.alpha = 0.0
controller.backgroundView.alpha = 1.0
}
public func playDissolveAnimation() {
guard let component = self.component, let resultImage = component.mediaEditor.resultImage, let environment = self.environment, let controller = environment.controller() as? MediaCutoutScreen else {
return
}
let previewView = controller.previewView
let maxSize = CGSize(width: 320.0, height: 568.0)
let fittedSize = previewView.bounds.size.aspectFitted(maxSize)
let scale = previewView.bounds.width / fittedSize.width
let dustEffectLayer = DustEffectLayer()
dustEffectLayer.position = previewView.center
dustEffectLayer.bounds = CGRect(origin: .zero, size: fittedSize)
dustEffectLayer.transform = CATransform3DMakeScale(scale, scale, 1.0)
previewView.superview?.layer.insertSublayer(dustEffectLayer, below: previewView.layer)
dustEffectLayer.animationSpeed = 2.2
dustEffectLayer.becameEmpty = { [weak dustEffectLayer] in
dustEffectLayer?.removeFromSuperlayer()
}
dustEffectLayer.addItem(frame: dustEffectLayer.bounds, image: resultImage)
controller.completedWithCutout()
controller.requestDismiss(animated: true)
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if let controller = self.environment?.controller() as? MediaCutoutScreen, [.erase, .restore].contains(controller.mode), result == self.previewContainerView {
return nil
}
return result
}
func update(component: MediaCutoutScreenComponent, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.environment = environment
guard let controller = environment.controller() as? MediaCutoutScreen else {
return .zero
}
let isFirstTime = self.component == nil
self.component = component
self.state = state
let isTablet: Bool
if case .regular = environment.metrics.widthClass {
isTablet = true
} else {
isTablet = false
}
let buttonSideInset: CGFloat
let buttonBottomInset: CGFloat = 8.0
var controlsBottomInset: CGFloat = 0.0
let previewSize: CGSize
var topInset: CGFloat = environment.statusBarHeight + 5.0
if isTablet {
let previewHeight = availableSize.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
buttonSideInset = 30.0
} else {
previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778))
buttonSideInset = 10.0
if availableSize.height < previewSize.height + 30.0 {
topInset = 0.0
controlsBottomInset = -75.0
} else {
self.buttonsBackgroundView.backgroundColor = .clear
}
}
let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom + controlsBottomInset))
let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom + controlsBottomInset), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom - controlsBottomInset))
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(
LottieAnimationComponent(
animation: LottieAnimationComponent.AnimationItem(
name: "media_backToCancel",
mode: .animating(loop: false),
range: self.animatingOut ? (0.5, 1.0) : (0.0, 0.5)
),
colors: ["__allcolors__": .white],
size: CGSize(width: 33.0, height: 33.0)
)
),
action: { [weak controller] in
controller?.requestDismiss(animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let cancelButtonFrame = CGRect(
origin: CGPoint(x: buttonSideInset, y: buttonBottomInset),
size: cancelButtonSize
)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.buttonsContainerView.addSubview(cancelButtonView)
}
transition.setFrame(view: cancelButtonView, frame: cancelButtonFrame)
}
if case .cutout = controller.mode {
} else {
let doneButtonSize = self.doneButton.update(
transition: transition,
component: AnyComponent(Button(
content: AnyComponent(Image(
image: state.image(.done),
size: CGSize(width: 33.0, height: 33.0)
)),
action: { [weak controller] in
controller?.requestDismiss(animated: true)
}
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let doneButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width, y: buttonBottomInset),
size: doneButtonSize
)
if let doneButtonView = self.doneButton.view {
if doneButtonView.superview == nil {
self.buttonsContainerView.addSubview(doneButtonView)
}
transition.setFrame(view: doneButtonView, frame: doneButtonFrame)
}
}
let helpText: String
switch controller.mode {
case .cutout:
helpText = environment.strings.MediaEditor_CutoutInfo
case .erase:
helpText = environment.strings.MediaEditor_EraseInfo
case .restore:
helpText = environment.strings.MediaEditor_RestoreInfo
}
let labelSize = self.label.update(
transition: transition,
component: AnyComponent(Text(text: helpText, font: Font.regular(17.0), color: UIColor(rgb: 0x8d8d93))),
environment: {},
containerSize: CGSize(width: availableSize.width - 88.0, height: 44.0)
)
let labelFrame = CGRect(
origin: CGPoint(x: floorToScreenPixels((availableSize.width - labelSize.width) / 2.0), y: buttonBottomInset + 4.0),
size: labelSize
)
if let labelView = self.label.view {
if labelView.superview == nil {
self.buttonsContainerView.addSubview(labelView)
}
if labelView.bounds.width > 0.0 && labelFrame.width != labelView.bounds.width {
if let snapshotView = labelView.snapshotView(afterScreenUpdates: false) {
snapshotView.center = labelView.center
self.buttonsContainerView.addSubview(snapshotView)
labelView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
snapshotView.removeFromSuperview()
})
}
}
labelView.bounds = CGRect(origin: .zero, size: labelFrame.size)
transition.setPosition(view: labelView, position: labelFrame.center)
}
transition.setFrame(view: self.buttonsContainerView, frame: buttonsContainerFrame)
transition.setFrame(view: self.buttonsBackgroundView, frame: CGRect(origin: .zero, size: buttonsContainerFrame.size))
transition.setFrame(view: self.previewContainerView, frame: previewContainerFrame)
if case .cutout = controller.mode {
for view in self.outlineViews {
transition.setFrame(view: view, frame: previewContainerFrame)
}
let frameWidth = floorToScreenPixels(previewContainerFrame.width * 0.97)
self.fadeView.frame = CGRect(x: floorToScreenPixels((previewContainerFrame.width - frameWidth) / 2.0), y: previewContainerFrame.minY + floorToScreenPixels((previewContainerFrame.height - frameWidth) / 2.0), width: frameWidth, height: frameWidth)
self.fadeView.layer.cornerRadius = frameWidth / 8.0
if isFirstTime {
let values = component.mediaEditor.values
component.mediaEditor.processImage { originalImage, editedImage in
cutoutImage(from: originalImage, editedImage: editedImage, crop: values.cropValues, target: .all, completion: { results in
Queue.mainQueue().async {
if !results.isEmpty {
for result in results {
if let extractedImage = result.extractedImage, let maskImage = result.edgesMaskImage {
if case let .image(image, _) = extractedImage, case let .image(_, mask) = maskImage {
let outlineView = StickerCutoutOutlineView(frame: self.previewContainerView.frame)
outlineView.update(image: image, maskImage: mask, size: self.previewContainerView.bounds.size, values: values)
self.insertSubview(outlineView, belowSubview: self.previewContainerView)
self.outlineViews.append(outlineView)
}
}
}
self.state?.updated(transition: .easeInOut(duration: 0.4))
}
}
})
}
} else {
transition.setAlpha(view: self.fadeView, alpha: !self.outlineViews.isEmpty ? 1.0 : 0.0)
}
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class MediaCutoutScreen: ViewController {
fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate {
private weak var controller: MediaCutoutScreen?
private let context: AccountContext
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
private var presentationData: PresentationData
private var validLayout: ContainerViewLayout?
init(controller: MediaCutoutScreen) {
self.controller = controller
self.context = controller.context
self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
self.componentHost = ComponentView<ViewControllerComponentContainer.Environment>()
super.init()
self.backgroundColor = .clear
}
override func didLoad() {
super.didLoad()
self.view.disablesInteractiveModalDismiss = true
self.view.disablesInteractiveKeyboardGestureRecognizer = true
}
@objc func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
func animateInFromEditor() {
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.animateInFromEditor()
}
}
func animateOutToEditor(completion: @escaping () -> Void) {
if let mediaEditor = self.controller?.mediaEditor {
mediaEditor.play()
}
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.animateOutToEditor(completion: completion)
}
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result === self.view {
return nil
}
return result
}
func requestLayout(transition: ComponentTransition) {
if let layout = self.validLayout {
self.containerLayoutUpdated(layout: layout, forceUpdate: true, transition: transition)
if let view = self.componentHost.view as? MediaCutoutScreenComponent.View {
view.updateBackgroundViews()
}
}
}
func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: ComponentTransition) {
guard let controller = self.controller else {
return
}
let isFirstTime = self.validLayout == nil
self.validLayout = layout
let isTablet = layout.metrics.isTablet
let previewSize: CGSize
let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 5.0
if isTablet {
let previewHeight = layout.size.height - topInset - 75.0
previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight)
} else {
previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
}
let bottomInset = layout.size.height - previewSize.height - topInset
let environment = ViewControllerComponentContainer.Environment(
statusBarHeight: layout.statusBarHeight ?? 0.0,
navigationHeight: 0.0,
safeInsets: UIEdgeInsets(
top: topInset,
left: layout.safeInsets.left,
bottom: bottomInset,
right: layout.safeInsets.right
),
additionalInsets: layout.additionalInsets,
inputHeight: layout.inputHeight ?? 0.0,
metrics: layout.metrics,
deviceMetrics: layout.deviceMetrics,
orientation: nil,
isVisible: true,
theme: self.presentationData.theme,
strings: self.presentationData.strings,
dateTimeFormat: self.presentationData.dateTimeFormat,
controller: { [weak self] in
return self?.controller
}
)
let componentSize = self.componentHost.update(
transition: transition,
component: AnyComponent(
MediaCutoutScreenComponent(
context: self.context,
mediaEditor: controller.mediaEditor
)
),
environment: {
environment
},
forceUpdate: forceUpdate || animateOut,
containerSize: layout.size
)
if let componentView = self.componentHost.view {
if componentView.superview == nil {
self.view.insertSubview(componentView, at: 3)
componentView.clipsToBounds = true
}
let componentFrame = CGRect(origin: .zero, size: componentSize)
transition.setFrame(view: componentView, frame: CGRect(origin: componentFrame.origin, size: CGSize(width: componentFrame.width, height: componentFrame.height)))
}
if isFirstTime {
self.animateInFromEditor()
}
}
}
fileprivate var node: Node {
return self.displayNode as! Node
}
fileprivate let context: AccountContext
public var mode: Mode {
didSet {
self.updateDrawingState()
self.node.requestLayout(transition: .easeInOut(duration: 0.2))
}
}
fileprivate let mediaEditor: MediaEditor
fileprivate let maskWrapperView: UIView
fileprivate let previewView: MediaEditorPreviewView
fileprivate let drawingView: DrawingView
fileprivate let overlayView: UIView
fileprivate let backgroundView: UIView
var completed: () -> Void = {}
var completedWithCutout: () -> Void = {}
var dismissed: () -> Void = {}
private var initialValues: MediaEditorValues
enum Mode {
case cutout
case erase
case restore
}
init(
context: AccountContext,
mode: Mode,
mediaEditor: MediaEditor,
previewView: MediaEditorPreviewView,
maskWrapperView: UIView,
drawingView: DrawingView,
overlayView: UIView,
backgroundView: UIView
) {
self.context = context
self.mode = mode
self.mediaEditor = mediaEditor
self.previewView = previewView
self.maskWrapperView = maskWrapperView
self.drawingView = drawingView
self.overlayView = overlayView
self.backgroundView = backgroundView
self.initialValues = mediaEditor.values.makeCopy()
super.init(navigationBarPresentationData: nil)
self.navigationPresentation = .flatModal
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
self.statusBar.statusBarStyle = .White
self.updateDrawingState()
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func loadDisplayNode() {
self.displayNode = Node(controller: self)
super.displayNodeDidLoad()
}
private func updateDrawingState() {
if let toolState = self.drawingView.appliedToolState {
if case .erase = mode {
self.drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .black)))
} else if case .restore = mode {
self.drawingView.updateToolState(toolState.withUpdatedColor(DrawingColor(color: .white)))
}
}
}
func requestDismiss(animated: Bool) {
self.dismissed()
self.node.animateOutToEditor(completion: {
self.dismiss()
})
}
override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition))
}
}
@@ -0,0 +1,269 @@
import Foundation
import UIKit
import Display
import CoreLocation
import Photos
import Postbox
import TelegramCore
import AccountContext
import MediaEditor
import DrawingUI
extension MediaEditorScreenImpl {
func isEligibleForDraft() -> Bool {
guard !self.isEditingStory else {
return false
}
if case .avatarEditor = self.mode {
return false
}
if case .coverEditor = self.mode {
return false
}
if case .multiple = self.node.actualSubject {
return false
}
guard let mediaEditor = self.node.mediaEditor else {
return false
}
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
let filteredEntities = self.node.entitiesView.entities.filter { entity in
if entity is DrawingMediaEntity {
return false
} else if let entity = entity as? DrawingStickerEntity {
switch entity.content {
case .message, .gift:
return false
default:
break
}
}
return true
}
let values = mediaEditor.values
let filteredValues = values.withUpdatedEntities([])
let caption = self.node.getCaption()
if let subject = self.node.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?, 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.node.getCaption()
let duration = mediaEditor.duration ?? 0.0
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
var timestamp: Int32
var location: CLLocationCoordinate2D?
let expiresOn: Int32
if case let .draft(draft, _) = actualSubject {
timestamp = draft.timestamp
location = draft.location
if let _ = id {
expiresOn = draft.expiresOn ?? currentTimestamp + 3600 * 24 * 7
} else {
expiresOn = currentTimestamp + 3600 * 24 * 7
}
} else {
timestamp = currentTimestamp
if case let .asset(asset) = subject {
location = asset.location?.coordinate
}
if let _ = id {
expiresOn = currentTimestamp + Int32(self.state.privacy.timeout)
} else {
expiresOn = currentTimestamp + 3600 * 24 * 7
}
}
if let resultImage = mediaEditor.resultImage {
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
guard let resultImage else {
return
}
enum MediaInput {
case image(image: UIImage, dimensions: PixelDimensions)
case video(path: String, dimensions: PixelDimensions, duration: Double)
var isVideo: Bool {
switch self {
case .video:
return true
case .image:
return false
}
}
var dimensions: PixelDimensions {
switch self {
case let .image(_, dimensions):
return dimensions
case let .video(_, dimensions, _):
return dimensions
}
}
var duration: Double? {
switch self {
case .image:
return nil
case let .video(_, _, duration):
return duration
}
}
var fileExtension: String {
switch self {
case .image:
return "jpg"
case .video:
return "mp4"
}
}
}
let context = self.context
func innerSaveDraft(media: MediaInput, save: Bool = true) -> MediaEditorDraft? {
let fittedSize = resultImage.size.aspectFitted(CGSize(width: 128.0, height: 128.0))
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, _, _, _):
if let draft = innerSaveDraft(media: .image(image: image, dimensions: dimensions)) {
completion?(draft)
}
case let .video(path, _, _, _, _, dimensions, _, _, _, _):
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 {
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 {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
if let draft = innerSaveDraft(media: .image(image: image, dimensions: PixelDimensions(image.size))) {
completion?(draft)
}
}
}
}
case let .draft(draft, _):
if draft.isVideo {
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)) {
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) {
if let draft = innerSaveDraft(media: .image(image: pixel, dimensions: PixelDimensions(width: 1080, height: 1920))) {
completion?(draft)
}
}
case .sticker:
break
case .multiple:
break
}
if case let .draft(draft, _) = actualSubject {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
}
})
}
}
}
@@ -0,0 +1,187 @@
import Foundation
import UIKit
import Display
import SwiftSignalKit
import MediaEditor
import DrawingUI
import ChatPresentationInterfaceState
import PresentationDataUtils
import TelegramPresentationData
import DeviceAccess
import AccountContext
extension MediaEditorScreenImpl {
final class Recording {
private weak var controller: MediaEditorScreenImpl?
private var recorder: EntityVideoRecorder?
private let idleTimerExtensionDisposable = MetaDisposable()
private var authorizationStatusDisposables = DisposableSet()
private var cameraAuthorizationStatus: AccessType = .notDetermined
private var microphoneAuthorizationStatus: AccessType = .notDetermined
fileprivate var cameraIsActive = true {
didSet {
guard let context = self.controller?.context else {
return
}
if self.cameraIsActive {
self.idleTimerExtensionDisposable.set(context.sharedContext.applicationBindings.pushIdleTimerExtension())
} else {
self.idleTimerExtensionDisposable.set(nil)
}
}
}
var isLocked = false
init(controller: MediaEditorScreenImpl) {
self.controller = controller
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .camera(.video))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.cameraAuthorizationStatus = status
}
}))
self.authorizationStatusDisposables.add((DeviceAccess.authorizationStatus(subject: .microphone(.video))
|> deliverOnMainQueue).start(next: { [weak self] status in
if let self {
self.microphoneAuthorizationStatus = status
}
}))
}
deinit {
self.idleTimerExtensionDisposable.dispose()
self.authorizationStatusDisposables.dispose()
}
func requestDeviceAccess() {
guard let controller = self.controller else {
return
}
let context = controller.context
let presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
DeviceAccess.authorizeAccess(to: .camera(.video), presentationData: presentationData, present: { c, a in
c.presentationArguments = a
context.sharedContext.mainWindow?.present(c, on: .root)
}, openSettings: {
context.sharedContext.applicationBindings.openSettings()
}, { granted in
if granted {
DeviceAccess.authorizeAccess(to: .microphone(.video), presentationData: presentationData, present: { c, a in
c.presentationArguments = a
context.sharedContext.mainWindow?.present(c, on: .root)
}, openSettings: {
context.sharedContext.applicationBindings.openSettings()
})
}
})
}
func setMediaRecordingActive(_ isActive: Bool, finished: Bool, sourceView: UIView?) {
guard let controller, let mediaEditor = controller.node.mediaEditor else {
return
}
if mediaEditor.values.additionalVideoPath != nil {
controller.node.presentVideoRemoveConfirmation()
return
}
if isActive {
if self.cameraAuthorizationStatus != .allowed || self.microphoneAuthorizationStatus != .allowed {
self.requestDeviceAccess()
return
}
guard self.recorder == nil else {
return
}
HapticFeedback().impact(.light)
let recorder = EntityVideoRecorder(mediaEditor: mediaEditor, entitiesView: controller.node.entitiesView)
recorder.setup(
referenceDrawingSize: storyDimensions,
scale: 1.625,
position: PIPPosition.topRight.getPosition(storyDimensions)
)
recorder.onAutomaticStop = { [weak self] in
if let self {
self.recorder = nil
self.controller?.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
}
}
self.recorder = recorder
controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
self.cameraIsActive = true
} else {
if let recorder = self.recorder {
recorder.stopRecording(save: finished, completion: { [weak self] in
guard let self else {
return
}
self.recorder = nil
self.isLocked = false
self.controller?.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
})
controller.node.requestLayout(forceUpdate: true, transition: .easeInOut(duration: 0.2))
self.cameraIsActive = false
} else {
if self.cameraAuthorizationStatus != .allowed || self.microphoneAuthorizationStatus != .allowed {
self.requestDeviceAccess()
return
}
guard self.tooltipController == nil, let sourceView else {
return
}
let rect = sourceView.convert(sourceView.bounds, to: nil)
let presentationData = controller.context.sharedContext.currentPresentationData.with { $0 }
let text = presentationData.strings.MediaEditor_HoldToRecordVideo
let tooltipController = TooltipController(content: .text(text), baseFontSize: presentationData.listsFontSize.baseDisplaySize, padding: 2.0)
tooltipController.dismissed = { [weak self] _ in
if let self {
self.tooltipController = nil
}
}
controller.present(tooltipController, in: .window(.root), with: TooltipControllerPresentationArguments(sourceViewAndRect: { [weak self] in
if let view = self?.controller?.view {
return (view, rect)
}
return nil
}))
self.tooltipController = tooltipController
}
}
}
private var tooltipController: TooltipController?
func togglePosition() {
if let recorder = self.recorder {
recorder.togglePosition()
}
}
var status: InstantVideoControllerRecordingStatus? {
if let recorder = self.recorder {
return InstantVideoControllerRecordingStatus(
micLevel: recorder.micLevel,
duration: recorder.duration
)
} else {
return nil
}
}
var isActive: Bool {
return self.status != nil
}
}
}
@@ -0,0 +1,869 @@
import Foundation
import UIKit
import Display
import AVFoundation
import SwiftSignalKit
import TelegramCore
import TextFormat
import Photos
import MediaEditor
import DrawingUI
extension MediaEditorScreenImpl {
func requestStoryCompletion(animated: Bool) {
guard let mediaEditor = self.node.mediaEditor, !self.didComplete else {
return
}
self.didComplete = true
self.updateMediaEditorEntities()
mediaEditor.stop()
mediaEditor.invalidate()
self.node.entitiesView.invalidate()
if let navigationController = self.navigationController as? NavigationController {
navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate)
}
var multipleItems: [EditingItem] = []
var isLongVideo = false
if self.node.items.count > 1 {
multipleItems = self.node.items.filter({ $0.isEnabled })
} else if case let .asset(asset) = self.node.subject {
let duration: Double
if let playerDuration = mediaEditor.duration {
duration = playerDuration
} else {
duration = asset.duration
}
if duration > storyMaxVideoDuration {
let originalDuration = mediaEditor.originalDuration ?? asset.duration
let values = mediaEditor.values
let storyCount = min(storyMaxCombinedVideoCount, Int(ceil(duration / storyMaxVideoDuration)))
var start = values.videoTrimRange?.lowerBound ?? 0
let end = values.videoTrimRange?.upperBound ?? (min(originalDuration, start + storyMaxCombinedVideoDuration))
for i in 0 ..< storyCount {
guard var editingItem = EditingItem(subject: .asset(asset)) else {
continue
}
let trimmedValues = values.withUpdatedVideoTrimRange(start ..< min(end, start + storyMaxVideoDuration))
if i == 0 {
editingItem.caption = self.node.getCaption()
}
editingItem.values = trimmedValues
multipleItems.append(editingItem)
start += storyMaxVideoDuration
}
isLongVideo = true
}
}
if multipleItems.count > 1 {
self.processMultipleItems(items: multipleItems, isLongVideo: isLongVideo)
} else {
self.processSingleItem()
}
self.dismissAllTooltips()
}
private func processSingleItem() {
guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let actualSubject = self.node.actualSubject else {
return
}
var caption = self.node.getCaption()
caption = convertMarkdownToAttributes(caption)
var hasEntityChanges = false
let randomId: Int64
if case let .draft(_, id) = actualSubject, let id {
randomId = id
} else {
randomId = Int64.random(in: .min ... .max)
}
let codableEntities = mediaEditor.values.entities
var mediaAreas: [MediaArea] = []
if case let .draft(draft, _) = actualSubject {
if draft.values.entities != codableEntities {
hasEntityChanges = true
}
} else {
mediaAreas = self.initialMediaAreas ?? []
}
var stickers: [TelegramMediaFile] = []
for entity in codableEntities {
switch entity {
case let .sticker(stickerEntity):
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
case let .text(textEntity):
if let subEntities = textEntity.renderSubEntities {
for entity in subEntities {
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
}
}
default:
break
}
if let mediaArea = entity.mediaArea {
mediaAreas.append(mediaArea)
}
}
var hasAnyChanges = self.node.hasAnyChanges
if self.isEditingStoryCover {
hasAnyChanges = false
}
if self.isEmbeddedEditor && !(hasAnyChanges || hasEntityChanges) {
self.saveDraft(id: randomId, isEdit: true)
self.completion([MediaEditorScreenImpl.Result(media: nil, mediaAreas: [], caption: caption, coverTimestamp: mediaEditor.values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
return
}
if !(self.isEditingStory || self.isEditingStoryCover) {
let privacy = self.state.privacy
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
if let current {
return current.withUpdatedPrivacy(privacy)
} else {
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
}
}).start()
}
if mediaEditor.resultIsVideo {
self.saveDraft(id: randomId)
var firstFrame: Signal<(UIImage?, UIImage?), NoError>
let firstFrameTime: CMTime
if let coverImageTimestamp = mediaEditor.values.coverImageTimestamp {
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
} else {
firstFrameTime = CMTime(seconds: mediaEditor.values.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
}
let videoResult: Signal<MediaResult.VideoResult, NoError>
var videoIsMirrored = false
let duration: Double
switch subject {
case let .empty(dimensions):
let image = generateImage(dimensions.cgSize, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 3.0
firstFrame = .single((image, nil))
case let .image(image, _, _, _, _):
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 5.0
firstFrame = .single((image, nil))
case let .video(path, _, mirror, additionalPath, _, _, durationValue, _, _, _):
videoIsMirrored = mirror
videoResult = .single(.videoFile(path: path))
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = durationValue
}
var additionalPath = additionalPath
if additionalPath == nil, let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
additionalPath = valuesAdditionalPath
}
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
let avAsset = AVURLAsset(url: URL(fileURLWithPath: path))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
if let cgImage {
if let additionalPath {
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
if let additionalCGImage {
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
subscriber.putCompletion()
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
})
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
}
})
return ActionDisposable {
avAssetGenerator.cancelAllCGImageGeneration()
}
}
case let .videoCollage(items):
var maxDurationItem: (Double, Subject.VideoCollageItem)?
for item in items {
switch item.content {
case .image:
break
case let .video(_, duration):
if let (maxDuration, _) = maxDurationItem {
if duration > maxDuration {
maxDurationItem = (duration, item)
}
} else {
maxDurationItem = (duration, item)
}
case let .asset(asset):
if let (maxDuration, _) = maxDurationItem {
if asset.duration > maxDuration {
maxDurationItem = (asset.duration, item)
}
} else {
maxDurationItem = (asset.duration, item)
}
}
}
guard let (maxDuration, mainItem) = maxDurationItem else {
fatalError()
}
switch mainItem.content {
case let .video(path, _):
videoResult = .single(.videoFile(path: path))
case let .asset(asset):
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
default:
fatalError()
}
let image = generateImage(storyDimensions, opaque: false, scale: 1.0, rotatedContext: { size, context in
context.clear(CGRect(origin: .zero, size: size))
})!
firstFrame = .single((image, nil))
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(maxDuration, storyMaxVideoDuration)
}
case let .asset(asset):
videoResult = .single(.asset(localIdentifier: asset.localIdentifier))
if asset.mediaType == .video {
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(asset.duration, storyMaxVideoDuration)
}
} else {
duration = 5.0
}
var additionalPath: String?
if let valuesAdditionalPath = mediaEditor.values.additionalVideoPath {
additionalPath = valuesAdditionalPath
}
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
if asset.mediaType == .video {
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
if let avAsset {
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
if let cgImage {
if let additionalPath {
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
if let additionalCGImage {
subscriber.putNext((UIImage(cgImage: cgImage), UIImage(cgImage: additionalCGImage)))
subscriber.putCompletion()
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
})
} else {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
}
})
}
}
} else {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
if let image {
if let additionalPath {
let avAsset = AVURLAsset(url: URL(fileURLWithPath: additionalPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, additionalCGImage, _, _, _ in
if let additionalCGImage {
subscriber.putNext((image, UIImage(cgImage: additionalCGImage)))
subscriber.putCompletion()
} else {
subscriber.putNext((image, nil))
subscriber.putCompletion()
}
})
} else {
subscriber.putNext((image, nil))
subscriber.putCompletion()
}
}
}
}
return EmptyDisposable
}
case let .draft(draft, _):
let draftPath = draft.fullPath(engine: context.engine)
if draft.isVideo {
videoResult = .single(.videoFile(path: draftPath))
if let videoTrimRange = mediaEditor.values.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(draft.duration ?? 5.0, storyMaxVideoDuration)
}
firstFrame = Signal<(UIImage?, UIImage?), NoError> { subscriber in
let avAsset = AVURLAsset(url: URL(fileURLWithPath: draftPath))
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)], completionHandler: { _, cgImage, _, _, _ in
if let cgImage {
subscriber.putNext((UIImage(cgImage: cgImage), nil))
subscriber.putCompletion()
}
})
return ActionDisposable {
avAssetGenerator.cancelAllCGImageGeneration()
}
}
} else {
videoResult = .single(.imageFile(path: draftPath))
duration = 5.0
if let image = UIImage(contentsOfFile: draftPath) {
firstFrame = .single((image, nil))
} else {
firstFrame = .single((UIImage(), nil))
}
}
case .message, .gift:
let peerId: EnginePeer.Id
if case let .message(messageIds) = subject {
peerId = messageIds.first!.peerId
} else {
peerId = self.context.account.peerId
}
let isNightTheme = mediaEditor.values.nightTheme
let wallpaper = getChatWallpaperImage(context: self.context, peerId: peerId)
|> map { _, image, nightImage -> UIImage? in
if isNightTheme {
return nightImage ?? image
} else {
return image
}
}
videoResult = wallpaper
|> mapToSignal { image in
if let image {
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg"
if let data = image.jpegData(compressionQuality: 0.85) {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
return .single(.imageFile(path: tempImagePath))
} else {
return .complete()
}
}
firstFrame = wallpaper
|> map { image in
return (image, nil)
}
duration = 5.0
case .sticker:
let image = generateImage(storyDimensions, contextGenerator: { size, context in
context.clear(CGRect(origin: .zero, size: size))
}, opaque: false, scale: 1.0)
let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).png"
if let data = image?.pngData() {
try? data.write(to: URL(fileURLWithPath: tempImagePath))
}
videoResult = .single(.imageFile(path: tempImagePath))
duration = 3.0
firstFrame = .single((image, nil))
case .multiple:
fatalError()
}
let _ = combineLatest(queue: Queue.mainQueue(), firstFrame, videoResult)
.start(next: { [weak self] images, videoResult in
if let self {
let (image, additionalImage) = images
var currentImage = mediaEditor.resultImage
if let image {
mediaEditor.replaceSource(image, additionalImage: additionalImage, time: firstFrameTime, mirror: true)
if let updatedImage = mediaEditor.getResultImage(mirror: videoIsMirrored) {
currentImage = updatedImage
}
}
var inputImage: UIImage
if let currentImage {
inputImage = currentImage
} else if let image {
inputImage = image
} else {
inputImage = UIImage()
}
var values = mediaEditor.values
if case .avatarEditor = self.mode, values.videoTrimRange == nil && duration > avatarMaxVideoDuration {
values = values.withUpdatedVideoTrimRange(0 ..< avatarMaxVideoDuration)
}
makeEditorImageComposition(context: self.node.ciContext, postbox: self.context.account.postbox, inputImage: inputImage, dimensions: storyDimensions, values: values, time: firstFrameTime, textScale: 2.0, completion: { [weak self] coverImage in
if let self {
self.willComplete(coverImage, true, { [weak self] in
guard let self else {
return
}
Logger.shared.log("MediaEditor", "Completed with video \(videoResult)")
self.completion([MediaEditorScreenImpl.Result(media: .video(video: videoResult, coverImage: coverImage, values: values, duration: duration, dimensions: values.resultDimensions), mediaAreas: mediaAreas, caption: caption, coverTimestamp: values.coverImageTimestamp, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
})
}
})
}
})
if case let .draft(draft, id) = actualSubject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: false)
}
} else if let image = mediaEditor.resultImage {
self.saveDraft(id: randomId)
var values = mediaEditor.values
var outputDimensions: CGSize?
if case .avatarEditor = self.mode {
outputDimensions = CGSize(width: 640.0, height: 640.0)
values = values.withUpdatedQualityPreset(.profile)
}
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: image,
dimensions: storyDimensions,
outputDimensions: outputDimensions,
values: values,
time: .zero,
textScale: 2.0,
completion: { [weak self] resultImage in
if let self, let resultImage {
self.willComplete(resultImage, false, { [weak self] in
guard let self else {
return
}
Logger.shared.log("MediaEditor", "Completed with image \(resultImage)")
self.completion([MediaEditorScreenImpl.Result(media: .image(image: resultImage, dimensions: PixelDimensions(resultImage.size)), mediaAreas: mediaAreas, caption: caption, coverTimestamp: nil, options: self.state.privacy, stickers: stickers, randomId: randomId)], { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
if case let .draft(draft, id) = actualSubject, id == nil {
removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true)
}
})
}
})
}
}
private func processMultipleItems(items: [EditingItem], isLongVideo: Bool) {
guard !items.isEmpty else {
return
}
var items = items
if !isLongVideo, let mediaEditor = self.node.mediaEditor, let subject = self.node.subject, let currentItemIndex = items.firstIndex(where: { $0.source.identifier == subject.sourceIdentifier }) {
var updatedCurrentItem = items[currentItemIndex]
updatedCurrentItem.caption = self.node.getCaption()
updatedCurrentItem.values = mediaEditor.values
items[currentItemIndex] = updatedCurrentItem
}
let multipleResults = Atomic<[MediaEditorScreenImpl.Result]>(value: [])
let totalItems = items.count
let dispatchGroup = DispatchGroup()
let privacy = self.state.privacy
if !(self.isEditingStory || self.isEditingStoryCover) {
let _ = updateMediaEditorStoredStateInteractively(engine: self.context.engine, { current in
if let current {
return current.withUpdatedPrivacy(privacy)
} else {
return MediaEditorStoredState(privacy: privacy, textSettings: nil)
}
}).start()
}
var order: [Int64] = []
for (index, item) in items.enumerated() {
guard item.isEnabled else {
continue
}
dispatchGroup.enter()
let randomId = Int64.random(in: .min ... .max)
order.append(randomId)
if item.source.isVideo {
processVideoItem(item: item, index: index, randomId: randomId, isLongVideo: isLongVideo) { result in
let _ = multipleResults.modify { results in
var updatedResults = results
updatedResults.append(result)
return updatedResults
}
dispatchGroup.leave()
}
} else {
processImageItem(item: item, index: index, randomId: randomId) { result in
let _ = multipleResults.modify { results in
var updatedResults = results
updatedResults.append(result)
return updatedResults
}
dispatchGroup.leave()
}
}
}
dispatchGroup.notify(queue: .main) {
let results = multipleResults.with { $0 }
if results.count == totalItems {
var orderedResults: [MediaEditorScreenImpl.Result] = []
for id in order {
if let item = results.first(where: { $0.randomId == id }) {
orderedResults.append(item)
}
}
self.completion(orderedResults, { [weak self] finished in
self?.node.animateOut(finished: true, saveDraft: false, completion: { [weak self] in
self?.dismiss()
Queue.mainQueue().justDispatch {
finished()
}
})
})
}
}
}
private func processVideoItem(item: EditingItem, index: Int, randomId: Int64, isLongVideo: Bool, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
let itemMediaEditor = setupMediaEditorForItem(item: item)
var mediaAreas: [MediaArea] = []
var stickers: [TelegramMediaFile] = []
if let entities = item.values?.entities {
for entity in entities {
if let mediaArea = entity.mediaArea {
mediaAreas.append(mediaArea)
}
extractStickersFromEntity(entity, into: &stickers)
}
}
let firstFrameTime: CMTime
if let coverImageTimestamp = item.values?.coverImageTimestamp, !isLongVideo || index == 0 {
firstFrameTime = CMTime(seconds: coverImageTimestamp, preferredTimescale: CMTimeScale(60))
} else {
firstFrameTime = CMTime(seconds: item.values?.videoTrimRange?.lowerBound ?? 0.0, preferredTimescale: CMTimeScale(60))
}
let process: (AVAsset?, MediaResult.VideoResult) -> Void = { [weak self] avAsset, videoResult in
guard let self else {
return
}
guard let avAsset else {
Queue.mainQueue().async {
completion(self.createEmptyResult(randomId: randomId))
}
return
}
let duration: Double
if let videoTrimRange = item.values?.videoTrimRange {
duration = videoTrimRange.upperBound - videoTrimRange.lowerBound
} else {
duration = min(avAsset.duration.seconds, storyMaxVideoDuration)
}
let avAssetGenerator = AVAssetImageGenerator(asset: avAsset)
avAssetGenerator.appliesPreferredTrackTransform = true
avAssetGenerator.generateCGImagesAsynchronously(forTimes: [NSValue(time: firstFrameTime)]) { [weak self] _, cgImage, _, _, _ in
guard let self else {
return
}
Queue.mainQueue().async {
if let cgImage {
let image = UIImage(cgImage: cgImage)
itemMediaEditor.replaceSource(image, additionalImage: nil, time: firstFrameTime, mirror: false)
if let resultImage = itemMediaEditor.resultImage {
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: itemMediaEditor.values,
time: firstFrameTime,
textScale: 2.0
) { coverImage in
if let coverImage = coverImage {
let result = MediaEditorScreenImpl.Result(
media: .video(
video: videoResult,
coverImage: coverImage,
values: itemMediaEditor.values,
duration: duration,
dimensions: itemMediaEditor.values.resultDimensions
),
mediaAreas: mediaAreas,
caption: convertMarkdownToAttributes(item.caption),
coverTimestamp: itemMediaEditor.values.coverImageTimestamp,
options: self.state.privacy,
stickers: stickers,
randomId: randomId
)
completion(result)
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
}
}
switch item.source {
case let .video(videoPath, _, _, _):
let avAsset = AVURLAsset(url: URL(fileURLWithPath: videoPath))
process(avAsset, .videoFile(path: videoPath))
case let .asset(asset):
PHImageManager.default().requestAVAsset(forVideo: asset, options: nil) { avAsset, _, _ in
process(avAsset, .asset(localIdentifier: asset.localIdentifier))
}
default:
fatalError()
}
}
private func processImageItem(item: EditingItem, index: Int, randomId: Int64, completion: @escaping (MediaEditorScreenImpl.Result) -> Void) {
let itemMediaEditor = setupMediaEditorForItem(item: item)
var caption = item.caption
caption = convertMarkdownToAttributes(caption)
var mediaAreas: [MediaArea] = []
var stickers: [TelegramMediaFile] = []
if let entities = item.values?.entities {
for entity in entities {
if let mediaArea = entity.mediaArea {
mediaAreas.append(mediaArea)
}
extractStickersFromEntity(entity, into: &stickers)
}
}
let process: (UIImage?) -> Void = { [weak self] image in
guard let self else {
return
}
guard let image else {
completion(self.createEmptyResult(randomId: randomId))
return
}
itemMediaEditor.replaceSource(image, additionalImage: nil, time: .zero, mirror: false)
if itemMediaEditor.values.gradientColors == nil {
itemMediaEditor.setGradientColors(mediaEditorGetGradientColors(from: image))
}
if let resultImage = itemMediaEditor.resultImage {
makeEditorImageComposition(
context: self.node.ciContext,
postbox: self.context.account.postbox,
inputImage: resultImage,
dimensions: storyDimensions,
values: itemMediaEditor.values,
time: .zero,
textScale: 2.0
) { resultImage in
if let resultImage = resultImage {
let result = MediaEditorScreenImpl.Result(
media: .image(
image: resultImage,
dimensions: PixelDimensions(resultImage.size)
),
mediaAreas: mediaAreas,
caption: caption,
coverTimestamp: nil,
options: self.state.privacy,
stickers: stickers,
randomId: randomId
)
completion(result)
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
} else {
completion(self.createEmptyResult(randomId: randomId))
}
}
switch item.source {
case let .image(image, _):
process(image)
case let .asset(asset):
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isNetworkAccessAllowed = true
PHImageManager.default().requestImage(for: asset, targetSize: PHImageManagerMaximumSize, contentMode: .default, options: options) { image, _ in
Queue.mainQueue().async {
process(image)
}
}
default:
fatalError()
}
}
private func setupMediaEditorForItem(item: EditingItem) -> MediaEditor {
var values = item.values
if values?.videoTrimRange == nil {
values = values?.withUpdatedVideoTrimRange(0 ..< storyMaxVideoDuration)
}
let editorSubject: MediaEditor.Subject
switch item.source {
case let .image(image, dimensions):
editorSubject = .image(image, dimensions)
case let .video(videoPath, thumbnailImage, dimensions, duration):
editorSubject = .video(videoPath, thumbnailImage, false, nil, dimensions, duration)
case let .asset(asset):
editorSubject = .asset(asset)
}
return MediaEditor(
context: self.context,
mode: .default,
subject: editorSubject,
values: values,
hasHistogram: false,
isStandalone: true
)
}
private func extractStickersFromEntity(_ entity: CodableDrawingEntity, into stickers: inout [TelegramMediaFile]) {
switch entity {
case let .sticker(stickerEntity):
if case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
case let .text(textEntity):
if let subEntities = textEntity.renderSubEntities {
for entity in subEntities {
if let stickerEntity = entity as? DrawingStickerEntity, case let .file(file, fileType) = stickerEntity.content, case .sticker = fileType {
stickers.append(file.media)
}
}
}
default:
break
}
}
private func createEmptyResult(randomId: Int64) -> MediaEditorScreenImpl.Result {
let emptyImage = UIImage()
return MediaEditorScreenImpl.Result(
media: .image(
image: emptyImage,
dimensions: PixelDimensions(emptyImage.size)
),
mediaAreas: [],
caption: NSAttributedString(),
coverTimestamp: nil,
options: self.state.privacy,
stickers: [],
randomId: randomId
)
}
func updateMediaEditorEntities() {
guard let mediaEditor = self.node.mediaEditor else {
return
}
let entities = self.node.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }
let codableEntities = DrawingEntitiesView.encodeEntities(entities, entitiesView: self.node.entitiesView)
mediaEditor.setDrawingAndEntities(data: nil, image: mediaEditor.values.drawing, entities: codableEntities)
}
}
@@ -0,0 +1,140 @@
import Foundation
import UIKit
import Display
import ComponentFlow
final class SelectionPanelButtonContentComponent: Component {
let count: Int32
let isSelected: Bool
let tag: AnyObject?
init(
count: Int32,
isSelected: Bool,
tag: AnyObject?
) {
self.count = count
self.isSelected = isSelected
self.tag = tag
}
static func ==(lhs: SelectionPanelButtonContentComponent, rhs: SelectionPanelButtonContentComponent) -> Bool {
return lhs.count == rhs.count && lhs.isSelected == rhs.isSelected
}
final class View: UIView, ComponentTaggedView {
private var component: SelectionPanelButtonContentComponent?
public func matches(tag: Any) -> Bool {
if let component = self.component, let componentTag = component.tag {
let tag = tag as AnyObject
if componentTag === tag {
return true
}
}
return false
}
let backgroundView: BlurredBackgroundView
private let outline = SimpleLayer()
private let icon = SimpleLayer()
private let label = ComponentView<Empty>()
init() {
self.backgroundView = BlurredBackgroundView(color: UIColor(white: 0.2, alpha: 0.45), enableBlur: true)
self.icon.opacity = 0.0
super.init(frame: CGRect())
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.icon)
self.layer.addSublayer(self.outline)
self.outline.contents = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let lineWidth: CGFloat = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(UIColor.white.cgColor)
context.strokeEllipse(in: bounds.insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
})?.cgImage
self.icon.contents = generateImage(CGSize(width: 33.0, height: 33.0), rotatedContext: { size, context in
let bounds = CGRect(origin: .zero, size: size)
context.clear(bounds)
let lineWidth: CGFloat = 2.0 - UIScreenPixel
context.setLineWidth(lineWidth)
context.setStrokeColor(UIColor.white.cgColor)
context.move(to: CGPoint(x: 11.0, y: 11.0))
context.addLine(to: CGPoint(x: size.width - 11.0, y: size.height - 11.0))
context.strokePath()
context.move(to: CGPoint(x: size.width - 11.0, y: 11.0))
context.addLine(to: CGPoint(x: 11.0, y: size.height - 11.0))
context.strokePath()
})?.cgImage
}
required init?(coder aDecoder: NSCoder) {
preconditionFailure()
}
func update(component: SelectionPanelButtonContentComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
let previousComponent = self.component
self.component = component
let size = CGSize(width: 33.0, height: 33.0)
let backgroundFrame = CGRect(origin: .zero, size: size)
self.backgroundView.frame = backgroundFrame
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: backgroundFrame.width / 2.0, transition: .immediate)
self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.icon.bounds = CGRect(origin: .zero, size: size)
self.outline.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0)
self.outline.bounds = CGRect(origin: .zero, size: size)
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(
Text(
text: "\(component.count)",
font: Font.with(size: 18.0, design: .round, weight: .semibold),
color: .white
)
),
environment: {},
containerSize: size
)
let labelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - labelSize.width) / 2.0), y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
if let labelView = self.label.view {
if labelView.superview == nil {
self.addSubview(labelView)
}
labelView.center = labelFrame.center
labelView.bounds = CGRect(origin: .zero, size: labelFrame.size)
}
if (previousComponent?.isSelected ?? false) != component.isSelected {
let changeTransition: ComponentTransition = .easeInOut(duration: 0.2)
changeTransition.setAlpha(layer: self.icon, alpha: component.isSelected ? 1.0 : 0.0)
changeTransition.setTransform(layer: self.icon, transform: !component.isSelected ? CATransform3DMakeRotation(.pi / 4.0, 0.0, 0.0, 1.0) : CATransform3DIdentity)
if let labelView = self.label.view {
changeTransition.setAlpha(view: labelView, alpha: component.isSelected ? 0.0 : 1.0)
changeTransition.setTransform(view: labelView, transform: component.isSelected ? CATransform3DMakeRotation(-.pi / 4.0, 0.0, 0.0, 1.0) : CATransform3DIdentity)
}
}
return size
}
}
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, transition: transition)
}
}
@@ -0,0 +1,750 @@
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?.identifier != item.identifier || previousItem?.version != item.version {
let imageSignal: Signal<UIImage?, NoError>
if let thumbnail = item.thumbnail {
imageSignal = .single(thumbnail)
self.imageNode.contentMode = .scaleAspectFill
} else {
switch item.source {
case let .image(image, _):
imageSignal = .single(image)
case let .video(_, image, _, _):
imageSignal = .single(image)
case let .asset(asset):
imageSignal = assetImage(asset: 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)
}
} else if let selectionLayer = self.selectionLayer {
self.selectionLayer = nil
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.identifier != component.selectedItemId {
component.itemTapped(item.identifier)
} 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?.identifier {
for (_, visibleItem) in self.itemViews {
if visibleItem === visibleReorderingItem {
continue
}
if visibleItem.frame.contains(mappedPosition), let toId = visibleItem.item?.identifier {
component.itemReordered(fromId, toId)
break
}
}
}
}
}
func animateIn(from buttonView: SelectionPanelButtonContentComponent.View) {
guard let component = self.component else {
return
}
self.scrollView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
let fromPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
self.scrollView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.scrollView.layer.animateBounds(from: CGRect(origin: CGPoint(x: buttonFrame.minX - self.scrollView.frame.minX, y: buttonFrame.minY - self.scrollView.frame.minY), size: buttonFrame.size), to: self.scrollView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundMaskPanelView.layer.animatePosition(from: fromPoint, to: .zero, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(16.5)), to: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4)
self.backgroundMaskPanelView.layer.animateBounds(from: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), to: self.backgroundMaskPanelView.bounds, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
let mainCircleDelay: Double = 0.02
let backgroundWidth = self.backgroundMaskPanelView.frame.width
for item in component.items {
guard let itemView = self.itemViews[item.identifier] else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundWidth)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.14
itemView.isHidden = true
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
guard let itemView else {
return
}
itemView.isHidden = false
itemView.layer.animateSpring(from: 0.01 as NSNumber, to: 1.0 as NSNumber, keyPath: "transform.scale", duration: 0.4)
}
}
}
func animateOut(to buttonView: SelectionPanelButtonContentComponent.View, completion: @escaping () -> Void) {
guard let component = self.component else {
completion()
return
}
self.scrollView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
let buttonFrame = buttonView.convert(buttonView.bounds, to: self)
let scrollButtonFrame = buttonView.convert(buttonView.bounds, to: self.scrollView)
let toPoint = CGPoint(x: buttonFrame.center.x - self.scrollView.center.x, y: buttonFrame.center.y - self.scrollView.center.y)
self.scrollView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
self.scrollView.layer.animateBounds(from: self.scrollView.bounds, to: CGRect(origin: CGPoint(x: (buttonFrame.minX - self.scrollView.frame.minX) / 2.0, y: (buttonFrame.minY - self.scrollView.frame.minY) / 2.0), size: buttonFrame.size), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring)
self.backgroundMaskPanelView.layer.animatePosition(from: .zero, to: toPoint, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
self.backgroundMaskPanelView.layer.animate(from: NSNumber(value: Float(self.backgroundMaskPanelView.layer.cornerRadius)), to: NSNumber(value: Float(16.5)), keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, removeOnCompletion: false)
self.backgroundMaskPanelView.layer.animateBounds(from: self.backgroundMaskPanelView.bounds, to: CGRect(origin: .zero, size: CGSize(width: 33.0, height: 33.0)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { finished in
if finished {
completion()
self.backgroundMaskPanelView.layer.removeAllAnimations()
for (_, itemView) in self.itemViews {
itemView.layer.removeAllAnimations()
}
}
})
let mainCircleDelay: Double = 0.0
let backgroundWidth = self.backgroundMaskPanelView.frame.width
for item in component.items {
guard let itemView = self.itemViews[item.identifier] else {
continue
}
let distance = abs(itemView.frame.center.x - backgroundWidth)
let distanceNorm = distance / backgroundWidth
let adjustedDistanceNorm = distanceNorm
let itemDelay = mainCircleDelay + adjustedDistanceNorm * 0.05
Queue.mainQueue().after(itemDelay * UIView.animationDurationFactor()) { [weak itemView] in
guard let itemView else {
return
}
itemView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
itemView.layer.animatePosition(from: itemView.center, to: scrollButtonFrame.center, duration: 0.4)
}
}
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.identifier
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.identifier == 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
}
}
}
}
@@ -0,0 +1,613 @@
import Foundation
import UIKit
import Display
import CoreImage
import MediaEditor
private var previousBeginTime: Int = 3
final class StickerCutoutOutlineView: UIView {
let strokeLayer = SimpleShapeLayer()
let imageLayer = SimpleLayer()
var outlineLayer = CAEmitterLayer()
var glowLayer = CAEmitterLayer()
override init(frame: CGRect) {
super.init(frame: frame)
self.strokeLayer.fillColor = UIColor.clear.cgColor
self.strokeLayer.strokeColor = UIColor.clear.cgColor
self.strokeLayer.shadowColor = UIColor.white.cgColor
self.strokeLayer.shadowOpacity = 0.35
self.strokeLayer.shadowRadius = 4.0
self.layer.allowsGroupOpacity = true
self.layer.addSublayer(self.strokeLayer)
self.layer.addSublayer(self.imageLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
var hasContents: Bool {
self.imageLayer.contents != nil
}
func update(image: UIImage, maskImage: CIImage, size: CGSize, values: MediaEditorValues) {
self.imageLayer.contents = image.cgImage
if let path = getPathFromMaskImage(maskImage, size: size, values: values) {
self.strokeLayer.shadowPath = path.path.cgPath.expand(width: 1.5)
self.setupAnimation(path: path)
}
}
private func setupAnimation(path: BezierPath) {
self.outlineLayer.removeFromSuperlayer()
self.glowLayer.removeFromSuperlayer()
self.outlineLayer = CAEmitterLayer()
self.outlineLayer.opacity = 0.75
self.glowLayer = CAEmitterLayer()
self.layer.addSublayer(self.outlineLayer)
self.layer.addSublayer(self.glowLayer)
let randomBeginTime = (previousBeginTime + 4) % 6
previousBeginTime = randomBeginTime
let duration = min(8.0, max(3.0, path.length / 135.0))
let outlineAnimation = CAKeyframeAnimation(keyPath: "emitterPosition")
outlineAnimation.path = path.path.cgPath
outlineAnimation.duration = duration
outlineAnimation.repeatCount = .infinity
outlineAnimation.calculationMode = .paced
outlineAnimation.fillMode = .forwards
outlineAnimation.beginTime = Double(randomBeginTime)
self.outlineLayer.add(outlineAnimation, forKey: "emitterPosition")
let lineEmitterCell = CAEmitterCell()
lineEmitterCell.beginTime = CACurrentMediaTime()
let lineAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
lineAlphaBehavior.setValue("color.alpha", forKey: "keyPath")
lineAlphaBehavior.setValue([0.0, 0.5, 0.8, 0.5, 0.0], forKey: "values")
lineEmitterCell.setValue([lineAlphaBehavior], forKey: "emitterBehaviors")
lineEmitterCell.color = UIColor.white.cgColor
lineEmitterCell.contents = UIImage(named: "Media Editor/ParticleDot")?.cgImage
lineEmitterCell.lifetime = 2.2
lineEmitterCell.birthRate = 1700
lineEmitterCell.scale = 0.185
lineEmitterCell.alphaSpeed = -0.4
self.outlineLayer.emitterCells = [lineEmitterCell]
self.outlineLayer.emitterMode = .outline
self.outlineLayer.emitterSize = CGSize(width: 2.0, height: 2.0)
self.outlineLayer.emitterShape = .line
let glowAnimation = CAKeyframeAnimation(keyPath: "emitterPosition")
glowAnimation.path = path.path.cgPath
glowAnimation.duration = duration
glowAnimation.repeatCount = .infinity
glowAnimation.calculationMode = .cubicPaced
glowAnimation.beginTime = Double(randomBeginTime)
self.glowLayer.add(glowAnimation, forKey: "emitterPosition")
let glowEmitterCell = CAEmitterCell()
glowEmitterCell.beginTime = CACurrentMediaTime()
let glowAlphaBehavior = CAEmitterCell.createEmitterBehavior(type: "valueOverLife")
glowAlphaBehavior.setValue("color.alpha", forKey: "keyPath")
glowAlphaBehavior.setValue([0.0, 0.32, 0.4, 0.2, 0.0], forKey: "values")
glowEmitterCell.setValue([glowAlphaBehavior], forKey: "emitterBehaviors")
glowEmitterCell.color = UIColor.white.cgColor
glowEmitterCell.contents = UIImage(named: "Media Editor/ParticleGlow")?.cgImage
glowEmitterCell.lifetime = 2.0
glowEmitterCell.birthRate = 30
glowEmitterCell.scale = 1.9
glowEmitterCell.alphaSpeed = -0.1
self.glowLayer.emitterCells = [glowEmitterCell]
self.glowLayer.emitterMode = .points
self.glowLayer.emitterSize = CGSize(width: 1.0, height: 1.0)
self.glowLayer.emitterShape = .point
self.strokeLayer.animateAlpha(from: 0.0, to: CGFloat(self.strokeLayer.opacity), duration: 0.4)
self.outlineLayer.animateAlpha(from: 0.0, to: CGFloat(self.outlineLayer.opacity), duration: 0.4, delay: 0.0)
self.glowLayer.animateAlpha(from: 0.0, to: CGFloat(self.glowLayer.opacity), duration: 0.4, delay: 0.0)
self.animateBump(path: path)
}
private func animateBump(path: BezierPath) {
let boundingBox = path.path.cgPath.boundingBox
let pathCenter = CGPoint(x: boundingBox.midX, y: boundingBox.midY)
let layerPathCenter = self.imageLayer.convert(pathCenter, from: self.imageLayer.superlayer)
self.imageLayer.anchorPoint = CGPoint(x: layerPathCenter.x / layer.bounds.width, y: layerPathCenter.y / layer.bounds.height)
self.imageLayer.position = layerPathCenter
let values = [1.0, 1.07, 1.0]
let keyTimes = [0.0, 0.67, 1.0]
self.imageLayer.animateKeyframes(values: values as [NSNumber], keyTimes: keyTimes as [NSNumber], duration: 0.4, keyPath: "transform.scale", timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue)
}
override func layoutSubviews() {
self.strokeLayer.frame = self.bounds.offsetBy(dx: 0.0, dy: 1.0)
self.outlineLayer.frame = self.bounds
self.imageLayer.frame = self.bounds
self.glowLayer.frame = self.bounds
}
}
private func getPathFromMaskImage(_ image: CIImage, size: CGSize, values: MediaEditorValues) -> BezierPath? {
let extendedImage = image.applyingFilter("CIMorphologyMaximum", parameters: ["inputRadius": 3.0])
guard let pixelBuffer = getEdgesBitmap(extendedImage) else {
return nil
}
let minSide = min(size.width, size.height)
let scaledImageSize = image.extent.size.aspectFilled(CGSize(width: minSide, height: minSide))
let contourImageSize = image.extent.size.aspectFilled(CGSize(width: 256.0, height: 256.0))
var contour = findEdgePoints(in: pixelBuffer)
guard contour.count > 1 else {
return nil
}
contour = simplify(contour, tolerance: 1.0)
let path = BezierPath(points: contour, smooth: false)
let contoursScale = min(size.width, size.height) / 256.0
let valuesScale = size.width / 1080.0
let position = values.cropOffset
let rotation = values.cropRotation
let scale = values.cropScale
let positionOffset = CGPoint(
x: (size.width - scaledImageSize.width * scale) / 2.0,
y: (size.height - scaledImageSize.height * scale) / 2.0
)
var transform = CGAffineTransform.identity
transform = transform.translatedBy(x: contourImageSize.width / 2.0, y: contourImageSize.height / 2.0)
transform = transform.rotated(by: rotation)
transform = transform.translatedBy(x: -contourImageSize.width / 2.0, y: -contourImageSize.height / 2.0)
path.apply(transform, scale: 1.0)
transform = CGAffineTransform.identity
transform = transform.translatedBy(x: positionOffset.x + position.x * valuesScale, y: positionOffset.y + position.y * valuesScale)
transform = transform.scaledBy(x: scale * contoursScale, y: scale * contoursScale)
if !path.path.isEmpty {
path.apply(transform, scale: scale)
return path
}
return nil
}
func findEdgePoints(in pixelBuffer: CVPixelBuffer) -> [CGPoint] {
struct Point: Hashable {
let x: Int
let y: Int
var cgPoint: CGPoint {
return CGPoint(x: x, y: y)
}
}
let width = CVPixelBufferGetWidth(pixelBuffer)
let height = CVPixelBufferGetHeight(pixelBuffer)
var edgePoints: Set<Point> = []
var edgePath: [Point] = []
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
func isPixelWhiteAt(x: Int, y: Int) -> Bool {
let pixelOffset = y * bytesPerRow + x
let pixelPtr = baseAddress?.advanced(by: pixelOffset)
let pixel = pixelPtr?.load(as: UInt8.self) ?? 0
return pixel >= 235
}
var startPoint: Point? = nil
var visited = Set<Point>()
var componentSize = 0
func floodFill(from point: Point) -> Int {
var stack = [point]
var size = 0
while let current = stack.popLast() {
let x = Int(current.x)
let y = Int(current.y)
if x < 0 || x >= width || y < 0 || y >= height || visited.contains(current) || !isPixelWhiteAt(x: x, y: y) {
continue
}
visited.insert(current)
size += 1
stack.append(contentsOf: [Point(x: x+1, y: y), Point(x: x-1, y: y), Point(x: x, y: y+1), Point(x: x, y: y-1)])
}
return size
}
for y in 0..<height {
for x in 0..<width {
let point = Point(x: x, y: y)
if isPixelWhiteAt(x: x, y: y) && !visited.contains(point) {
let size = floodFill(from: point)
if size > componentSize {
componentSize = size
startPoint = point
}
}
}
}
let directions = [(1, 0), (1, 1), (0, 1), (-1, 1), (-1, 0), (-1, -1), (0, -1), (1, -1)]
var lastDirectionIndex = 0
guard let startingPoint = startPoint, componentSize > 60 else { return [] }
edgePoints.insert(startingPoint)
edgePath.append(startingPoint)
var currentPoint = startingPoint
let tolerance: Int = 1
func isCloseEnough(_ point: Point, to startPoint: Point) -> Bool {
return abs(point.x - startPoint.x) <= tolerance && abs(point.y - startPoint.y) <= tolerance
}
repeat {
var foundNextPoint = false
for i in 0..<directions.count {
let directionIndex = (lastDirectionIndex + i) % directions.count
let dir = directions[directionIndex]
let nextX = Int(currentPoint.x) + dir.0
let nextY = Int(currentPoint.y) + dir.1
if nextX >= 0, nextX < width, nextY >= 0, nextY < height, isPixelWhiteAt(x: nextX, y: nextY) {
let nextPoint = Point(x: nextX, y: nextY)
if !edgePoints.contains(nextPoint) {
edgePoints.insert(nextPoint)
edgePath.append(nextPoint)
currentPoint = nextPoint
lastDirectionIndex = (directionIndex + 6) % directions.count
foundNextPoint = true
break
}
}
}
if !foundNextPoint || (edgePath.count > 3 && isCloseEnough(currentPoint, to: startingPoint)) {
break
}
} while true
CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly)
return Array(edgePath.map { $0.cgPoint })
}
private func getEdgesBitmap(_ ciImage: CIImage) -> CVPixelBuffer? {
let context = CIContext(options: nil)
guard let contourCgImage = context.createCGImage(ciImage, from: ciImage.extent) else {
return nil
}
let image = UIImage(cgImage: contourCgImage)
let size = image.size.aspectFilled(CGSize(width: 256, height: 256))
let attrs = [kCVPixelBufferCGImageCompatibilityKey: kCFBooleanTrue,
kCVPixelBufferCGBitmapContextCompatibilityKey: kCFBooleanTrue] as CFDictionary
var pixelBuffer: CVPixelBuffer?
let status = CVPixelBufferCreate(kCFAllocatorDefault,
Int(size.width),
Int(size.height),
kCVPixelFormatType_OneComponent8,
attrs,
&pixelBuffer)
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
return nil
}
CVPixelBufferLockBaseAddress(buffer, [])
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
let pixelData = CVPixelBufferGetBaseAddress(buffer)
let rgbColorSpace = CGColorSpaceCreateDeviceGray()
guard let context = CGContext(data: pixelData,
width: Int(size.width),
height: Int(size.height),
bitsPerComponent: 8,
bytesPerRow: CVPixelBufferGetBytesPerRow(buffer),
space: rgbColorSpace,
bitmapInfo: 0) else {
return nil
}
context.translateBy(x: 0, y: size.height)
context.scaleBy(x: 1.0, y: -1.0)
UIGraphicsPushContext(context)
context.setFillColor(UIColor.white.cgColor)
context.fill(CGRect(origin: .zero, size: size))
image.draw(in: CGRect(origin: .zero, size: size))
UIGraphicsPopContext()
return buffer
}
private extension CGPath {
func expand(width: CGFloat) -> CGPath {
let expandedPath = self.copy(strokingWithWidth: width * 2.0, lineCap: .round, lineJoin: .round, miterLimit: 0.0)
class UserInfo {
let outputPath = CGMutablePath()
var passedFirst = false
}
var userInfo = UserInfo()
withUnsafeMutablePointer(to: &userInfo) { userInfoPointer in
expandedPath.apply(info: userInfoPointer) { (userInfo, nextElementPointer) in
let element = nextElementPointer.pointee
let userInfoPointer = userInfo!.assumingMemoryBound(to: UserInfo.self)
let userInfo = userInfoPointer.pointee
if !userInfo.passedFirst {
if case .closeSubpath = element.type {
userInfo.passedFirst = true
}
} else {
switch element.type {
case .moveToPoint:
userInfo.outputPath.move(to: element.points[0])
case .addLineToPoint:
userInfo.outputPath.addLine(to: element.points[0])
case .addQuadCurveToPoint:
userInfo.outputPath.addQuadCurve(to: element.points[1], control: element.points[0])
case .addCurveToPoint:
userInfo.outputPath.addCurve(to: element.points[2], control1: element.points[0], control2: element.points[1])
case .closeSubpath:
userInfo.outputPath.closeSubpath()
@unknown default:
userInfo.outputPath.closeSubpath()
}
}
}
}
return userInfo.outputPath
}
}
private func simplify(_ points: [CGPoint], tolerance: CGFloat?) -> [CGPoint] {
guard points.count > 1 else {
return points
}
let sqTolerance = tolerance != nil ? (tolerance! * tolerance!) : 1.0
var result = simplifyRadialDistance(points, tolerance: sqTolerance)
result = simplifyDouglasPeucker(result, sqTolerance: sqTolerance)
return result
}
private func simplifyRadialDistance(_ points: [CGPoint], tolerance: CGFloat) -> [CGPoint] {
guard points.count > 2 else {
return points
}
var prevPoint = points.first!
var newPoints = [prevPoint]
var currentPoint: CGPoint!
for i in 1..<points.count {
currentPoint = points[i]
if currentPoint.distanceFrom(prevPoint) > tolerance {
newPoints.append(currentPoint)
prevPoint = currentPoint
}
}
if prevPoint.equalsTo(currentPoint) == false {
newPoints.append(currentPoint)
}
return newPoints
}
private func simplifyDPStep(_ points: [CGPoint], first: Int, last: Int, sqTolerance: CGFloat, simplified: inout [CGPoint]) {
guard last > first else {
return
}
var maxSqDistance = sqTolerance
var index = 0
for currentIndex in first+1..<last {
let sqDistance = points[currentIndex].distanceToSegment(points[first], points[last])
if sqDistance > maxSqDistance {
maxSqDistance = sqDistance
index = currentIndex
}
}
if maxSqDistance > sqTolerance {
if (index - first) > 1 {
simplifyDPStep(points, first: first, last: index, sqTolerance: sqTolerance, simplified: &simplified)
}
simplified.append(points[index])
if (last - index) > 1 {
simplifyDPStep(points, first: index, last: last, sqTolerance: sqTolerance, simplified: &simplified)
}
}
}
private func simplifyDouglasPeucker(_ points: [CGPoint], sqTolerance: CGFloat) -> [CGPoint] {
guard points.count > 1 else {
return []
}
let last = (points.count - 1)
var simplied = [points.first!]
simplifyDPStep(points, first: 0, last: last, sqTolerance: sqTolerance, simplified: &simplied)
simplied.append(points.last!)
return simplied
}
private extension CGPoint {
func equalsTo(_ compare: CGPoint) -> Bool {
return self.x == compare.self.x && self.y == compare.y
}
func distanceFrom(_ otherPoint: CGPoint) -> CGFloat {
let dx = self.x - otherPoint.x
let dy = self.y - otherPoint.y
return sqrt((dx * dx) + (dy * dy))
}
func distanceToSegment(_ p1: CGPoint, _ p2: CGPoint) -> CGFloat {
var x = p1.x
var y = p1.y
var dx = p2.x - x
var dy = p2.y - y
if dx != 0 || dy != 0 {
let t = ((self.x - x) * dx + (self.y - y) * dy) / (dx * dx + dy * dy)
if t > 1 {
x = p2.x
y = p2.y
} else if t > 0 {
x += dx * t
y += dy * t
}
}
dx = self.x - x
dy = self.y - y
return dx * dx + dy * dy
}
}
fileprivate extension Array {
subscript(circularIndex index: Int) -> Element {
get {
assert(self.count > 0)
let index = (index + self.count) % self.count
return self[index]
}
set {
assert(self.count > 0)
let index = (index + self.count) % self.count
return self[index] = newValue
}
}
func circularIndex(_ index: Int) -> Int {
return (index + self.count) % self.count
}
}
private class BezierPath {
let path: UIBezierPath
var length: CGFloat = 0.0
init(points: [CGPoint], smooth: Bool) {
self.path = UIBezierPath()
if smooth {
if points.count < 3 {
self.path.move(to: points.first ?? CGPoint.zero)
self.path.addLine(to: points[1])
self.length = points[1].distanceFrom(points[0])
return
} else {
self.path.move(to: points.first!)
let n = points.count - 1
let tension = 0.5
for i in 0 ..< n {
let currentPoint = points[i]
var nextIndex = (i + 1) % points.count
var prevIndex = i == 0 ? points.count - 1 : i - 1
var nextNextIndex = (nextIndex + 1) % points.count
let prevPoint = points[prevIndex]
let nextPoint = points[nextIndex]
let nextNextPoint = points[nextNextIndex]
let d1 = sqrt(pow(currentPoint.x - prevPoint.x, 2) + pow(currentPoint.y - prevPoint.y, 2))
let d2 = sqrt(pow(nextPoint.x - currentPoint.x, 2) + pow(nextPoint.y - currentPoint.y, 2))
let d3 = sqrt(pow(nextNextPoint.x - nextPoint.x, 2) + pow(nextNextPoint.y - nextPoint.y, 2))
var controlPoint1: CGPoint
if d1 < 0.0001 {
controlPoint1 = currentPoint
} else {
controlPoint1 = CGPoint(x: currentPoint.x + (tension * d2 / (d2 + d3)) * (nextPoint.x - prevPoint.x),
y: currentPoint.y + (tension * d2 / (d2 + d3)) * (nextPoint.y - prevPoint.y))
}
prevIndex = i
nextIndex = (i + 1) % points.count
nextNextIndex = (nextIndex + 1) % points.count
let controlPoint2: CGPoint
if d3 < 0.0001 {
controlPoint2 = nextPoint
} else {
controlPoint2 = CGPoint(x: nextPoint.x - (tension * d2 / (d1 + d2)) * (nextNextPoint.x - currentPoint.x),
y: nextPoint.y - (tension * d2 / (d1 + d2)) * (nextNextPoint.y - currentPoint.y))
}
self.path.addCurve(to: nextPoint, controlPoint1: controlPoint1, controlPoint2: controlPoint2)
self.length += nextPoint.distanceFrom(currentPoint)
}
self.path.close()
}
} else if smooth {
let K: CGFloat = 0.2
var c1 = [Int: CGPoint]()
var c2 = [Int: CGPoint]()
let count = points.count - 1
for index in 1 ..< count {
let p = points[circularIndex: index]
let vP1 = points[circularIndex: index + 1]
let vP2 = points[index - 1]
let vP = CGPoint(x: vP1.x - vP2.x, y: vP1.y - vP2.y)
let v = CGPoint(x: vP.x * K, y: vP.y * K)
c2[(index + points.count - 1) % points.count] = CGPoint(x: p.x - v.x, y: p.y - v.y) //(p - v)
c1[(index + points.count) % points.count] = CGPoint(x: p.x + v.x, y: p.y + v.y) //(p + v)
}
self.path.move(to: points[0])
for index in 0 ..< points.count - 1 {
let c1 = c1[index] ?? points[points.circularIndex(index)]
let c2 = c2[index] ?? points[points.circularIndex(index + 1)]
self.path.addCurve(to: points[circularIndex: index + 1], controlPoint1: c1, controlPoint2: c2)
}
self.path.close()
} else {
self.path.move(to: points[0])
for index in 1 ..< points.count - 1 {
self.length += points[index].distanceFrom(points[index - 1])
self.path.addLine(to: points[index])
}
self.path.close()
}
}
func apply(_ transform: CGAffineTransform, scale: CGFloat) {
self.path.apply(transform)
self.length *= scale
}
}
@@ -0,0 +1,195 @@
import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
import StickerResources
import ContextUI
final class StickerPackListContextItem: ContextMenuCustomItem {
let context: AccountContext
let packs: [(StickerPackCollectionInfo, StickerPackItem?)]
let packSelected: (StickerPackCollectionInfo) -> Bool
init(context: AccountContext, packs: [(StickerPackCollectionInfo, StickerPackItem?)], packSelected: @escaping (StickerPackCollectionInfo) -> Bool) {
self.context = context
self.packs = packs
self.packSelected = packSelected
}
func node(presentationData: PresentationData, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) -> ContextMenuCustomNode {
return StickerPackListContextItemNode(presentationData: presentationData, item: self, getController: getController, actionSelected: actionSelected)
}
}
private final class StickerPackListContextItemNode: ASDisplayNode, ContextMenuCustomNode, ContextActionNodeProtocol, ASScrollViewDelegate {
private let item: StickerPackListContextItem
private let presentationData: PresentationData
private let getController: () -> ContextControllerProtocol?
private let actionSelected: (ContextMenuActionResult) -> Void
private let scrollNode: ASScrollNode
private let actionNodes: [ContextControllerActionsListActionItemNode]
private let separatorNodes: [ASDisplayNode]
init(presentationData: PresentationData, item: StickerPackListContextItem, getController: @escaping () -> ContextControllerProtocol?, actionSelected: @escaping (ContextMenuActionResult) -> Void) {
self.item = item
self.presentationData = presentationData
self.getController = getController
self.actionSelected = actionSelected
self.scrollNode = ASScrollNode()
var actionNodes: [ContextControllerActionsListActionItemNode] = []
var separatorNodes: [ASDisplayNode] = []
var i = 0
for (pack, topItem) in item.packs {
if pack.flags.contains(.isEmoji) {
continue
}
let thumbSize = CGSize(width: 24.0, height: 24.0)
let topItemFile = topItem?.file._parse()
let thumbnailResource = pack.thumbnail?.resource ?? topItemFile?.resource
let thumbnailIconSource: ContextMenuActionItemIconSource?
if let thumbnailResource {
var resourceId: Int64 = 0
if let resource = thumbnailResource as? CloudDocumentMediaResource {
resourceId = resource.fileId
}
let thumbnailFile = topItemFile ?? TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: resourceId), partialReference: nil, resource: thumbnailResource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "image/webp", size: thumbnailResource.size ?? 0, attributes: [], alternativeRepresentations: [])
let _ = freeMediaFileInteractiveFetched(account: item.context.account, userLocation: .other, fileReference: .stickerPack(stickerPack: .id(id: pack.id.id, accessHash: pack.accessHash), media: thumbnailFile)).start()
thumbnailIconSource = ContextMenuActionItemIconSource(
size: thumbSize,
signal: chatMessageStickerPackThumbnail(postbox: item.context.account.postbox, resource: thumbnailResource)
|> map { generator -> UIImage? in
return generator(TransformImageArguments(corners: ImageCorners(), imageSize: thumbSize, boundingSize: thumbSize, intrinsicInsets: .zero))?.generateImage()
}
)
} else {
thumbnailIconSource = nil
}
let action = ContextMenuActionItem(text: pack.title, textLayout: .singleLine, icon: { _ in nil }, iconSource: thumbnailIconSource, iconPosition: .left, action: { _, f in
if item.packSelected(pack) {
f(.dismissWithoutContent)
}
})
let actionNode = ContextControllerActionsListActionItemNode(context: nil, getController: getController, requestDismiss: actionSelected, requestUpdateAction: { _, _ in }, item: action)
actionNodes.append(actionNode)
if actionNodes.count != item.packs.count {
let separatorNode = ASDisplayNode()
separatorNode.backgroundColor = presentationData.theme.contextMenu.itemSeparatorColor
separatorNodes.append(separatorNode)
}
i += 1
}
self.actionNodes = actionNodes
self.separatorNodes = separatorNodes
super.init()
self.addSubnode(self.scrollNode)
for separatorNode in self.separatorNodes {
self.scrollNode.addSubnode(separatorNode)
}
for actionNode in self.actionNodes {
self.scrollNode.addSubnode(actionNode)
}
}
override func didLoad() {
super.didLoad()
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
self.scrollNode.view.alwaysBounceVertical = false
self.scrollNode.view.showsHorizontalScrollIndicator = false
self.scrollNode.view.scrollIndicatorInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 5.0, right: 0.0)
}
func updateLayout(constrainedWidth: CGFloat, constrainedHeight: CGFloat) -> (CGSize, (CGSize, ContainedViewLayoutTransition) -> Void) {
let minActionsWidth: CGFloat = 250.0
let maxActionsWidth: CGFloat = 300.0
let constrainedWidth = min(constrainedWidth, maxActionsWidth)
var maxWidth: CGFloat = 0.0
var contentHeight: CGFloat = 0.0
var heightsAndCompletions: [(CGFloat, (CGSize, ContainedViewLayoutTransition) -> Void)?] = []
for i in 0 ..< self.actionNodes.count {
let itemNode = self.actionNodes[i]
let (minSize, complete) = itemNode.update(presentationData: self.presentationData, constrainedSize: CGSize(width: constrainedWidth, height: constrainedHeight))
maxWidth = max(maxWidth, minSize.width)
heightsAndCompletions.append((minSize.height, complete))
contentHeight += minSize.height
}
maxWidth = max(maxWidth, minActionsWidth)
let maxHeight: CGFloat = min(155.0, constrainedHeight - 108.0)
return (CGSize(width: maxWidth, height: min(maxHeight, contentHeight)), { size, transition in
var verticalOffset: CGFloat = 0.0
for i in 0 ..< heightsAndCompletions.count {
let itemNode = self.actionNodes[i]
if let (itemHeight, itemCompletion) = heightsAndCompletions[i] {
let itemSize = CGSize(width: maxWidth, height: itemHeight)
transition.updateFrame(node: itemNode, frame: CGRect(origin: CGPoint(x: 0.0, y: verticalOffset), size: itemSize))
itemCompletion(itemSize, transition)
verticalOffset += itemHeight
}
if i < self.actionNodes.count - 1 {
let separatorNode = self.separatorNodes[i]
separatorNode.frame = CGRect(x: 0, y: verticalOffset, width: size.width, height: UIScreenPixel)
}
}
transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
})
}
func updateTheme(presentationData: PresentationData) {
// for actionNode in self.actionNodes {
// actionNode.updateTheme(presentationData: presentationData)
// }
}
var isActionEnabled: Bool {
return true
}
func performAction() {
}
func setIsHighlighted(_ value: Bool) {
}
func canBeHighlighted() -> Bool {
return self.isActionEnabled
}
func updateIsHighlighted(isHighlighted: Bool) {
self.setIsHighlighted(isHighlighted)
}
func actionNode(at point: CGPoint) -> ContextActionNodeProtocol {
// for actionNode in self.actionNodes {
// let frame = actionNode.convert(actionNode.bounds, to: self)
// if frame.contains(point) {
// return actionNode
// }
// }
return self
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
for actionNode in self.actionNodes {
actionNode.updateIsHighlighted(isHighlighted: false)
}
}
}
@@ -0,0 +1,329 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import SwiftSignalKit
import TelegramCore
import AvatarNode
import AccountContext
import MessageInputPanelComponent
import BundleIconComponent
private final class AvatarComponent: Component {
let context: AccountContext
let peer: EnginePeer
init(context: AccountContext, peer: EnginePeer) {
self.context = context
self.peer = peer
}
static func ==(lhs: AvatarComponent, rhs: AvatarComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.peer != rhs.peer {
return false
}
return true
}
final class View: UIView {
private let avatarNode: AvatarNode
private var component: AvatarComponent?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.avatarNode = AvatarNode(font: avatarPlaceholderFont(size: 18.0))
super.init(frame: frame)
self.addSubnode(self.avatarNode)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: AvatarComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 32.0, height: 32.0)
self.avatarNode.frame = CGRect(origin: CGPoint(), size: size)
self.avatarNode.setPeer(
context: component.context,
theme: component.context.sharedContext.currentPresentationData.with({ $0 }).theme,
peer: component.peer,
synchronousLoad: true
)
return size
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class StoryPreviewComponent: Component {
typealias EnvironmentType = Empty
let context: AccountContext
let caption: String
init(
context: AccountContext,
caption: String
) {
self.context = context
self.caption = caption
}
static func ==(lhs: StoryPreviewComponent, rhs: StoryPreviewComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.caption != rhs.caption {
return false
}
return true
}
final class State: ComponentState {
private let context: AccountContext
private var peerDisposable: Disposable?
fileprivate var accountPeer: EnginePeer?
init(context: AccountContext) {
self.context = context
super.init()
self.peerDisposable = (context.engine.data.get(TelegramEngine.EngineData.Item.Peer.Peer(id: context.account.peerId))
|> deliverOnMainQueue).start(next: { [weak self] peer in
if let self {
self.accountPeer = peer
self.updated()
}
})
}
deinit {
self.peerDisposable?.dispose()
}
}
func makeState() -> State {
return State(
context: self.context
)
}
public final class View: UIView {
private let line = ComponentView<Empty>()
private let title = ComponentView<Empty>()
private let avatar = ComponentView<Empty>()
private let cancelButton = ComponentView<Empty>()
private let inputPanel = ComponentView<Empty>()
private let inputPanelExternalState = MessageInputPanelComponent.ExternalState()
private let scrubber = ComponentView<Empty>()
private var component: StoryPreviewComponent?
private weak var state: State?
override init(frame: CGRect) {
super.init(frame: frame)
self.backgroundColor = .clear
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StoryPreviewComponent, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let lineSize = self.line.update(
transition: transition,
component: AnyComponent(Rectangle(color: UIColor(white: 1.0, alpha: 0.5))),
environment: {},
containerSize: CGSize(width: availableSize.width - 8.0 * 2.0, height: 2.0)
)
let lineFrame = CGRect(
origin: CGPoint(x: 8.0, y: 8.0),
size: lineSize
)
if let lineView = self.line.view {
if lineView.superview == nil {
lineView.layer.cornerRadius = 1.0
self.addSubview(lineView)
}
transition.setPosition(view: lineView, position: lineFrame.center)
transition.setBounds(view: lineView, bounds: CGRect(origin: .zero, size: lineFrame.size))
}
let cancelButtonSize = self.cancelButton.update(
transition: transition,
component: AnyComponent(BundleIconComponent(
name: "Stories/Close",
tintColor: UIColor.white
)),
environment: {},
containerSize: CGSize(width: 44.0, height: 44.0)
)
let cancelButtonFrame = CGRect(
origin: CGPoint(x: availableSize.width - 40.0, y: 19.0),
size: cancelButtonSize
)
if let cancelButtonView = self.cancelButton.view {
if cancelButtonView.superview == nil {
self.addSubview(cancelButtonView)
}
transition.setPosition(view: cancelButtonView, position: cancelButtonFrame.center)
transition.setBounds(view: cancelButtonView, bounds: CGRect(origin: .zero, size: cancelButtonFrame.size))
}
if let accountPeer = state.accountPeer {
let avatarSize = self.avatar.update(
transition: transition,
component: AnyComponent(AvatarComponent(
context: component.context,
peer: accountPeer
)),
environment: {},
containerSize: CGSize(width: 32.0, height: 32.0)
)
let avatarFrame = CGRect(
origin: CGPoint(x: 12.0, y: 18.0),
size: avatarSize
)
if let avatarView = self.avatar.view {
if avatarView.superview == nil {
self.addSubview(avatarView)
}
transition.setPosition(view: avatarView, position: avatarFrame.center)
transition.setBounds(view: avatarView, bounds: CGRect(origin: .zero, size: avatarFrame.size))
}
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
let titleSize = self.title.update(
transition: transition,
component: AnyComponent(Text(
text: presentationData.strings.Story_HeaderYourStory,
font: Font.medium(14.0),
color: .white
)),
environment: {},
containerSize: CGSize(width: 180.0, height: 44.0)
)
let titleFrame = CGRect(
origin: CGPoint(x: 53.0, y: floorToScreenPixels(33.0 - titleSize.height / 2.0)),
size: titleSize
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setPosition(view: titleView, position: titleFrame.center)
transition.setBounds(view: titleView, bounds: CGRect(origin: .zero, size: titleFrame.size))
}
let inputPanelSize = self.inputPanel.update(
transition: transition,
component: AnyComponent(MessageInputPanelComponent(
externalState: self.inputPanelExternalState,
context: component.context,
theme: presentationData.theme,
strings: presentationData.strings,
style: .story,
placeholder: .plain(presentationData.strings.Story_InputPlaceholderReplyPrivately),
sendPaidMessageStars: nil,
maxLength: nil,
queryTypes: [],
alwaysDarkWhenHasText: false,
resetInputContents: nil,
nextInputMode: { _ in return .stickers },
areVoiceMessagesAvailable: false,
presentController: { _ in },
presentInGlobalOverlay: { _ in },
sendMessageAction: { _ in },
sendMessageOptionsAction: nil,
sendStickerAction: { _ in },
setMediaRecordingActive: { _, _, _, _ in },
lockMediaRecording: nil,
stopAndPreviewMediaRecording: nil,
discardMediaRecordingPreview: nil,
attachmentAction: { },
attachmentButtonMode: .attach,
myReaction: nil,
likeAction: nil,
likeOptionsAction: nil,
inputModeAction: nil,
timeoutAction: nil,
forwardAction: {},
paidMessageAction: nil,
moreAction: { _, _ in },
presentCaptionPositionTooltip: nil,
presentVoiceMessagesUnavailableTooltip: nil,
presentTextLengthLimitTooltip: nil,
presentTextFormattingTooltip: nil,
paste: { _ in },
audioRecorder: nil,
videoRecordingStatus: nil,
isRecordingLocked: false,
hasRecordedVideo: false,
recordedAudioPreview: nil,
hasRecordedVideoPreview: false,
wasRecordingDismissed: false,
timeoutValue: nil,
timeoutSelected: false,
displayGradient: false,
bottomInset: 0.0,
isFormattingLocked: false,
hideKeyboard: false,
customInputView: nil,
forceIsEditing: false,
disabledPlaceholder: nil,
header: nil,
isChannel: false,
storyItem: nil,
chatLocation: nil
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 200.0)
)
let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - inputPanelSize.height - 3.0), size: inputPanelSize)
if let inputPanelView = self.inputPanel.view {
if inputPanelView.superview == nil {
self.addSubview(inputPanelView)
}
transition.setFrame(view: inputPanelView, frame: inputPanelFrame)
}
return availableSize
}
}
func makeView() -> View {
return View()
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,104 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import MediaEditor
import AccountContext
public func updateStorySources(engine: TelegramEngine) {
let currentTimestamp = Int32(CFAbsoluteTimeGetCurrent() + kCFAbsoluteTimeIntervalSince1970)
let _ = engine.data.get(
TelegramEngine.EngineData.Item.OrderedLists.ListItems(collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources)
).start(next: { items in
for item in items {
let key = EngineDataBuffer(item.id)
let _ = getStorySource(engine: engine, key: key).start(next: { source in
if let source {
if let expiresOn = source.expiresOn, expiresOn < currentTimestamp {
let _ = removeStorySource(engine: engine, key: key, delete: true).start()
}
}
})
}
})
}
private func key(peerId: EnginePeer.Id, id: Int64) -> EngineDataBuffer {
let key = EngineDataBuffer(length: 16)
key.setInt64(0, value: peerId.toInt64())
key.setInt64(8, value: id)
return key
}
private class StorySourceItem: Codable {
}
private func addStorySource(engine: TelegramEngine, key: EngineDataBuffer) {
let _ = engine.orderedLists.addOrMoveToFirstPosition(
collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources,
id: key.toMemoryBuffer(),
item: StorySourceItem(),
removeTailIfCountExceeds: nil
).start()
}
private func removeStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: Int64, delete: Bool) -> Signal<Never, NoError> {
let key = key(peerId: peerId, id: id)
return getStorySource(engine: engine, peerId: peerId, id: id)
|> mapToSignal { source in
if let source {
let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start()
removeStoryDraft(engine: engine, path: source.path, delete: delete)
}
return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources, id: key.toMemoryBuffer())
}
}
private func removeStorySource(engine: TelegramEngine, key: EngineDataBuffer, delete: Bool) -> Signal<Never, NoError> {
return getStorySource(engine: engine, key: key)
|> mapToSignal { source in
if let source {
let _ = engine.itemCache.remove(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key).start()
removeStoryDraft(engine: engine, path: source.path, delete: delete)
}
return engine.orderedLists.removeItem(collectionId: ApplicationSpecificOrderedItemListCollectionId.storySources, id: key.toMemoryBuffer())
}
}
public func saveStorySource(engine: TelegramEngine, item: MediaEditorDraft, peerId: EnginePeer.Id, id: Int64) {
let key = key(peerId: peerId, id: id)
addStorySource(engine: engine, key: key)
let _ = engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key, item: item).start()
}
public func getStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, id: Int64) -> Signal<MediaEditorDraft?, NoError> {
let key = key(peerId: peerId, id: id)
return getStorySource(engine: engine, key: key)
}
private func getStorySource(engine: TelegramEngine, key: EngineDataBuffer) -> Signal<MediaEditorDraft?, NoError> {
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: key))
|> map { result -> MediaEditorDraft? in
return result?.get(MediaEditorDraft.self)
}
}
public func moveStorySource(engine: TelegramEngine, peerId: EnginePeer.Id, from fromId: Int64, to toId: Int64) {
let fromKey = key(peerId: peerId, id: fromId)
let toKey = key(peerId: peerId, id: toId)
let _ = (engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: fromKey))
|> mapToSignal { item -> Signal<Never, NoError> in
if let item = item?.get(MediaEditorDraft.self) {
addStorySource(engine: engine, key: toKey)
return engine.itemCache.put(collectionId: ApplicationSpecificItemCacheCollectionId.storySource, id: toKey, item: item)
|> then(
removeStorySource(engine: engine, key: fromKey, delete: false)
)
} else {
return .complete()
}
}).start()
}
@@ -0,0 +1,410 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import LegacyComponents
import MediaEditor
import TelegramPresentationData
private final class TintColorComponent: Component {
typealias EnvironmentType = Empty
let color: UIColor
let isSelected: Bool
init(
color: UIColor,
isSelected: Bool
) {
self.color = color
self.isSelected = isSelected
}
static func ==(lhs: TintColorComponent, rhs: TintColorComponent) -> Bool {
if lhs.color != rhs.color {
return false
}
if lhs.isSelected != rhs.isSelected {
return false
}
return true
}
final class View: UIView {
private var background = SimpleShapeLayer()
private var selection = SimpleShapeLayer()
private var component: TintColorComponent?
private weak var state: EmptyComponentState?
private let size = CGSize(width: 24.0, height: 24.0)
override init(frame: CGRect) {
super.init(frame: frame)
self.background.path = CGPath(ellipseIn: CGRect(origin: .zero, size: size).insetBy(dx: 3.0, dy: 3.0), transform: nil)
let lineWidth = 1.0 + UIScreenPixel
self.selection.lineWidth = lineWidth
self.selection.path = CGPath(ellipseIn: CGRect(origin: .zero, size: size).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0), transform: nil)
self.layer.addSublayer(self.selection)
self.layer.addSublayer(self.background)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: TintColorComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
let size = CGSize(width: 24.0, height: 24.0)
let bounds = CGRect(origin: .zero, size: size)
let color: UIColor
let selectionColor: UIColor
if component.color == .clear {
if component.isSelected {
color = UIColor(rgb: 0x000000)
} else {
color = UIColor(rgb: 0x1c1f22)
}
selectionColor = UIColor(rgb: 0x808080)
} else {
color = component.color
selectionColor = component.color
}
self.background.fillColor = color.cgColor
self.selection.strokeColor = selectionColor.cgColor
self.selection.fillColor = UIColor.clear.cgColor
self.background.frame = bounds
self.selection.frame = bounds
self.selection.isHidden = !component.isSelected
return size
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
final class TintComponent: Component {
enum Section {
case shadows
case highlights
}
typealias EnvironmentType = Empty
let strings: PresentationStrings
let shadowsValue: TintValue
let highlightsValue: TintValue
let shadowsValueUpdated: (TintValue) -> Void
let highlightsValueUpdated: (TintValue) -> Void
let isTrackingUpdated: (Bool) -> Void
init(
strings: PresentationStrings,
shadowsValue: TintValue,
highlightsValue: TintValue,
shadowsValueUpdated: @escaping (TintValue) -> Void,
highlightsValueUpdated: @escaping (TintValue) -> Void,
isTrackingUpdated: @escaping (Bool) -> Void
) {
self.strings = strings
self.shadowsValue = shadowsValue
self.highlightsValue = highlightsValue
self.shadowsValueUpdated = shadowsValueUpdated
self.highlightsValueUpdated = highlightsValueUpdated
self.isTrackingUpdated = isTrackingUpdated
}
static func ==(lhs: TintComponent, rhs: TintComponent) -> Bool {
if lhs.strings !== rhs.strings {
return false
}
if lhs.highlightsValue != rhs.highlightsValue {
return false
}
if lhs.shadowsValue != rhs.shadowsValue {
return false
}
return true
}
final class State: ComponentState {
var section: Section
var shadowsValue: TintValue
var highlightsValue: TintValue
init(section: Section, shadowsValue: TintValue, highlightsValue: TintValue) {
self.section = section
self.shadowsValue = shadowsValue
self.highlightsValue = highlightsValue
}
}
func makeState() -> State {
return State(section: .shadows, shadowsValue: self.shadowsValue, highlightsValue: self.highlightsValue)
}
final class View: UIView {
private var shadowsButton = ComponentView<Empty>()
private var highlightsButton = ComponentView<Empty>()
private var colorViews: [ComponentView<Empty>] = []
private var slider = ComponentView<Empty>()
private var component: TintComponent?
private weak var state: State?
override init(frame: CGRect) {
super.init(frame: frame)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: TintComponent, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
self.component = component
self.state = state
state.shadowsValue = component.shadowsValue
state.highlightsValue = component.highlightsValue
let shadowsValueUpdated = component.shadowsValueUpdated
let highlightsValueUpdated = component.highlightsValueUpdated
let topInset: CGFloat = 11.0
let shadowsButtonSize = self.shadowsButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: component.strings.Story_Editor_Tint_Shadows,
font: Font.regular(14.0),
color: state.section == .shadows ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .shadows
state?.updated()
}
)
),
environment: {},
containerSize: availableSize
)
let shadowsButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 3.0 - shadowsButtonSize.width / 2.0), y: topInset), size: shadowsButtonSize)
if let view = self.shadowsButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: shadowsButtonFrame)
}
let highlightsButtonSize = self.highlightsButton.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
Text(
text: component.strings.Story_Editor_Tint_Highlights,
font: Font.regular(14.0),
color: state.section == .highlights ? .white : UIColor(rgb: 0x808080)
)
),
action: { [weak state] in
state?.section = .highlights
state?.updated()
}
)
),
environment: {},
containerSize: availableSize
)
let highlightsButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(availableSize.width / 3.0 * 2.0 - highlightsButtonSize.width / 2.0), y: topInset), size: highlightsButtonSize)
if let view = self.highlightsButton.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: highlightsButtonFrame)
}
let currentColor: UIColor
let colors: [UIColor]
switch state.section {
case .shadows:
currentColor = component.shadowsValue.color
colors = [
UIColor.clear,
UIColor(rgb: 0xff4d4d),
UIColor(rgb: 0xf48022),
UIColor(rgb: 0xffcd00),
UIColor(rgb: 0x81d281),
UIColor(rgb: 0x71c5d6),
UIColor(rgb: 0x0072bc),
UIColor(rgb: 0x662d91)
]
case .highlights:
currentColor = component.highlightsValue.color
colors = [
UIColor.clear,
UIColor(rgb: 0xef9286),
UIColor(rgb: 0xeacea2),
UIColor(rgb: 0xf2e17c),
UIColor(rgb: 0xa4edae),
UIColor(rgb: 0x89dce5),
UIColor(rgb: 0x2e8bc8),
UIColor(rgb: 0xcd98e5)
]
}
var sizes: [CGSize] = []
for i in 0 ..< colors.count {
let color = colors[i]
let componentView: ComponentView<Empty>
if i >= self.colorViews.count {
componentView = ComponentView<Empty>()
self.colorViews.append(componentView)
} else {
componentView = self.colorViews[i]
}
let size = componentView.update(
transition: transition,
component: AnyComponent(
Button(
content: AnyComponent(
TintColorComponent(
color: color,
isSelected: color == currentColor
)
),
action: { [weak state] in
if let state {
switch state.section {
case .shadows:
shadowsValueUpdated(state.shadowsValue.withUpdatedColor(color))
case .highlights:
highlightsValueUpdated(state.highlightsValue.withUpdatedColor(color))
}
}
}
)
),
environment: {},
containerSize: availableSize
)
sizes.append(size)
}
let isTrackingUpdated: (Bool) -> Void = { [weak self] isTracking in
component.isTrackingUpdated(isTracking)
if let self {
let transition: ComponentTransition
if isTracking {
transition = .immediate
} else {
transition = .easeInOut(duration: 0.25)
}
let alpha: CGFloat = isTracking ? 0.0 : 1.0
if let view = self.shadowsButton.view {
transition.setAlpha(view: view, alpha: alpha)
}
if let view = self.highlightsButton.view {
transition.setAlpha(view: view, alpha: alpha)
}
for color in self.colorViews {
if let view = color.view {
transition.setAlpha(view: view, alpha: alpha)
}
}
}
}
let sliderSize = self.slider.update(
transition: transition,
component: AnyComponent(
AdjustmentSliderComponent(
title: "",
value: state.section == .shadows ? component.shadowsValue.intensity : component.highlightsValue.intensity,
minValue: 0.0,
maxValue: 1.0,
startValue: 0.0,
isEnabled: currentColor != .clear,
trackColor: currentColor != .clear ? currentColor : .white,
displayValue: false,
valueUpdated: { [weak state] value in
if let state {
switch state.section {
case .shadows:
shadowsValueUpdated(state.shadowsValue.withUpdatedIntensity(value))
case .highlights:
highlightsValueUpdated(state.highlightsValue.withUpdatedIntensity(value))
}
}
},
isTrackingUpdated: { isTracking in
isTrackingUpdated(isTracking)
}
)
),
environment: {},
containerSize: availableSize
)
let colorsVerticalSpacing: CGFloat = 9.0
let leftInset: CGFloat = 30.0
let itemSpacing = min(33.0, floorToScreenPixels((availableSize.width - leftInset * 2.0 - sizes.first!.width * CGFloat(colors.count)) / CGFloat(colors.count - 1)))
let finalLeftInset: CGFloat = floorToScreenPixels((availableSize.width - ((sizes.first!.width + itemSpacing) * CGFloat(colors.count) - itemSpacing)) / 2.0)
var origin: CGPoint = CGPoint(x: finalLeftInset, y: topInset + highlightsButtonSize.height + colorsVerticalSpacing)
for i in 0 ..< colors.count {
let size = sizes[i]
let componentView = self.colorViews[i]
if let view = componentView.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: CGRect(origin: origin, size: size))
}
origin = origin.offsetBy(dx: size.width + itemSpacing, dy: 0.0)
}
let verticalSpacing: CGFloat = 3.0
let sliderFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset + highlightsButtonSize.height + verticalSpacing + sizes.first!.height + verticalSpacing), size: sliderSize)
if let view = self.slider.view {
if view.superview == nil {
self.addSubview(view)
}
transition.setFrame(view: view, frame: sliderFrame)
}
return CGSize(width: availableSize.width, height: topInset + highlightsButtonSize.height + colorsVerticalSpacing + sizes.first!.height + verticalSpacing + sliderSize.height)
}
}
public func makeView() -> View {
return View(frame: CGRect())
}
public func update(view: View, availableSize: CGSize, state: State, environment: Environment<EnvironmentType>, transition: ComponentTransition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}
@@ -0,0 +1,236 @@
import Foundation
import CoreLocation
import SwiftSignalKit
import TelegramCore
import StickerPickerScreen
import AccountContext
import DeviceLocationManager
import DeviceAccess
struct StoryWeather {
let emoji: String
let temperature: Double
}
private func getWeatherData(context: AccountContext, location: CLLocationCoordinate2D) -> Signal<StoryWeather?, NoError> {
let appConfiguration = context.currentAppConfiguration.with { $0 }
let botConfiguration = WeatherBotConfiguration.with(appConfiguration: appConfiguration)
if let botUsername = botConfiguration.botName {
return context.engine.peers.resolvePeerByName(name: botUsername, referrer: nil)
|> mapToSignal { result -> Signal<EnginePeer?, NoError> in
guard case let .result(result) = result else {
return .complete()
}
return .single(result)
}
|> mapToSignal { peer -> Signal<ChatContextResultCollection?, NoError> in
guard let peer = peer else {
return .single(nil)
}
return context.engine.messages.requestChatContextResults(botId: peer.id, peerId: context.account.peerId, query: "", location: .single((location.latitude, location.longitude)), offset: "")
|> map { results -> ChatContextResultCollection? in
return results?.results
}
|> `catch` { error -> Signal<ChatContextResultCollection?, NoError> in
return .single(nil)
}
}
|> map { contextResult -> StoryWeather? in
guard let contextResult, let result = contextResult.results.first, let emoji = result.title, let temperature = result.description.flatMap(Double.init) else {
return nil
}
return StoryWeather(emoji: emoji, temperature: temperature)
}
} else {
return .single(nil)
}
}
func getWeather(context: AccountContext, load: Bool) -> Signal<StickerPickerScreen.Weather, NoError> {
guard let locationManager = context.sharedContext.locationManager else {
return .single(.none)
}
return DeviceAccess.authorizationStatus(subject: .location(.send))
|> mapToSignal { status in
switch status {
case .notDetermined:
return .single(.notDetermined)
case .denied, .restricted, .unreachable, .limited:
return .single(.notAllowed)
case .allowed:
if load {
return .single(.fetching)
|> then(
currentLocationManagerCoordinate(manager: locationManager, timeout: 5.0)
|> mapToSignal { location in
if let location {
return getWeatherData(context: context, location: location)
|> mapToSignal { weather in
if let weather {
let effectiveEmoji = emojiFor(for: weather.emoji.strippedEmoji, date: Date(), location: location)
if let match = context.animatedEmojiStickersValue[effectiveEmoji]?.first {
return .single(.loaded(StickerPickerScreen.Weather.LoadedWeather(
emoji: effectiveEmoji,
emojiFile: match.file._parse(),
temperature: weather.temperature
)))
} else {
return .single(.none)
}
} else {
return .single(.none)
}
}
} else {
return .single(.none)
}
}
)
} else {
return .single(.notPreloaded)
}
}
}
}
private struct WeatherBotConfiguration {
static var defaultValue: WeatherBotConfiguration {
return WeatherBotConfiguration(botName: "izweatherbot")
}
let botName: String?
fileprivate init(botName: String?) {
self.botName = botName
}
public static func with(appConfiguration: AppConfiguration) -> WeatherBotConfiguration {
if let data = appConfiguration.data, let botName = data["weather_search_username"] as? String {
return WeatherBotConfiguration(botName: botName)
} else {
return .defaultValue
}
}
}
private let J1970: Double = 2440588.0
private let moonEmojis = ["🌑", "🌒", "🌓", "🌔", "🌕", "🌖", "🌗", "🌘", "🌑"]
private func emojiFor(for emoji: String, date: Date, location: CLLocationCoordinate2D) -> String {
var emoji = emoji
if !"".isEmpty, ["☀️", "🌤️"].contains(emoji) && !isDay(latitude: location.latitude, longitude: location.longitude, dateTime: date) {
emoji = moonPhaseEmoji(for: date)
}
return emoji
}
private func moonPhaseEmoji(for date: Date) -> String {
let julianDate = toJulianDate(date: date)
let referenceNewMoon: Double = 2451550.1
let synodicMonth: Double = 29.53058867
let daysSinceNewMoon = julianDate - referenceNewMoon
let newMoons = daysSinceNewMoon / synodicMonth
let currentMoonPhase = (newMoons - floor(newMoons)) * synodicMonth
switch currentMoonPhase {
case 0..<1.84566:
return moonEmojis[0]
case 1.84566..<5.53699:
return moonEmojis[1]
case 5.53699..<9.22831:
return moonEmojis[2]
case 9.22831..<12.91963:
return moonEmojis[3]
case 12.91963..<16.61096:
return moonEmojis[4]
case 16.61096..<20.30228:
return moonEmojis[5]
case 20.30228..<23.99361:
return moonEmojis[6]
case 23.99361..<27.68493:
return moonEmojis[7]
default:
return moonEmojis[8]
}
}
private func isDay(latitude: Double, longitude: Double, dateTime: Date) -> Bool {
let calendar = Calendar.current
let date = calendar.startOfDay(for: dateTime)
let time = dateTime.timeIntervalSince(date)
let sunrise = calculateSunrise(latitude: latitude, longitude: longitude, date: date)
let sunset = calculateSunset(latitude: latitude, longitude: longitude, date: date)
return time >= sunrise * 3600 && time <= sunset * 3600
}
private func calculateSunrise(latitude: Double, longitude: Double, date: Date) -> Double {
return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: true)
}
private func calculateSunset(latitude: Double, longitude: Double, date: Date) -> Double {
return calculateSunTime(latitude: latitude, longitude: longitude, date: date, isSunrise: false)
}
private func calculateSunTime(latitude: Double, longitude: Double, date: Date, isSunrise: Bool) -> Double {
let calendar = Calendar.current
let dayOfYear = calendar.ordinality(of: .day, in: .year, for: date)!
let zenith = 90.833
let D2R = Double.pi / 180.0
let R2D = 180.0 / Double.pi
let lngHour = longitude / 15.0
let t = Double(dayOfYear) + ((isSunrise ? 6.0 : 18.0) - lngHour) / 24.0
let M = (0.9856 * t) - 3.289
var L = M + (1.916 * sin(M * D2R)) + (0.020 * sin(2 * M * D2R)) + 282.634
if L > 360.0 {
L -= 360.0
} else if L < 0.0 {
L += 360.0
}
var RA = R2D * atan(0.91764 * tan(L * D2R))
if RA > 360.0 {
RA -= 360.0
} else if RA < 0.0 {
RA += 360.0
}
let Lquadrant = (floor(L / 90.0)) * 90.0
let RAquadrant = (floor(RA / 90.0)) * 90.0
RA += (Lquadrant - RAquadrant)
RA /= 15.0
let sinDec = 0.39782 * sin(L * D2R)
let cosDec = cos(asin(sinDec))
let cosH = (cos(zenith * D2R) - (sinDec * sin(latitude * D2R))) / (cosDec * cos(latitude * D2R))
if cosH > 1.0 || cosH < -1.0 {
return -1
}
var H = isSunrise ? (360.0 - R2D * acos(cosH)) : R2D * acos(cosH)
H /= 15.0
let T = H + RA - (0.06571 * t) - 6.622
var UT = T - lngHour
if UT > 24.0 {
UT -= 24.0
} else if UT < 0.0 {
UT += 24.0
}
return UT
}
private func toJulianDate(date: Date) -> Double {
return date.timeIntervalSince1970 / 86400.0 + J1970 - 0.5
}