Storage improvements

This commit is contained in:
Ali 2022-12-24 00:04:48 +04:00
parent dd06922e85
commit 808f5b80ff
25 changed files with 3384 additions and 655 deletions

View File

@ -308,7 +308,7 @@ public final class CallListController: TelegramBaseController {
var cancelImpl: (() -> Void)?
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))

View File

@ -950,7 +950,9 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
}
self.scrollNode.bounds = updatedBounds
}
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
if abs(previousScrollBounds.minX - self.scrollNode.bounds.minX) > .ulpOfOne {
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
}
self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
self.previousSelectedFrame = selectedFrame

View File

@ -321,6 +321,19 @@ public class CheckLayer: CALayer {
self.isOpaque = false
}
public override init(layer: Any) {
guard let layer = layer as? CheckLayer else {
preconditionFailure()
}
self.theme = layer.theme
self.content = layer.content
super.init(layer: layer)
self.isOpaque = false
}
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) {
self.theme = theme
self.content = content

View File

@ -765,4 +765,36 @@ public struct Transition {
}
}
}
public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) {
self.setBackgroundColor(layer: view.layer, color: color, completion: completion)
}
public func setBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) {
if let current = layer.backgroundColor, current == color.cgColor {
completion?(true)
return
}
switch self.animation {
case .none:
layer.backgroundColor = color.cgColor
completion?(true)
case let .curve(duration, curve):
let previousColor: CGColor = layer.backgroundColor ?? UIColor.clear.cgColor
layer.backgroundColor = color.cgColor
layer.animate(
from: previousColor,
to: color.cgColor,
keyPath: "backgroundColor",
duration: duration,
delay: 0.0,
curve: curve,
removeOnCompletion: true,
additive: false,
completion: completion
)
}
}
}

View File

@ -155,6 +155,15 @@ public final class ComponentView<EnvironmentType> {
self.currentSize = size
return size
}
public func updateEnvironment(transition: Transition, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>) -> CGSize? {
guard let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize else {
return nil
}
let size = self._update(transition: transition, component: currentComponent, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: currentContainerSize)
self.currentSize = size
return size
}
private func _update(transition: Transition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize {
precondition(!self.isUpdating)

View File

@ -77,6 +77,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case resetDatabaseAndCache(PresentationTheme)
case resetHoles(PresentationTheme)
case reindexUnread(PresentationTheme)
case reindexCache
case resetBiometricsData(PresentationTheme)
case resetWebViewCache(PresentationTheme)
case optimizeDatabase(PresentationTheme)
@ -111,7 +112,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases:
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases:
return DebugControllerSection.experiments.rawValue
case .preferredVideoCodec:
return DebugControllerSection.videoExperiments.rawValue
@ -170,40 +171,42 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 21
case .reindexUnread:
return 22
case .resetBiometricsData:
case .reindexCache:
return 23
case .resetWebViewCache:
case .resetBiometricsData:
return 24
case .optimizeDatabase:
case .resetWebViewCache:
return 25
case .photoPreview:
case .optimizeDatabase:
return 26
case .knockoutWallpaper:
case .photoPreview:
return 27
case .experimentalCompatibility:
case .knockoutWallpaper:
return 28
case .enableDebugDataDisplay:
case .experimentalCompatibility:
return 29
case .acceleratedStickers:
case .enableDebugDataDisplay:
return 30
case .experimentalBackground:
case .acceleratedStickers:
return 31
case .inlineForums:
case .experimentalBackground:
return 32
case .localTranscription:
case .inlineForums:
return 33
case .enableReactionOverrides:
case .localTranscription:
return 34
case .restorePurchases:
case .enableReactionOverrides:
return 35
case .playerEmbedding:
case .restorePurchases:
return 36
case .playlistPlayback:
case .playerEmbedding:
return 37
case .voiceConference:
case .playlistPlayback:
return 38
case .voiceConference:
return 39
case let .preferredVideoCodec(index, _, _, _):
return 39 + index
return 40 + index
case .disableVideoAspectScaling:
return 100
case .enableVoipTcp:
@ -967,6 +970,46 @@ private enum DebugControllerEntry: ItemListNodeEntry {
controller.dismiss()
})
})
case .reindexCache:
return ItemListActionItem(presentationData: presentationData, title: "Reindex Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
return
}
var signal = context.engine.resources.reindexCacheInBackground(lowImpact: false)
var cancelImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
arguments.presentController(controller, nil)
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
let reindexDisposable = MetaDisposable()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
reindexDisposable.set(nil)
}
reindexDisposable.set((signal
|> deliverOnMainQueue).start(completed: {
}))
})
case .resetBiometricsData:
return ItemListActionItem(presentationData: presentationData, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
let _ = updatePresentationPasscodeSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
@ -1210,6 +1253,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.resetHoles(presentationData.theme))
if isMainApp {
entries.append(.reindexUnread(presentationData.theme))
entries.append(.reindexCache)
entries.append(.resetWebViewCache(presentationData.theme))
}
entries.append(.optimizeDatabase(presentationData.theme))

View File

@ -34,6 +34,47 @@ private let completionKey = "CAAnimationUtils_completion"
public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve"
public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve"
private final class FrameRangeContext {
private var animationCount: Int = 0
private var displayLink: CADisplayLink?
init() {
}
func add() {
self.animationCount += 1
self.update()
}
func remove() {
self.animationCount -= 1
if self.animationCount < 0 {
self.animationCount = 0
assertionFailure()
}
self.update()
}
@objc func displayEvent() {
}
private func update() {
if self.animationCount != 0 {
if self.displayLink == nil {
let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent))
self.displayLink = displayLink
displayLink.add(to: .main, forMode: .common)
displayLink.isPaused = false
}
} else if let displayLink = self.displayLink {
self.displayLink = nil
displayLink.invalidate()
}
}
}
private let frameRangeContext = FrameRangeContext()
public extension CAAnimation {
var completion: ((Bool) -> Void)? {
get {
@ -56,7 +97,7 @@ private func adjustFrameRate(animation: CAAnimation) {
if #available(iOS 15.0, *) {
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
if maxFps > 61.0 {
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps)
}
}
}
@ -176,6 +217,8 @@ public extension CALayer {
animationGroup.delegate = CALayerAnimationDelegate(animation: animationGroup, completion: completion)
}
adjustFrameRate(animation: animationGroup)
self.add(animationGroup, forKey: key)
}

View File

@ -81,16 +81,12 @@ public final class ConstantDisplayLinkAnimator {
guard let displayLink = self.displayLink else {
return
}
if #available(iOS 10.0, *) {
let preferredFramesPerSecond: Int
if self.frameInterval == 1 {
preferredFramesPerSecond = 60
} else {
preferredFramesPerSecond = 30
if self.frameInterval == 1 {
if #available(iOS 15.0, *) {
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0)
}
displayLink.preferredFramesPerSecond = preferredFramesPerSecond
} else {
displayLink.frameInterval = self.frameInterval
displayLink.preferredFramesPerSecond = 30
}
}

View File

