Storage calculation and UI improvements

This commit is contained in:
Ali 2022-12-21 23:53:11 +04:00
parent d4a3568686
commit eb1947b8b3
9 changed files with 1345 additions and 109 deletions

View File

@ -19,14 +19,24 @@ private func md5Hash(_ data: Data) -> HashId {
}
public final class StorageBox {
public struct Stats {
public var contentTypes: [UInt8: Int64]
public final class Stats {
public fileprivate(set) var contentTypes: [UInt8: Int64]
public init(contentTypes: [UInt8: Int64]) {
self.contentTypes = contentTypes
}
}
public final class AllStats {
public fileprivate(set) var total: Stats
public fileprivate(set) var peers: [PeerId: Stats]
public init(total: Stats, peers: [PeerId: Stats]) {
self.total = total
self.peers = peers
}
}
public struct Reference {
public var peerId: Int64
public var messageNamespace: UInt8
@ -122,6 +132,10 @@ public final class StorageBox {
}
}
private struct Metadata: Codable {
var version: Int32
}
private final class Impl {
let queue: Queue
let logger: StorageBox.Logger
@ -133,6 +147,7 @@ public final class StorageBox {
let peerIdTable: ValueBoxTable
let peerContentTypeStatsTable: ValueBoxTable
let contentTypeStatsTable: ValueBoxTable
let metadataTable: ValueBoxTable
init(queue: Queue, logger: StorageBox.Logger, basePath: String) {
self.queue = queue
@ -157,6 +172,54 @@ public final class StorageBox {
self.peerIdTable = ValueBoxTable(id: 18, keyType: .binary, compactValuesOnCreation: true)
self.peerContentTypeStatsTable = ValueBoxTable(id: 19, keyType: .binary, compactValuesOnCreation: true)
self.contentTypeStatsTable = ValueBoxTable(id: 20, keyType: .binary, compactValuesOnCreation: true)
self.metadataTable = ValueBoxTable(id: 21, keyType: .binary, compactValuesOnCreation: true)
self.performUpdatesIfNeeded()
}
private func performUpdatesIfNeeded() {
self.valueBox.begin()
let mainMetadataKey = ValueBoxKey(length: 2)
mainMetadataKey.setUInt8(0, value: 0)
mainMetadataKey.setUInt8(1, value: 0)
var metadata: Metadata
if let value = self.valueBox.get(self.metadataTable, key: mainMetadataKey), let parsedValue = try? JSONDecoder().decode(Metadata.self, from: value.makeData()) {
metadata = parsedValue
} else {
metadata = Metadata(version: 0)
}
if metadata.version != 2 {
self.reindexPeerStats()
metadata.version = 2
if let data = try? JSONEncoder().encode(metadata) {
self.valueBox.set(self.metadataTable, key: mainMetadataKey, value: MemoryBuffer(data: data))
}
}
self.valueBox.commit()
}
private func reindexPeerStats() {
self.valueBox.removeAllFromTable(self.peerContentTypeStatsTable)
let mainKey = ValueBoxKey(length: 16)
self.valueBox.scan(self.peerIdToIdTable, keys: { key in
let peerId = key.getInt64(0)
let hashId = key.getData(8, length: 16)
mainKey.setData(0, value: hashId)
if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) {
let info = ItemInfo(buffer: currentInfoValue)
self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: info.size)
}
return true
})
}
private func internalAddSize(contentType: UInt8, delta: Int64) {
@ -171,7 +234,7 @@ public final class StorageBox {
currentSize += delta
if currentSize < 0 {
assertionFailure()
//assertionFailure()
currentSize = 0
}
@ -184,18 +247,18 @@ public final class StorageBox {
key.setUInt8(8, value: contentType)
var currentSize: Int64 = 0
if let value = self.valueBox.get(self.contentTypeStatsTable, key: key) {
if let value = self.valueBox.get(self.peerContentTypeStatsTable, key: key) {
value.read(&currentSize, offset: 0, length: 8)
}
currentSize += delta
if currentSize < 0 {
assertionFailure()
//assertionFailure()
currentSize = 0
}
self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: &currentSize, capacity: 8, length: 8, freeWhenDone: false))
self.valueBox.set(self.peerContentTypeStatsTable, key: key, value: MemoryBuffer(memory: &currentSize, capacity: 8, length: 8, freeWhenDone: false))
}
func add(reference: Reference, to id: Data, contentType: UInt8) {
@ -224,6 +287,10 @@ public final class StorageBox {
if size != 0 {
self.internalAddSize(contentType: previousContentType, delta: -size)
self.internalAddSize(contentType: contentType, delta: size)
for peerId in self.peerIdsReferencing(hashId: hashId) {
self.internalAddSize(peerId: peerId, contentType: previousContentType, delta: -size)
}
}
}
@ -274,9 +341,31 @@ public final class StorageBox {
}
}
if let previousContentType = previousContentType, previousContentType != contentType {
if size != 0 {
for peerId in self.peerIdsReferencing(hashId: hashId) {
self.internalAddSize(peerId: peerId, contentType: contentType, delta: size)
}
}
}
self.valueBox.commit()
}
private func peerIdsReferencing(hashId: HashId) -> Set<Int64> {
let mainKey = ValueBoxKey(length: 16)
mainKey.setData(0, value: hashId.data)
var peerIds = Set<Int64>()
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
let peerId = key.getInt64(16)
peerIds.insert(peerId)
return true
}, limit: 0)
return peerIds
}
func update(id: Data, size: Int64) {
self.valueBox.begin()
@ -300,14 +389,8 @@ public final class StorageBox {
self.internalAddSize(contentType: info.contentType, delta: sizeDelta)
}
var peerIds: [Int64] = []
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
peerIds.append(key.getInt64(0))
return true
}, limit: 0)
for peerId in peerIds {
let _ = peerId
for peerId in self.peerIdsReferencing(hashId: hashId) {
self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta)
}
}
@ -334,6 +417,8 @@ public final class StorageBox {
self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: ItemInfo(id: id, contentType: contentType, size: size).serialize())
self.internalAddSize(contentType: contentType, delta: size)
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
idKey.setData(0, value: hashId.data)
idKey.setInt64(hashId.data.count, value: reference.peerId)
@ -378,6 +463,8 @@ public final class StorageBox {
}
peerIdCount += 1
self.valueBox.set(self.peerIdTable, key: peerIdKey, value: MemoryBuffer(memory: &peerIdCount, capacity: 4, length: 4, freeWhenDone: false))
self.internalAddSize(peerId: 0, contentType: contentType, delta: size)
}
}
}
@ -564,18 +651,39 @@ public final class StorageBox {
return result
}
func getStats() -> Stats {
var contentTypes: [UInt8: Int64] = [:]
func getAllStats() -> AllStats {
self.valueBox.begin()
let allStats = AllStats(total: StorageBox.Stats(contentTypes: [:]), peers: [:])
self.valueBox.scan(self.contentTypeStatsTable, values: { key, value in
var size: Int64 = 0
value.read(&size, offset: 0, length: 8)
contentTypes[key.getUInt8(0)] = size
allStats.total.contentTypes[key.getUInt8(0)] = size
return true
})
return Stats(contentTypes: contentTypes)
self.valueBox.scan(self.peerContentTypeStatsTable, values: { key, value in
var size: Int64 = 0
value.read(&size, offset: 0, length: 8)
let peerId = key.getInt64(0)
if peerId != 0 {
assert(true)
}
let contentType = key.getUInt8(0)
if allStats.peers[PeerId(peerId)] == nil {
allStats.peers[PeerId(peerId)] = StorageBox.Stats(contentTypes: [:])
}
allStats.peers[PeerId(peerId)]?.contentTypes[contentType] = size
return true
})
self.valueBox.commit()
return allStats
}
}
@ -651,9 +759,9 @@ public final class StorageBox {
}
}
public func getStats() -> Signal<Stats, NoError> {
public func getAllStats() -> Signal<AllStats, NoError> {
return self.impl.signalWith { impl, subscriber in
subscriber.putNext(impl.getStats())
subscriber.putNext(impl.getAllStats())
subscriber.putCompletion()
return EmptyDisposable

View File

@ -52,7 +52,7 @@ private final class CacheUsageStatsState {
var upperBound: MessageIndex?
}
public struct StorageUsageStats: Equatable {
public final class StorageUsageStats {
public enum CategoryKey: Hashable {
case photos
case videos
@ -71,17 +71,17 @@ public struct StorageUsageStats: Equatable {
}
}
public var categories: [CategoryKey: CategoryData]
public fileprivate(set) var categories: [CategoryKey: CategoryData]
public init(categories: [CategoryKey: CategoryData]) {
self.categories = categories
}
}
public struct AllStorageUsageStats: Equatable {
public struct PeerStats: Equatable {
public var peer: EnginePeer
public var stats: StorageUsageStats
public final class AllStorageUsageStats {
public final class PeerStats {
public let peer: EnginePeer
public let stats: StorageUsageStats
public init(peer: EnginePeer, stats: StorageUsageStats) {
self.peer = peer
@ -89,8 +89,8 @@ public struct AllStorageUsageStats: Equatable {
}
}
public var totalStats: StorageUsageStats
public var peers: [EnginePeer.Id: PeerStats]
public fileprivate(set) var totalStats: StorageUsageStats
public fileprivate(set) var peers: [EnginePeer.Id: PeerStats]
public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) {
self.totalStats = totalStats
@ -98,53 +98,10 @@ public struct AllStorageUsageStats: Equatable {
}
}
func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> {
let additionalStats = Signal<Int64, NoError> { subscriber in
DispatchQueue.global().async {
var totalSize: Int64 = 0
let additionalPaths: [String] = [
"cache",
"animation-cache",
"short-cache",
]
for path in additionalPaths {
let fullPath: String
if path.isEmpty {
fullPath = account.postbox.mediaBox.basePath
} else {
fullPath = account.postbox.mediaBox.basePath + "/\(path)"
}
var s = darwin_dirstat()
var result = dirstat_np(fullPath, 1, &s, MemoryLayout<darwin_dirstat>.size)
if result != -1 {
totalSize += Int64(s.total_size)
} else {
result = dirstat_np(fullPath, 0, &s, MemoryLayout<darwin_dirstat>.size)
if result != -1 {
totalSize += Int64(s.total_size)
print(s.descendants)
}
}
}
subscriber.putNext(totalSize)
subscriber.putCompletion()
}
return EmptyDisposable
}
return combineLatest(
additionalStats,
account.postbox.mediaBox.storageBox.getStats()
)
|> deliverOnMainQueue
|> mapToSignal { additionalStats, allStats -> Signal<AllStorageUsageStats, NoError> in
private extension StorageUsageStats {
convenience init(_ stats: StorageBox.Stats) {
var mappedCategories: [StorageUsageStats.CategoryKey: StorageUsageStats.CategoryData] = [:]
for (key, value) in allStats.contentTypes {
for (key, value) in stats.contentTypes {
let mappedCategory: StorageUsageStats.CategoryKey
switch key {
case MediaResourceUserContentType.image.rawValue:
@ -165,14 +122,126 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUs
mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value)
}
if additionalStats != 0 {
mappedCategories[.misc, default: StorageUsageStats.CategoryData(size: 0)].size += additionalStats
self.init(categories: mappedCategories)
}
}
func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> {
let additionalStats = Signal<Int64, NoError> { subscriber in
DispatchQueue.global().async {
var totalSize: Int64 = 0
let additionalPaths: [String] = [
"cache",
"animation-cache",
"short-cache",
]
func statForDirectory(path: String) -> Int64 {
var s = darwin_dirstat()
var result = dirstat_np(path, 1, &s, MemoryLayout<darwin_dirstat>.size)
if result != -1 {
return Int64(s.total_size)
} else {
result = dirstat_np(path, 0, &s, MemoryLayout<darwin_dirstat>.size)
if result != -1 {
return Int64(s.total_size)
} else {
return 0
}
}
}
var delayedDirs: [String] = []
for path in additionalPaths {
let fullPath: String
if path.isEmpty {
fullPath = account.postbox.mediaBox.basePath
} else {
fullPath = account.postbox.mediaBox.basePath + "/\(path)"
}
if path == "animation-cache" {
if let enumerator = FileManager.default.enumerator(at: URL(fileURLWithPath: fullPath), includingPropertiesForKeys: [.isDirectoryKey], options: .skipsSubdirectoryDescendants) {
for url in enumerator {
guard let url = url as? URL else {
continue
}
delayedDirs.append(fullPath + "/" + url.lastPathComponent)
}
}
} else {
totalSize += statForDirectory(path: fullPath)
}
}
if !delayedDirs.isEmpty {
let concurrentSize = Atomic<[Int64]>(value: [])
DispatchQueue.concurrentPerform(iterations: delayedDirs.count, execute: { index in
let directorySize = statForDirectory(path: delayedDirs[index])
let result = concurrentSize.modify { current in
return current + [directorySize]
}
if result.count == delayedDirs.count {
var aggregatedCount: Int64 = 0
for item in result {
aggregatedCount += item
}
subscriber.putNext(totalSize + aggregatedCount)
subscriber.putCompletion()
}
})
} else {
subscriber.putNext(totalSize)
subscriber.putCompletion()
}
}
return .single(AllStorageUsageStats(
totalStats: StorageUsageStats(categories: mappedCategories),
peers: [:]
))
return EmptyDisposable
}
return combineLatest(
additionalStats,
account.postbox.mediaBox.storageBox.getAllStats()
)
|> deliverOnMainQueue
|> mapToSignal { additionalStats, allStats -> Signal<AllStorageUsageStats, NoError> in
return account.postbox.transaction { transaction -> AllStorageUsageStats in
let total = StorageUsageStats(allStats.total)
if additionalStats != 0 {
total.categories[.misc, default: StorageUsageStats.CategoryData(size: 0)].size += additionalStats
}
var peers: [EnginePeer.Id: AllStorageUsageStats.PeerStats] = [:]
for (peerId, peerStats) in allStats.peers {
if peerId.id._internalGetInt64Value() == 0 {
continue
}
var peerSize: Int64 = 0
for (_, size) in peerStats.contentTypes {
peerSize += size
}
if peerSize == 0 {
continue
}
if let peer = transaction.getPeer(peerId), transaction.getPeerChatListIndex(peerId) != nil {
peers[peerId] = AllStorageUsageStats.PeerStats(
peer: EnginePeer(peer),
stats: StorageUsageStats(peerStats)
)
}
}
return AllStorageUsageStats(
totalStats: total,
peers: peers
)
}
}
}

View File

@ -30,6 +30,7 @@ swift_library(
"//submodules/Markdown",
"//submodules/ContextUI",
"//submodules/AnimatedAvatarSetNode",
"//submodules/AvatarNode",
],
visibility = [
"//visibility:public",

View File

@ -110,10 +110,12 @@ final class PieChartComponent: Component {
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 < minAngle {
angle = minAngle
if angle > .ulpOfOne {
if angle < minAngle {
angle = minAngle
}
totalAngle += angle
}
totalAngle += angle
angles.append(angle)
}
if totalAngle > CGFloat.pi * 2.0 {
@ -207,41 +209,160 @@ final class PieChartComponent: Component {
}
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))
var centerOffset: CGFloat = 0.5
var labelFrame: CGRect?
var labelScale: CGFloat = 1.0
if angleValue < 0.38 {
labelScale = angleValue / 0.38
centerOffset = labelScale * 0.6 + (1.0 - labelScale) * 0.5
}
for step in 0 ... 6 {
let stepFraction: CGFloat = CGFloat(step) / 6.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 labelCenter = CGPoint(
x: shapeLayerFrame.midX + cos(midAngle) * centerDistance,
y: shapeLayerFrame.midY + sin(midAngle) * centerDistance
)
let labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - labelSize.width * 0.5, y: labelCenter.y - labelSize.height * 0.5), size: labelSize)
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
}
if let labelView = label.view {
if let labelView = label.view, let labelFrame {
if labelView.superview == nil {
self.addSubview(labelView)
}
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size)
transition.setPosition(view: labelView, position: labelFrame.center)
transition.setScale(view: labelView, scale: labelScale)
let normalAlpha: CGFloat = labelScale < 0.5 ? 0.0 : 1.0
labelView.bounds = CGRect(origin: CGPoint(), size: labelSize)
var labelScale = labelFrame.width / labelSize.width
let normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0
var relLabelCenter = CGPoint(
x: labelFrame.midX - shapeLayerFrame.midX,
y: labelFrame.midY - shapeLayerFrame.midY
)
if let selectedKey = self.selectedKey {
if selectedKey == item.id {
transition.setAlpha(view: labelView, alpha: normalAlpha)
} else {
transition.setAlpha(view: labelView, alpha: 0.0)
let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter)
let reducedDiameterFactor: CGFloat = reducedDiameter / diameter
labelScale *= reducedFactor
relLabelCenter.x *= reducedDiameterFactor
relLabelCenter.y *= reducedDiameterFactor
}
} else {
transition.setAlpha(view: labelView, alpha: normalAlpha)
}
let labelCenter = CGPoint(
x: shapeLayerFrame.midX + relLabelCenter.x,
y: shapeLayerFrame.midY + relLabelCenter.y
)
transition.setPosition(view: labelView, position: labelCenter)
transition.setScale(view: labelView, scale: labelScale)
}
}

View File

@ -0,0 +1,435 @@
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 AvatarNode
private let avatarFont = avatarPlaceholderFont(size: 15.0)
private final class PeerListItemComponent: Component {
let context: AccountContext
let theme: PresentationTheme
let sideInset: CGFloat
let title: String
let peer: EnginePeer?
let label: String
let hasNext: Bool
init(
context: AccountContext,
theme: PresentationTheme,
sideInset: CGFloat,
title: String,
peer: EnginePeer?,
label: String,
hasNext: Bool
) {
self.context = context
self.theme = theme
self.sideInset = sideInset
self.title = title
self.peer = peer
self.label = label
self.hasNext = hasNext
}
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.theme !== rhs.theme {
return false
}
if lhs.sideInset != rhs.sideInset {
return false
}
if lhs.title != rhs.title {
return false
}
if lhs.peer != rhs.peer {
return false
}
if lhs.label != rhs.label {
return false
}
if lhs.hasNext != rhs.hasNext {
return false
}
return true
}
final class View: HighlightTrackingButton {
private let title = ComponentView<Empty>()
private let label = ComponentView<Empty>()
private let separatorLayer: SimpleLayer
private let avatarNode: AvatarNode
private var component: PeerListItemComponent?
override init(frame: CGRect) {
self.separatorLayer = SimpleLayer()
self.avatarNode = AvatarNode(font: avatarFont)
self.avatarNode.isLayerBacked = true
super.init(frame: frame)
self.layer.addSublayer(self.separatorLayer)
self.layer.addSublayer(self.avatarNode.layer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
let height: CGFloat = 52.0
let leftInset: CGFloat = 62.0 + component.sideInset
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))
if let peer = component.peer {
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
}
let labelSize = self.label.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.label, font: Font.regular(17.0), textColor: component.theme.list.itemSecondaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
)
let titleSize = self.title.update(
transition: .immediate,
component: AnyComponent(MultilineTextComponent(
text: .plain(NSAttributedString(string: component.title, font: Font.semibold(17.0), textColor: component.theme.list.itemPrimaryTextColor))
)),
environment: {},
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0)
)
if let titleView = self.title.view {
if titleView.superview == nil {
self.addSubview(titleView)
}
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize))
}
if let labelView = self.label.view {
if labelView.superview == nil {
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))
}
if themeUpdated {
self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor
}
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
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)
}
}
final class StoragePeerListPanelComponent: Component {
typealias EnvironmentType = StorageUsagePanelEnvironment
final class Item: Equatable {
let peer: EnginePeer
let size: Int64
init(
peer: EnginePeer,
size: Int64
) {
self.peer = peer
self.size = size
}
static func ==(lhs: Item, rhs: Item) -> Bool {
if lhs.peer != rhs.peer {
return false
}
if lhs.size != rhs.size {
return false
}
return true
}
}
final class Items: Equatable {
let items: [Item]
init(items: [Item]) {
self.items = items
}
static func ==(lhs: Items, rhs: Items) -> Bool {
if lhs === rhs {
return true
}
return lhs.items == rhs.items
}
}
let context: AccountContext
let items: Items?
init(
context: AccountContext,
items: Items?
) {
self.context = context
self.items = items
}
static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool {
if lhs.context !== rhs.context {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
private struct ItemLayout: Equatable {
let containerInsets: UIEdgeInsets
let containerWidth: CGFloat
let itemHeight: CGFloat
let itemCount: Int
let contentHeight: CGFloat
init(
containerInsets: UIEdgeInsets,
containerWidth: CGFloat,
itemHeight: CGFloat,
itemCount: Int
) {
self.containerInsets = containerInsets
self.containerWidth = containerWidth
self.itemHeight = itemHeight
self.itemCount = itemCount
self.contentHeight = containerInsets.top + containerInsets.bottom + CGFloat(itemCount) * itemHeight
}
func visibleItems(for rect: CGRect) -> Range<Int>? {
let offsetRect = rect.offsetBy(dx: -self.containerInsets.left, dy: -self.containerInsets.top)
var minVisibleRow = Int(floor((offsetRect.minY) / (self.itemHeight)))
minVisibleRow = max(0, minVisibleRow)
let maxVisibleRow = Int(ceil((offsetRect.maxY) / (self.itemHeight)))
let minVisibleIndex = minVisibleRow
let maxVisibleIndex = maxVisibleRow
if maxVisibleIndex >= minVisibleIndex {
return minVisibleIndex ..< (maxVisibleIndex + 1)
} else {
return nil
}
}
func itemFrame(for index: Int) -> CGRect {
return CGRect(origin: CGPoint(x: 0.0, y: self.containerInsets.top + CGFloat(index) * self.itemHeight), size: CGSize(width: self.containerWidth, height: self.itemHeight))
}
}
class View: UIView, UIScrollViewDelegate {
private let scrollView: UIScrollView
private let measureItem = ComponentView<Empty>()
private var visibleItems: [EnginePeer.Id: ComponentView<Empty>] = [:]
private var ignoreScrolling: Bool = false
private var component: StoragePeerListPanelComponent?
private var environment: StorageUsagePanelEnvironment?
private var itemLayout: ItemLayout?
override init(frame: CGRect) {
self.scrollView = UIScrollView()
super.init(frame: frame)
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
self.scrollView.contentInsetAdjustmentBehavior = .never
}
if #available(iOS 13.0, *) {
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
}
self.scrollView.showsVerticalScrollIndicator = true
self.scrollView.showsHorizontalScrollIndicator = false
self.scrollView.alwaysBounceHorizontal = false
self.scrollView.scrollsToTop = false
self.scrollView.delegate = self
self.scrollView.clipsToBounds = true
self.addSubview(self.scrollView)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
self.updateScrolling(transition: .immediate)
}
}
private func updateScrolling(transition: Transition) {
guard let component = self.component, let environment = self.environment, let items = component.items, let itemLayout = self.itemLayout else {
return
}
let visibleBounds = self.scrollView.bounds.insetBy(dx: 0.0, dy: -100.0)
let dataSizeFormatting = DataSizeStringFormatting(strings: environment.strings, decimalSeparator: ".")
var validIds = Set<EnginePeer.Id>()
if let visibleItems = itemLayout.visibleItems(for: visibleBounds) {
for index in visibleItems.lowerBound ..< visibleItems.upperBound {
if index >= items.items.count {
continue
}
let item = items.items[index]
let id = item.peer.id
validIds.insert(id)
var itemTransition = transition
let itemView: ComponentView<Empty>
if let current = self.visibleItems[id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[id] = itemView
}
let _ = itemView.update(
transition: itemTransition,
component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
sideInset: environment.containerInsets.left,
title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
peer: item.peer,
label: dataSizeString(item.size, formatting: dataSizeFormatting),
hasNext: index != items.items.count - 1
)),
environment: {},
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
)
let itemFrame = itemLayout.itemFrame(for: index)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.scrollView.addSubview(itemComponentView)
}
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
}
}
}
var removeIds: [EnginePeer.Id] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
itemComponentView?.removeFromSuperview()
})
}
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
}
func update(component: StoragePeerListPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: Transition) -> CGSize {
self.component = component
let environment = environment[StorageUsagePanelEnvironment.self].value
self.environment = environment
let measureItemSize = self.measureItem.update(
transition: .immediate,
component: AnyComponent(PeerListItemComponent(
context: component.context,
theme: environment.theme,
sideInset: environment.containerInsets.left,
title: "ABCDEF",
peer: nil,
label: "1000",
hasNext: false
)),
environment: {},
containerSize: CGSize(width: availableSize.width, height: 1000.0)
)
let itemLayout = ItemLayout(
containerInsets: environment.containerInsets,
containerWidth: availableSize.width,
itemHeight: measureItemSize.height,
itemCount: component.items?.items.count ?? 0
)
self.itemLayout = itemLayout
self.ignoreScrolling = true
let contentOffset = self.scrollView.bounds.minY
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
self.scrollView.scrollIndicatorInsets = environment.containerInsets
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
let deltaOffset = self.scrollView.bounds.minY - contentOffset
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
}
self.ignoreScrolling = false
self.updateScrolling(transition: .immediate)
return availableSize
}
}
func makeView() -> View {
return View(frame: CGRect())
}
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelEnvironment>, transition: Transition) -> CGSize {
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
}
}

