Browser improvements

This commit is contained in:
Ilya Laktyushin
2024-07-25 16:15:45 +02:00
parent 3e99101e07
commit 2eeb3be2b2
15 changed files with 131 additions and 16 deletions

View 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",
],
)

View File

@@ -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))
}
}