Swiftgram/submodules/ShareController/Sources/ShareLoadingContainerNode.swift
2024-03-08 18:43:18 +04:00

370 lines
18 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import SwiftSignalKit
import Display
import TelegramPresentationData
import ActivityIndicator
import RadialStatusNode
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import AppBundle
import TelegramUniversalVideoContent
import TelegramCore
import Postbox
import AccountContext
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
if useTotalFileAllocatedSize {
let url = URL(fileURLWithPath: path)
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
if values.isRegularFile ?? false {
if let fileSize = values.totalFileAllocatedSize {
return Int64(fileSize)
}
}
}
}
var value = stat()
if stat(path, &value) == 0 {
return value.st_size
} else {
return nil
}
}
public enum ShareLoadingState {
case preparing
case progress(Float)
case done
}
protocol ShareLoadingContainer: ASDisplayNode {
var state: ShareLoadingState { get set }
}
public final class ShareLoadingContainerNode: ASDisplayNode, ShareContentContainerNode, ShareLoadingContainer {
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var theme: PresentationTheme
private let activityIndicator: ActivityIndicator
private let statusNode: RadialStatusNode
private let doneStatusNode: RadialStatusNode
public var state: ShareLoadingState = .preparing {
didSet {
switch self.state {
case .preparing:
self.activityIndicator.isHidden = false
self.statusNode.isHidden = true
case let .progress(value):
self.activityIndicator.isHidden = true
self.statusNode.isHidden = false
self.statusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: max(0.12, CGFloat(value)), cancelEnabled: false, animateRotation: true), completion: {})
case .done:
self.activityIndicator.isHidden = true
self.statusNode.isHidden = false
self.statusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: 1.0, cancelEnabled: false, animateRotation: true), completion: {})
self.doneStatusNode.transitionToState(.check(self.theme.actionSheet.controlAccentColor), completion: {})
}
}
}
public init(theme: PresentationTheme, forceNativeAppearance: Bool) {
self.theme = theme
self.activityIndicator = ActivityIndicator(type: .custom(theme.actionSheet.controlAccentColor, !forceNativeAppearance ? 22.0 : 50.0, 2.0, forceNativeAppearance))
self.statusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
super.init()
self.addSubnode(self.activityIndicator)
self.addSubnode(self.statusNode)
self.addSubnode(self.doneStatusNode)
self.doneStatusNode.transitionToState(.progress(color: self.theme.actionSheet.controlAccentColor, lineWidth: 2.0, value: 0.0, cancelEnabled: false, animateRotation: true), completion: {})
}
public func activate() {
}
public func deactivate() {
}
public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) {
}
public func setDidBeginDragging(_ f: (() -> Void)?) {
}
public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
self.contentOffsetUpdated = f
}
public func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
}
public func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
let nodeHeight: CGFloat = 125.0
let indicatorSize = self.activityIndicator.calculateSizeThatFits(size)
let indicatorFrame = CGRect(origin: CGPoint(x: floor((size.width - indicatorSize.width) / 2.0), y: size.height - nodeHeight + floor((nodeHeight - indicatorSize.height) / 2.0)), size: indicatorSize)
transition.updateFrame(node: self.activityIndicator, frame: indicatorFrame)
let statusFrame = indicatorFrame
transition.updateFrame(node: self.statusNode, frame: statusFrame)
transition.updateFrame(node: self.doneStatusNode, frame: statusFrame)
self.contentOffsetUpdated?(-size.height + 64.0, transition)
}
public func updateSelectedPeers(animated: Bool) {
}
}
public final class ShareProlongedLoadingContainerNode: ASDisplayNode, ShareContentContainerNode, ShareLoadingContainer {
private var contentOffsetUpdated: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
private var theme: PresentationTheme
private let strings: PresentationStrings
private let animationNode: AnimatedStickerNode
private let doneAnimationNode: AnimatedStickerNode
private let progressTextNode: ImmediateTextNode
private let progressBackgroundNode: ASDisplayNode
private let progressForegroundNode: ASDisplayNode
private let animationStatusDisposable = MetaDisposable()
private var progressValue: CGFloat = 0.0
private var targetProgressValue: CGFloat = 0.0
private var animator: ConstantDisplayLinkAnimator?
private var randomCompletionStart: CGFloat = .random(in: 0.94...0.97)
private var isDone: Bool = false
private var startTimestamp: Double?
private var videoNode: UniversalVideoNode?
public var state: ShareLoadingState = .preparing {
didSet {
switch self.state {
case .preparing:
break
case let .progress(value):
let currentTimestamp = CACurrentMediaTime()
if self.startTimestamp == nil {
self.startTimestamp = currentTimestamp
} else if let startTimestamp = self.startTimestamp, currentTimestamp - startTimestamp < 1.0, value > 0.5 && value < 0.9 {
self.randomCompletionStart = 0.8
}
self.targetProgressValue = CGFloat(value) * self.randomCompletionStart
if self.animator == nil {
self.animator = ConstantDisplayLinkAnimator(update: { [weak self] in
if let strongSelf = self, strongSelf.targetProgressValue > strongSelf.progressValue {
let updatedProgress = strongSelf.progressValue + 0.005
strongSelf.progressValue = min(1.0, updatedProgress)
if strongSelf.progressValue == 1.0 && !strongSelf.isDone {
strongSelf.isDone = true
strongSelf.animator?.invalidate()
if let snapshotView = strongSelf.progressTextNode.view.snapshotContentTree() {
snapshotView.frame = strongSelf.progressTextNode.frame
strongSelf.view.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
snapshotView.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -20.0), duration: 0.25, removeOnCompletion: false, additive: true)
strongSelf.progressTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
strongSelf.progressTextNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 20.0), to: CGPoint(), duration: 0.25, additive: true)
}
}
if let (size, isLandscape, bottomInset) = strongSelf.validLayout {
strongSelf.updateLayout(size: size, isLandscape: isLandscape, bottomInset: bottomInset, transition: .immediate)
}
}
})
self.animator?.isPaused = false
}
case .done:
if let (size, isLandscape, bottomInset) = self.validLayout {
self.updateLayout(size: size, isLandscape: isLandscape, bottomInset: bottomInset, transition: .animated(duration: 0.2, curve: .easeInOut))
}
self.animationNode.stopAtNearestLoop = true
self.animationNode.completed = { [weak self] _ in
if let strongSelf = self {
strongSelf.animationNode.visibility = false
strongSelf.doneAnimationNode.visibility = true
strongSelf.doneAnimationNode.isHidden = false
}
}
self.animationNode.frameUpdated = { [weak self] index, total in
if let strongSelf = self {
let progress = min(1.0, CGFloat(index) / CGFloat(total))
let delta = 1.0 - strongSelf.randomCompletionStart
strongSelf.targetProgressValue = strongSelf.randomCompletionStart + delta * progress * 0.5
}
}
self.doneAnimationNode.frameUpdated = { [weak self] index, total in
if let strongSelf = self {
let progress = min(1.0, CGFloat(index) / CGFloat(total) * 2.1)
let delta = 1.0 - strongSelf.randomCompletionStart
strongSelf.targetProgressValue = strongSelf.randomCompletionStart + delta * 0.5 + delta * progress * 0.5
}
}
self.doneAnimationNode.started = { [weak self] in
guard let strongSelf = self else {
return
}
strongSelf.animationNode.isHidden = true
}
}
}
}
private var elapsedTime: Double = 0.0
public var completionDuration: Double {
return self.elapsedTime + 3.0 + 0.15
}
public init(theme: PresentationTheme, strings: PresentationStrings, forceNativeAppearance: Bool, postbox: Postbox?, environment: ShareControllerEnvironment) {
self.theme = theme
self.strings = strings
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ShareProgress"), width: 384, height: 384, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.doneAnimationNode = DefaultAnimatedStickerNodeImpl()
self.doneAnimationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ShareDone"), width: 384, height: 384, playbackMode: .once, mode: .direct(cachePathPrefix: nil))
self.doneAnimationNode.visibility = false
self.doneAnimationNode.isHidden = true
self.progressTextNode = ImmediateTextNode()
self.progressTextNode.textAlignment = .center
self.progressBackgroundNode = ASDisplayNode()
self.progressBackgroundNode.backgroundColor = theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2)
self.progressBackgroundNode.cornerRadius = 3.0
self.progressForegroundNode = ASDisplayNode()
self.progressForegroundNode.backgroundColor = theme.actionSheet.controlAccentColor
self.progressForegroundNode.cornerRadius = 3.0
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.doneAnimationNode)
self.addSubnode(self.progressTextNode)
self.addSubnode(self.progressBackgroundNode)
self.addSubnode(self.progressForegroundNode)
self.animationStatusDisposable.set((self.animationNode.status
|> deliverOnMainQueue).start(next: { [weak self] status in
if let strongSelf = self {
strongSelf.elapsedTime = status.duration - status.timestamp
}
}))
if let postbox, let mediaManager = environment.mediaManager, let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [], preloadSize: nil)])
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(postbox: postbox, audioSession: mediaManager.audioSession, manager: mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
videoNode.alpha = 0.01
self.videoNode = videoNode
self.addSubnode(videoNode)
videoNode.canAttachContent = true
videoNode.play()
}
}
deinit {
self.animationStatusDisposable.dispose()
}
public func updateTheme(_ theme: PresentationTheme) {
self.theme = theme
}
public func activate() {
}
public func deactivate() {
}
public func setEnsurePeerVisibleOnLayout(_ peerId: EnginePeer.Id?) {
}
public func setDidBeginDragging(_ f: (() -> Void)?) {
}
public func setContentOffsetUpdated(_ f: ((CGFloat, ContainedViewLayoutTransition) -> Void)?) {
self.contentOffsetUpdated = f
}
private var validLayout: (CGSize, Bool, CGFloat)?
public func updateLayout(size: CGSize, isLandscape: Bool, bottomInset: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (size, isLandscape, bottomInset)
let nodeHeight: CGFloat = 450.0
let inset: CGFloat = 24.0
let progressHeight: CGFloat = 6.0
let spacing: CGFloat = 16.0
let progress = self.progressValue
let progressFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: size.width - inset * 2.0, height: progressHeight)
self.progressBackgroundNode.frame = progressFrame
let progressForegroundFrame = CGRect(x: progressFrame.minX, y: progressFrame.minY, width: floorToScreenPixels(progressFrame.width * progress), height: progressHeight)
if !self.progressForegroundNode.frame.origin.x.isZero {
transition.updateFrame(node: self.progressForegroundNode, frame: progressForegroundFrame, beginWithCurrentState: true)
} else {
self.progressForegroundNode.frame = progressForegroundFrame
}
let progressText: String
if self.isDone {
progressText = self.strings.Share_UploadDone
} else {
progressText = self.strings.Share_UploadProgress(Int(progress * 100.0)).string
}
self.progressTextNode.attributedText = NSAttributedString(string: progressText, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.theme.actionSheet.primaryTextColor)
let progressTextSize = self.progressTextNode.updateLayout(size)
let progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: progressFrame.minY - spacing - 9.0 - progressTextSize.height), size: progressTextSize)
self.progressTextNode.frame = progressTextFrame
let imageSide: CGFloat = 160.0
let imageSize = CGSize(width: imageSide, height: imageSide)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (progressTextFrame.minY - imageSize.height - 20.0)), size: imageSize)
self.animationNode.frame = animationFrame
self.animationNode.updateLayout(size: imageSize)
self.doneAnimationNode.frame = animationFrame
self.doneAnimationNode.updateLayout(size: imageSize)
self.contentOffsetUpdated?(-size.height + nodeHeight * 0.5, transition)
}
public func updateSelectedPeers(animated: Bool) {
}
}