View File

@ -205,7 +205,6 @@ final class StoragePeerTypeItemComponent: Component {
}
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
transition.setAlpha(layer: self.separatorLayer, alpha: component.hasNext ? 1.0 : 0.0)
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + (component.hasNext ? UIScreenPixel : 0.0)))

View File

@ -0,0 +1,377 @@
import Foundation
import UIKit
import Display
import ComponentFlow
import ComponentDisplayAdapters
import TelegramPresentationData
final class StorageUsagePanelEnvironment: Equatable {
let theme: PresentationTheme
let strings: PresentationStrings
let containerInsets: UIEdgeInsets
init(
theme: PresentationTheme,
strings: PresentationStrings,
containerInsets: UIEdgeInsets
) {
self.theme = theme
self.strings = strings
self.containerInsets = containerInsets
}
static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.containerInsets != rhs.containerInsets {
return false
}
return true
}
}
private final class StorageUsageHeaderItemComponent: CombinedComponent {
let theme: PresentationTheme
let title: String
init(
theme: PresentationTheme,
title: String
) {
self.theme = theme
self.title = title
}
static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.title != rhs.title {
return false
}
return true
}
static var body: Body {
let text = 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),
availableSize: context.availableSize,
transition: .immediate
)
context.add(text.position(CGPoint(x: text.size.width * 0.5, y: text.size.height * 0.5)))
return text.size
}
}
}
private final class StorageUsageHeaderComponent: Component {
struct Item: Equatable {
let id: AnyHashable
let title: String
init(
id: AnyHashable,
title: String
) {
self.id = id
self.title = title
}
}
let theme: PresentationTheme
let items: [Item]
init(
theme: PresentationTheme,
items: [Item]
) {
self.theme = theme
self.items = items
}
static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
class View: UIView {
private var component: StorageUsageHeaderComponent?
private var visibleItems: [AnyHashable: ComponentView<Empty>] = [:]
private let activeItemLayer: SimpleLayer
override init(frame: CGRect) {
self.activeItemLayer = SimpleLayer()
self.activeItemLayer.cornerRadius = 2.0
self.activeItemLayer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
super.init(frame: frame)
self.layer.addSublayer(self.activeItemLayer)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
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 {
validIds.insert(item.id)
let itemView: ComponentView<Empty>
var itemTransition = transition
if let current = self.visibleItems[item.id] {
itemView = current
} else {
itemTransition = .immediate
itemView = ComponentView()
self.visibleItems[item.id] = itemView
}
let itemSize = itemView.update(
transition: itemTransition,
component: AnyComponent(StorageUsageHeaderItemComponent(
theme: component.theme,
title: item.title
)),
environment: {},
containerSize: availableSize
)
let itemFrame = CGRect(origin: CGPoint(x: 34.0, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
if let itemComponentView = itemView.view {
if itemComponentView.superview == nil {
self.addSubview(itemComponentView)
}
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 themeUpdated {
self.activeItemLayer.backgroundColor = component.theme.list.itemAccentColor.cgColor
}
var removeIds: [AnyHashable] = []
for (id, itemView) in self.visibleItems {
if !validIds.contains(id) {
removeIds.append(id)
if let itemComponentView = itemView.view {
itemComponentView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visibleItems.removeValue(forKey: id)
}
return availableSize
}
}
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)
}
}
final class StorageUsagePanelContainerComponent: Component {
struct Item: Equatable {
let id: AnyHashable
let title: String
let panel: AnyComponent<StorageUsagePanelEnvironment>
init(
id: AnyHashable,
title: String,
panel: AnyComponent<StorageUsagePanelEnvironment>
) {
self.id = id
self.title = title
self.panel = panel
}
}
let theme: PresentationTheme
let strings: PresentationStrings
let insets: UIEdgeInsets
let items: [Item]
init(
theme: PresentationTheme,
strings: PresentationStrings,
insets: UIEdgeInsets,
items: [Item]
) {
self.theme = theme
self.strings = strings
self.insets = insets
self.items = items
}
static func ==(lhs: StorageUsagePanelContainerComponent, rhs: StorageUsagePanelContainerComponent) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.strings !== rhs.strings {
return false
}
if lhs.insets != rhs.insets {
return false
}
if lhs.items != rhs.items {
return false
}
return true
}
class View: UIView {
private let topPanelBackgroundView: BlurredBackgroundView
private let topPanelSeparatorLayer: SimpleLayer
private let header = ComponentView<Empty>()
private var component: StorageUsagePanelContainerComponent?
private var visiblePanels: [AnyHashable: ComponentView<StorageUsagePanelEnvironment>] = [:]
private var currentId: AnyHashable?
override init(frame: CGRect) {
self.topPanelBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.topPanelSeparatorLayer = SimpleLayer()
super.init(frame: frame)
self.addSubview(self.topPanelBackgroundView)
self.layer.addSublayer(self.topPanelSeparatorLayer)
}
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 {
let themeUpdated = self.component?.theme !== component.theme
self.component = component
if themeUpdated {
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
self.topPanelBackgroundView.updateColor(color: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.8), transition: .immediate)
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
}
let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 44.0))
transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame)
self.topPanelBackgroundView.update(size: topPanelFrame.size, transition: transition.containedViewLayoutTransition)
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
let _ = self.header.update(
transition: transition,
component: AnyComponent(StorageUsageHeaderComponent(
theme: component.theme,
items: component.items.map { item -> StorageUsageHeaderComponent.Item in
return StorageUsageHeaderComponent.Item(
id: item.id,
title: item.title
)
}
)),
environment: {},
containerSize: topPanelFrame.size
)
if let headerView = self.header.view {
if headerView.superview == nil {
self.addSubview(headerView)
}
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)
)
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)
}
panelTransition.setFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: availableSize))
}
}
var removeIds: [AnyHashable] = []
for (id, panel) in self.visiblePanels {
if !validIds.contains(id) {
removeIds.append(id)
if let panelView = panel.view {
panelView.removeFromSuperview()
}
}
}
for id in removeIds {
self.visiblePanels.removeValue(forKey: id)
}
return availableSize
}
}
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

