mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-12-22 14:20:20 +00:00
Browser improvements
This commit is contained in:
24
submodules/TelegramUI/Components/SaveProgressScreen/BUILD
Normal file
24
submodules/TelegramUI/Components/SaveProgressScreen/BUILD
Normal file
@@ -0,0 +1,24 @@
|
||||
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||
|
||||
swift_library(
|
||||
name = "SaveProgressScreen",
|
||||
module_name = "SaveProgressScreen",
|
||||
srcs = glob([
|
||||
"Sources/**/*.swift",
|
||||
]),
|
||||
copts = [
|
||||
"-warnings-as-errors",
|
||||
],
|
||||
deps = [
|
||||
"//submodules/Display",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/LottieAnimationComponent",
|
||||
"//submodules/AccountContext",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,577 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import MultilineTextComponent
|
||||
import LottieAnimationComponent
|
||||
import BundleIconComponent
|
||||
|
||||
private final class ProgressComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let title: String
|
||||
let value: Float
|
||||
let cancel: () -> Void
|
||||
|
||||
init(
|
||||
title: String,
|
||||
value: Float,
|
||||
cancel: @escaping () -> Void
|
||||
) {
|
||||
self.title = title
|
||||
self.value = value
|
||||
self.cancel = cancel
|
||||
}
|
||||
|
||||
static func ==(lhs: ProgressComponent, rhs: ProgressComponent) -> Bool {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.value != rhs.value {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let title = ComponentView<Empty>()
|
||||
private let progressLayer = SimpleShapeLayer()
|
||||
private let cancelButton = ComponentView<Empty>()
|
||||
|
||||
private var component: ProgressComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
let lineWidth: CGFloat = 3.0
|
||||
let progressSize = CGSize(width: 42.0, height: 42.0)
|
||||
|
||||
self.progressLayer.path = CGPath(ellipseIn: CGRect(origin: .zero, size: progressSize).insetBy(dx: lineWidth * 0.5, dy: lineWidth * 0.5), transform: nil)
|
||||
self.progressLayer.lineWidth = lineWidth
|
||||
self.progressLayer.strokeColor = UIColor.white.cgColor
|
||||
self.progressLayer.fillColor = UIColor.clear.cgColor
|
||||
self.progressLayer.lineCap = .round
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.progressLayer.bounds = CGRect(origin: .zero, size: progressSize)
|
||||
|
||||
self.layer.addSublayer(self.progressLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: ProgressComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let minWidth: CGFloat = 98.0
|
||||
let inset: CGFloat = 16.0
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(Text(text: component.title, font: Font.regular(14.0), color: .white)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: 40.0)
|
||||
)
|
||||
|
||||
let width: CGFloat = max(minWidth, titleSize.width + inset * 2.0)
|
||||
let titleFrame = CGRect(
|
||||
origin: CGPoint(x: floorToScreenPixels((width - titleSize.width) / 2.0), y: 16.0),
|
||||
size: titleSize
|
||||
)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
titleView.frame = titleFrame
|
||||
}
|
||||
|
||||
let progressPosition = CGPoint(x: width / 2.0, y: titleFrame.maxY + 34.0)
|
||||
self.progressLayer.position = progressPosition
|
||||
transition.setShapeLayerStrokeEnd(layer: self.progressLayer, strokeEnd: CGFloat(max(0.027, component.value)))
|
||||
|
||||
if self.progressLayer.animation(forKey: "rotation") == nil {
|
||||
let basicAnimation = CABasicAnimation(keyPath: "transform.rotation.z")
|
||||
basicAnimation.duration = 2.0
|
||||
basicAnimation.fromValue = NSNumber(value: Float(0.0))
|
||||
basicAnimation.toValue = NSNumber(value: Float(Double.pi * 2.0))
|
||||
basicAnimation.repeatCount = Float.infinity
|
||||
basicAnimation.timingFunction = CAMediaTimingFunction(name: CAMediaTimingFunctionName.linear)
|
||||
self.progressLayer.add(basicAnimation, forKey: "rotation")
|
||||
}
|
||||
|
||||
let cancelSize = self.cancelButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
Button(
|
||||
content: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Media Gallery/Close",
|
||||
tintColor: UIColor.white
|
||||
)
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let self, let component = self.component {
|
||||
component.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: 40.0)
|
||||
)
|
||||
let cancelButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(progressPosition.x - cancelSize.width / 2.0), y: floorToScreenPixels(progressPosition.y - cancelSize.height / 2.0)), size: cancelSize)
|
||||
if let cancelButtonView = self.cancelButton.view {
|
||||
if cancelButtonView.superview == nil {
|
||||
self.addSubview(cancelButtonView)
|
||||
}
|
||||
cancelButtonView.frame = cancelButtonFrame
|
||||
}
|
||||
|
||||
return CGSize(width: width, height: 104.0)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
private final class BannerComponent: Component {
|
||||
typealias EnvironmentType = Empty
|
||||
|
||||
let iconName: String
|
||||
let text: String
|
||||
|
||||
init(
|
||||
iconName: String,
|
||||
text: String
|
||||
) {
|
||||
self.iconName = iconName
|
||||
self.text = text
|
||||
}
|
||||
|
||||
static func ==(lhs: BannerComponent, rhs: BannerComponent) -> Bool {
|
||||
if lhs.iconName != rhs.iconName {
|
||||
return false
|
||||
}
|
||||
if lhs.text != rhs.text {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let icon = ComponentView<Empty>()
|
||||
private let text = ComponentView<Empty>()
|
||||
|
||||
private var component: BannerComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
func update(component: BannerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let height: CGFloat = 49.0
|
||||
|
||||
let iconSize = self.icon.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
LottieAnimationComponent(animation: LottieAnimationComponent.AnimationItem(name: component.iconName, mode: .animating(loop: false)), colors: [:], size: CGSize(width: 32.0, height: 32.0))
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 32.0, height: 32.0)
|
||||
)
|
||||
let iconFrame = CGRect(
|
||||
origin: CGPoint(x: 9.0, y: floorToScreenPixels((height - iconSize.height) / 2.0)),
|
||||
size: iconSize
|
||||
)
|
||||
if let iconView = self.icon.view {
|
||||
if iconView.superview == nil {
|
||||
self.addSubview(iconView)
|
||||
}
|
||||
iconView.frame = iconFrame
|
||||
}
|
||||
|
||||
let textSize = self.text.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(
|
||||
Text(text: component.text, font: Font.regular(14.0), color: .white)
|
||||
),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 200.0, height: height)
|
||||
)
|
||||
|
||||
let textFrame = CGRect(
|
||||
origin: CGPoint(x: iconFrame.maxX + 9.0, y: floorToScreenPixels((height - textSize.height) / 2.0)),
|
||||
size: textSize
|
||||
)
|
||||
if let textView = self.text.view {
|
||||
if textView.superview == nil {
|
||||
self.addSubview(textView)
|
||||
}
|
||||
textView.frame = textFrame
|
||||
}
|
||||
|
||||
return CGSize(width: textFrame.maxX + 12.0, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public 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)
|
||||
}
|
||||
}
|
||||
|
||||
public final class SaveProgressScreenComponent: Component {
|
||||
public typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
public enum Content: Equatable {
|
||||
enum ContentType: Equatable {
|
||||
case progress
|
||||
case completion
|
||||
}
|
||||
|
||||
case progress(String, Float)
|
||||
case completion(String)
|
||||
|
||||
var type: ContentType {
|
||||
switch self {
|
||||
case .progress:
|
||||
return .progress
|
||||
case .completion:
|
||||
return .completion
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public let context: AccountContext
|
||||
public let content: Content
|
||||
public let cancel: () -> Void
|
||||
|
||||
public init(
|
||||
context: AccountContext,
|
||||
content: Content,
|
||||
cancel: @escaping () -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
self.cancel = cancel
|
||||
}
|
||||
|
||||
public static func ==(lhs: SaveProgressScreenComponent, rhs: SaveProgressScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.content != rhs.content {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
public final class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private var content = ComponentView<Empty>()
|
||||
|
||||
private var component: SaveProgressScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var environment: ViewControllerComponentContainer.Environment?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: UIColor(rgb: 0x000000, alpha: 0.5))
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.backgroundColor = .clear
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: SaveProgressScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
self.environment = environment
|
||||
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
var animateIn = false
|
||||
var disappearingView: UIView?
|
||||
if let previousComponent, previousComponent.content.type != component.content.type {
|
||||
if let view = self.content.view {
|
||||
disappearingView = view
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak view] _ in
|
||||
view?.removeFromSuperview()
|
||||
})
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.25, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
self.content = ComponentView<Empty>()
|
||||
animateIn = true
|
||||
}
|
||||
|
||||
let cornerRadius: CGFloat
|
||||
let content: AnyComponent<Empty>
|
||||
switch component.content {
|
||||
case let .progress(title, progress):
|
||||
content = AnyComponent(ProgressComponent(title: title, value: progress, cancel: component.cancel))
|
||||
cornerRadius = 18.0
|
||||
case let .completion(text):
|
||||
content = AnyComponent(BannerComponent(iconName: "anim_savemedia", text: text))
|
||||
cornerRadius = 9.0
|
||||
}
|
||||
|
||||
let contentSize = self.content.update(
|
||||
transition: transition,
|
||||
component: content,
|
||||
environment: {},
|
||||
containerSize: CGSize(width: 160.0, height: 160.0)
|
||||
)
|
||||
let contentFrame = CGRect(
|
||||
origin: .zero,
|
||||
size: contentSize
|
||||
)
|
||||
if let contentView = self.content.view {
|
||||
if contentView.superview == nil {
|
||||
self.backgroundView.addSubview(contentView)
|
||||
if animateIn {
|
||||
contentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
contentView.layer.animateScale(from: 0.1, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
transition.setFrame(view: contentView, frame: contentFrame)
|
||||
if let disappearingView {
|
||||
transition.setPosition(view: disappearingView, position: contentFrame.center)
|
||||
}
|
||||
}
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - contentFrame.size.width) / 2.0), y: floorToScreenPixels((availableSize.height - contentFrame.size.height) / 2.0)), size: contentFrame.size)
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, cornerRadius: cornerRadius, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
public func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
public final class SaveProgressScreen: ViewController {
|
||||
fileprivate final class Node: ViewControllerTracingNode, ASGestureRecognizerDelegate {
|
||||
private weak var controller: SaveProgressScreen?
|
||||
private let context: AccountContext
|
||||
|
||||
fileprivate let componentHost: ComponentView<ViewControllerComponentContainer.Environment>
|
||||
|
||||
private var presentationData: PresentationData
|
||||
private var validLayout: ContainerViewLayout?
|
||||
|
||||
init(controller: SaveProgressScreen) {
|
||||
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
|
||||
}
|
||||
|
||||
private func animateIn() {
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animateScale(from: 0.4, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring)
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
if let view = self.componentHost.view {
|
||||
view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.25, removeOnCompletion: false)
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { _ in
|
||||
completion()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func containerLayoutUpdated(layout: ContainerViewLayout, transition: ComponentTransition) {
|
||||
guard let controller = self.controller else {
|
||||
return
|
||||
}
|
||||
let isFirstTime = self.validLayout == nil
|
||||
self.validLayout = layout
|
||||
|
||||
let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778))
|
||||
let topInset: CGFloat = floorToScreenPixels(layout.size.height - previewSize.height) / 2.0
|
||||
|
||||
let environment = ViewControllerComponentContainer.Environment(
|
||||
statusBarHeight: layout.statusBarHeight ?? 0.0,
|
||||
navigationHeight: 0.0,
|
||||
safeInsets: UIEdgeInsets(
|
||||
top: topInset,
|
||||
left: layout.safeInsets.left,
|
||||
bottom: topInset,
|
||||
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(
|
||||
SaveProgressScreenComponent(
|
||||
context: self.context,
|
||||
content: controller.content,
|
||||
cancel: { [weak self] in
|
||||
if let self, let controller = self.controller {
|
||||
controller.cancel()
|
||||
}
|
||||
}
|
||||
)
|
||||
),
|
||||
environment: {
|
||||
environment
|
||||
},
|
||||
forceUpdate: false,
|
||||
containerSize: layout.size
|
||||
)
|
||||
if let componentView = self.componentHost.view {
|
||||
if componentView.superview == nil {
|
||||
self.view.addSubview(componentView)
|
||||
}
|
||||
let componentFrame = CGRect(origin: .zero, size: componentSize)
|
||||
componentView.center = componentFrame.center
|
||||
componentView.bounds = CGRect(origin: .zero, size: componentFrame.size)
|
||||
}
|
||||
|
||||
if isFirstTime {
|
||||
self.animateIn()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var node: Node {
|
||||
return self.displayNode as! Node
|
||||
}
|
||||
|
||||
fileprivate let context: AccountContext
|
||||
public var content: SaveProgressScreenComponent.Content {
|
||||
didSet {
|
||||
if let layout = self.validLayout {
|
||||
self.containerLayoutUpdated(layout, transition: .animated(duration: 0.25, curve: .easeInOut))
|
||||
}
|
||||
self.maybeSetupDismissTimer()
|
||||
}
|
||||
}
|
||||
|
||||
private var dismissTimer: SwiftSignalKit.Timer?
|
||||
|
||||
public var cancelled: () -> Void = {}
|
||||
|
||||
public init(context: AccountContext, content: SaveProgressScreenComponent.Content) {
|
||||
self.context = context
|
||||
self.content = content
|
||||
|
||||
super.init(navigationBarPresentationData: nil)
|
||||
|
||||
self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
|
||||
self.maybeSetupDismissTimer()
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override public func loadDisplayNode() {
|
||||
self.displayNode = Node(controller: self)
|
||||
|
||||
super.displayNodeDidLoad()
|
||||
}
|
||||
|
||||
fileprivate func cancel() {
|
||||
self.cancelled()
|
||||
|
||||
self.node.animateOut(completion: { [weak self] in
|
||||
if let self {
|
||||
self.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private func maybeSetupDismissTimer() {
|
||||
if case .completion = self.content {
|
||||
self.node.isUserInteractionEnabled = false
|
||||
if self.dismissTimer == nil {
|
||||
let timer = SwiftSignalKit.Timer(timeout: 3.0, repeat: false, completion: { [weak self] in
|
||||
if let self {
|
||||
self.node.animateOut(completion: { [weak self] in
|
||||
if let self {
|
||||
self.dismiss()
|
||||
}
|
||||
})
|
||||
}
|
||||
}, queue: Queue.mainQueue())
|
||||
timer.start()
|
||||
self.dismissTimer = timer
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var validLayout: ContainerViewLayout?
|
||||
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
|
||||
self.validLayout = layout
|
||||
|
||||
super.containerLayoutUpdated(layout, transition: transition)
|
||||
|
||||
(self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: ComponentTransition(transition))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user