mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-23 14:45:21 +00:00
Camera and editor improvements
This commit is contained in:
@@ -180,6 +180,7 @@ public final class _UpdatedChildComponent {
|
|||||||
var _opacity: CGFloat?
|
var _opacity: CGFloat?
|
||||||
var _cornerRadius: CGFloat?
|
var _cornerRadius: CGFloat?
|
||||||
var _clipsToBounds: Bool?
|
var _clipsToBounds: Bool?
|
||||||
|
var _shadow: Shadow?
|
||||||
|
|
||||||
fileprivate var transitionAppear: Transition.Appear?
|
fileprivate var transitionAppear: Transition.Appear?
|
||||||
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
fileprivate var transitionAppearWithGuide: (Transition.AppearWithGuide, _AnyChildComponent.Id)?
|
||||||
@@ -260,6 +261,11 @@ public final class _UpdatedChildComponent {
|
|||||||
self._clipsToBounds = clipsToBounds
|
self._clipsToBounds = clipsToBounds
|
||||||
return self
|
return self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@discardableResult public func shadow(_ shadow: Shadow?) -> _UpdatedChildComponent {
|
||||||
|
self._shadow = shadow
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
|
@discardableResult public func gesture(_ gesture: Gesture) -> _UpdatedChildComponent {
|
||||||
self.gestures.append(gesture)
|
self.gestures.append(gesture)
|
||||||
@@ -706,6 +712,16 @@ public extension CombinedComponent {
|
|||||||
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
updatedChild.view.alpha = updatedChild._opacity ?? 1.0
|
||||||
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
|
updatedChild.view.clipsToBounds = updatedChild._clipsToBounds ?? false
|
||||||
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0
|
updatedChild.view.layer.cornerRadius = updatedChild._cornerRadius ?? 0.0
|
||||||
|
if let shadow = updatedChild._shadow {
|
||||||
|
updatedChild.view.layer.shadowColor = shadow.color.withAlphaComponent(1.0).cgColor
|
||||||
|
updatedChild.view.layer.shadowRadius = shadow.radius
|
||||||
|
updatedChild.view.layer.shadowOpacity = Float(shadow.color.alpha)
|
||||||
|
updatedChild.view.layer.shadowOffset = shadow.offset
|
||||||
|
} else {
|
||||||
|
updatedChild.view.layer.shadowColor = nil
|
||||||
|
updatedChild.view.layer.shadowRadius = 0.0
|
||||||
|
updatedChild.view.layer.shadowOpacity = 0.0
|
||||||
|
}
|
||||||
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
updatedChild.view.context(typeErasedComponent: updatedChild.component).erasedState._updated = { [weak viewContext] transition in
|
||||||
guard let viewContext = viewContext else {
|
guard let viewContext = viewContext else {
|
||||||
return
|
return
|
||||||
@@ -834,3 +850,19 @@ public extension CombinedComponent {
|
|||||||
return ActionSlot<Arguments>()
|
return ActionSlot<Arguments>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct Shadow {
|
||||||
|
public let color: UIColor
|
||||||
|
public let radius: CGFloat
|
||||||
|
public let offset: CGSize
|
||||||
|
|
||||||
|
public init(
|
||||||
|
color: UIColor,
|
||||||
|
radius: CGFloat,
|
||||||
|
offset: CGSize
|
||||||
|
) {
|
||||||
|
self.color = color
|
||||||
|
self.radius = radius
|
||||||
|
self.offset = offset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -178,6 +178,11 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView {
|
|||||||
return entities
|
return entities
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var hasEntities: Bool {
|
||||||
|
let entities = self.entities.filter { !($0 is DrawingMediaEntity) }
|
||||||
|
return !entities.isEmpty
|
||||||
|
}
|
||||||
|
|
||||||
private var initialEntitiesData: Data?
|
private var initialEntitiesData: Data?
|
||||||
public func setup(withEntitiesData entitiesData: Data?) {
|
public func setup(withEntitiesData entitiesData: Data?) {
|
||||||
self.clear()
|
self.clear()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import ComponentDisplayAdapters
|
|||||||
import LottieAnimationComponent
|
import LottieAnimationComponent
|
||||||
import ViewControllerComponent
|
import ViewControllerComponent
|
||||||
import BlurredBackgroundComponent
|
import BlurredBackgroundComponent
|
||||||
|
import MultilineTextComponent
|
||||||
import ContextUI
|
import ContextUI
|
||||||
import ChatEntityKeyboardInputNode
|
import ChatEntityKeyboardInputNode
|
||||||
import EntityKeyboard
|
import EntityKeyboard
|
||||||
@@ -1676,6 +1677,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0))
|
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 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))
|
||||||
|
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1774,6 +1776,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
|
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
|
||||||
.scale(isEditingText ? 0.01 : 1.0)
|
.scale(isEditingText ? 0.01 : 1.0)
|
||||||
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
|
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
|
||||||
|
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -1794,12 +1797,17 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||||
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
||||||
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
|
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
|
||||||
|
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
let clearAllButton = clearAllButton.update(
|
let clearAllButton = clearAllButton.update(
|
||||||
component: Button(
|
component: Button(
|
||||||
content: AnyComponent(
|
content: AnyComponent(
|
||||||
Text(text: strings.Paint_Clear, font: Font.regular(17.0), color: .white)
|
MultilineTextComponent(
|
||||||
|
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)),
|
||||||
|
textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil,
|
||||||
|
textShadowBlur: 2.0
|
||||||
|
)
|
||||||
),
|
),
|
||||||
isEnabled: state.drawingViewState.canClear,
|
isEnabled: state.drawingViewState.canClear,
|
||||||
action: {
|
action: {
|
||||||
|
|||||||
@@ -895,7 +895,7 @@ public final class DrawingView: UIView, UIGestureRecognizerDelegate, UIPencilInt
|
|||||||
self.stateUpdated(NavigationState(
|
self.stateUpdated(NavigationState(
|
||||||
canUndo: !self.undoStack.isEmpty,
|
canUndo: !self.undoStack.isEmpty,
|
||||||
canRedo: !self.redoStack.isEmpty,
|
canRedo: !self.redoStack.isEmpty,
|
||||||
canClear: !self.undoStack.isEmpty || self.hasOpaqueData || !(self.entitiesView?.entities.isEmpty ?? true),
|
canClear: !self.undoStack.isEmpty || self.hasOpaqueData || (self.entitiesView?.hasEntities ?? false),
|
||||||
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
canZoomOut: self.zoomScale > 1.0 + .ulpOfOne,
|
||||||
isDrawing: self.isDrawing
|
isDrawing: self.isDrawing
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import TelegramPresentationData
|
|||||||
import FastBlur
|
import FastBlur
|
||||||
|
|
||||||
public struct MediaEditorPlayerState {
|
public struct MediaEditorPlayerState {
|
||||||
|
public let generationTimestamp: Double
|
||||||
public let duration: Double
|
public let duration: Double
|
||||||
public let timeRange: Range<Double>?
|
public let timeRange: Range<Double>?
|
||||||
public let position: Double
|
public let position: Double
|
||||||
|
public let isPlaying: Bool
|
||||||
public let frames: [UIImage]
|
public let frames: [UIImage]
|
||||||
public let framesCount: Int
|
public let framesCount: Int
|
||||||
public let framesUpdateTimestamp: Double
|
public let framesUpdateTimestamp: Double
|
||||||
@@ -107,12 +109,12 @@ public final class MediaEditor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let playerPromise = Promise<AVPlayer?>()
|
private let playerPromise = Promise<AVPlayer?>()
|
||||||
private var playerPlaybackState: (Double, Double, Bool) = (0.0, 0.0, false) {
|
private var playerPlaybackState: (Double, Double, Bool, Bool) = (0.0, 0.0, false, false) {
|
||||||
didSet {
|
didSet {
|
||||||
self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState))
|
self.playerPlaybackStatePromise.set(.single(self.playerPlaybackState))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private let playerPlaybackStatePromise = Promise<(Double, Double, Bool)>((0.0, 0.0, false))
|
private let playerPlaybackStatePromise = Promise<(Double, Double, Bool, Bool)>((0.0, 0.0, false, false))
|
||||||
|
|
||||||
public var onFirstDisplay: () -> Void = {}
|
public var onFirstDisplay: () -> Void = {}
|
||||||
|
|
||||||
@@ -122,12 +124,14 @@ public final class MediaEditor {
|
|||||||
if let self, let asset = player?.currentItem?.asset {
|
if let self, let asset = player?.currentItem?.asset {
|
||||||
return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount))
|
return combineLatest(self.valuesPromise.get(), self.playerPlaybackStatePromise.get(), self.videoFrames(asset: asset, count: framesCount))
|
||||||
|> map { values, durationAndPosition, framesAndUpdateTimestamp in
|
|> map { values, durationAndPosition, framesAndUpdateTimestamp in
|
||||||
let (duration, position, hasAudio) = durationAndPosition
|
let (duration, position, isPlaying, hasAudio) = durationAndPosition
|
||||||
let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp
|
let (frames, framesUpdateTimestamp) = framesAndUpdateTimestamp
|
||||||
return MediaEditorPlayerState(
|
return MediaEditorPlayerState(
|
||||||
|
generationTimestamp: CACurrentMediaTime(),
|
||||||
duration: duration,
|
duration: duration,
|
||||||
timeRange: values.videoTrimRange,
|
timeRange: values.videoTrimRange,
|
||||||
position: position,
|
position: position,
|
||||||
|
isPlaying: isPlaying,
|
||||||
frames: frames,
|
frames: frames,
|
||||||
framesCount: framesCount,
|
framesCount: framesCount,
|
||||||
framesUpdateTimestamp: framesUpdateTimestamp,
|
framesUpdateTimestamp: framesUpdateTimestamp,
|
||||||
@@ -409,7 +413,7 @@ public final class MediaEditor {
|
|||||||
if let audioTracks = player.currentItem?.asset.tracks(withMediaType: .audio) {
|
if let audioTracks = player.currentItem?.asset.tracks(withMediaType: .audio) {
|
||||||
hasAudio = !audioTracks.isEmpty
|
hasAudio = !audioTracks.isEmpty
|
||||||
}
|
}
|
||||||
self.playerPlaybackState = (duration, time.seconds, hasAudio)
|
self.playerPlaybackState = (duration, time.seconds, player.rate > 0.0, hasAudio)
|
||||||
}
|
}
|
||||||
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
|
self.didPlayToEndTimeObserver = NotificationCenter.default.addObserver(forName: NSNotification.Name.AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: nil, using: { [weak self] notification in
|
||||||
if let self {
|
if let self {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ public final class MediaEditorDraft: Codable, Equatable {
|
|||||||
case dimensionsWidth
|
case dimensionsWidth
|
||||||
case dimensionsHeight
|
case dimensionsHeight
|
||||||
case values
|
case values
|
||||||
|
case caption
|
||||||
|
case privacy
|
||||||
}
|
}
|
||||||
|
|
||||||
public let path: String
|
public let path: String
|
||||||
@@ -30,14 +32,16 @@ public final class MediaEditorDraft: Codable, Equatable {
|
|||||||
public let thumbnail: UIImage
|
public let thumbnail: UIImage
|
||||||
public let dimensions: PixelDimensions
|
public let dimensions: PixelDimensions
|
||||||
public let values: MediaEditorValues
|
public let values: MediaEditorValues
|
||||||
|
public let caption: NSAttributedString
|
||||||
public let privacy: MediaEditorResultPrivacy?
|
public let privacy: MediaEditorResultPrivacy?
|
||||||
|
|
||||||
public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, values: MediaEditorValues, privacy: MediaEditorResultPrivacy?) {
|
public init(path: String, isVideo: Bool, thumbnail: UIImage, dimensions: PixelDimensions, values: MediaEditorValues, caption: NSAttributedString, privacy: MediaEditorResultPrivacy?) {
|
||||||
self.path = path
|
self.path = path
|
||||||
self.isVideo = isVideo
|
self.isVideo = isVideo
|
||||||
self.thumbnail = thumbnail
|
self.thumbnail = thumbnail
|
||||||
self.dimensions = dimensions
|
self.dimensions = dimensions
|
||||||
self.values = values
|
self.values = values
|
||||||
|
self.caption = caption
|
||||||
self.privacy = privacy
|
self.privacy = privacy
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,6 +66,7 @@ public final class MediaEditorDraft: Codable, Equatable {
|
|||||||
} else {
|
} else {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
self.caption = NSAttributedString()
|
||||||
self.privacy = nil
|
self.privacy = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -647,11 +647,13 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
transition: transition,
|
transition: transition,
|
||||||
component: AnyComponent(VideoScrubberComponent(
|
component: AnyComponent(VideoScrubberComponent(
|
||||||
context: component.context,
|
context: component.context,
|
||||||
|
generationTimestamp: playerState.generationTimestamp,
|
||||||
duration: playerState.duration,
|
duration: playerState.duration,
|
||||||
startPosition: playerState.timeRange?.lowerBound ?? 0.0,
|
startPosition: playerState.timeRange?.lowerBound ?? 0.0,
|
||||||
endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration),
|
endPosition: playerState.timeRange?.upperBound ?? min(playerState.duration, storyMaxVideoDuration),
|
||||||
position: playerState.position,
|
position: playerState.position,
|
||||||
maxDuration: storyMaxVideoDuration,
|
maxDuration: storyMaxVideoDuration,
|
||||||
|
isPlaying: playerState.isPlaying,
|
||||||
frames: playerState.frames,
|
frames: playerState.frames,
|
||||||
framesUpdateTimestamp: playerState.framesUpdateTimestamp,
|
framesUpdateTimestamp: playerState.framesUpdateTimestamp,
|
||||||
trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in
|
trimUpdated: { [weak mediaEditor] start, end, updatedEnd, done in
|
||||||
@@ -893,7 +895,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
if let saveButtonView = self.saveButton.view {
|
if let saveButtonView = self.saveButton.view {
|
||||||
if saveButtonView.superview == nil {
|
if saveButtonView.superview == nil {
|
||||||
saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
saveButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||||
saveButtonView.layer.shadowRadius = 3.0
|
saveButtonView.layer.shadowRadius = 2.0
|
||||||
saveButtonView.layer.shadowColor = UIColor.black.cgColor
|
saveButtonView.layer.shadowColor = UIColor.black.cgColor
|
||||||
saveButtonView.layer.shadowOpacity = 0.35
|
saveButtonView.layer.shadowOpacity = 0.35
|
||||||
self.addSubview(saveButtonView)
|
self.addSubview(saveButtonView)
|
||||||
@@ -941,7 +943,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
if let muteButtonView = self.muteButton.view {
|
if let muteButtonView = self.muteButton.view {
|
||||||
if muteButtonView.superview == nil {
|
if muteButtonView.superview == nil {
|
||||||
muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
muteButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||||
muteButtonView.layer.shadowRadius = 3.0
|
muteButtonView.layer.shadowRadius = 2.0
|
||||||
muteButtonView.layer.shadowColor = UIColor.black.cgColor
|
muteButtonView.layer.shadowColor = UIColor.black.cgColor
|
||||||
muteButtonView.layer.shadowOpacity = 0.35
|
muteButtonView.layer.shadowOpacity = 0.35
|
||||||
self.addSubview(muteButtonView)
|
self.addSubview(muteButtonView)
|
||||||
@@ -979,7 +981,7 @@ final class MediaEditorScreenComponent: Component {
|
|||||||
if let settingsButtonView = self.settingsButton.view {
|
if let settingsButtonView = self.settingsButton.view {
|
||||||
if settingsButtonView.superview == nil {
|
if settingsButtonView.superview == nil {
|
||||||
settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
settingsButtonView.layer.shadowOffset = CGSize(width: 0.0, height: 0.0)
|
||||||
settingsButtonView.layer.shadowRadius = 3.0
|
settingsButtonView.layer.shadowRadius = 2.0
|
||||||
settingsButtonView.layer.shadowColor = UIColor.black.cgColor
|
settingsButtonView.layer.shadowColor = UIColor.black.cgColor
|
||||||
settingsButtonView.layer.shadowOpacity = 0.35
|
settingsButtonView.layer.shadowOpacity = 0.35
|
||||||
//self.addSubview(settingsButtonView)
|
//self.addSubview(settingsButtonView)
|
||||||
@@ -2644,7 +2646,7 @@ public final class MediaEditorScreen: ViewController {
|
|||||||
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg"
|
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).jpg"
|
||||||
if let data = image.jpegData(compressionQuality: 0.87) {
|
if let data = image.jpegData(compressionQuality: 0.87) {
|
||||||
try? data.write(to: URL(fileURLWithPath: path))
|
try? data.write(to: URL(fileURLWithPath: path))
|
||||||
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values, privacy: privacy)
|
let draft = MediaEditorDraft(path: path, isVideo: false, thumbnail: thumbnailImage, dimensions: dimensions, values: values, caption: NSAttributedString(), privacy: privacy)
|
||||||
if let id {
|
if let id {
|
||||||
saveStorySource(engine: self.context.engine, item: draft, id: id)
|
saveStorySource(engine: self.context.engine, item: draft, id: id)
|
||||||
} else {
|
} else {
|
||||||
@@ -2658,7 +2660,7 @@ public final class MediaEditorScreen: ViewController {
|
|||||||
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
if let thumbnailImage = generateScaledImage(image: resultImage, size: fittedSize) {
|
||||||
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4"
|
let path = draftPath() + "/\(Int64.random(in: .min ... .max)).mp4"
|
||||||
try? FileManager.default.moveItem(atPath: videoPath, toPath: path)
|
try? FileManager.default.moveItem(atPath: videoPath, toPath: path)
|
||||||
let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, values: values, privacy: privacy)
|
let draft = MediaEditorDraft(path: path, isVideo: true, thumbnail: thumbnailImage, dimensions: dimensions, values: values, caption: NSAttributedString(), privacy: privacy)
|
||||||
if let id {
|
if let id {
|
||||||
saveStorySource(engine: self.context.engine, item: draft, id: id)
|
saveStorySource(engine: self.context.engine, item: draft, id: id)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -41,11 +41,13 @@ final class VideoScrubberComponent: Component {
|
|||||||
typealias EnvironmentType = Empty
|
typealias EnvironmentType = Empty
|
||||||
|
|
||||||
let context: AccountContext
|
let context: AccountContext
|
||||||
|
let generationTimestamp: Double
|
||||||
let duration: Double
|
let duration: Double
|
||||||
let startPosition: Double
|
let startPosition: Double
|
||||||
let endPosition: Double
|
let endPosition: Double
|
||||||
let position: Double
|
let position: Double
|
||||||
let maxDuration: Double
|
let maxDuration: Double
|
||||||
|
let isPlaying: Bool
|
||||||
let frames: [UIImage]
|
let frames: [UIImage]
|
||||||
let framesUpdateTimestamp: Double
|
let framesUpdateTimestamp: Double
|
||||||
let trimUpdated: (Double, Double, Bool, Bool) -> Void
|
let trimUpdated: (Double, Double, Bool, Bool) -> Void
|
||||||
@@ -53,22 +55,26 @@ final class VideoScrubberComponent: Component {
|
|||||||
|
|
||||||
init(
|
init(
|
||||||
context: AccountContext,
|
context: AccountContext,
|
||||||
|
generationTimestamp: Double,
|
||||||
duration: Double,
|
duration: Double,
|
||||||
startPosition: Double,
|
startPosition: Double,
|
||||||
endPosition: Double,
|
endPosition: Double,
|
||||||
position: Double,
|
position: Double,
|
||||||
maxDuration: Double,
|
maxDuration: Double,
|
||||||
|
isPlaying: Bool,
|
||||||
frames: [UIImage],
|
frames: [UIImage],
|
||||||
framesUpdateTimestamp: Double,
|
framesUpdateTimestamp: Double,
|
||||||
trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void,
|
trimUpdated: @escaping (Double, Double, Bool, Bool) -> Void,
|
||||||
positionUpdated: @escaping (Double, Bool) -> Void
|
positionUpdated: @escaping (Double, Bool) -> Void
|
||||||
) {
|
) {
|
||||||
self.context = context
|
self.context = context
|
||||||
|
self.generationTimestamp = generationTimestamp
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.startPosition = startPosition
|
self.startPosition = startPosition
|
||||||
self.endPosition = endPosition
|
self.endPosition = endPosition
|
||||||
self.position = position
|
self.position = position
|
||||||
self.maxDuration = maxDuration
|
self.maxDuration = maxDuration
|
||||||
|
self.isPlaying = isPlaying
|
||||||
self.frames = frames
|
self.frames = frames
|
||||||
self.framesUpdateTimestamp = framesUpdateTimestamp
|
self.framesUpdateTimestamp = framesUpdateTimestamp
|
||||||
self.trimUpdated = trimUpdated
|
self.trimUpdated = trimUpdated
|
||||||
@@ -79,6 +85,9 @@ final class VideoScrubberComponent: Component {
|
|||||||
if lhs.context !== rhs.context {
|
if lhs.context !== rhs.context {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.generationTimestamp != rhs.generationTimestamp {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.duration != rhs.duration {
|
if lhs.duration != rhs.duration {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -94,6 +103,9 @@ final class VideoScrubberComponent: Component {
|
|||||||
if lhs.maxDuration != rhs.maxDuration {
|
if lhs.maxDuration != rhs.maxDuration {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.isPlaying != rhs.isPlaying {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp {
|
if lhs.framesUpdateTimestamp != rhs.framesUpdateTimestamp {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -114,6 +126,13 @@ final class VideoScrubberComponent: Component {
|
|||||||
|
|
||||||
private var component: VideoScrubberComponent?
|
private var component: VideoScrubberComponent?
|
||||||
private weak var state: EmptyComponentState?
|
private weak var state: EmptyComponentState?
|
||||||
|
private var scrubberSize: CGSize?
|
||||||
|
|
||||||
|
private var isPanningTrimHandle = false
|
||||||
|
private var isPanningPositionHandle = false
|
||||||
|
|
||||||
|
private var displayLink: SharedDisplayLinkDriver.Link?
|
||||||
|
private var positionAnimation: (start: Double, from: Double, to: Double)?
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
@@ -134,17 +153,19 @@ final class VideoScrubberComponent: Component {
|
|||||||
context.addPath(innerPath.cgPath)
|
context.addPath(innerPath.cgPath)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
|
|
||||||
|
context.setBlendMode(.clear)
|
||||||
let holeSize = CGSize(width: 2.0, height: 11.0)
|
let holeSize = CGSize(width: 2.0, height: 11.0)
|
||||||
let holePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 5.0 - UIScreenPixel, y: (size.height - holeSize.height) / 2.0), size: holeSize), cornerRadius: holeSize.width / 2.0)
|
let holePath = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 5.0 - UIScreenPixel, y: (size.height - holeSize.height) / 2.0), size: holeSize), cornerRadius: holeSize.width / 2.0)
|
||||||
context.addPath(holePath.cgPath)
|
context.addPath(holePath.cgPath)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
})?.withRenderingMode(.alwaysTemplate)
|
})?.withRenderingMode(.alwaysTemplate)
|
||||||
|
|
||||||
let positionImage = generateImage(CGSize(width: 2.0, height: 42.0), rotatedContext: { size, context in
|
let positionImage = generateImage(CGSize(width: handleWidth, height: 50.0), rotatedContext: { size, context in
|
||||||
context.clear(CGRect(origin: .zero, size: size))
|
context.clear(CGRect(origin: .zero, size: size))
|
||||||
context.setFillColor(UIColor.white.cgColor)
|
context.setFillColor(UIColor.white.cgColor)
|
||||||
|
context.setShadow(offset: .zero, blur: 2.0, color: UIColor(rgb: 0x000000, alpha: 0.55).cgColor)
|
||||||
|
|
||||||
let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: 2.0, height: 42.0)), cornerRadius: 1.0)
|
let path = UIBezierPath(roundedRect: CGRect(origin: CGPoint(x: 6.0, y: 4.0), size: CGSize(width: 2.0, height: 42.0)), cornerRadius: 1.0)
|
||||||
context.addPath(path.cgPath)
|
context.addPath(path.cgPath)
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
})
|
})
|
||||||
@@ -189,20 +210,28 @@ final class VideoScrubberComponent: Component {
|
|||||||
self.leftHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftHandlePan(_:))))
|
self.leftHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleLeftHandlePan(_:))))
|
||||||
self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleRightHandlePan(_:))))
|
self.rightHandleView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handleRightHandlePan(_:))))
|
||||||
self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:))))
|
self.cursorView.addGestureRecognizer(UIPanGestureRecognizer(target: self, action: #selector(self.handlePositionHandlePan(_:))))
|
||||||
|
|
||||||
|
self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in
|
||||||
|
self?.updateCursorPosition()
|
||||||
|
}
|
||||||
|
self.displayLink?.isPaused = true
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
required init?(coder: NSCoder) {
|
||||||
fatalError("init(coder:) has not been implemented")
|
fatalError("init(coder:) has not been implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
private var isPanningHandle = false
|
deinit {
|
||||||
|
self.displayLink?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
@objc private func handleLeftHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handleLeftHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let component = self.component else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let location = gestureRecognizer.location(in: self)
|
let location = gestureRecognizer.location(in: self)
|
||||||
let start = handleWidth / 2.0
|
let start = handleWidth / 2.0
|
||||||
let end = self.frame.width - handleWidth
|
let end = self.frame.width - handleWidth / 2.0
|
||||||
let length = end - start
|
let length = end - start
|
||||||
let fraction = (location.x - start) / length
|
let fraction = (location.x - start) / length
|
||||||
|
|
||||||
@@ -219,13 +248,13 @@ final class VideoScrubberComponent: Component {
|
|||||||
var transition: Transition = .immediate
|
var transition: Transition = .immediate
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began, .changed:
|
case .began, .changed:
|
||||||
self.isPanningHandle = true
|
self.isPanningTrimHandle = true
|
||||||
component.trimUpdated(startValue, endValue, false, false)
|
component.trimUpdated(startValue, endValue, false, false)
|
||||||
if case .began = gestureRecognizer.state {
|
if case .began = gestureRecognizer.state {
|
||||||
transition = .easeInOut(duration: 0.25)
|
transition = .easeInOut(duration: 0.25)
|
||||||
}
|
}
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
self.isPanningHandle = false
|
self.isPanningTrimHandle = false
|
||||||
component.trimUpdated(startValue, endValue, false, true)
|
component.trimUpdated(startValue, endValue, false, true)
|
||||||
transition = .easeInOut(duration: 0.25)
|
transition = .easeInOut(duration: 0.25)
|
||||||
default:
|
default:
|
||||||
@@ -240,7 +269,7 @@ final class VideoScrubberComponent: Component {
|
|||||||
}
|
}
|
||||||
let location = gestureRecognizer.location(in: self)
|
let location = gestureRecognizer.location(in: self)
|
||||||
let start = handleWidth / 2.0
|
let start = handleWidth / 2.0
|
||||||
let end = self.frame.width - handleWidth
|
let end = self.frame.width - handleWidth / 2.0
|
||||||
let length = end - start
|
let length = end - start
|
||||||
let fraction = (location.x - start) / length
|
let fraction = (location.x - start) / length
|
||||||
|
|
||||||
@@ -257,13 +286,13 @@ final class VideoScrubberComponent: Component {
|
|||||||
var transition: Transition = .immediate
|
var transition: Transition = .immediate
|
||||||
switch gestureRecognizer.state {
|
switch gestureRecognizer.state {
|
||||||
case .began, .changed:
|
case .began, .changed:
|
||||||
self.isPanningHandle = true
|
self.isPanningTrimHandle = true
|
||||||
component.trimUpdated(startValue, endValue, true, false)
|
component.trimUpdated(startValue, endValue, true, false)
|
||||||
if case .began = gestureRecognizer.state {
|
if case .began = gestureRecognizer.state {
|
||||||
transition = .easeInOut(duration: 0.25)
|
transition = .easeInOut(duration: 0.25)
|
||||||
}
|
}
|
||||||
case .ended, .cancelled:
|
case .ended, .cancelled:
|
||||||
self.isPanningHandle = false
|
self.isPanningTrimHandle = false
|
||||||
component.trimUpdated(startValue, endValue, true, true)
|
component.trimUpdated(startValue, endValue, true, true)
|
||||||
transition = .easeInOut(duration: 0.25)
|
transition = .easeInOut(duration: 0.25)
|
||||||
default:
|
default:
|
||||||
@@ -273,10 +302,59 @@ final class VideoScrubberComponent: Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc private func handlePositionHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
@objc private func handlePositionHandlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||||
guard let _ = self.component else {
|
guard let component = self.component else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
//let location = gestureRecognizer.location(in: self)
|
let location = gestureRecognizer.location(in: self)
|
||||||
|
let start = handleWidth
|
||||||
|
let end = self.frame.width - handleWidth
|
||||||
|
let length = end - start
|
||||||
|
let fraction = (location.x - start) / length
|
||||||
|
|
||||||
|
let position = max(component.startPosition, min(component.endPosition, component.duration * fraction))
|
||||||
|
let transition: Transition = .immediate
|
||||||
|
switch gestureRecognizer.state {
|
||||||
|
case .began, .changed:
|
||||||
|
self.isPanningPositionHandle = true
|
||||||
|
component.positionUpdated(position, false)
|
||||||
|
case .ended, .cancelled:
|
||||||
|
self.isPanningPositionHandle = false
|
||||||
|
component.positionUpdated(position, true)
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
self.state?.updated(transition: transition)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func cursorFrame(size: CGSize, position: Double, duration : Double) -> CGRect {
|
||||||
|
let cursorPadding: CGFloat = 8.0
|
||||||
|
let cursorPositionFraction = duration > 0.0 ? position / duration : 0.0
|
||||||
|
let cursorPosition = floorToScreenPixels(handleWidth + handleWidth / 2.0 - cursorPadding + (size.width - handleWidth * 3.0 + cursorPadding * 2.0) * cursorPositionFraction)
|
||||||
|
var cursorFrame = CGRect(origin: CGPoint(x: cursorPosition - handleWidth / 2.0, y: -5.0 - UIScreenPixel), size: CGSize(width: handleWidth, height: 50.0))
|
||||||
|
cursorFrame.origin.x = max(self.leftHandleView.frame.maxX - cursorPadding, cursorFrame.origin.x)
|
||||||
|
cursorFrame.origin.x = min(self.rightHandleView.frame.minX + cursorPadding, cursorFrame.origin.x)
|
||||||
|
return cursorFrame
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateCursorPosition() {
|
||||||
|
guard let component = self.component, let scrubberSize = self.scrubberSize else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
|
let updatedPosition: Double
|
||||||
|
if let (start, from, to) = self.positionAnimation {
|
||||||
|
let duration = to - from
|
||||||
|
let fraction = duration > 0.0 ? (timestamp - start) / duration : 0.0
|
||||||
|
updatedPosition = max(component.startPosition, min(component.endPosition, from + (to - from) * fraction))
|
||||||
|
if fraction >= 1.0 {
|
||||||
|
self.positionAnimation = (timestamp, component.startPosition, component.endPosition)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let advance = component.isPlaying ? timestamp - component.generationTimestamp : 0.0
|
||||||
|
updatedPosition = max(component.startPosition, min(component.endPosition, component.position + advance))
|
||||||
|
}
|
||||||
|
self.cursorView.frame = cursorFrame(size: scrubberSize, position: updatedPosition, duration: component.duration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
func update(component: VideoScrubberComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<EnvironmentType>, transition: Transition) -> CGSize {
|
||||||
@@ -285,6 +363,8 @@ final class VideoScrubberComponent: Component {
|
|||||||
self.state = state
|
self.state = state
|
||||||
|
|
||||||
let scrubberSize = CGSize(width: availableSize.width, height: scrubberHeight)
|
let scrubberSize = CGSize(width: availableSize.width, height: scrubberHeight)
|
||||||
|
self.scrubberSize = scrubberSize
|
||||||
|
|
||||||
let bounds = CGRect(origin: .zero, size: scrubberSize)
|
let bounds = CGRect(origin: .zero, size: scrubberSize)
|
||||||
|
|
||||||
if component.framesUpdateTimestamp != previousFramesUpdateTimestamp {
|
if component.framesUpdateTimestamp != previousFramesUpdateTimestamp {
|
||||||
@@ -316,7 +396,7 @@ final class VideoScrubberComponent: Component {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let trimColor = self.isPanningHandle ? UIColor(rgb: 0xf8d74a) : .white
|
let trimColor = self.isPanningTrimHandle ? UIColor(rgb: 0xf8d74a) : .white
|
||||||
transition.setTintColor(view: self.leftHandleView, color: trimColor)
|
transition.setTintColor(view: self.leftHandleView, color: trimColor)
|
||||||
transition.setTintColor(view: self.rightHandleView, color: trimColor)
|
transition.setTintColor(view: self.rightHandleView, color: trimColor)
|
||||||
transition.setTintColor(view: self.borderView, color: trimColor)
|
transition.setTintColor(view: self.borderView, color: trimColor)
|
||||||
@@ -333,6 +413,19 @@ final class VideoScrubberComponent: Component {
|
|||||||
|
|
||||||
let rightHandleFrame = CGRect(origin: CGPoint(x: max(leftHandleFrame.maxX, rightHandlePosition - handleWidth / 2.0), y: 0.0), size: CGSize(width: handleWidth, height: scrubberSize.height))
|
let rightHandleFrame = CGRect(origin: CGPoint(x: max(leftHandleFrame.maxX, rightHandlePosition - handleWidth / 2.0), y: 0.0), size: CGSize(width: handleWidth, height: scrubberSize.height))
|
||||||
transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame)
|
transition.setFrame(view: self.rightHandleView, frame: rightHandleFrame)
|
||||||
|
|
||||||
|
if self.isPanningPositionHandle || !component.isPlaying {
|
||||||
|
self.positionAnimation = nil
|
||||||
|
self.displayLink?.isPaused = true
|
||||||
|
transition.setFrame(view: self.cursorView, frame: cursorFrame(size: scrubberSize, position: component.position, duration: component.duration))
|
||||||
|
} else {
|
||||||
|
if self.positionAnimation == nil {
|
||||||
|
self.positionAnimation = (CACurrentMediaTime(), component.position, component.endPosition)
|
||||||
|
}
|
||||||
|
self.displayLink?.isPaused = false
|
||||||
|
self.updateCursorPosition()
|
||||||
|
}
|
||||||
|
transition.setAlpha(view: self.cursorView, alpha: self.isPanningTrimHandle ? 0.0 : 1.0)
|
||||||
|
|
||||||
let borderFrame = CGRect(origin: CGPoint(x: leftHandleFrame.maxX, y: 0.0), size: CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX, height: scrubberSize.height))
|
let borderFrame = CGRect(origin: CGPoint(x: leftHandleFrame.maxX, y: 0.0), size: CGSize(width: rightHandleFrame.minX - leftHandleFrame.maxX, height: scrubberSize.height))
|
||||||
transition.setFrame(view: self.borderView, frame: borderFrame)
|
transition.setFrame(view: self.borderView, frame: borderFrame)
|
||||||
|
|||||||
Reference in New Issue
Block a user