@ -41,6 +41,21 @@ private final class StorageUsageScreenComponent: Component {
override func touchesShouldCancel(in view: UIView) -> Bool {
return true
}
override var contentOffset: CGPoint {
set(value) {
var value = value
if value.y > self.contentSize.height - self.bounds.height {
value.y = self.contentSize.height - self.bounds.height
self.bounces = false
} else {
self.bounces = true
}
super.contentOffset = value
} get {
return super.contentOffset
}
}
}
private final class AnimationHint {
@ -56,6 +71,7 @@ private final class StorageUsageScreenComponent: Component {
private var currentStats: AllStorageUsageStats?
private var cacheSettings: CacheStorageSettings?
private var peerItems: StoragePeerListPanelComponent.Items?
private var selectedCategories: Set<AnyHashable> = Set()
@ -79,11 +95,17 @@ private final class StorageUsageScreenComponent: Component {
private var keepDurationSectionContainerView: UIView
private var keepDurationItems: [AnyHashable: ComponentView<Empty>] = [:]
private let panelContainer = ComponentView<Empty>()
private var component: StorageUsageScreenComponent?
private weak var state: EmptyComponentState?
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
private var controller: (() -> ViewController?)?
private var enableVelocityTracking: Bool = false
private var previousVelocityM1: CGFloat = 0.0
private var previousVelocity: CGFloat = 0.0
private var ignoreScrolling: Bool = false
private var statsDisposable: Disposable?
@ -107,7 +129,6 @@ private final class StorageUsageScreenComponent: Component {
super.init(frame: frame)
self.scrollView.layer.anchorPoint = CGPoint()
self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false
@ -144,12 +165,49 @@ private final class StorageUsageScreenComponent: Component {
self.statsDisposable?.dispose()
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.enableVelocityTracking = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling {
if self.enableVelocityTracking {
self.previousVelocityM1 = self.previousVelocity
if let value = (scrollView.value(forKey: (["_", "verticalVelocity"] as [String]).joined()) as? NSNumber)?.doubleValue {
self.previousVelocity = CGFloat(value)
}
}
self.updateScrolling(transition: .immediate)
}
}
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
guard let navigationMetrics = self.navigationMetrics else {
return
}
let _ = navigationMetrics
let paneAreaExpansionDistance: CGFloat = 32.0
let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height
if targetContentOffset.pointee.y > paneAreaExpansionFinalPoint - paneAreaExpansionDistance && targetContentOffset.pointee.y < paneAreaExpansionFinalPoint {
targetContentOffset.pointee.y = paneAreaExpansionFinalPoint
self.enableVelocityTracking = false
self.previousVelocity = 0.0
self.previousVelocityM1 = 0.0
}
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
if let panelContainerView = self.panelContainer.view as? StorageUsagePanelContainerComponent.View {
let _ = panelContainerView
let paneAreaExpansionFinalPoint: CGFloat = scrollView.contentSize.height - scrollView.bounds.height
if abs(scrollView.contentOffset.y - paneAreaExpansionFinalPoint) < .ulpOfOne {
//panelContainerView.transferVelocity(self.previousVelocityM1)
}
}
}
private func updateScrolling(transition: Transition) {
let scrollBounds = self.scrollView.bounds
@ -183,6 +241,8 @@ private final class StorageUsageScreenComponent: Component {
self.component = component
self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
if self.statsDisposable == nil {
self.cacheSettingsDisposable = (component.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|> map { sharedData -> CacheStorageSettings in
@ -210,10 +270,40 @@ private final class StorageUsageScreenComponent: Component {
return
}
self.currentStats = stats
var peerItems: [StoragePeerListPanelComponent.Item] = []
for item in stats.peers.values.sorted(by: { lhs, rhs in
let lhsSize: Int64 = lhs.stats.categories.values.reduce(0, {
$0 + $1.size
})
let rhsSize: Int64 = rhs.stats.categories.values.reduce(0, {
$0 + $1.size
})
return lhsSize > rhsSize
}) {
let itemSize: Int64 = item.stats.categories.values.reduce(0, {
$0 + $1.size
})
peerItems.append(StoragePeerListPanelComponent.Item(
peer: item.peer,
size: itemSize
))
}
self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems)
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(isFirstStatsUpdate: true)))
})
}
var wasLockedAtPanels = false
if let panelContainerView = self.panelContainer.view, let navigationMetrics = self.navigationMetrics {
if abs(self.scrollView.bounds.minY - (panelContainerView.frame.minY - navigationMetrics.navigationHeight)) <= UIScreenPixel {
wasLockedAtPanels = true
}
}
let animationHint = transition.userData(AnimationHint.self)
if let animationHint, animationHint.isFirstStatsUpdate {
@ -228,8 +318,6 @@ private final class StorageUsageScreenComponent: Component {
transition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0)
}
let environment = environment[ViewControllerComponentContainer.Environment.self].value
self.controller = environment.controller
self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight)
@ -300,7 +388,7 @@ private final class StorageUsageScreenComponent: Component {
var contentHeight: CGFloat = 0.0
let topInset: CGFloat = 19.0
let sideInset: CGFloat = 16.0
let sideInset: CGFloat = 16.0 + environment.safeInsets.left
contentHeight += environment.statusBarHeight + topInset
@ -682,17 +770,52 @@ private final class StorageUsageScreenComponent: Component {
contentHeight += keepDurationDescriptionSize.height
contentHeight += 40.0
contentHeight += availableSize.height
//TODO:localize
let panelContainerSize = self.panelContainer.update(
transition: transition,
component: AnyComponent(StorageUsagePanelContainerComponent(
theme: environment.theme,
strings: environment.strings,
insets: UIEdgeInsets(top: 0.0, left: environment.safeInsets.left, bottom: environment.safeInsets.bottom, right: environment.safeInsets.right),
items: [
StorageUsagePanelContainerComponent.Item(
id: "peers",
title: "Chats",
panel: AnyComponent(StoragePeerListPanelComponent(
context: component.context,
items: self.peerItems
))
)
])
),
environment: {},
containerSize: CGSize(width: availableSize.width, height: availableSize.height - environment.navigationHeight)
)
if let panelContainerView = self.panelContainer.view {
if panelContainerView.superview == nil {
self.scrollView.addSubview(panelContainerView)
}
transition.setFrame(view: panelContainerView, frame: CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: panelContainerSize))
}
contentHeight += panelContainerSize.height
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)
let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.contentSize != contentSize {
self.scrollView.contentSize = contentSize
}
if !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
var scrollViewBounds = self.scrollView.bounds
scrollViewBounds.size = availableSize
if wasLockedAtPanels, let panelContainerView = self.panelContainer.view {
scrollViewBounds.origin.y = panelContainerView.frame.minY - environment.navigationHeight
}
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
if !wasLockedAtPanels && !transition.animation.isImmediate && self.scrollView.bounds.minY != contentOffset {
let deltaOffset = self.scrollView.bounds.minY - contentOffset
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
}

View File

@ -116,6 +116,9 @@ static bool notyfyingShiftState = false;
@implementation UIScrollView (FrameRateRangeOverride)
- (void)fixScrollDisplayLink {
if (@available(iOS 16.0, *)) {
return;
}
static NSString *scrollHeartbeatKey = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{