@ -55,7 +55,7 @@ public func representationFetchRangeForDisplayAtSize(representation: TelegramMed
return nil
}
public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false) -> Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError> {
public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError> {
if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 {
enum SizeSource {
case miniThumbnail(data: Data)
@ -129,10 +129,12 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU
}
})
var fetchDisposable: Disposable?
if autoFetchFullSize {
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start()
} else if useMiniThumbnailIfAvailable {
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start()
if automaticFetch {
if autoFetchFullSize {
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start()
} else if useMiniThumbnailIfAvailable {
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start()
}
}
return ActionDisposable {
@ -1353,7 +1355,7 @@ public func avatarGalleryThumbnailPhoto(account: Account, representations: [Imag
}
}
public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let useMiniThumbnailIfAvailable: Bool = fullRepresentationSize.width < 40.0
var updatedFullRepresentationSize = fullRepresentationSize
if useMiniThumbnailIfAvailable, let largest = largestImageRepresentation(photoReference.media.representations) {
@ -1361,7 +1363,7 @@ public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceU
updatedFullRepresentationSize = largest.dimensions.cgSize
}
}
let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred)
let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred, automaticFetch: automaticFetch)
return signal
|> map { value in
@ -1468,7 +1470,7 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef
}, completed: {
subscriber.putCompletion()
})
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .gif, reference: videoReference.resourceReference(thumbnailResource)).start()
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: videoReference.resourceReference(thumbnailResource)).start()
return ActionDisposable {
data.dispose()
fetched.dispose()
@ -1847,10 +1849,10 @@ public func chatMessageWebFileCancelInteractiveFetch(account: Account, image: Te
return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource)
}
public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource) -> Signal<Data?, NoError> {
public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource, automaticFetch: Bool = true) -> Signal<Data?, NoError> {
let resourceData = account.postbox.mediaBox.resourceData(resource)
|> map { next in
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe)
|> map { next in
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe)
}
return Signal { subscriber in
@ -1861,7 +1863,16 @@ public func chatWebpageSnippetFileData(account: Account, userLocation: MediaReso
}, completed: {
subscriber.putCompletion()
}))
disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: mediaReference.resourceReference(resource)).start())
if automaticFetch {
var userContentType: MediaResourceUserContentType = .other
if let file = mediaReference.media as? TelegramMediaFile {
userContentType = MediaResourceUserContentType(file: file)
} else if let _ = mediaReference.media as? TelegramMediaImage {
userContentType = .image
}
disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start())
}
return disposable
}
}
@ -1889,8 +1900,8 @@ public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaRes
}
}
public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource)
public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch)
return signal |> map { fullSizeData in
return { arguments in
@ -1904,7 +1915,42 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource
}
}
if let fullSizeImage = fullSizeImage {
var blurredImage: UIImage?
if fullSizeImage == nil {
var immediateThumbnailData: Data?
if let file = mediaReference.media as? TelegramMediaFile {
immediateThumbnailData = file.immediateThumbnailData
} else if let image = mediaReference.media as? TelegramMediaImage {
immediateThumbnailData = image.immediateThumbnailData
}
if let decodedThumbnailData = immediateThumbnailData.flatMap(decodeTinyThumbnail), let imageSource = CGImageSourceCreateWithData(decodedThumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
let thumbnailSize = CGSize(width: image.width, height: image.height)
let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0))
if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) {
thumbnailContext.withFlippedContext { c in
c.interpolationQuality = .none
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize))
}
imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes)
let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0))
if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) {
thumbnailContext2.withFlippedContext { c in
c.interpolationQuality = .none
if let image = thumbnailContext.generateImage()?.cgImage {
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size))
}
}
imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes)
blurredImage = thumbnailContext2.generateImage()
}
}
}
}
if let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) {
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
@ -2455,9 +2501,9 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR
if let _ = decodedThumbnailData {
fetchedThumbnail = .complete()
} else {
fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[smallestIndex].reference)
fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[smallestIndex].reference)
}
let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference)
let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[largestIndex].reference)
let thumbnail = Signal<Data?, NoError> { subscriber in
if let decodedThumbnailData = decodedThumbnailData {

View File

@ -113,7 +113,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
guard let file = self.file else {
return
}
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .gif, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .other, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
self.frameManager = frameManager
frameManager.started = { [weak self] in
guard let strongSelf = self else {

View File

@ -464,7 +464,7 @@ public final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
layerHolder.layer.frame = item.frame
self.scrollNode.layer.addSublayer(layerHolder.layer)
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .gif, fileReference: item.file.file, layerHolder: layerHolder)
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: item.file.file, layerHolder: layerHolder)
self.visibleLayers[item.id] = (manager, layerHolder)
self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
if let strongSelf = self {

View File

@ -31,6 +31,8 @@ swift_library(
"//submodules/ContextUI",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/SemanticStatusNode",
],
visibility = [
"//visibility:public",

View File

@ -13,15 +13,88 @@ import MultilineTextComponent
import EmojiStatusComponent
import Postbox
private func interpolateChartData(start: PieChartComponent.ChartData, end: PieChartComponent.ChartData, progress: CGFloat) -> PieChartComponent.ChartData {
if start.items.count != end.items.count {
return start
}
var result = end
for i in 0 ..< result.items.count {
result.items[i].value = (1.0 - progress) * start.items[i].value + progress * end.items[i].value
result.items[i].color = start.items[i].color.interpolateTo(end.items[i].color, fraction: progress) ?? end.items[i].color
}
return result
}
private func processChartData(data: PieChartComponent.ChartData) -> PieChartComponent.ChartData {
var data = data
let minValue: Double = 0.01
var totalSum: CGFloat = 0.0
for i in 0 ..< data.items.count {
if data.items[i].value > 0.00001 {
data.items[i].value = max(data.items[i].value, minValue)
}
totalSum += data.items[i].value
}
var hasOneItem = false
for i in 0 ..< data.items.count {
if data.items[i].value != 0 && totalSum == data.items[i].value {
data.items[i].value = 1.0
hasOneItem = true
break
}
}
if !hasOneItem {
if abs(totalSum - 1.0) > 0.0001 {
let deltaValue = totalSum - 1.0
var availableSum: Double = 0.0
for i in 0 ..< data.items.count {
let itemValue = data.items[i].value
let availableItemValue = max(0.0, itemValue - minValue)
if availableItemValue > 0.0 {
availableSum += availableItemValue
}
}
totalSum = 0.0
let itemFraction = deltaValue / availableSum
for i in 0 ..< data.items.count {
let itemValue = data.items[i].value
let availableItemValue = max(0.0, itemValue - minValue)
if availableItemValue > 0.0 {
let itemDelta = availableItemValue * itemFraction
data.items[i].value -= itemDelta
}
totalSum += data.items[i].value
}
}
if totalSum > 0.0 && totalSum < 1.0 - 0.0001 {
for i in 0 ..< data.items.count {
data.items[i].value /= totalSum
}
}
}
return data
}
final class PieChartComponent: Component {
struct ChartData: Equatable {
struct Item: Equatable {
var id: AnyHashable
var displayValue: Double
var value: Double
var color: UIColor
init(id: AnyHashable, value: Double, color: UIColor) {
init(id: AnyHashable, displayValue: Double, value: Double, color: UIColor) {
self.id = id
self.displayValue = displayValue
self.value = value
self.color = color
}
@ -55,96 +128,98 @@ final class PieChartComponent: Component {
return true
}
class View: UIView {
private var shapeLayers: [AnyHashable: SimpleShapeLayer] = [:]
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
var selectedKey: AnyHashable?
private final class ChartDataView: UIView {
private(set) var theme: PresentationTheme?
private(set) var data: ChartData?
private(set) var selectedKey: AnyHashable?
private weak var state: EmptyComponentState?
private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)?
private var animator: DisplayLinkAnimator?
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
self.backgroundColor = nil
self.isOpaque = false
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
for (key, layer) in self.shapeLayers {
if layer.frame.contains(point), let path = layer.path {
if path.contains(self.layer.convert(point, to: layer)) {
if self.selectedKey == key {
self.selectedKey = nil
} else {
self.selectedKey = key
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
break
}
deinit {
self.animator?.invalidate()
}
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) {
let data = processChartData(data: data)
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
self.theme = theme
self.selectedKey = selectedKey
if animated, let previous = self.data {
var initialState = previous
if let currentAnimation = self.currentAnimation {
initialState = currentAnimation.current
}
self.currentAnimation = (initialState, data, initialState, 0.0)
self.animator?.invalidate()
self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in
guard let self else {
return
}
let progress = listViewAnimationCurveSystem(progress)
if let currentAnimationValue = self.currentAnimation {
self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress), progress)
self.setNeedsDisplay()
}
}, completion: { [weak self] in
guard let self else {
return
}
self.currentAnimation = nil
self.setNeedsDisplay()
})
}
self.data = data
self.setNeedsDisplay()
}
}
func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.state = state
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
guard let theme = self.theme, let data = self.currentAnimation?.current ?? self.data else {
return
}
if data.items.isEmpty {
return
}
let innerDiameter: CGFloat = 100.0
let spacing: CGFloat = 2.0
let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5)
//let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5)
var valueSum: Double = 0.0
for item in component.chartData.items {
valueSum += item.value
}
var angles: [Double] = []
var totalAngle: Double = 0.0
for i in 0 ..< component.chartData.items.count {
let item = component.chartData.items[i]
var angle = item.value / valueSum * CGFloat.pi * 2.0
if angle > .ulpOfOne {
if angle < minAngle {
angle = minAngle
}
totalAngle += angle
}
for i in 0 ..< data.items.count {
let item = data.items[i]
let angle = item.value * CGFloat.pi * 2.0
angles.append(angle)
}
if totalAngle > CGFloat.pi * 2.0 {
let deltaAngle = totalAngle - CGFloat.pi * 2.0
var availableAngleSum: Double = 0.0
for i in 0 ..< angles.count {
let itemAngle = angles[i]
let availableItemAngle = max(0.0, itemAngle - minAngle)
if availableItemAngle > 0.0 {
availableAngleSum += availableItemAngle
}
}
let itemFraction = deltaAngle / availableAngleSum
for i in 0 ..< angles.count {
let availableItemAngle = max(0.0, angles[i] - minAngle)
if availableItemAngle > 0.0 {
let itemDelta = availableItemAngle * itemFraction
angles[i] -= itemDelta
}
}
}
let diameter: CGFloat = 200.0
let reducedDiameter: CGFloat = 170.0
var startAngle: CGFloat = 0.0
for i in 0 ..< component.chartData.items.count {
let item = component.chartData.items[i]
for i in 0 ..< data.items.count {
let item = data.items[i]
let itemOuterDiameter: CGFloat
if let selectedKey = self.selectedKey {
@ -157,21 +232,10 @@ final class PieChartComponent: Component {
itemOuterDiameter = diameter
}
let shapeLayerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: 0.0), size: CGSize(width: diameter, height: diameter))
let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))
let angleSpacing: CGFloat = spacing / (itemOuterDiameter * 0.5)
let shapeLayer: SimpleShapeLayer
if let current = self.shapeLayers[item.id] {
shapeLayer = current
} else {
shapeLayer = SimpleShapeLayer()
self.shapeLayers[item.id] = shapeLayer
self.layer.insertSublayer(shapeLayer, at: 0)
}
transition.setFrame(layer: shapeLayer, frame: shapeLayerFrame)
let angleValue: CGFloat = angles[i]
let innerStartAngle = startAngle + innerAngleSpacing * 0.5
@ -187,14 +251,17 @@ final class PieChartComponent: Component {
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: innerEndAngle, endAngle: innerStartAngle, clockwise: true)
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: false)
transition.setShapeLayerPath(layer: shapeLayer, path: path)
context.addPath(path)
context.setFillColor(item.color.cgColor)
context.fillPath()
startAngle += angleValue
shapeLayer.fillColor = item.color.cgColor
let fractionValue: Double = floor(item.value * 100.0 * 10.0) / 10.0
let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0
let fractionString: String
if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
if fractionValue < 0.1 {
fractionString = "<0.1"
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
fractionString = "\(Int(fractionValue))"
} else {
fractionString = "\(fractionValue)"
@ -207,16 +274,125 @@ final class PieChartComponent: Component {
label = ComponentView<Empty>()
self.labels[item.id] = label
}
let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 16.0, design: .round, weight: .semibold), color: component.theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0))
let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 16.0, design: .round, weight: .semibold), color: theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0))
var labelFrame: CGRect?
for step in 0 ... 6 {
let stepFraction: CGFloat = CGFloat(step) / 6.0
let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
if angleValue >= 0.001 {
for step in 0 ... 20 {
let stepFraction: CGFloat = CGFloat(step) / 20.0
let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset)
let relLabelCenter = CGPoint(
x: cos(midAngle) * centerDistance,
y: sin(midAngle) * centerDistance
)
let labelCenter = CGPoint(
x: shapeLayerFrame.midX + relLabelCenter.x,
y: shapeLayerFrame.midY + relLabelCenter.y
)
func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat {
let dx: CGFloat = p2.x - p1.x
let dy: CGFloat = p2.y - p1.y
let dr: CGFloat = sqrt(dx * dx + dy * dy)
let D: CGFloat = p1.x * p2.y - p2.x * p1.y
var minDistance: CGFloat = 10000.0
for i in 0 ..< 2 {
let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0)
let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0
let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0))
minDistance = min(minDistance, distance)
}
return minDistance
}
func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat {
let x1 = p1.x
let y1 = p1.y
let x2 = p2.x
let y2 = p2.y
let x3 = p3.x
let y3 = p3.y
let x4 = p4.x
let y4 = p4.y
let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
if abs(d) <= 0.00001 {
return 10000.0
}
let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d
let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0))
return distance
}
let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5)
let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5)
let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5)
let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5)
let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
var distances: [CGFloat] = [
intersectionOuterTopRight,
intersectionInnerTopRight,
intersectionOuterBottomRight,
intersectionInnerBottomRight
]
if angleValue < CGFloat.pi / 2.0 {
distances.append(contentsOf: [
intersectionLine1TopRight,
intersectionLine1BottomRight,
intersectionLine2TopRight,
intersectionLine2BottomRight
] as [CGFloat])
}
var minDistance: CGFloat = 1000.0
for distance in distances {
minDistance = min(minDistance, distance + 1.0)
}
let diagonalAngle = atan2(labelSize.height, labelSize.width)
let maxHalfWidth = cos(diagonalAngle) * minDistance
let maxHalfHeight = sin(diagonalAngle) * minDistance
let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0)
let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height))
let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize)
if finalSize.width >= labelSize.width {
labelFrame = currentFrame
break
}
if let labelFrame {
if labelFrame.width > finalSize.width {
continue
}
}
labelFrame = currentFrame
}
} else {
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset)
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * 0.5)
let relLabelCenter = CGPoint(
x: cos(midAngle) * centerDistance,
@ -228,103 +404,14 @@ final class PieChartComponent: Component {
y: shapeLayerFrame.midY + relLabelCenter.y
)
func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat {
let dx: CGFloat = p2.x - p1.x
let dy: CGFloat = p2.y - p1.y
let dr: CGFloat = sqrt(dx * dx + dy * dy)
let D: CGFloat = p1.x * p2.y - p2.x * p1.y
var minDistance: CGFloat = 10000.0
for i in 0 ..< 2 {
let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0)
let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0
let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0))
minDistance = min(minDistance, distance)
}
return minDistance
}
func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat {
let x1 = p1.x
let y1 = p1.y
let x2 = p2.x
let y2 = p2.y
let x3 = p3.x
let y3 = p3.y
let x4 = p4.x
let y4 = p4.y
let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
if abs(d) <= 0.00001 {
return 10000.0
}
let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d
let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0))
return distance
}
let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5)
let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5)
let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5)
let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5)
let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
var distances: [CGFloat] = [
intersectionOuterTopRight,
intersectionInnerTopRight,
intersectionOuterBottomRight,
intersectionInnerBottomRight
]
if angleValue < CGFloat.pi / 2.0 {
distances.append(contentsOf: [
intersectionLine1TopRight,
intersectionLine1BottomRight,
intersectionLine2TopRight,
intersectionLine2BottomRight
] as [CGFloat])
}
var minDistance: CGFloat = 1000.0
for distance in distances {
minDistance = min(minDistance, distance + 1.0)
}
let diagonalAngle = atan2(labelSize.height, labelSize.width)
let maxHalfWidth = cos(diagonalAngle) * minDistance
let maxHalfHeight = sin(diagonalAngle) * minDistance
let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0)
let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height))
let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize)
if finalSize.width >= labelSize.width {
labelFrame = currentFrame
break
}
if let labelFrame {
if labelFrame.width > finalSize.width {
continue
}
}
labelFrame = currentFrame
let minSize = labelSize.aspectFitted(CGSize(width: 4.0, height: 4.0))
labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - minSize.width * 0.5, y: labelCenter.y - minSize.height * 0.5), size: minSize)
}
if let labelView = label.view, let labelFrame {
var animateIn: Bool = false
if labelView.superview == nil {
animateIn = true
self.addSubview(labelView)
}
@ -338,11 +425,12 @@ final class PieChartComponent: Component {
y: labelFrame.midY - shapeLayerFrame.midY
)
let labelAlpha: CGFloat
if let selectedKey = self.selectedKey {
if selectedKey == item.id {
transition.setAlpha(view: labelView, alpha: normalAlpha)
labelAlpha = normalAlpha
} else {
transition.setAlpha(view: labelView, alpha: 0.0)
labelAlpha = 0.0
let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter)
let reducedDiameterFactor: CGFloat = reducedDiameter / diameter
@ -353,7 +441,16 @@ final class PieChartComponent: Component {
relLabelCenter.y *= reducedDiameterFactor
}
} else {
transition.setAlpha(view: labelView, alpha: normalAlpha)
labelAlpha = normalAlpha
}
if labelView.alpha != labelAlpha {
let transition: Transition
if animateIn {
transition = .immediate
} else {
transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
}
transition.setAlpha(view: labelView, alpha: labelAlpha)
}
let labelCenter = CGPoint(
@ -361,10 +458,61 @@ final class PieChartComponent: Component {
y: shapeLayerFrame.midY + relLabelCenter.y
)
transition.setPosition(view: labelView, position: labelCenter)
transition.setScale(view: labelView, scale: labelScale)
labelView.center = labelCenter
labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale)
}
}
}
}
class View: UIView {
private let dataView: ChartDataView
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
var selectedKey: AnyHashable?
private weak var state: EmptyComponentState?
override init(frame: CGRect) {
self.dataView = ChartDataView()
super.init(frame: frame)
self.addSubview(self.dataView)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
let _ = point
/*for (key, layer) in self.shapeLayers {
if layer.frame.contains(point), let path = layer.path {
if path.contains(self.layer.convert(point, to: layer)) {
if self.selectedKey == key {
self.selectedKey = nil
} else {
self.selectedKey = key
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
break
}
}
}*/
}
}
func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
self.state = state
transition.setFrame(view: self.dataView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - 200.0) / 2.0), y: 0.0), size: CGSize(width: 200.0, height: 200.0)))
self.dataView.setItems(theme: component.theme, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate)
return CGSize(width: availableSize.width, height: 200.0)
}

