mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-08 08:31:13 +00:00
Various improvements
This commit is contained in:
parent
4509384a6c
commit
d2cfb46b49
@ -216,6 +216,7 @@ private let addButtonTag = GenericComponentViewTag()
|
|||||||
private let toolsTag = GenericComponentViewTag()
|
private let toolsTag = GenericComponentViewTag()
|
||||||
private let modeTag = GenericComponentViewTag()
|
private let modeTag = GenericComponentViewTag()
|
||||||
private let flipButtonTag = GenericComponentViewTag()
|
private let flipButtonTag = GenericComponentViewTag()
|
||||||
|
private let fillButtonTag = GenericComponentViewTag()
|
||||||
private let textSettingsTag = GenericComponentViewTag()
|
private let textSettingsTag = GenericComponentViewTag()
|
||||||
private let sizeSliderTag = GenericComponentViewTag()
|
private let sizeSliderTag = GenericComponentViewTag()
|
||||||
private let color1Tag = GenericComponentViewTag()
|
private let color1Tag = GenericComponentViewTag()
|
||||||
@ -449,15 +450,19 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self.updated(transition: animated ? .easeInOut(duration: 0.2) : .immediate)
|
self.updated(transition: animated ? .easeInOut(duration: 0.2) : .immediate)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSelectedTool(_ tool: DrawingToolState.Key) {
|
func updateSelectedTool(_ tool: DrawingToolState.Key, update: Bool = true) {
|
||||||
if self.selectedEntity != nil {
|
if self.selectedEntity != nil {
|
||||||
self.updateCurrentMode(.drawing)
|
self.skipSelectedEntityUpdate = true
|
||||||
|
self.updateCurrentMode(.drawing, update: false)
|
||||||
|
self.skipSelectedEntityUpdate = false
|
||||||
}
|
}
|
||||||
self.drawingState = self.drawingState.withUpdatedSelectedTool(tool)
|
self.drawingState = self.drawingState.withUpdatedSelectedTool(tool)
|
||||||
self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor
|
self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor
|
||||||
self.updateToolState.invoke(self.drawingState.currentToolState)
|
self.updateToolState.invoke(self.drawingState.currentToolState)
|
||||||
|
if update {
|
||||||
self.updated(transition: .easeInOut(duration: 0.2))
|
self.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func updateBrushSize(_ size: CGFloat) {
|
func updateBrushSize(_ size: CGFloat) {
|
||||||
if let selectedEntity = self.selectedEntity {
|
if let selectedEntity = self.selectedEntity {
|
||||||
@ -479,6 +484,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self.updated(transition: .easeInOut(duration: 0.2))
|
self.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var skipSelectedEntityUpdate = false
|
||||||
func updateSelectedEntity(_ entity: DrawingEntity?) {
|
func updateSelectedEntity(_ entity: DrawingEntity?) {
|
||||||
self.selectedEntity = entity
|
self.selectedEntity = entity
|
||||||
if let entity = entity {
|
if let entity = entity {
|
||||||
@ -496,8 +502,10 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
self.currentMode = .drawing
|
self.currentMode = .drawing
|
||||||
self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor
|
self.currentColor = self.drawingState.currentToolState.color ?? self.currentColor
|
||||||
}
|
}
|
||||||
|
if !self.skipSelectedEntityUpdate {
|
||||||
self.updated(transition: .easeInOut(duration: 0.2))
|
self.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func presentShapePicker(_ sourceView: UIView) {
|
func presentShapePicker(_ sourceView: UIView) {
|
||||||
let items: [ContextMenuItem] = [
|
let items: [ContextMenuItem] = [
|
||||||
@ -636,9 +644,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let flipButton = Child(Button.self)
|
let flipButton = Child(Button.self)
|
||||||
let fillButton = Child(Button.self)
|
let fillButton = Child(Button.self)
|
||||||
let fillButtonTag = GenericComponentViewTag()
|
|
||||||
|
|
||||||
let stickerFlipButton = Child(Button.self)
|
|
||||||
|
|
||||||
let backButton = Child(Button.self)
|
let backButton = Child(Button.self)
|
||||||
let doneButton = Child(Button.self)
|
let doneButton = Child(Button.self)
|
||||||
@ -684,6 +689,9 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
let updateFastColorPickerPan = component.updateFastColorPickerPan
|
let updateFastColorPickerPan = component.updateFastColorPickerPan
|
||||||
let dismissFastColorPicker = component.dismissFastColorPicker
|
let dismissFastColorPicker = component.dismissFastColorPicker
|
||||||
|
|
||||||
|
let topInset = environment.safeInsets.top + 31.0
|
||||||
|
let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0
|
||||||
|
|
||||||
if let textEntity = state.selectedEntity as? DrawingTextEntity {
|
if let textEntity = state.selectedEntity as? DrawingTextEntity {
|
||||||
let textSettings = textSettings.update(
|
let textSettings = textSettings.update(
|
||||||
component: TextSettingsComponent(
|
component: TextSettingsComponent(
|
||||||
@ -773,16 +781,21 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
|
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
|
||||||
if let state = state {
|
if let state = state {
|
||||||
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
|
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
|
||||||
state.updateSelectedTool(.pen)
|
state.updateSelectedTool(.pen, update: false)
|
||||||
}
|
}
|
||||||
state.updateColor(color, animated: true)
|
state.updateColor(color, animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var currentColor: DrawingColor? = state.currentColor
|
||||||
|
if [.eraser, .blur].contains(state.drawingState.selectedTool) || state.selectedEntity is DrawingStickerEntity {
|
||||||
|
currentColor = nil
|
||||||
|
}
|
||||||
|
|
||||||
var delay: Double = 0.0
|
var delay: Double = 0.0
|
||||||
let swatch1Button = swatch1Button.update(
|
let swatch1Button = swatch1Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[0]),
|
type: .pallete(currentColor == presetColors[0]),
|
||||||
color: presetColors[0],
|
color: presetColors[0],
|
||||||
tag: color1Tag,
|
tag: color1Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -809,7 +822,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch2Button = swatch2Button.update(
|
let swatch2Button = swatch2Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[1]),
|
type: .pallete(currentColor == presetColors[1]),
|
||||||
color: presetColors[1],
|
color: presetColors[1],
|
||||||
tag: color2Tag,
|
tag: color2Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -836,7 +849,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch3Button = swatch3Button.update(
|
let swatch3Button = swatch3Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[2]),
|
type: .pallete(currentColor == presetColors[2]),
|
||||||
color: presetColors[2],
|
color: presetColors[2],
|
||||||
tag: color3Tag,
|
tag: color3Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -863,7 +876,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch4Button = swatch4Button.update(
|
let swatch4Button = swatch4Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[3]),
|
type: .pallete(currentColor == presetColors[3]),
|
||||||
color: presetColors[3],
|
color: presetColors[3],
|
||||||
tag: color4Tag,
|
tag: color4Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -890,7 +903,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch5Button = swatch5Button.update(
|
let swatch5Button = swatch5Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[4]),
|
type: .pallete(currentColor == presetColors[4]),
|
||||||
color: presetColors[4],
|
color: presetColors[4],
|
||||||
tag: color5Tag,
|
tag: color5Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -918,7 +931,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch6Button = swatch6Button.update(
|
let swatch6Button = swatch6Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[5]),
|
type: .pallete(currentColor == presetColors[5]),
|
||||||
color: presetColors[5],
|
color: presetColors[5],
|
||||||
tag: color6Tag,
|
tag: color6Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -945,7 +958,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch7Button = swatch7Button.update(
|
let swatch7Button = swatch7Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[6]),
|
type: .pallete(currentColor == presetColors[6]),
|
||||||
color: presetColors[6],
|
color: presetColors[6],
|
||||||
tag: color7Tag,
|
tag: color7Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -972,7 +985,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
|
|
||||||
let swatch8Button = swatch8Button.update(
|
let swatch8Button = swatch8Button.update(
|
||||||
component: ColorSwatchComponent(
|
component: ColorSwatchComponent(
|
||||||
type: .pallete(state.currentColor == presetColors[7]),
|
type: .pallete(currentColor == presetColors[7]),
|
||||||
color: presetColors[7],
|
color: presetColors[7],
|
||||||
tag: color8Tag,
|
tag: color8Tag,
|
||||||
action: {
|
action: {
|
||||||
@ -996,7 +1009,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
if state.selectedEntity == nil {
|
if state.selectedEntity is DrawingStickerEntity {
|
||||||
|
} else {
|
||||||
let tools = tools.update(
|
let tools = tools.update(
|
||||||
component: ToolsComponent(
|
component: ToolsComponent(
|
||||||
state: state.drawingState,
|
state: state.drawingState,
|
||||||
@ -1037,286 +1051,25 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sizeSliderVisible = false
|
var hasTopButtons = false
|
||||||
var isEditingText = false
|
|
||||||
var sizeValue: CGFloat?
|
|
||||||
if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView {
|
|
||||||
sizeSliderVisible = true
|
|
||||||
isEditingText = entityView.isEditing
|
|
||||||
sizeValue = textEntity.fontSize
|
|
||||||
} else {
|
|
||||||
if state.selectedEntity == nil {
|
|
||||||
sizeSliderVisible = true
|
|
||||||
sizeValue = state.drawingState.currentToolState.size
|
|
||||||
}
|
|
||||||
if state.drawingViewState.canZoomOut {
|
|
||||||
let zoomOutButton = zoomOutButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
ZoomOutButtonContent(
|
|
||||||
title: "Zoom Out",
|
|
||||||
image: state.image(.zoomOut)
|
|
||||||
)
|
|
||||||
),
|
|
||||||
action: {
|
|
||||||
performAction.invoke(.zoomOut)
|
|
||||||
}
|
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
|
||||||
availableSize: CGSize(width: 120.0, height: 33.0),
|
|
||||||
transition: .immediate
|
|
||||||
)
|
|
||||||
context.add(zoomOutButton
|
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel))
|
|
||||||
.appear(.default(scale: true, alpha: true))
|
|
||||||
.disappear(.default(scale: true, alpha: true))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let sizeValue {
|
|
||||||
state.lastSize = sizeValue
|
|
||||||
}
|
|
||||||
if state.drawingViewState.isDrawing {
|
|
||||||
sizeSliderVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
let topInset = environment.safeInsets.top + 31.0
|
|
||||||
let bottomInset: CGFloat = environment.inputHeight > 0.0 ? environment.inputHeight : 145.0
|
|
||||||
let textSize = textSize.update(
|
|
||||||
component: TextSizeSliderComponent(
|
|
||||||
value: sizeValue ?? state.lastSize,
|
|
||||||
tag: sizeSliderTag,
|
|
||||||
updated: { [weak state] size in
|
|
||||||
if let state = state {
|
|
||||||
state.updateBrushSize(size)
|
|
||||||
if state.selectedEntity == nil {
|
|
||||||
previewBrushSize.invoke(size)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, released: {
|
|
||||||
previewBrushSize.invoke(nil)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: 30.0, height: 240.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(textSize
|
|
||||||
.position(CGPoint(x: sizeSliderVisible ? textSize.size.width / 2.0 : textSize.size.width / 2.0 - 33.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0))
|
|
||||||
)
|
|
||||||
|
|
||||||
let undoButton = undoButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Image(image: state.image(.undo))
|
|
||||||
),
|
|
||||||
isEnabled: state.drawingViewState.canUndo,
|
|
||||||
action: {
|
|
||||||
performAction.invoke(.undo)
|
|
||||||
}
|
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag),
|
|
||||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(undoButton
|
|
||||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: environment.safeInsets.top + 31.0))
|
|
||||||
.scale(isEditingText ? 0.01 : 1.0)
|
|
||||||
.opacity(isEditingText ? 0.0 : 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
if state.drawingViewState.canRedo && !isEditingText {
|
|
||||||
let redoButton = redoButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Image(image: state.image(.redo))
|
|
||||||
),
|
|
||||||
action: {
|
|
||||||
performAction.invoke(.redo)
|
|
||||||
}
|
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
|
||||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(redoButton
|
|
||||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: environment.safeInsets.top + 31.0))
|
|
||||||
.appear(.default(scale: true, alpha: true))
|
|
||||||
.disappear(.default(scale: true, alpha: true))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let clearAllButton = clearAllButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Text(text: "Clear All", font: Font.regular(17.0), color: .white)
|
|
||||||
),
|
|
||||||
isEnabled: state.drawingViewState.canClear,
|
|
||||||
action: {
|
|
||||||
performAction.invoke(.clear)
|
|
||||||
}
|
|
||||||
).tagged(clearAllButtonTag),
|
|
||||||
availableSize: CGSize(width: 100.0, height: 30.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(clearAllButton
|
|
||||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: environment.safeInsets.top + 31.0))
|
|
||||||
.scale(isEditingText ? 0.01 : 1.0)
|
|
||||||
.opacity(isEditingText ? 0.0 : 1.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
let textCancelButton = textCancelButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
|
|
||||||
),
|
|
||||||
action: { [weak state] in
|
|
||||||
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
|
||||||
entityView.endEditing(reset: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: 100.0, height: 30.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(textCancelButton
|
|
||||||
.position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: environment.safeInsets.top + 31.0))
|
|
||||||
.scale(isEditingText ? 1.0 : 0.01)
|
|
||||||
.opacity(isEditingText ? 1.0 : 0.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
let textDoneButton = textDoneButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
|
|
||||||
),
|
|
||||||
action: { [weak state] in
|
|
||||||
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
|
||||||
entityView.endEditing()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: 100.0, height: 30.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(textDoneButton
|
|
||||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: environment.safeInsets.top + 31.0))
|
|
||||||
.scale(isEditingText ? 1.0 : 0.01)
|
|
||||||
.opacity(isEditingText ? 1.0 : 0.0)
|
|
||||||
)
|
|
||||||
|
|
||||||
var isEditingShapeSize = false
|
|
||||||
if let entity = state.selectedEntity {
|
if let entity = state.selectedEntity {
|
||||||
if entity is DrawingSimpleShapeEntity || entity is DrawingVectorEntity || entity is DrawingBubbleEntity {
|
var isFilled: Bool?
|
||||||
isEditingShapeSize = true
|
if let entity = entity as? DrawingSimpleShapeEntity {
|
||||||
}
|
isFilled = entity.drawType == .fill
|
||||||
|
} else if let entity = entity as? DrawingBubbleEntity {
|
||||||
|
isFilled = entity.drawType == .fill
|
||||||
|
} else if let _ = entity as? DrawingVectorEntity {
|
||||||
|
isFilled = false
|
||||||
}
|
}
|
||||||
|
|
||||||
var color: DrawingColor?
|
var hasFlip = false
|
||||||
if let entity = state.selectedEntity, presetColors.contains(entity.color) {
|
if state.selectedEntity is DrawingBubbleEntity || state.selectedEntity is DrawingStickerEntity {
|
||||||
color = nil
|
hasFlip = true
|
||||||
} else if presetColors.contains(state.currentColor) {
|
|
||||||
color = nil
|
|
||||||
} else {
|
|
||||||
color = state.currentColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = state.selectedEntity as? DrawingStickerEntity {
|
hasTopButtons = isFilled != nil || hasFlip
|
||||||
let stickerFlipButton = stickerFlipButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Image(image: state.image(.flip))
|
|
||||||
),
|
|
||||||
action: { [weak state] in
|
|
||||||
guard let state = state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let entity = state.selectedEntity as? DrawingStickerEntity {
|
|
||||||
entity.mirrored = !entity.mirrored
|
|
||||||
entity.currentEntityView?.update(animated: true)
|
|
||||||
}
|
|
||||||
state.updated(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag),
|
|
||||||
availableSize: CGSize(width: 33.0, height: 33.0),
|
|
||||||
transition: .immediate
|
|
||||||
)
|
|
||||||
context.add(stickerFlipButton
|
|
||||||
.position(CGPoint(x: environment.safeInsets.left + stickerFlipButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - stickerFlipButton.size.height / 2.0 - 89.0))
|
|
||||||
.appear(.default(scale: true))
|
|
||||||
.disappear(.default(scale: true))
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let colorButton = colorButton.update(
|
|
||||||
component: ColorSwatchComponent(
|
|
||||||
type: .main,
|
|
||||||
color: color,
|
|
||||||
tag: colorButtonTag,
|
|
||||||
action: { [weak state] in
|
|
||||||
if let state = state {
|
|
||||||
presentColorPicker(state.currentColor)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
holdAction: {
|
|
||||||
if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: colorButtonTag) {
|
|
||||||
presentFastColorPicker(buttonView)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
pan: { point in
|
|
||||||
updateFastColorPickerPan(point)
|
|
||||||
},
|
|
||||||
release: {
|
|
||||||
dismissFastColorPicker()
|
|
||||||
}
|
|
||||||
),
|
|
||||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
|
||||||
transition: context.transition
|
|
||||||
)
|
|
||||||
context.add(colorButton
|
|
||||||
.position(CGPoint(x: environment.safeInsets.left + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0))
|
|
||||||
.appear(.default(scale: true))
|
|
||||||
.disappear(.default(scale: true))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isModeControlEnabled = true
|
|
||||||
var modeRightInset: CGFloat = 57.0
|
|
||||||
if isEditingShapeSize {
|
|
||||||
var isFilled = false
|
|
||||||
if let entity = state.selectedEntity as? DrawingSimpleShapeEntity, case .fill = entity.drawType {
|
|
||||||
isFilled = true
|
|
||||||
isModeControlEnabled = false
|
|
||||||
} else if let entity = state.selectedEntity as? DrawingBubbleEntity, case .fill = entity.drawType {
|
|
||||||
isFilled = true
|
|
||||||
isModeControlEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
if let _ = state.selectedEntity as? DrawingBubbleEntity {
|
|
||||||
let flipButton = flipButton.update(
|
|
||||||
component: Button(
|
|
||||||
content: AnyComponent(
|
|
||||||
Image(image: state.image(.flip))
|
|
||||||
),
|
|
||||||
action: { [weak state] in
|
|
||||||
guard let state = state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if let entity = state.selectedEntity as? DrawingBubbleEntity {
|
|
||||||
var updatedTailPosition = entity.tailPosition
|
|
||||||
updatedTailPosition.x = 1.0 - updatedTailPosition.x
|
|
||||||
entity.tailPosition = updatedTailPosition
|
|
||||||
entity.currentEntityView?.update()
|
|
||||||
}
|
|
||||||
state.updated(transition: .easeInOut(duration: 0.2))
|
|
||||||
}
|
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)),
|
|
||||||
availableSize: CGSize(width: 33.0, height: 33.0),
|
|
||||||
transition: .immediate
|
|
||||||
)
|
|
||||||
context.add(flipButton
|
|
||||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - flipButton.size.width / 2.0 - 3.0 - flipButton.size.width, y: context.availableSize.height - environment.safeInsets.bottom - flipButton.size.height / 2.0 - 2.0 - UIScreenPixel))
|
|
||||||
.appear(.default(scale: true))
|
|
||||||
.disappear(.default(scale: true))
|
|
||||||
)
|
|
||||||
modeRightInset += 35.0
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if let isFilled = isFilled {
|
||||||
let fillButton = fillButton.update(
|
let fillButton = fillButton.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
@ -1353,15 +1106,264 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
state.updated(transition: .easeInOut(duration: 0.2))
|
state.updated(transition: .easeInOut(duration: 0.2))
|
||||||
}
|
}
|
||||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag),
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag),
|
||||||
availableSize: CGSize(width: 33.0, height: 33.0),
|
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||||
transition: .immediate
|
transition: .immediate
|
||||||
)
|
)
|
||||||
context.add(fillButton
|
context.add(fillButton
|
||||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - fillButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - fillButton.size.height / 2.0 - 2.0 - UIScreenPixel))
|
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
|
||||||
.appear(.default(scale: true))
|
.appear(.default(scale: true))
|
||||||
.disappear(.default(scale: true))
|
.disappear(.default(scale: true))
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasFlip {
|
||||||
|
let flipButton = flipButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
Image(image: state.image(.flip))
|
||||||
|
),
|
||||||
|
action: { [weak state] in
|
||||||
|
guard let state = state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if let entity = state.selectedEntity as? DrawingBubbleEntity {
|
||||||
|
var updatedTailPosition = entity.tailPosition
|
||||||
|
updatedTailPosition.x = 1.0 - updatedTailPosition.x
|
||||||
|
entity.tailPosition = updatedTailPosition
|
||||||
|
entity.currentEntityView?.update()
|
||||||
|
} else if let entity = state.selectedEntity as? DrawingStickerEntity {
|
||||||
|
entity.mirrored = !entity.mirrored
|
||||||
|
entity.currentEntityView?.update(animated: true)
|
||||||
|
}
|
||||||
|
state.updated(transition: .easeInOut(duration: 0.2))
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag),
|
||||||
|
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
context.add(flipButton
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
|
||||||
|
.appear(.default(scale: true))
|
||||||
|
.disappear(.default(scale: true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var sizeSliderVisible = false
|
||||||
|
var isEditingText = false
|
||||||
|
var sizeValue: CGFloat?
|
||||||
|
if let textEntity = state.selectedEntity as? DrawingTextEntity, let entityView = textEntity.currentEntityView as? DrawingTextEntityView {
|
||||||
|
sizeSliderVisible = true
|
||||||
|
isEditingText = entityView.isEditing
|
||||||
|
sizeValue = textEntity.fontSize
|
||||||
} else {
|
} else {
|
||||||
|
if state.selectedEntity == nil || !(state.selectedEntity is DrawingStickerEntity) {
|
||||||
|
sizeSliderVisible = true
|
||||||
|
if state.selectedEntity == nil {
|
||||||
|
sizeValue = state.drawingState.currentToolState.size
|
||||||
|
} else if let entity = state.selectedEntity {
|
||||||
|
if let entity = entity as? DrawingSimpleShapeEntity {
|
||||||
|
sizeSliderVisible = entity.drawType == .stroke
|
||||||
|
} else if let entity = entity as? DrawingBubbleEntity {
|
||||||
|
sizeSliderVisible = entity.drawType == .stroke
|
||||||
|
}
|
||||||
|
sizeValue = entity.lineWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if state.drawingViewState.canZoomOut && !hasTopButtons {
|
||||||
|
let zoomOutButton = zoomOutButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
ZoomOutButtonContent(
|
||||||
|
title: "Zoom Out",
|
||||||
|
image: state.image(.zoomOut)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
action: {
|
||||||
|
performAction.invoke(.zoomOut)
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 44.0)),
|
||||||
|
availableSize: CGSize(width: 120.0, height: 33.0),
|
||||||
|
transition: .immediate
|
||||||
|
)
|
||||||
|
context.add(zoomOutButton
|
||||||
|
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel))
|
||||||
|
.appear(.default(scale: true, alpha: true))
|
||||||
|
.disappear(.default(scale: true, alpha: true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let sizeValue {
|
||||||
|
state.lastSize = sizeValue
|
||||||
|
}
|
||||||
|
if state.drawingViewState.isDrawing {
|
||||||
|
sizeSliderVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
let textSize = textSize.update(
|
||||||
|
component: TextSizeSliderComponent(
|
||||||
|
value: sizeValue ?? state.lastSize,
|
||||||
|
tag: sizeSliderTag,
|
||||||
|
updated: { [weak state] size in
|
||||||
|
if let state = state {
|
||||||
|
state.updateBrushSize(size)
|
||||||
|
if state.selectedEntity == nil {
|
||||||
|
previewBrushSize.invoke(size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, released: {
|
||||||
|
previewBrushSize.invoke(nil)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 30.0, height: 240.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(textSize
|
||||||
|
.position(CGPoint(x: sizeSliderVisible ? textSize.size.width / 2.0 : textSize.size.width / 2.0 - 33.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0))
|
||||||
|
)
|
||||||
|
|
||||||
|
let undoButton = undoButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
Image(image: state.image(.undo))
|
||||||
|
),
|
||||||
|
isEnabled: state.drawingViewState.canUndo,
|
||||||
|
action: {
|
||||||
|
performAction.invoke(.undo)
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag),
|
||||||
|
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(undoButton
|
||||||
|
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
|
||||||
|
.scale(isEditingText ? 0.01 : 1.0)
|
||||||
|
.opacity(isEditingText ? 0.0 : 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
if state.drawingViewState.canRedo && !isEditingText {
|
||||||
|
let redoButton = redoButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
Image(image: state.image(.redo))
|
||||||
|
),
|
||||||
|
action: {
|
||||||
|
performAction.invoke(.redo)
|
||||||
|
}
|
||||||
|
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||||
|
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(redoButton
|
||||||
|
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||||
|
.appear(.default(scale: true, alpha: true))
|
||||||
|
.disappear(.default(scale: true, alpha: true))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let clearAllButton = clearAllButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
Text(text: "Clear All", font: Font.regular(17.0), color: .white)
|
||||||
|
),
|
||||||
|
isEnabled: state.drawingViewState.canClear,
|
||||||
|
action: {
|
||||||
|
performAction.invoke(.clear)
|
||||||
|
}
|
||||||
|
).tagged(clearAllButtonTag),
|
||||||
|
availableSize: CGSize(width: 100.0, height: 30.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(clearAllButton
|
||||||
|
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset))
|
||||||
|
.scale(isEditingText ? 0.01 : 1.0)
|
||||||
|
.opacity(isEditingText ? 0.0 : 1.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let textCancelButton = textCancelButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
|
||||||
|
),
|
||||||
|
action: { [weak state] in
|
||||||
|
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
||||||
|
entityView.endEditing(reset: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 100.0, height: 30.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(textCancelButton
|
||||||
|
.position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: topInset))
|
||||||
|
.scale(isEditingText ? 1.0 : 0.01)
|
||||||
|
.opacity(isEditingText ? 1.0 : 0.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let textDoneButton = textDoneButton.update(
|
||||||
|
component: Button(
|
||||||
|
content: AnyComponent(
|
||||||
|
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
|
||||||
|
),
|
||||||
|
action: { [weak state] in
|
||||||
|
if let entity = state?.selectedEntity as? DrawingTextEntity, let entityView = entity.currentEntityView as? DrawingTextEntityView {
|
||||||
|
entityView.endEditing()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 100.0, height: 30.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(textDoneButton
|
||||||
|
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset))
|
||||||
|
.scale(isEditingText ? 1.0 : 0.01)
|
||||||
|
.opacity(isEditingText ? 1.0 : 0.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
var color: DrawingColor?
|
||||||
|
if let entity = state.selectedEntity, presetColors.contains(entity.color) {
|
||||||
|
color = nil
|
||||||
|
} else if presetColors.contains(state.currentColor) {
|
||||||
|
color = nil
|
||||||
|
} else if state.selectedEntity is DrawingStickerEntity {
|
||||||
|
color = nil
|
||||||
|
} else {
|
||||||
|
color = state.currentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
let colorButton = colorButton.update(
|
||||||
|
component: ColorSwatchComponent(
|
||||||
|
type: .main,
|
||||||
|
color: color,
|
||||||
|
tag: colorButtonTag,
|
||||||
|
action: { [weak state] in
|
||||||
|
if let state = state {
|
||||||
|
presentColorPicker(state.currentColor)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
holdAction: {
|
||||||
|
if let controller = controller() as? DrawingScreen, let buttonView = controller.node.componentHost.findTaggedView(tag: colorButtonTag) {
|
||||||
|
presentFastColorPicker(buttonView)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
pan: { point in
|
||||||
|
updateFastColorPickerPan(point)
|
||||||
|
},
|
||||||
|
release: {
|
||||||
|
dismissFastColorPicker()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||||
|
transition: context.transition
|
||||||
|
)
|
||||||
|
context.add(colorButton
|
||||||
|
.position(CGPoint(x: environment.safeInsets.left + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0))
|
||||||
|
.appear(.default(scale: true))
|
||||||
|
.disappear(.default(scale: true))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
let modeRightInset: CGFloat = 57.0
|
||||||
let addButton = addButton.update(
|
let addButton = addButton.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(ZStack([
|
content: AnyComponent(ZStack([
|
||||||
@ -1434,7 +1436,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true)
|
transition.animatePosition(view: view, from: CGPoint(), to: CGPoint(x: 12.0, y: 0.0), additive: true)
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
let selectedIndex: Int
|
let selectedIndex: Int
|
||||||
switch state.currentMode {
|
switch state.currentMode {
|
||||||
@ -1457,8 +1458,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
component: ModeAndSizeComponent(
|
component: ModeAndSizeComponent(
|
||||||
values: ["Draw", "Sticker", "Text"],
|
values: ["Draw", "Sticker", "Text"],
|
||||||
sizeValue: selectedSize,
|
sizeValue: selectedSize,
|
||||||
isEditing: isEditingShapeSize,
|
isEditing: false,
|
||||||
isEnabled: isModeControlEnabled,
|
isEnabled: true,
|
||||||
rightInset: modeRightInset - 57.0,
|
rightInset: modeRightInset - 57.0,
|
||||||
tag: modeTag,
|
tag: modeTag,
|
||||||
selectedIndex: selectedIndex,
|
selectedIndex: selectedIndex,
|
||||||
@ -1492,7 +1493,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
)
|
)
|
||||||
context.add(modeAndSize
|
context.add(modeAndSize
|
||||||
.position(CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0))
|
.position(CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0))
|
||||||
.opacity(isModeControlEnabled ? 1.0 : 0.4)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var animatingOut = false
|
var animatingOut = false
|
||||||
@ -1508,7 +1508,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
animation: LottieAnimationComponent.AnimationItem(
|
animation: LottieAnimationComponent.AnimationItem(
|
||||||
name: "media_backToCancel",
|
name: "media_backToCancel",
|
||||||
mode: .animating(loop: false),
|
mode: .animating(loop: false),
|
||||||
range: isEditingShapeSize || animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5)
|
range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5)
|
||||||
),
|
),
|
||||||
colors: ["__allcolors__": .white],
|
colors: ["__allcolors__": .white],
|
||||||
size: CGSize(width: 33.0, height: 33.0)
|
size: CGSize(width: 33.0, height: 33.0)
|
||||||
@ -2007,6 +2007,11 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||||
}
|
}
|
||||||
|
if let buttonView = self.componentHost.findTaggedView(tag: fillButtonTag) {
|
||||||
|
buttonView.alpha = 0.0
|
||||||
|
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||||
|
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||||
|
}
|
||||||
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
||||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
|
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
|
||||||
}
|
}
|
||||||
@ -2057,7 +2062,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController {
|
|||||||
let environment = ViewControllerComponentContainer.Environment(
|
let environment = ViewControllerComponentContainer.Environment(
|
||||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||||
navigationHeight: 0.0,
|
navigationHeight: 0.0,
|
||||||
safeInsets: UIEdgeInsets(top: layout.intrinsicInsets.top + layout.safeInsets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom, right: layout.safeInsets.right),
|
safeInsets: UIEdgeInsets(
|
||||||
|
top: layout.intrinsicInsets.top + layout.safeInsets.top,
|
||||||
|
left: layout.safeInsets.left,
|
||||||
|
bottom: layout.intrinsicInsets.bottom + layout.safeInsets.bottom,
|
||||||
|
right: layout.safeInsets.right
|
||||||
|
),
|
||||||
inputHeight: layout.inputHeight ?? 0.0,
|
inputHeight: layout.inputHeight ?? 0.0,
|
||||||
metrics: layout.metrics,
|
metrics: layout.metrics,
|
||||||
deviceMetrics: layout.deviceMetrics,
|
deviceMetrics: layout.deviceMetrics,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user