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 final class StorageBox {
public struct Stats { public final class Stats {
public var contentTypes: [UInt8: Int64] public fileprivate(set) var contentTypes: [UInt8: Int64]
public init(contentTypes: [UInt8: Int64]) { public init(contentTypes: [UInt8: Int64]) {
self.contentTypes = contentTypes 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 struct Reference {
public var peerId: Int64 public var peerId: Int64
public var messageNamespace: UInt8 public var messageNamespace: UInt8
@ -122,6 +132,10 @@ public final class StorageBox {
} }
} }
private struct Metadata: Codable {
var version: Int32
}
private final class Impl { private final class Impl {
let queue: Queue let queue: Queue
let logger: StorageBox.Logger let logger: StorageBox.Logger
@ -133,6 +147,7 @@ public final class StorageBox {
let peerIdTable: ValueBoxTable let peerIdTable: ValueBoxTable
let peerContentTypeStatsTable: ValueBoxTable let peerContentTypeStatsTable: ValueBoxTable
let contentTypeStatsTable: ValueBoxTable let contentTypeStatsTable: ValueBoxTable
let metadataTable: ValueBoxTable
init(queue: Queue, logger: StorageBox.Logger, basePath: String) { init(queue: Queue, logger: StorageBox.Logger, basePath: String) {
self.queue = queue self.queue = queue
@ -157,6 +172,54 @@ public final class StorageBox {
self.peerIdTable = ValueBoxTable(id: 18, keyType: .binary, compactValuesOnCreation: true) self.peerIdTable = ValueBoxTable(id: 18, keyType: .binary, compactValuesOnCreation: true)
self.peerContentTypeStatsTable = ValueBoxTable(id: 19, keyType: .binary, compactValuesOnCreation: true) self.peerContentTypeStatsTable = ValueBoxTable(id: 19, keyType: .binary, compactValuesOnCreation: true)
self.contentTypeStatsTable = ValueBoxTable(id: 20, 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) { private func internalAddSize(contentType: UInt8, delta: Int64) {
@ -171,7 +234,7 @@ public final class StorageBox {
currentSize += delta currentSize += delta
if currentSize < 0 { if currentSize < 0 {
assertionFailure() //assertionFailure()
currentSize = 0 currentSize = 0
} }
@ -184,18 +247,18 @@ public final class StorageBox {
key.setUInt8(8, value: contentType) key.setUInt8(8, value: contentType)
var currentSize: Int64 = 0 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) value.read(&currentSize, offset: 0, length: 8)
} }
currentSize += delta currentSize += delta
if currentSize < 0 { if currentSize < 0 {
assertionFailure() //assertionFailure()
currentSize = 0 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) { func add(reference: Reference, to id: Data, contentType: UInt8) {
@ -224,6 +287,10 @@ public final class StorageBox {
if size != 0 { if size != 0 {
self.internalAddSize(contentType: previousContentType, delta: -size) self.internalAddSize(contentType: previousContentType, delta: -size)
self.internalAddSize(contentType: contentType, 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() 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) { func update(id: Data, size: Int64) {
self.valueBox.begin() self.valueBox.begin()
@ -300,14 +389,8 @@ public final class StorageBox {
self.internalAddSize(contentType: info.contentType, delta: sizeDelta) self.internalAddSize(contentType: info.contentType, delta: sizeDelta)
} }
var peerIds: [Int64] = [] for peerId in self.peerIdsReferencing(hashId: hashId) {
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in self.internalAddSize(peerId: peerId, contentType: info.contentType, delta: sizeDelta)
peerIds.append(key.getInt64(0))
return true
}, limit: 0)
for peerId in peerIds {
let _ = peerId
} }
} }
@ -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.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) let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
idKey.setData(0, value: hashId.data) idKey.setData(0, value: hashId.data)
idKey.setInt64(hashId.data.count, value: reference.peerId) idKey.setInt64(hashId.data.count, value: reference.peerId)
@ -378,6 +463,8 @@ public final class StorageBox {
} }
peerIdCount += 1 peerIdCount += 1
self.valueBox.set(self.peerIdTable, key: peerIdKey, value: MemoryBuffer(memory: &peerIdCount, capacity: 4, length: 4, freeWhenDone: false)) 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 return result
} }
func getStats() -> Stats { func getAllStats() -> AllStats {
var contentTypes: [UInt8: Int64] = [:] self.valueBox.begin()
let allStats = AllStats(total: StorageBox.Stats(contentTypes: [:]), peers: [:])
self.valueBox.scan(self.contentTypeStatsTable, values: { key, value in self.valueBox.scan(self.contentTypeStatsTable, values: { key, value in
var size: Int64 = 0 var size: Int64 = 0
value.read(&size, offset: 0, length: 8) value.read(&size, offset: 0, length: 8)
contentTypes[key.getUInt8(0)] = size allStats.total.contentTypes[key.getUInt8(0)] = size
return true 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 return self.impl.signalWith { impl, subscriber in
subscriber.putNext(impl.getStats()) subscriber.putNext(impl.getAllStats())
subscriber.putCompletion() subscriber.putCompletion()
return EmptyDisposable return EmptyDisposable

View File

@ -52,7 +52,7 @@ private final class CacheUsageStatsState {
var upperBound: MessageIndex? var upperBound: MessageIndex?
} }
public struct StorageUsageStats: Equatable { public final class StorageUsageStats {
public enum CategoryKey: Hashable { public enum CategoryKey: Hashable {
case photos case photos
case videos 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]) { public init(categories: [CategoryKey: CategoryData]) {
self.categories = categories self.categories = categories
} }
} }
public struct AllStorageUsageStats: Equatable { public final class AllStorageUsageStats {
public struct PeerStats: Equatable { public final class PeerStats {
public var peer: EnginePeer public let peer: EnginePeer
public var stats: StorageUsageStats public let stats: StorageUsageStats
public init(peer: EnginePeer, stats: StorageUsageStats) { public init(peer: EnginePeer, stats: StorageUsageStats) {
self.peer = peer self.peer = peer
@ -89,8 +89,8 @@ public struct AllStorageUsageStats: Equatable {
} }
} }
public var totalStats: StorageUsageStats public fileprivate(set) var totalStats: StorageUsageStats
public var peers: [EnginePeer.Id: PeerStats] public fileprivate(set) var peers: [EnginePeer.Id: PeerStats]
public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) { public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) {
self.totalStats = totalStats self.totalStats = totalStats
@ -98,53 +98,10 @@ public struct AllStorageUsageStats: Equatable {
} }
} }
func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> { private extension StorageUsageStats {
let additionalStats = Signal<Int64, NoError> { subscriber in convenience init(_ stats: StorageBox.Stats) {
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
var mappedCategories: [StorageUsageStats.CategoryKey: StorageUsageStats.CategoryData] = [:] var mappedCategories: [StorageUsageStats.CategoryKey: StorageUsageStats.CategoryData] = [:]
for (key, value) in allStats.contentTypes { for (key, value) in stats.contentTypes {
let mappedCategory: StorageUsageStats.CategoryKey let mappedCategory: StorageUsageStats.CategoryKey
switch key { switch key {
case MediaResourceUserContentType.image.rawValue: case MediaResourceUserContentType.image.rawValue:
@ -165,14 +122,126 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUs
mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value) mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value)
} }
if additionalStats != 0 { self.init(categories: mappedCategories)
mappedCategories[.misc, default: StorageUsageStats.CategoryData(size: 0)].size += additionalStats }
}
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( return EmptyDisposable
totalStats: StorageUsageStats(categories: mappedCategories), }
peers: [:]
)) 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/Markdown",
"//submodules/ContextUI", "//submodules/ContextUI",
"//submodules/AnimatedAvatarSetNode", "//submodules/AnimatedAvatarSetNode",
"//submodules/AvatarNode",
], ],
visibility = [ visibility = [
"//visibility:public", "//visibility:public",

View File

@ -110,10 +110,12 @@ final class PieChartComponent: Component {
for i in 0 ..< component.chartData.items.count { for i in 0 ..< component.chartData.items.count {
let item = component.chartData.items[i] let item = component.chartData.items[i]
var angle = item.value / valueSum * CGFloat.pi * 2.0 var angle = item.value / valueSum * CGFloat.pi * 2.0
if angle < minAngle { if angle > .ulpOfOne {
angle = minAngle if angle < minAngle {
angle = minAngle
}
totalAngle += angle
} }
totalAngle += angle
angles.append(angle) angles.append(angle)
} }
if totalAngle > CGFloat.pi * 2.0 { 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)) 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 for step in 0 ... 6 {
if angleValue < 0.38 { let stepFraction: CGFloat = CGFloat(step) / 6.0
labelScale = angleValue / 0.38 let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
centerOffset = labelScale * 0.6 + (1.0 - labelScale) * 0.5
}
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 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) * centerOffset)
let labelCenter = CGPoint(
x: shapeLayerFrame.midX + cos(midAngle) * centerDistance, let relLabelCenter = CGPoint(
y: shapeLayerFrame.midY + sin(midAngle) * centerDistance x: cos(midAngle) * centerDistance,
) y: 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 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 { if labelView.superview == nil {
self.addSubview(labelView) 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 let selectedKey = self.selectedKey {
if selectedKey == item.id { if selectedKey == item.id {
transition.setAlpha(view: labelView, alpha: normalAlpha) transition.setAlpha(view: labelView, alpha: normalAlpha)
} else { } else {
transition.setAlpha(view: labelView, alpha: 0.0) 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 { } else {
transition.setAlpha(view: labelView, alpha: normalAlpha) 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.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) 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))) 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 { override func touchesShouldCancel(in view: UIView) -> Bool {
return true 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 { private final class AnimationHint {
@ -56,6 +71,7 @@ private final class StorageUsageScreenComponent: Component {
private var currentStats: AllStorageUsageStats? private var currentStats: AllStorageUsageStats?
private var cacheSettings: CacheStorageSettings? private var cacheSettings: CacheStorageSettings?
private var peerItems: StoragePeerListPanelComponent.Items?
private var selectedCategories: Set<AnyHashable> = Set() private var selectedCategories: Set<AnyHashable> = Set()
@ -79,11 +95,17 @@ private final class StorageUsageScreenComponent: Component {
private var keepDurationSectionContainerView: UIView private var keepDurationSectionContainerView: UIView
private var keepDurationItems: [AnyHashable: ComponentView<Empty>] = [:] private var keepDurationItems: [AnyHashable: ComponentView<Empty>] = [:]
private let panelContainer = ComponentView<Empty>()
private var component: StorageUsageScreenComponent? private var component: StorageUsageScreenComponent?
private weak var state: EmptyComponentState? private weak var state: EmptyComponentState?
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)? private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
private var controller: (() -> ViewController?)? 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 ignoreScrolling: Bool = false
private var statsDisposable: Disposable? private var statsDisposable: Disposable?
@ -107,7 +129,6 @@ private final class StorageUsageScreenComponent: Component {
super.init(frame: frame) super.init(frame: frame)
self.scrollView.layer.anchorPoint = CGPoint()
self.scrollView.delaysContentTouches = true self.scrollView.delaysContentTouches = true
self.scrollView.canCancelContentTouches = true self.scrollView.canCancelContentTouches = true
self.scrollView.clipsToBounds = false self.scrollView.clipsToBounds = false
@ -144,12 +165,49 @@ private final class StorageUsageScreenComponent: Component {
self.statsDisposable?.dispose() self.statsDisposable?.dispose()
} }
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
self.enableVelocityTracking = true
}
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
if !self.ignoreScrolling { 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) 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) { private func updateScrolling(transition: Transition) {
let scrollBounds = self.scrollView.bounds let scrollBounds = self.scrollView.bounds
@ -183,6 +241,8 @@ private final class StorageUsageScreenComponent: Component {
self.component = component self.component = component
self.state = state self.state = state
let environment = environment[ViewControllerComponentContainer.Environment.self].value
if self.statsDisposable == nil { if self.statsDisposable == nil {
self.cacheSettingsDisposable = (component.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings]) self.cacheSettingsDisposable = (component.context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|> map { sharedData -> CacheStorageSettings in |> map { sharedData -> CacheStorageSettings in
@ -210,10 +270,40 @@ private final class StorageUsageScreenComponent: Component {
return return
} }
self.currentStats = stats 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))) 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) let animationHint = transition.userData(AnimationHint.self)
if let animationHint, animationHint.isFirstStatsUpdate { 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) 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.controller = environment.controller
self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight) self.navigationMetrics = (environment.navigationHeight, environment.statusBarHeight)
@ -300,7 +388,7 @@ private final class StorageUsageScreenComponent: Component {
var contentHeight: CGFloat = 0.0 var contentHeight: CGFloat = 0.0
let topInset: CGFloat = 19.0 let topInset: CGFloat = 19.0
let sideInset: CGFloat = 16.0 let sideInset: CGFloat = 16.0 + environment.safeInsets.left
contentHeight += environment.statusBarHeight + topInset contentHeight += environment.statusBarHeight + topInset
@ -682,17 +770,52 @@ private final class StorageUsageScreenComponent: Component {
contentHeight += keepDurationDescriptionSize.height contentHeight += keepDurationDescriptionSize.height
contentHeight += 40.0 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 self.ignoreScrolling = true
let contentOffset = self.scrollView.bounds.minY 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) let contentSize = CGSize(width: availableSize.width, height: contentHeight)
if self.scrollView.contentSize != contentSize { if self.scrollView.contentSize != contentSize {
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 let deltaOffset = self.scrollView.bounds.minY - contentOffset
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true) 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) @implementation UIScrollView (FrameRateRangeOverride)
- (void)fixScrollDisplayLink { - (void)fixScrollDisplayLink {
if (@available(iOS 16.0, *)) {
return;
}
static NSString *scrollHeartbeatKey = nil; static NSString *scrollHeartbeatKey = nil;
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{