View File

@ -17,7 +17,7 @@ import SolidRoundedButtonComponent
final class StorageCategoriesComponent: Component {
struct CategoryData: Equatable {
var key: AnyHashable
var key: StorageUsageScreenComponent.Category
var color: UIColor
var title: String
var size: Int64
@ -25,7 +25,7 @@ final class StorageCategoriesComponent: Component {
var isSelected: Bool
var subcategories: [CategoryData]
init(key: AnyHashable, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) {
init(key: StorageUsageScreenComponent.Category, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) {
self.key = key
self.title = title
self.color = color
@ -39,18 +39,27 @@ final class StorageCategoriesComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let categories: [CategoryData]
let toggleCategorySelection: (AnyHashable) -> Void
let isOtherExpanded: Bool
let toggleCategorySelection: (StorageUsageScreenComponent.Category) -> Void
let toggleOtherExpanded: () -> Void
let clearAction: () -> Void
init(
theme: PresentationTheme,
strings: PresentationStrings,
categories: [CategoryData],
toggleCategorySelection: @escaping (AnyHashable) -> Void
isOtherExpanded: Bool,
toggleCategorySelection: @escaping (StorageUsageScreenComponent.Category) -> Void,
toggleOtherExpanded: @escaping () -> Void,
clearAction: @escaping () -> Void
) {
self.theme = theme
self.strings = strings
self.categories = categories
self.isOtherExpanded = isOtherExpanded
self.toggleCategorySelection = toggleCategorySelection
self.toggleOtherExpanded = toggleOtherExpanded
self.clearAction = clearAction
}
static func ==(lhs: StorageCategoriesComponent, rhs: StorageCategoriesComponent) -> Bool {
@ -63,14 +72,16 @@ final class StorageCategoriesComponent: Component {
if lhs.categories != rhs.categories {
return false
}
if lhs.isOtherExpanded != rhs.isOtherExpanded {
return false
}
return true
}
class View: UIView {
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var itemViews: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
private let button = ComponentView<Empty>()
private var expandedCategory: AnyHashable?
private var component: StorageCategoriesComponent?
private weak var state: EmptyComponentState?
@ -89,6 +100,8 @@ final class StorageCategoriesComponent: Component {
self.component = component
self.state = state
let expandedCategory: StorageUsageScreenComponent.Category? = component.isOtherExpanded ? .other : nil
var totalSelectedSize: Int64 = 0
var hasDeselected = false
for category in component.categories {
@ -111,7 +124,7 @@ final class StorageCategoriesComponent: Component {
var contentHeight: CGFloat = 0.0
var validKeys = Set<AnyHashable>()
var validKeys = Set<StorageUsageScreenComponent.Category>()
for i in 0 ..< component.categories.count {
let category = component.categories[i]
validKeys.insert(category.key)
@ -134,7 +147,7 @@ final class StorageCategoriesComponent: Component {
strings: component.strings,
category: category,
isExpandedLevel: false,
isExpanded: self.expandedCategory == category.key,
isExpanded: expandedCategory == category.key,
hasNext: i != component.categories.count - 1,
action: { [weak self] key, actionType in
guard let self, let component = self.component else {
@ -144,12 +157,7 @@ final class StorageCategoriesComponent: Component {
switch actionType {
case .generic:
if let category = component.categories.first(where: { $0.key == key }), !category.subcategories.isEmpty {
if self.expandedCategory == category.key {
self.expandedCategory = nil
} else {
self.expandedCategory = category.key
}
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
component.toggleOtherExpanded()
} else {
component.toggleCategorySelection(key)
}
@ -172,7 +180,7 @@ final class StorageCategoriesComponent: Component {
contentHeight += itemSize.height
}
var removeKeys: [AnyHashable] = []
var removeKeys: [StorageUsageScreenComponent.Category] = []
for (key, itemView) in self.itemViews {
if !validKeys.contains(key) {
if let itemComponentView = itemView.view {
@ -221,7 +229,11 @@ final class StorageCategoriesComponent: Component {
animationName: nil,
iconPosition: .right,
iconSpacing: 4.0,
action: {
action: { [weak self] in
guard let self, let component = self.component else {
return
}
component.clearAction()
}
)),
environment: {},

View File

@ -27,7 +27,7 @@ final class StorageCategoryItemComponent: Component {
let isExpandedLevel: Bool
let isExpanded: Bool
let hasNext: Bool
let action: (AnyHashable, ActionType) -> Void
let action: (StorageUsageScreenComponent.Category, ActionType) -> Void
init(
theme: PresentationTheme,
@ -36,7 +36,7 @@ final class StorageCategoryItemComponent: Component {
isExpandedLevel: Bool,
isExpanded: Bool,
hasNext: Bool,
action: @escaping (AnyHashable, ActionType) -> Void
action: @escaping (StorageUsageScreenComponent.Category, ActionType) -> Void
) {
self.theme = theme
self.strings = strings
@ -80,7 +80,7 @@ final class StorageCategoryItemComponent: Component {
private let checkButtonArea: HighlightTrackingButton
private let subcategoryClippingContainer: UIView
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
private var itemViews: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
private var component: StorageCategoryItemComponent?
@ -306,7 +306,7 @@ final class StorageCategoryItemComponent: Component {
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0)))
var validKeys = Set<AnyHashable>()
var validKeys = Set<StorageUsageScreenComponent.Category>()
if component.isExpanded {
for i in 0 ..< component.category.subcategories.count {
let category = component.category.subcategories[i]
@ -358,7 +358,7 @@ final class StorageCategoryItemComponent: Component {
}
}
var removeKeys: [AnyHashable] = []
var removeKeys: [StorageUsageScreenComponent.Category] = []
for (key, itemView) in self.itemViews {
if !validKeys.contains(key) {
if let itemComponentView = itemView.view {

View File

@ -19,13 +19,20 @@ import AvatarNode
private let avatarFont = avatarPlaceholderFont(size: 15.0)
private final class PeerListItemComponent: Component {
enum SelectionState: Equatable {
case none
case editing(isSelected: Bool)
}
let context: AccountContext
let theme: PresentationTheme
let sideInset: CGFloat
let title: String
let peer: EnginePeer?
let label: String
let selectionState: SelectionState
let hasNext: Bool
let action: (EnginePeer) -> Void
init(
context: AccountContext,
@ -34,7 +41,9 @@ private final class PeerListItemComponent: Component {
title: String,
peer: EnginePeer?,
label: String,
hasNext: Bool
selectionState: SelectionState,
hasNext: Bool,
action: @escaping (EnginePeer) -> Void
) {
self.context = context
self.theme = theme
@ -42,7 +51,9 @@ private final class PeerListItemComponent: Component {
self.title = title
self.peer = peer
self.label = label
self.selectionState = selectionState
self.hasNext = hasNext
self.action = action
}
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
@ -64,6 +75,9 @@ private final class PeerListItemComponent: Component {
if lhs.label != rhs.label {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
@ -76,6 +90,11 @@ private final class PeerListItemComponent: Component {
private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode
private var checkLayer: CheckLayer?
private var highlightBackgroundFrame: CGRect?
private var highlightBackgroundLayer: SimpleLayer?
private var component: PeerListItemComponent?
override init(frame: CGRect) {
@ -87,23 +106,115 @@ private final class PeerListItemComponent: Component {
self.layer.addSublayer(self.separatorLayer)
self.layer.addSublayer(self.avatarNode.layer)
self.highligthedChanged = { [weak self] isHighlighted in
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
return
}
if isHighlighted, case .none = component.selectionState {
self.superview?.bringSubviewToFront(self)
let highlightBackgroundLayer: SimpleLayer
if let current = self.highlightBackgroundLayer {
highlightBackgroundLayer = current
} else {
highlightBackgroundLayer = SimpleLayer()
self.highlightBackgroundLayer = highlightBackgroundLayer
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
}
highlightBackgroundLayer.frame = highlightBackgroundFrame
highlightBackgroundLayer.opacity = 1.0
} else {
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
self.highlightBackgroundLayer = nil
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
highlightBackgroundLayer?.removeFromSuperlayer()
})
}
}
}
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func pressed() {
guard let component = self.component, let peer = component.peer else {
return
}
component.action(peer)
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
var hasSelectionUpdated = false
if let previousComponent = self.component {
switch previousComponent.selectionState {
case .none:
if case .none = component.selectionState {
} else {
hasSelectionUpdated = true
}
case .editing:
if case .editing = component.selectionState {
} else {
hasSelectionUpdated = true
}
}
}
self.component = component
let height: CGFloat = 52.0
let leftInset: CGFloat = 62.0 + component.sideInset
var leftInset: CGFloat = 62.0 + component.sideInset
var avatarLeftInset: CGFloat = component.sideInset + 10.0
if case let .editing(isSelected) = component.selectionState {
leftInset += 48.0
avatarLeftInset += 48.0
let checkSize: CGFloat = 22.0
let checkLayer: CheckLayer
if let current = self.checkLayer {
checkLayer = current
if themeUpdated {
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
}
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
} else {
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
self.checkLayer = checkLayer
self.layer.addSublayer(checkLayer)
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
checkLayer.setSelected(isSelected, animated: false)
checkLayer.setNeedsDisplay()
}
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
} else {
if let checkLayer = self.checkLayer {
self.checkLayer = nil
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
checkLayer?.removeFromSuperlayer()
})
}
}
let rightInset: CGFloat = 16.0 + component.sideInset
let avatarSize: CGFloat = 40.0
self.avatarNode.frame = CGRect(origin: CGPoint(x: component.sideInset + 10.0, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
if self.avatarNode.bounds.isEmpty {
self.avatarNode.frame = avatarFrame
} else {
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
}
if let peer = component.peer {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
@ -117,6 +228,12 @@ private final class PeerListItemComponent: Component {
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let previousTitleFrame = self.title.view?.frame
var previousTitleContents: UIView?
if hasSelectionUpdated {
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
}
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
@ -125,15 +242,31 @@ private final class PeerListItemComponent: Component {
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0)
)
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
if let titleView = self.title.view {
if titleView.superview == nil {
titleView.isUserInteractionEnabled = false
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize))
titleView.frame = titleFrame
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
}
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
self.addSubview(previousTitleContents)
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
previousTitleContents?.removeFromSuperview()
})
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
}
}
if let labelView = self.label.view {
if labelView.superview == nil {
labelView.isUserInteractionEnabled = false
self.addSubview(labelView)
}
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize))
@ -145,6 +278,8 @@ private final class PeerListItemComponent: Component {
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
self.separatorLayer.isHidden = !component.hasNext
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
return CGSize(width: availableSize.width, height: height)
}
}
@ -201,13 +336,19 @@ final class StoragePeerListPanelComponent: Component {
let context: AccountContext
let items: Items?
let selectionState: StorageUsageScreenComponent.SelectionState?
let peerAction: (EnginePeer) -> Void
init(
context: AccountContext,
items: Items?
items: Items?,
selectionState: StorageUsageScreenComponent.SelectionState?,
peerAction: @escaping (EnginePeer) -> Void
) {
self.context = context
self.items = items
self.selectionState = selectionState
self.peerAction = peerAction
}
static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool {
@ -217,6 +358,9 @@ final class StoragePeerListPanelComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.selectionState != rhs.selectionState {
return false
}
return true
}
@ -337,6 +481,13 @@ final class StoragePeerListPanelComponent: Component {
self.visibleItems[id] = itemView
}
let itemSelectionState: PeerListItemComponent.SelectionState
if let selectionState = component.selectionState {
itemSelectionState = .editing(isSelected: selectionState.selectedPeers.contains(id))
} else {
itemSelectionState = .none
}
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
@ -346,7 +497,9 @@ final class StoragePeerListPanelComponent: Component {
title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: item.peer,
label: dataSizeString(item.size, formatting: dataSizeFormatting),
hasNext: index != items.items.count - 1
selectionState: itemSelectionState,
hasNext: index != items.items.count - 1,
action: component.peerAction
)),
environment: {},
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
@ -392,7 +545,10 @@ final class StoragePeerListPanelComponent: Component {
title: "ABCDEF",
peer: nil,
label: "1000",
hasNext: false
selectionState: .none,
hasNext: false,
action: { _ in
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
@ -408,7 +564,14 @@ final class StoragePeerListPanelComponent: Component {
self.ignoreScrolling = true
let contentOffset = self.scrollView.bounds.minY
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
var scrollBounds = self.scrollView.bounds
scrollBounds.size = availableSize
if !environment.isScrollable {
scrollBounds.origin = CGPoint()
}
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
self.scrollView.isScrollEnabled = environment.isScrollable
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
@ -419,7 +582,7 @@ final class StoragePeerListPanelComponent: Component {
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
}
self.ignoreScrolling = false
self.updateScrolling(transition: .immediate)
self.updateScrolling(transition: transition)
return availableSize
}

View File

@ -5,19 +5,42 @@ import ComponentFlow
import ComponentDisplayAdapters
import TelegramPresentationData
final class StorageUsagePanelContainerEnvironment: Equatable {
let isScrollable: Bool
init(
isScrollable: Bool
) {
self.isScrollable = isScrollable
}
static func ==(lhs: StorageUsagePanelContainerEnvironment, rhs: StorageUsagePanelContainerEnvironment) -> Bool {
if lhs.isScrollable != rhs.isScrollable {
return false
}
return true
}
}
final class StorageUsagePanelEnvironment: Equatable {
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let containerInsets: UIEdgeInsets
let isScrollable: Bool
init(
theme: PresentationTheme,
strings: PresentationStrings,
containerInsets: UIEdgeInsets
dateTimeFormat: PresentationDateTimeFormat,
containerInsets: UIEdgeInsets,
isScrollable: Bool
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.containerInsets = containerInsets
self.isScrollable = isScrollable
}
static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool {
@ -27,9 +50,15 @@ final class StorageUsagePanelEnvironment: Equatable {
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.containerInsets != rhs.containerInsets {
return false
}
if lhs.isScrollable != rhs.isScrollable {
return false
}
return true
}
}
@ -37,13 +66,16 @@ final class StorageUsagePanelEnvironment: Equatable {
private final class StorageUsageHeaderItemComponent: CombinedComponent {
let theme: PresentationTheme
let title: String
let activityFraction: CGFloat
init(
theme: PresentationTheme,
title: String
title: String,
activityFraction: CGFloat
) {
self.theme = theme
self.title = title
self.activityFraction = activityFraction
}
static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool {
@ -53,22 +85,38 @@ private final class StorageUsageHeaderItemComponent: CombinedComponent {
if lhs.title != rhs.title {
return false
}
if lhs.activityFraction != rhs.activityFraction {
return false
}
return true
}
static var body: Body {
let text = Child(Text.self)
let activeText = Child(Text.self)
let inactiveText = Child(Text.self)
return { context in
let text = text.update(
component: Text(text: context.component.title, font: Font.semibold(15.0), color: context.component.theme.list.itemAccentColor),
let activeText = activeText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor),
availableSize: context.availableSize,
transition: .immediate
)
let inactiveText = inactiveText.update(
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor),
availableSize: context.availableSize,
transition: .immediate
)
context.add(text.position(CGPoint(x: text.size.width * 0.5, y: text.size.height * 0.5)))
context.add(activeText
.position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5))
.opacity(context.component.activityFraction)
)
context.add(inactiveText
.position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5))
.opacity(1.0 - context.component.activityFraction)
)
return text.size
return activeText.size
}
}
}
@ -89,13 +137,22 @@ private final class StorageUsageHeaderComponent: Component {
let theme: PresentationTheme
let items: [Item]
let activeIndex: Int
let transitionFraction: CGFloat
let switchToPanel: (AnyHashable) -> Void
init(
theme: PresentationTheme,
items: [Item]
items: [Item],
activeIndex: Int,
transitionFraction: CGFloat,
switchToPanel: @escaping (AnyHashable) -> Void
) {
self.theme = theme
self.items = items
self.activeIndex = activeIndex
self.transitionFraction = transitionFraction
self.switchToPanel = switchToPanel
}
static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool {
@ -105,6 +162,12 @@ private final class StorageUsageHeaderComponent: Component {
if lhs.items != rhs.items {
return false
}
if lhs.activeIndex != rhs.activeIndex {
return false
}
if lhs.transitionFraction != rhs.transitionFraction {
return false
}
return true
}
@ -122,19 +185,46 @@ private final class StorageUsageHeaderComponent: Component {
super.init(frame: frame)
self.layer.addSublayer(self.activeItemLayer)
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
if case .ended = recognizer.state {
let point = recognizer.location(in: self)
var closestId: (CGFloat, AnyHashable)?
if self.bounds.contains(point) {
for (id, item) in self.visibleItems {
if let itemView = item.view {
let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX))
if let closestIdValue = closestId {
if distance < closestIdValue.0 {
closestId = (distance, id)
}
} else {
closestId = (distance, id)
}
}
}
}
if let closestId = closestId, let component = self.component {
component.switchToPanel(closestId.1)
}
}
}
func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
var validIds = Set<AnyHashable>()
for item in component.items {
for i in 0 ..< component.items.count {
let item = component.items[i]
validIds.insert(item.id)
let itemView: ComponentView<Empty>
@ -146,24 +236,59 @@ private final class StorageUsageHeaderComponent: Component {
itemView = ComponentView()
self.visibleItems[item.id] = itemView
}
let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction
let activityDistance: CGFloat = abs(activeIndex - CGFloat(i))
let activityFraction: CGFloat
if activityDistance < 1.0 {
activityFraction = 1.0 - activityDistance
} else {
activityFraction = 0.0
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StorageUsageHeaderItemComponent(
theme: component.theme,
title: item.title
title: item.title,
activityFraction: activityFraction
)),
environment: {},
containerSize: availableSize
)
let itemFrame = CGRect(origin: CGPoint(x: 34.0, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count)
let itemX: CGFloat
if component.items.count == 1 {
itemX = 37.0
} else {
itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0)
}
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
itemComponentView.isUserInteractionEnabled = false
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: itemFrame.width, height: 3.0)))
}
if component.activeIndex < component.items.count {
let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view
let nextIndex: Int
if component.transitionFraction > 0.0 {
nextIndex = max(0, component.activeIndex - 1)
} else {
nextIndex = min(component.items.count - 1, component.activeIndex + 1)
}
let nextView = self.visibleItems[component.items[nextIndex].id]?.view
if let activeView = activeView, let nextView = nextView {
let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction))
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0)))
}
}
if themeUpdated {
@ -197,6 +322,8 @@ private final class StorageUsageHeaderComponent: Component {
}
final class StorageUsagePanelContainerComponent: Component {
typealias EnvironmentType = StorageUsagePanelContainerEnvironment
struct Item: Equatable {
let id: AnyHashable
let title: String
@ -215,17 +342,20 @@ final class StorageUsagePanelContainerComponent: Component {
let theme: PresentationTheme
let strings: PresentationStrings
let dateTimeFormat: PresentationDateTimeFormat
let insets: UIEdgeInsets
let items: [Item]
init(
theme: PresentationTheme,
strings: PresentationStrings,
dateTimeFormat: PresentationDateTimeFormat,
insets: UIEdgeInsets,
items: [Item]
) {
self.theme = theme
self.strings = strings
self.dateTimeFormat = dateTimeFormat
self.insets = insets
self.items = items
}
@ -237,6 +367,9 @@ final class StorageUsagePanelContainerComponent: Component {
if lhs.strings !== rhs.strings {
return false
}
if lhs.dateTimeFormat != rhs.dateTimeFormat {
return false
}
if lhs.insets != rhs.insets {
return false
}
@ -246,47 +379,211 @@ final class StorageUsagePanelContainerComponent: Component {
return true
}
class View: UIView {
private let topPanelBackgroundView: BlurredBackgroundView
class View: UIView, UIGestureRecognizerDelegate {
private let topPanelBackgroundView: UIView
private let topPanelSeparatorLayer: SimpleLayer
private let header = ComponentView<Empty>()
private var component: StorageUsagePanelContainerComponent?
private weak var state: EmptyComponentState?
private let panelsBackgroundLayer: SimpleLayer
private var visiblePanels: [AnyHashable: ComponentView<StorageUsagePanelEnvironment>] = [:]
private var actualVisibleIds = Set<AnyHashable>()
private var currentId: AnyHashable?
private var transitionFraction: CGFloat = 0.0
private var animatingTransition: Bool = false
override init(frame: CGRect) {
self.topPanelBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.topPanelBackgroundView = UIView()
self.topPanelSeparatorLayer = SimpleLayer()
self.panelsBackgroundLayer = SimpleLayer()
super.init(frame: frame)
self.layer.addSublayer(self.panelsBackgroundLayer)
self.addSubview(self.topPanelBackgroundView)
self.layer.addSublayer(self.topPanelSeparatorLayer)
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
guard let self, let component = self.component, let currentId = self.currentId else {
return []
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return []
}
/*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
return []
}*/
if index == 0 {
return .left
}
return [.left, .right]
})
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
self.addGestureRecognizer(panRecognizer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return false
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
return false
}
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
return true
}
return false
}
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
switch recognizer.state {
case .began:
func cancelContextGestures(view: UIView) {
if let gestureRecognizers = view.gestureRecognizers {
for gesture in gestureRecognizers {
if let gesture = gesture as? ContextGesture {
gesture.cancel()
}
}
}
for subview in view.subviews {
cancelContextGestures(view: subview)
}
}
cancelContextGestures(view: self)
//self.animatingTransition = true
case .changed:
guard let component = self.component, let currentId = self.currentId else {
return
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return
}
let translation = recognizer.translation(in: self)
var transitionFraction = translation.x / self.bounds.width
if index <= 0 {
transitionFraction = min(0.0, transitionFraction)
}
if index >= component.items.count - 1 {
transitionFraction = max(0.0, transitionFraction)
}
self.transitionFraction = transitionFraction
self.state?.updated(transition: .immediate)
// let nextKey = availablePanes[updatedIndex]
// print(transitionFraction)
//self.paneTransitionPromise.set(transitionFraction)
//self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate)
//self.currentPaneUpdated?(false)
case .cancelled, .ended:
guard let component = self.component, let currentId = self.currentId else {
return
}
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
return
}
let translation = recognizer.translation(in: self)
let velocity = recognizer.velocity(in: self)
var directionIsToRight: Bool?
if abs(velocity.x) > 10.0 {
directionIsToRight = velocity.x < 0.0
} else {
if abs(translation.x) > self.bounds.width / 2.0 {
directionIsToRight = translation.x > self.bounds.width / 2.0
}
}
if let directionIsToRight = directionIsToRight {
var updatedIndex = index
if directionIsToRight {
updatedIndex = min(updatedIndex + 1, component.items.count - 1)
} else {
updatedIndex = max(updatedIndex - 1, 0)
}
self.currentId = component.items[updatedIndex].id
}
self.transitionFraction = 0.0
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
self.animatingTransition = false
//self.currentPaneUpdated?(false)
//self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
default:
break
}
}
func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: Transition) -> CGSize {
let environment = environment[StorageUsagePanelContainerEnvironment.self].value
let themeUpdated = self.component?.theme !== component.theme
self.component = component
self.state = state
if themeUpdated {
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.topPanelBackgroundView.updateColor(color: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.8), transition: .immediate)
self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 44.0))
let topPanelCoverHeight: CGFloat = 10.0
let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0))
transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame)
self.topPanelBackgroundView.update(size: topPanelFrame.size, transition: transition.containedViewLayoutTransition)
let lockScrollingTransition: Transition
if themeUpdated {
lockScrollingTransition = transition
} else {
lockScrollingTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
}
lockScrollingTransition.setBackgroundColor(view: self.topPanelBackgroundView, color: environment.isScrollable ? component.theme.rootController.navigationBar.blurredBackgroundColor : component.theme.list.itemBlocksBackgroundColor)
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)))
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
self.currentId = nil
}
if self.currentId == nil {
self.currentId = component.items.first?.id
}
var visibleIds = Set<AnyHashable>()
var currentIndex: Int?
if let currentId = self.currentId {
visibleIds.insert(currentId)
if let index = component.items.firstIndex(where: { $0.id == currentId }) {
currentIndex = index
if index != 0 {
visibleIds.insert(component.items[index - 1].id)
}
if index != component.items.count - 1 {
visibleIds.insert(component.items[index + 1].id)
}
}
}
let _ = self.header.update(
transition: transition,
component: AnyComponent(StorageUsageHeaderComponent(
@ -296,6 +593,17 @@ final class StorageUsagePanelContainerComponent: Component {
id: item.id,
title: item.title
)
},
activeIndex: currentIndex ?? 0,
transitionFraction: self.transitionFraction,
switchToPanel: { [weak self] id in
guard let self, let component = self.component else {
return
}
if component.items.contains(where: { $0.id == id }) {
self.currentId = id
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
}
}
)),
environment: {},
@ -308,45 +616,114 @@ final class StorageUsagePanelContainerComponent: Component {
transition.setFrame(view: headerView, frame: topPanelFrame)
}
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
self.currentId = nil
}
if self.currentId == nil {
self.currentId = component.items.first?.id
}
let childEnvironment = StorageUsagePanelEnvironment(
theme: component.theme,
strings: component.strings,
containerInsets: UIEdgeInsets(top: topPanelFrame.height, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right)
dateTimeFormat: component.dateTimeFormat,
containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
isScrollable: environment.isScrollable
)
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
if self.animatingTransition {
visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil })
}
self.actualVisibleIds = visibleIds
for (id, _) in self.visiblePanels {
visibleIds.insert(id)
}
var validIds = Set<AnyHashable>()
if let currentId = self.currentId, let panelItem = component.items.first(where: { $0.id == currentId }) {
validIds.insert(panelItem.id)
let panel: ComponentView<StorageUsagePanelEnvironment>
var panelTransition = transition
if let current = self.visiblePanels[panelItem.id] {
panel = current
} else {
panelTransition = .immediate
panel = ComponentView()
self.visiblePanels[panelItem.id] = panel
}
let _ = panel.update(
transition: panelTransition,
component: panelItem.panel,
environment: {
childEnvironment
},
containerSize: availableSize
)
if let panelView = panel.view {
if panelView.superview == nil {
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
if let currentIndex {
var anyAnchorOffset: CGFloat = 0.0
for (id, panel) in self.visiblePanels {
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else {
continue
}
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
if itemIndex < currentIndex {
itemFrame.origin.x -= itemFrame.width
} else if itemIndex > currentIndex {
itemFrame.origin.x += itemFrame.width
}
anyAnchorOffset = itemFrame.minX - panelView.frame.minX
break
}
for id in visibleIds {
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else {
continue
}
let panelItem = component.items[itemIndex]
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
if itemIndex < currentIndex {
itemFrame.origin.x -= itemFrame.width
} else if itemIndex > currentIndex {
itemFrame.origin.x += itemFrame.width
}
validIds.insert(panelItem.id)
let panel: ComponentView<StorageUsagePanelEnvironment>
var panelTransition = transition
var animateInIfNeeded = false
if let current = self.visiblePanels[panelItem.id] {
panel = current
if let panelView = panel.view, !panelView.bounds.isEmpty {
var wasHidden = false
if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne {
wasHidden = true
}
var isHidden = false
if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne {
isHidden = true
}
if wasHidden && isHidden {
panelTransition = .immediate
}
}
} else {
panelTransition = .immediate
animateInIfNeeded = true
panel = ComponentView()
self.visiblePanels[panelItem.id] = panel
}
let _ = panel.update(
transition: panelTransition,
component: panelItem.panel,
environment: {
childEnvironment
},
containerSize: centralPanelFrame.size
)
if let panelView = panel.view {
if panelView.superview == nil {
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
}
panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in
guard let self else {
return
}
if !self.actualVisibleIds.contains(id) {
if let panel = self.visiblePanels[id] {
self.visiblePanels.removeValue(forKey: id)
panel.view?.removeFromSuperview()
}
}
})
if animateInIfNeeded && anyAnchorOffset != 0.0 {
transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true)
}
}
panelTransition.setFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
}
@ -371,7 +748,7 @@ final class StorageUsagePanelContainerComponent: Component {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -0,0 +1,157 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import ComponentFlow
import SwiftSignalKit
import ViewControllerComponent
import ComponentDisplayAdapters
import TelegramPresentationData
import AccountContext
import TelegramCore
import MultilineTextComponent
import EmojiStatusComponent
import Postbox
import TelegramStringFormatting
import CheckNode
import SolidRoundedButtonComponent
final class StorageUsageScreenSelectionPanelComponent: Component {
let theme: PresentationTheme
let title: String
let label: String?
let isEnabled: Bool
let insets: UIEdgeInsets
let action: () -> Void
init(
theme: PresentationTheme,
title: String,
label: String?,
isEnabled: Bool,
insets: UIEdgeInsets,
action: @escaping () -> Void
) {
self.theme = theme
self.title = title
self.label = label
self.isEnabled = isEnabled
self.insets = insets
self.action = action
}
static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.isEnabled != rhs.isEnabled {
return false
}
if lhs.insets != rhs.insets {
return false
}
return true
}
class View: UIView {
private let backgroundView: BlurredBackgroundView
private let separatorLayer: SimpleLayer
private let actionButton = ComponentView<Empty>()
private var component: StorageUsageScreenSelectionPanelComponent?
override init(frame: CGRect) {
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.separatorLayer = SimpleLayer()
super.init(frame: frame)
self.addSubview(self.backgroundView)
self.layer.addSublayer(self.separatorLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
let topInset: CGFloat = 8.0
let bottomInset: CGFloat
if component.insets.bottom == 0.0 {
bottomInset = topInset
} else {
bottomInset = component.insets.bottom + 10.0
}
let height: CGFloat = topInset + 50.0 + bottomInset
if themeUpdated {
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
}
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.height, height: height))
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
let actionButtonSize = self.actionButton.update(
transition: transition,
component: AnyComponent(SolidRoundedButtonComponent(
title: component.title,
label: component.label,
theme: SolidRoundedButtonComponent.Theme(
backgroundColor: component.theme.list.itemCheckColors.fillColor,
backgroundColors: [],
foregroundColor: component.theme.list.itemCheckColors.foregroundColor
),
font: .bold,
fontSize: 17.0,
height: 50.0,
cornerRadius: 10.0,
gloss: false,
isEnabled: component.isEnabled,
animationName: nil,
iconPosition: .right,
iconSpacing: 4.0,
action: { [weak self] in
guard let self else {
return
}
self.component?.action()
}
)),
environment: {},
containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: 50.0)
)
if let actionButtonView = self.actionButton.view {
if actionButtonView.superview == nil {
self.addSubview(actionButtonView)
}
transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topInset), size: actionButtonSize))
}
return CGSize(width: availableSize.width, height: height)
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -1317,7 +1317,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
var signals: Signal<Never, NoError> = .complete()
for (_, context, _) in activeAccounts.accounts {
signals = signals |> then(context.account.cleanupTasks())
signals = signals |> then(context.account.cleanupTasks(lowImpact: false))
}
disposable.set(signals.start(completed: {

View File

@ -268,7 +268,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
let layerHolder = takeSampleBufferLayer()
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
self.layer.addSublayer(layerHolder.layer)
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .gif, fileReference: videoFileReference, layerHolder: layerHolder)
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: videoFileReference, layerHolder: layerHolder)
self.videoLayer = (thumbnailLayer, manager, layerHolder)
thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {

View File

@ -399,7 +399,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
strongSelf.layer.addSublayer(layerHolder.layer)
let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .gif, fileReference: .standalone(media: videoFile), layerHolder: layerHolder)
let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .other, fileReference: .standalone(media: videoFile), layerHolder: layerHolder)
strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder)
thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {

View File

@ -75,7 +75,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode {
let source = UniversalSoftwareVideoSource(
mediaBox: self.context.account.postbox.mediaBox,
userLocation: userLocation,
userContentType: .gif,
userContentType: .other,
fileReference: self.file,
automaticallyFetchHeader: true
)

View File

@ -65,7 +65,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode {
let source = UniversalSoftwareVideoSource(
mediaBox: self.context.account.postbox.mediaBox,
userLocation: userLocation,
userContentType: .gif,
userContentType: .other,
fileReference: self.file,
automaticallyFetchHeader: true
)
@ -301,7 +301,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
self.imageNode.layer.addSublayer(sampleBufferLayer.layer)
}
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .gif, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer)
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .other, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer)
self.videoLayerFrameManager?.start()
}
} else {