A coarse approximation of storage management UI

This commit is contained in:
Ali 2022-12-24 20:25:56 +04:00
parent 808f5b80ff
commit d3f078410d
8 changed files with 614 additions and 100 deletions

View File

@ -7299,6 +7299,7 @@ Sorry for the inconvenience.";
"Contacts.Sort.ByLastSeen" = "by Last Seen";
"ClearCache.Progress" = "Clearing the Cache • %d%";
"ClearCache.NoProgress" = "Clearing the Cache";
"ClearCache.KeepOpenedDescription" = "Please keep this window open until the clearing is completed.";
"Share.ShareAsLink" = "Share as Link";

View File

@ -77,6 +77,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
case resetDatabaseAndCache(PresentationTheme)
case resetHoles(PresentationTheme)
case reindexUnread(PresentationTheme)
case resetCacheIndex
case reindexCache
case resetBiometricsData(PresentationTheme)
case resetWebViewCache(PresentationTheme)
@ -112,7 +113,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return DebugControllerSection.logging.rawValue
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
return DebugControllerSection.experiments.rawValue
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases:
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetCacheIndex, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases:
return DebugControllerSection.experiments.rawValue
case .preferredVideoCodec:
return DebugControllerSection.videoExperiments.rawValue
@ -171,42 +172,44 @@ private enum DebugControllerEntry: ItemListNodeEntry {
return 21
case .reindexUnread:
return 22
case .reindexCache:
case .resetCacheIndex:
return 23
case .resetBiometricsData:
case .reindexCache:
return 24
case .resetWebViewCache:
case .resetBiometricsData:
return 25
case .optimizeDatabase:
case .resetWebViewCache:
return 26
case .photoPreview:
case .optimizeDatabase:
return 27
case .knockoutWallpaper:
case .photoPreview:
return 28
case .experimentalCompatibility:
case .knockoutWallpaper:
return 29
case .enableDebugDataDisplay:
case .experimentalCompatibility:
return 30
case .acceleratedStickers:
case .enableDebugDataDisplay:
return 31
case .experimentalBackground:
case .acceleratedStickers:
return 32
case .inlineForums:
case .experimentalBackground:
return 33
case .localTranscription:
case .inlineForums:
return 34
case .enableReactionOverrides:
case .localTranscription:
return 35
case .restorePurchases:
case .enableReactionOverrides:
return 36
case .playerEmbedding:
case .restorePurchases:
return 37
case .playlistPlayback:
case .playerEmbedding:
return 38
case .voiceConference:
case .playlistPlayback:
return 39
case .voiceConference:
return 40
case let .preferredVideoCodec(index, _, _, _):
return 40 + index
return 41 + index
case .disableVideoAspectScaling:
return 100
case .enableVoipTcp:
@ -970,6 +973,14 @@ private enum DebugControllerEntry: ItemListNodeEntry {
controller.dismiss()
})
})
case .resetCacheIndex:
return ItemListActionItem(presentationData: presentationData, title: "Reset Cache Index [!]", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
return
}
context.account.postbox.mediaBox.storageBox.reset()
})
case .reindexCache:
return ItemListActionItem(presentationData: presentationData, title: "Reindex Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
guard let context = arguments.context else {
@ -1253,6 +1264,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
entries.append(.resetHoles(presentationData.theme))
if isMainApp {
entries.append(.reindexUnread(presentationData.theme))
entries.append(.resetCacheIndex)
entries.append(.reindexCache)
entries.append(.resetWebViewCache(presentationData.theme))
}

View File

@ -230,6 +230,19 @@ public final class StorageBox {
})
}
func reset() {
self.valueBox.begin()
self.valueBox.removeAllFromTable(self.hashIdToInfoTable)
self.valueBox.removeAllFromTable(self.idToReferenceTable)
self.valueBox.removeAllFromTable(self.peerIdToIdTable)
self.valueBox.removeAllFromTable(self.peerContentTypeStatsTable)
self.valueBox.removeAllFromTable(self.contentTypeStatsTable)
self.valueBox.removeAllFromTable(self.metadataTable)
self.valueBox.commit()
}
private func internalAddSize(contentType: UInt8, delta: Int64) {
let key = ValueBoxKey(length: 1)
key.setUInt8(0, value: contentType)
@ -893,4 +906,10 @@ public final class StorageBox {
completion(ids)
}
}
public func reset() {
self.impl.with { impl in
impl.reset()
}
}
}

View File

@ -91,10 +91,14 @@ public final class AllStorageUsageStats {
}
}
public var deviceAvailableSpace: Int64
public var deviceFreeSpace: Int64
public fileprivate(set) var totalStats: StorageUsageStats
public fileprivate(set) var peers: [EnginePeer.Id: PeerStats]
public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) {
public init(deviceAvailableSpace: Int64, deviceFreeSpace: Int64, totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) {
self.deviceAvailableSpace = deviceAvailableSpace
self.deviceFreeSpace = deviceFreeSpace
self.totalStats = totalStats
self.peers = peers
}
@ -242,7 +246,13 @@ func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUs
}
}
let systemAttributes = try? FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
let deviceAvailableSpace = (systemAttributes?[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
let deviceFreeSpace = (systemAttributes?[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
return AllStorageUsageStats(
deviceAvailableSpace: deviceAvailableSpace,
deviceFreeSpace: deviceFreeSpace,
totalStats: total,
peers: peers
)
@ -257,7 +267,8 @@ func _internal_renderStorageUsageStatsMessages(account: Account, stats: StorageU
if !categories.contains(category) {
continue
}
for id in value.messages.keys {
for (id, _) in value.messages.sorted(by: { $0.value >= $1.value }).prefix(1000) {
if result[id] == nil {
if let message = existingMessages[id] {
result[id] = message

View File

@ -33,6 +33,10 @@ swift_library(
"//submodules/AvatarNode",
"//submodules/PhotoResources",
"//submodules/SemanticStatusNode",
"//submodules/RadialStatusNode",
"//submodules/UndoUI",
"//submodules/AnimatedStickerNode",
"//submodules/TelegramAnimatedStickerNode",
],
visibility = [
"//visibility:public",

View File

@ -87,12 +87,12 @@ private func processChartData(data: PieChartComponent.ChartData) -> PieChartComp
final class PieChartComponent: Component {
struct ChartData: Equatable {
struct Item: Equatable {
var id: AnyHashable
var id: StorageUsageScreenComponent.Category
var displayValue: Double
var value: Double
var color: UIColor
init(id: AnyHashable, displayValue: Double, value: Double, color: UIColor) {
init(id: StorageUsageScreenComponent.Category, displayValue: Double, value: Double, color: UIColor) {
self.id = id
self.displayValue = displayValue
self.value = value
@ -131,12 +131,12 @@ final class PieChartComponent: Component {
private final class ChartDataView: UIView {
private(set) var theme: PresentationTheme?
private(set) var data: ChartData?
private(set) var selectedKey: AnyHashable?
private(set) var selectedKey: StorageUsageScreenComponent.Category?
private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)?
private var animator: DisplayLinkAnimator?
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
private var labels: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
override init(frame: CGRect) {
super.init(frame: frame)
@ -153,7 +153,7 @@ final class PieChartComponent: Component {
self.animator?.invalidate()
}
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) {
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: StorageUsageScreenComponent.Category?, animated: Bool) {
let data = processChartData(data: data)
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
@ -366,7 +366,7 @@ final class PieChartComponent: Component {
var minDistance: CGFloat = 1000.0
for distance in distances {
minDistance = min(minDistance, distance + 1.0)
minDistance = min(minDistance, distance)
}
let diagonalAngle = atan2(labelSize.height, labelSize.width)
@ -467,8 +467,8 @@ final class PieChartComponent: Component {
class View: UIView {
private let dataView: ChartDataView
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
var selectedKey: AnyHashable?
private var labels: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
var selectedKey: StorageUsageScreenComponent.Category?
private weak var state: EmptyComponentState?

View File

@ -167,7 +167,7 @@ final class StorageCategoryItemComponent: Component {
}
func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
let themeUpdated = self.component?.theme !== component.theme
let themeUpdated = self.component?.theme !== component.theme || self.component?.category.color != component.category.color
self.component = component
@ -276,7 +276,21 @@ final class StorageCategoryItemComponent: Component {
transition.setFrame(view: labelView, frame: labelFrame)
}
var copyCheckLayer: CheckLayer?
if themeUpdated {
if !transition.animation.isImmediate {
let copyLayer = CheckLayer(theme: self.checkLayer.theme)
copyLayer.frame = self.checkLayer.frame
copyLayer.setSelected(self.checkLayer.selected, animated: false)
self.layer.addSublayer(copyLayer)
copyCheckLayer = copyLayer
transition.setAlpha(layer: copyLayer, alpha: 0.0, completion: { [weak copyLayer] _ in
copyLayer?.removeFromSuperlayer()
})
self.checkLayer.opacity = 0.0
transition.setAlpha(layer: self.checkLayer, alpha: 1.0)
}
self.checkLayer.theme = CheckNodeTheme(
backgroundColor: component.category.color,
strokeColor: component.theme.list.itemCheckColors.foregroundColor,
@ -289,7 +303,11 @@ final class StorageCategoryItemComponent: Component {
let checkDiameter: CGFloat = 22.0
let checkFrame = CGRect(origin: CGPoint(x: titleFrame.minX - 20.0 - checkDiameter, y: floor((height - checkDiameter) / 2.0)), size: CGSize(width: checkDiameter, height: checkDiameter))
self.checkLayer.frame = checkFrame
transition.setFrame(layer: self.checkLayer, frame: checkFrame)
if let copyCheckLayer {
transition.setFrame(layer: copyCheckLayer, frame: checkFrame)
}
transition.setFrame(view: self.checkButtonArea, frame: CGRect(origin: CGPoint(x: additionalLeftInset, y: 0.0), size: CGSize(width: leftInset - additionalLeftInset, height: height)))

View File

@ -16,6 +16,11 @@ import Markdown
import ContextUI
import AnimatedAvatarSetNode
import AvatarNode
import RadialStatusNode
import UndoUI
import AnimatedStickerNode
import TelegramAnimatedStickerNode
import TelegramStringFormatting
private extension StorageUsageScreenComponent.Category {
init(_ category: StorageUsageStats.CategoryKey) {
@ -110,6 +115,13 @@ final class StorageUsageScreenComponent: Component {
self.selectedMessages = selectedMessages
}
convenience init() {
self.init(
selectedPeers: Set(),
selectedMessages: Set()
)
}
static func ==(lhs: SelectionState, rhs: SelectionState) -> Bool {
if lhs.selectedPeers != rhs.selectedPeers {
return false
@ -206,6 +218,8 @@ final class StorageUsageScreenComponent: Component {
private let scrollView: ScrollViewImpl
private var currentStats: AllStorageUsageStats?
private var existingCategories: Set<Category> = Set()
private var currentMessages: [MessageId: Message] = [:]
private var cacheSettings: CacheStorageSettings?
private var peerItems: StoragePeerListPanelComponent.Items?
@ -215,6 +229,8 @@ final class StorageUsageScreenComponent: Component {
private var selectionState: SelectionState?
private var isClearing: Bool = false
private var selectedCategories: Set<Category> = Set()
private var isOtherCategoryExpanded: Bool = false
@ -232,6 +248,9 @@ final class StorageUsageScreenComponent: Component {
private var chartAvatarNode: AvatarNode?
private var doneStatusCircle: SimpleShapeLayer?
private var doneStatusNode: RadialStatusNode?
private let pieChartView = ComponentView<Empty>()
private let chartTotalLabel = ComponentView<Empty>()
private let categoriesView = ComponentView<Empty>()
@ -246,6 +265,8 @@ final class StorageUsageScreenComponent: Component {
private var selectionPanel: ComponentView<Empty>?
private var clearingNode: StorageUsageClearProgressOverlayNode?
private var component: StorageUsageScreenComponent?
private weak var state: EmptyComponentState?
private var navigationMetrics: (navigationHeight: CGFloat, statusBarHeight: CGFloat)?
@ -267,6 +288,7 @@ final class StorageUsageScreenComponent: Component {
self.navigationBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
self.navigationSeparatorLayer = SimpleLayer()
self.navigationSeparatorLayer.opacity = 0.0
self.scrollView = ScrollViewImpl()
@ -434,7 +456,7 @@ final class StorageUsageScreenComponent: Component {
}
})
self.reloadStats(firstTime: true)
self.reloadStats(firstTime: true, completion: {})
}
var wasLockedAtPanels = false
@ -446,13 +468,20 @@ final class StorageUsageScreenComponent: Component {
let animationHint = transition.userData(AnimationHint.self)
if let animationHint, case .firstStatsUpdate = animationHint.value {
var alphaTransition = transition
if let animationHint {
if case .firstStatsUpdate = animationHint.value {
alphaTransition = .easeInOut(duration: 0.25)
}
let alphaTransition: Transition = .easeInOut(duration: 0.25)
alphaTransition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0)
alphaTransition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0)
} else if case .clearedItems = animationHint.value {
if let snapshotView = self.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = self.bounds
self.addSubview(snapshotView)
snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak snapshotView] _ in
snapshotView?.removeFromSuperview()
})
}
}
} else {
transition.setAlpha(view: self.scrollView, alpha: self.currentStats != nil ? 1.0 : 0.0)
transition.setAlpha(view: self.headerOffsetContainer, alpha: self.currentStats != nil ? 1.0 : 0.0)
@ -620,19 +649,17 @@ final class StorageUsageScreenComponent: Component {
.misc
]
if let animationHint, case .firstStatsUpdate = animationHint.value, let currentStats = self.currentStats {
let contextStats: StorageUsageStats
if let peer = component.peer {
contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
} else {
contextStats = currentStats.totalStats
if let _ = self.currentStats {
if let animationHint {
switch animationHint.value {
case .firstStatsUpdate, .clearedItems:
self.selectedCategories = self.existingCategories
}
}
for (category, value) in contextStats.categories {
if value.size != 0 {
self.selectedCategories.insert(StorageUsageScreenComponent.Category(category))
}
}
self.selectedCategories.formIntersection(self.existingCategories)
} else {
self.selectedCategories.removeAll()
}
var chartItems: [PieChartComponent.ChartData.Item] = []
@ -697,7 +724,7 @@ final class StorageUsageScreenComponent: Component {
var chartCategoryColor = category.color
if !self.isOtherCategoryExpanded && otherCategories.contains(category) {
chartCategoryColor = Category.other.color
chartCategoryColor = Category.misc.color
}
chartItems.append(PieChartComponent.ChartData.Item(id: category, displayValue: categoryFraction, value: categoryChartFraction, color: chartCategoryColor))
@ -732,8 +759,36 @@ final class StorageUsageScreenComponent: Component {
let isSelected = otherListCategories.allSatisfy { item in
return self.selectedCategories.contains(item.key)
}
let listColor: UIColor
if self.isOtherCategoryExpanded {
listColor = Category.other.color
} else {
listColor = Category.misc.color
}
listCategories.append(StorageCategoriesComponent.CategoryData(
key: Category.other, color: Category.other.color, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories))
key: Category.other, color: listColor, title: Category.other.title(strings: environment.strings), size: totalOtherSize, sizeFraction: categoryFraction, isSelected: isSelected, subcategories: otherListCategories))
}
if !self.isOtherCategoryExpanded {
var otherSum: CGFloat = 0.0
for i in 0 ..< chartItems.count {
if otherCategories.contains(chartItems[i].id) {
var itemValue = chartItems[i].value
if itemValue > 0.00001 {
itemValue = max(itemValue, 0.01)
}
otherSum += itemValue
if case .misc = chartItems[i].id {
} else {
chartItems[i].value = 0.0
}
}
}
if let index = chartItems.firstIndex(where: { $0.id == .misc }) {
chartItems[index].value = otherSum
}
}
let chartData = PieChartComponent.ChartData(items: chartItems)
@ -754,13 +809,76 @@ final class StorageUsageScreenComponent: Component {
}
transition.setFrame(view: pieChartComponentView, frame: pieChartFrame)
transition.setAlpha(view: pieChartComponentView, alpha: listCategories.isEmpty ? 0.0 : 1.0)
}
if let _ = self.currentStats, listCategories.isEmpty {
let checkColor = UIColor(rgb: 0x34C759)
let doneStatusNode: RadialStatusNode
var animateIn = false
if let current = self.doneStatusNode {
doneStatusNode = current
} else {
doneStatusNode = RadialStatusNode(backgroundNodeColor: .clear)
self.doneStatusNode = doneStatusNode
self.addSubnode(doneStatusNode)
animateIn = true
}
let doneSize = CGSize(width: 100.0, height: 100.0)
doneStatusNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize)
let doneStatusCircle: SimpleShapeLayer
if let current = self.doneStatusCircle {
doneStatusCircle = current
} else {
doneStatusCircle = SimpleShapeLayer()
self.doneStatusCircle = doneStatusCircle
self.layer.addSublayer(doneStatusCircle)
doneStatusCircle.opacity = 0.0
}
if animateIn {
Queue.mainQueue().after(0.18, {
doneStatusNode.transitionToState(.check(checkColor), animated: true)
doneStatusCircle.opacity = 1.0
doneStatusCircle.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
})
}
doneStatusCircle.lineWidth = 6.0
doneStatusCircle.strokeColor = checkColor.cgColor
doneStatusCircle.fillColor = nil
doneStatusCircle.path = UIBezierPath(ovalIn: CGRect(origin: CGPoint(x: doneStatusCircle.lineWidth * 0.5, y: doneStatusCircle.lineWidth * 0.5), size: CGSize(width: doneSize.width - doneStatusCircle.lineWidth * 0.5, height: doneSize.height - doneStatusCircle.lineWidth * 0.5))).cgPath
doneStatusCircle.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - doneSize.width) / 2.0), y: contentHeight), size: doneSize).insetBy(dx: -doneStatusCircle.lineWidth * 0.5, dy: -doneStatusCircle.lineWidth * 0.5)
contentHeight += doneSize.height
} else {
contentHeight += pieChartSize.height
if let doneStatusNode = self.doneStatusNode {
self.doneStatusNode = nil
doneStatusNode.removeFromSupernode()
}
if let doneStatusCircle = self.doneStatusCircle {
self.doneStatusCircle = nil
doneStatusCircle.removeFromSuperlayer()
}
}
contentHeight += 26.0
let headerText: String
if listCategories.isEmpty {
headerText = "Storage Cleared"
} else if let peer = component.peer {
headerText = peer.displayTitle(strings: environment.strings, displayOrder: .firstLast)
} else {
headerText = "Storage Usage"
}
let headerViewSize = self.headerView.update(
transition: transition,
component: AnyComponent(Text(text: "Storage Usage", font: Font.semibold(22.0), color: environment.theme.list.itemPrimaryTextColor)),
component: AnyComponent(Text(text: headerText, font: Font.semibold(22.0), color: environment.theme.list.itemPrimaryTextColor)),
environment: {},
containerSize: CGSize(width: floor((availableSize.width - navigationRightButtonMaxWidth * 2.0) / 0.8), height: 100.0)
)
@ -780,14 +898,78 @@ final class StorageUsageScreenComponent: Component {
let bold = MarkdownAttributeSet(font: Font.semibold(13.0), textColor: environment.theme.list.freeTextColor)
//TODO:localize
var usageFraction: Double = 0.0
let totalUsageText: String
if listCategories.isEmpty {
totalUsageText = "All media can be re-downloaded from the Telegram cloud if you need it again."
} else if let currentStats = self.currentStats {
let contextStats: StorageUsageStats
if let peer = component.peer {
contextStats = currentStats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
} else {
contextStats = currentStats.totalStats
}
var totalStatsSize: Int64 = 0
for (_, value) in contextStats.categories {
totalStatsSize += value.size
}
if let _ = component.peer {
var allStatsSize: Int64 = 0
for (_, value) in currentStats.totalStats.categories {
allStatsSize += value.size
}
let fraction: Double
if allStatsSize != 0 {
fraction = Double(totalStatsSize) / Double(allStatsSize)
} else {
fraction = 0.0
}
usageFraction = fraction
let fractionValue: Double = floor(fraction * 100.0 * 10.0) / 10.0
let fractionString: String
if fractionValue < 0.1 {
fractionString = "<0.1"
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
fractionString = "\(Int(fractionValue))"
} else {
fractionString = "\(fractionValue)"
}
totalUsageText = "This chat uses \(fractionString)% of your Telegram cache."
} else {
let fraction: Double
if currentStats.deviceFreeSpace != 0 && totalStatsSize != 0 {
fraction = Double(totalStatsSize) / Double(currentStats.deviceFreeSpace + totalStatsSize)
} else {
fraction = 0.0
}
usageFraction = fraction
let fractionValue: Double = floor(fraction * 100.0 * 10.0) / 10.0
let fractionString: String
if fractionValue < 0.1 {
fractionString = "<0.1"
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
fractionString = "\(Int(fractionValue))"
} else {
fractionString = "\(fractionValue)"
}
totalUsageText = "Telegram uses \(fractionString)% of your free disk space."
}
} else {
totalUsageText = " "
}
let headerDescriptionSize = self.headerDescriptionView.update(
transition: transition,
component: AnyComponent(MultilineTextComponent(text: .markdown(text: "Telegram users 9.7% of your free disk space.", attributes: MarkdownAttributes(
component: AnyComponent(MultilineTextComponent(text: .markdown(text: totalUsageText, attributes: MarkdownAttributes(
body: body,
bold: bold,
link: body,
linkAttribute: { _ in nil }
)), maximumNumberOfLines: 0)),
)), horizontalAlignment: .center, maximumNumberOfLines: 0)),
environment: {},
containerSize: CGSize(width: availableSize.width - sideInset * 2.0 - 15.0 * 2.0, height: 10000.0)
)
@ -807,12 +989,15 @@ final class StorageUsageScreenComponent: Component {
transition.setCornerRadius(layer: self.headerProgressBackgroundLayer, cornerRadius: headerProgressFrame.height * 0.5)
self.headerProgressBackgroundLayer.backgroundColor = environment.theme.list.itemAccentColor.withMultipliedAlpha(0.2).cgColor
let headerProgress: CGFloat = 0.097
transition.setFrame(layer: self.headerProgressForegroundLayer, frame: CGRect(origin: headerProgressFrame.origin, size: CGSize(width: floorToScreenPixels(headerProgress * headerProgressFrame.width), height: headerProgressFrame.height)))
let headerProgress: CGFloat = usageFraction
transition.setFrame(layer: self.headerProgressForegroundLayer, frame: CGRect(origin: headerProgressFrame.origin, size: CGSize(width: max(headerProgressFrame.height, floorToScreenPixels(headerProgress * headerProgressFrame.width)), height: headerProgressFrame.height)))
transition.setCornerRadius(layer: self.headerProgressForegroundLayer, cornerRadius: headerProgressFrame.height * 0.5)
self.headerProgressForegroundLayer.backgroundColor = environment.theme.list.itemAccentColor.cgColor
contentHeight += 4.0
transition.setAlpha(layer: self.headerProgressBackgroundLayer, alpha: listCategories.isEmpty ? 0.0 : 1.0)
transition.setAlpha(layer: self.headerProgressForegroundLayer, alpha: listCategories.isEmpty ? 0.0 : 1.0)
contentHeight += 24.0
if let peer = component.peer {
@ -831,6 +1016,7 @@ final class StorageUsageScreenComponent: Component {
chartAvatarNode.setPeer(context: component.context, theme: environment.theme, peer: peer, displayDimensions: avatarSize)
}
transition.setAlpha(view: chartAvatarNode.view, alpha: listCategories.isEmpty ? 0.0 : 1.0)
} else {
let chartTotalLabelSize = self.chartTotalLabel.update(
transition: transition,
@ -841,6 +1027,7 @@ final class StorageUsageScreenComponent: Component {
self.scrollView.addSubview(chartTotalLabelView)
}
transition.setFrame(view: chartTotalLabelView, frame: CGRect(origin: CGPoint(x: pieChartFrame.minX + floor((pieChartFrame.width - chartTotalLabelSize.width) / 2.0), y: pieChartFrame.minY + floor((pieChartFrame.height - chartTotalLabelSize.height) / 2.0)), size: chartTotalLabelSize))
transition.setAlpha(view: chartTotalLabelView, alpha: listCategories.isEmpty ? 0.0 : 1.0)
}
}
@ -858,17 +1045,18 @@ final class StorageUsageScreenComponent: Component {
return
}
if key == Category.other {
let otherCategories: [Category] = [.stickers, .avatars, .misc]
var otherCategories: [Category] = [.stickers, .avatars, .misc]
otherCategories = otherCategories.filter(self.existingCategories.contains)
if !otherCategories.isEmpty {
if otherCategories.allSatisfy(self.selectedCategories.contains) {
for item in otherCategories {
self.selectedCategories.remove(item)
}
self.selectedCategories.remove(Category.other)
} else {
for item in otherCategories {
let _ = self.selectedCategories.insert(item)
}
let _ = self.selectedCategories.insert(Category.other)
}
}
} else {
if self.selectedCategories.contains(key) {
@ -1097,7 +1285,7 @@ final class StorageUsageScreenComponent: Component {
return
}
if self.selectionState == nil {
self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set())
self.selectionState = SelectionState()
}
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
@ -1118,7 +1306,7 @@ final class StorageUsageScreenComponent: Component {
return
}
if self.selectionState == nil {
self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set())
self.selectionState = SelectionState()
}
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
@ -1139,7 +1327,7 @@ final class StorageUsageScreenComponent: Component {
return
}
if self.selectionState == nil {
self.selectionState = SelectionState(selectedPeers: Set(), selectedMessages: Set())
self.selectionState = SelectionState()
}
self.selectionState = self.selectionState?.toggleMessage(id: messageId)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
@ -1199,24 +1387,81 @@ final class StorageUsageScreenComponent: Component {
self.updateScrolling(transition: transition)
if self.isClearing {
let clearingNode: StorageUsageClearProgressOverlayNode
var animateIn = false
if let current = self.clearingNode {
clearingNode = current
} else {
animateIn = true
clearingNode = StorageUsageClearProgressOverlayNode(presentationData: component.context.sharedContext.currentPresentationData.with { $0 })
self.clearingNode = clearingNode
self.addSubnode(clearingNode)
}
let clearingSize = CGSize(width: availableSize.width, height: availableSize.height)
clearingNode.frame = CGRect(origin: CGPoint(x: floor((availableSize.width - clearingSize.width) / 2.0), y: floor((availableSize.height - clearingSize.height) / 2.0)), size: clearingSize)
clearingNode.updateLayout(size: clearingSize, transition: .immediate)
if animateIn {
clearingNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.15)
}
} else {
if let clearingNode = self.clearingNode {
self.clearingNode = nil
let animationTransition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut))
animationTransition.setAlpha(view: clearingNode.view, alpha: 0.0, completion: { [weak clearingNode] _ in
clearingNode?.removeFromSupernode()
})
}
}
return availableSize
}
private func reloadStats(firstTime: Bool) {
if let controller = self.controller?() as? StorageUsageScreen {
controller.reloadParent?()
private func reportClearedStorage(size: Int64) {
guard let component = self.component else {
return
}
guard let controller = self.controller?() else {
return
}
let presentationData = component.context.sharedContext.currentPresentationData.with { $0 }
controller.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(size, formatting: DataSizeStringFormatting(presentationData: presentationData)))", stringForDeviceType()).string), elevatedLayout: false, action: { _ in return false }), in: .window(.root))
}
private func reloadStats(firstTime: Bool, completion: @escaping () -> Void) {
guard let component = self.component else {
completion()
return
}
self.statsDisposable = (component.context.engine.resources.collectStorageUsageStats()
|> deliverOnMainQueue).start(next: { [weak self] stats in
guard let self, let component = self.component else {
completion()
return
}
var existingCategories = Set<Category>()
let contextStats: StorageUsageStats
if let peer = component.peer {
contextStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
} else {
contextStats = stats.totalStats
}
for (category, value) in contextStats.categories {
if value.size != 0 {
existingCategories.insert(StorageUsageScreenComponent.Category(category))
}
}
if firstTime {
self.currentStats = stats
self.existingCategories = existingCategories
}
var peerItems: [StoragePeerListPanelComponent.Item] = []
@ -1240,9 +1485,10 @@ final class StorageUsageScreenComponent: Component {
}
}
if firstTime {
self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems)
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: firstTime ? .firstStatsUpdate : .clearedItems)))
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .firstStatsUpdate)))
}
class RenderResult {
var messages: [MessageId: Message] = [:]
@ -1251,13 +1497,6 @@ final class StorageUsageScreenComponent: Component {
var musicItems: [StorageFileListPanelComponent.Item] = []
}
let contextStats: StorageUsageStats
if let peer = component.peer {
contextStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
} else {
contextStats = stats.totalStats
}
self.messagesDisposable = (component.context.engine.resources.renderStorageUsageStatsMessages(stats: contextStats, categories: [.files, .photos, .videos, .music], existingMessages: self.currentMessages)
|> deliverOn(Queue())
|> map { messages -> RenderResult in
@ -1344,17 +1583,59 @@ final class StorageUsageScreenComponent: Component {
return result
}
|> deliverOnMainQueue).start(next: { [weak self] result in
guard let self else {
guard let self, let component = self.component else {
completion()
return
}
if !firstTime {
if let peer = component.peer, let controller = self.controller?() as? StorageUsageScreen, let childCompleted = controller.childCompleted {
let contextStats: StorageUsageStats = stats.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
var totalSize: Int64 = 0
for (_, value) in contextStats.categories {
totalSize += value.size
}
if totalSize == 0 {
childCompleted({ [weak self] in
completion()
if let self {
self.controller?()?.dismiss(animated: true)
}
})
return
} else {
childCompleted({})
}
}
}
if !firstTime {
self.currentStats = stats
self.existingCategories = existingCategories
self.peerItems = StoragePeerListPanelComponent.Items(items: peerItems)
}
self.currentMessages = result.messages
self.imageItems = StorageFileListPanelComponent.Items(items: result.imageItems)
self.fileItems = StorageFileListPanelComponent.Items(items: result.fileItems)
self.musicItems = StorageFileListPanelComponent.Items(items: result.musicItems)
self.state?.updated(transition: Transition(animation: .none))
if self.selectionState != nil {
if result.imageItems.isEmpty && result.fileItems.isEmpty && result.musicItems.isEmpty && peerItems.isEmpty {
self.selectionState = nil
} else {
self.selectionState = SelectionState()
}
}
self.isClearing = false
self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(value: .clearedItems)))
completion()
})
})
}
@ -1368,11 +1649,13 @@ final class StorageUsageScreenComponent: Component {
}
let childController = StorageUsageScreen(context: component.context, makeStorageUsageExceptionsScreen: component.makeStorageUsageExceptionsScreen, peer: peer)
childController.reloadParent = { [weak self] in
childController.childCompleted = { [weak self] completed in
guard let self else {
return
}
self.reloadStats(firstTime: false)
self.reloadStats(firstTime: false, completion: {
completed()
})
}
controller.push(childController)
}
@ -1429,6 +1712,10 @@ final class StorageUsageScreenComponent: Component {
mappedCategories.append(.misc)
}
}
self.isClearing = true
self.state?.updated(transition: .immediate)
let _ = (component.context.engine.resources.clearStorage(peerId: peerId, categories: mappedCategories)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self, let component = self.component, let currentStats = self.currentStats else {
@ -1469,32 +1756,68 @@ final class StorageUsageScreenComponent: Component {
}
}
for category in categories {
self.selectedCategories.remove(category)
self.reloadStats(firstTime: false, completion: { [weak self] in
guard let self else {
return
}
self.selectionState = nil
self.reloadStats(firstTime: false)
self.state?.updated(transition: Transition(animation: .curve(duration: 0.45, curve: .spring)).withUserData(AnimationHint(value: .clearedItems)))
if totalSize != 0 {
self.reportClearedStorage(size: totalSize)
}
})
})
} else if !peers.isEmpty {
self.isClearing = true
self.state?.updated(transition: .immediate)
var totalSize: Int64 = 0
if let peerItems = self.peerItems {
for item in peerItems.items {
if peers.contains(item.peer.id) {
totalSize += item.size
}
}
}
let _ = (component.context.engine.resources.clearStorage(peerIds: peers)
|> deliverOnMainQueue).start(completed: { [weak self] in
guard let self else {
return
}
self.selectionState = nil
self.reloadStats(firstTime: false)
self.reloadStats(firstTime: false, completion: { [weak self] in
guard let self else {
return
}
if totalSize != 0 {
self.reportClearedStorage(size: totalSize)
}
})
})
} else if !messages.isEmpty {
var messageItems: [Message] = []
var totalSize: Int64 = 0
let contextStats: StorageUsageStats
if let peer = component.peer {
contextStats = self.currentStats?.peers[peer.id]?.stats ?? StorageUsageStats(categories: [:])
} else {
contextStats = self.currentStats?.totalStats ?? StorageUsageStats(categories: [:])
}
for id in messages {
if let message = self.currentMessages[id] {
messageItems.append(message)
for (_, value) in contextStats.categories {
if let size = value.messages[id] {
totalSize += size
}
}
}
}
self.isClearing = true
self.state?.updated(transition: .immediate)
let _ = (component.context.engine.resources.clearStorage(messages: messageItems)
|> deliverOnMainQueue).start(completed: { [weak self] in
@ -1502,8 +1825,15 @@ final class StorageUsageScreenComponent: Component {
return
}
self.selectionState = nil
self.reloadStats(firstTime: false)
self.reloadStats(firstTime: false, completion: { [weak self] in
guard let self else {
return
}
if totalSize != 0 {
self.reportClearedStorage(size: totalSize)
}
})
})
}
}
@ -1716,7 +2046,7 @@ final class StorageUsageScreenComponent: Component {
public final class StorageUsageScreen: ViewControllerComponentContainer {
private let context: AccountContext
fileprivate var reloadParent: (() -> Void)?
fileprivate var childCompleted: ((@escaping () -> Void) -> Void)?
public init(context: AccountContext, makeStorageUsageExceptionsScreen: @escaping (CacheStorageSettings.PeerStorageCategory) -> ViewController?, peer: EnginePeer? = nil) {
self.context = context
@ -1948,3 +2278,122 @@ private final class MultiplePeerAvatarsContextItemNode: ASDisplayNode, ContextMe
return self
}
}
private class StorageUsageClearProgressOverlayNode: ASDisplayNode {
private let presentationData: PresentationData
private let blurredView: BlurredBackgroundView
private let animationNode: AnimatedStickerNode
private let progressTextNode: ImmediateTextNode
private let descriptionTextNode: ImmediateTextNode
private let progressBackgroundNode: ASDisplayNode
private let progressForegroundNode: ASDisplayNode
private let progressDisposable = MetaDisposable()
private var validLayout: CGSize?
init(presentationData: PresentationData) {
self.presentationData = presentationData
self.blurredView = BlurredBackgroundView(color: presentationData.theme.list.plainBackgroundColor.withMultipliedAlpha(0.7), enableBlur: true)
self.animationNode = DefaultAnimatedStickerNodeImpl()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "ClearCache"), width: 256, height: 256, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.progressTextNode = ImmediateTextNode()
self.progressTextNode.textAlignment = .center
self.descriptionTextNode = ImmediateTextNode()
self.descriptionTextNode.textAlignment = .center
self.descriptionTextNode.maximumNumberOfLines = 0
self.progressBackgroundNode = ASDisplayNode()
self.progressBackgroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor.withMultipliedAlpha(0.2)
self.progressBackgroundNode.cornerRadius = 3.0
self.progressForegroundNode = ASDisplayNode()
self.progressForegroundNode.backgroundColor = self.presentationData.theme.actionSheet.controlAccentColor
self.progressForegroundNode.cornerRadius = 3.0
super.init()
self.view.addSubview(self.blurredView)
self.addSubnode(self.animationNode)
self.addSubnode(self.progressTextNode)
self.addSubnode(self.descriptionTextNode)
//self.addSubnode(self.progressBackgroundNode)
//self.addSubnode(self.progressForegroundNode)
}
deinit {
self.progressDisposable.dispose()
}
func setProgressSignal(_ signal: Signal<Float, NoError>) {
self.progressDisposable.set((signal
|> deliverOnMainQueue).start(next: { [weak self] progress in
if let strongSelf = self {
strongSelf.setProgress(progress)
}
}))
}
private var progress: Float = 0.0
private func setProgress(_ progress: Float) {
self.progress = progress
if let size = self.validLayout {
self.updateLayout(size: size, transition: .animated(duration: 0.5, curve: .linear))
}
}
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
self.validLayout = size
transition.updateFrame(view: self.blurredView, frame: CGRect(origin: CGPoint(), size: size))
self.blurredView.update(size: size, transition: transition)
let inset: CGFloat = 24.0
let progressHeight: CGFloat = 6.0
let spacing: CGFloat = 16.0
let imageSide = min(160.0, size.height - 30.0)
let imageSize = CGSize(width: imageSide, height: imageSide)
let animationFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: floorToScreenPixels((size.height - imageSize.height) / 2.0) - 50.0), size: imageSize)
self.animationNode.frame = animationFrame
self.animationNode.updateLayout(size: imageSize)
let progressFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: size.width - inset * 2.0, height: progressHeight)
self.progressBackgroundNode.frame = progressFrame
let progressForegroundFrame = CGRect(x: inset, y: size.height - inset - progressHeight, width: floorToScreenPixels(progressFrame.width * CGFloat(self.progress)), height: progressHeight)
if !self.progressForegroundNode.frame.origin.x.isZero {
transition.updateFrame(node: self.progressForegroundNode, frame: progressForegroundFrame, beginWithCurrentState: true)
} else {
self.progressForegroundNode.frame = progressForegroundFrame
}
self.descriptionTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_KeepOpenedDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.actionSheet.secondaryTextColor)
let descriptionTextSize = self.descriptionTextNode.updateLayout(CGSize(width: size.width - inset * 3.0, height: size.height))
var descriptionTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - descriptionTextSize.width) / 2.0), y: animationFrame.maxY + 52.0), size: descriptionTextSize)
self.progressTextNode.attributedText = NSAttributedString(string: self.presentationData.strings.ClearCache_NoProgress, font: Font.with(size: 17.0, design: .regular, weight: .semibold, traits: [.monospacedNumbers]), textColor: self.presentationData.theme.actionSheet.primaryTextColor)
let progressTextSize = self.progressTextNode.updateLayout(size)
var progressTextFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - progressTextSize.width) / 2.0), y: descriptionTextFrame.minY - spacing - progressTextSize.height), size: progressTextSize)
let availableHeight = progressTextFrame.minY
if availableHeight < 100.0 {
let offset = availableHeight / 2.0 - spacing
descriptionTextFrame = descriptionTextFrame.offsetBy(dx: 0.0, dy: -offset)
progressTextFrame = progressTextFrame.offsetBy(dx: 0.0, dy: -offset)
self.animationNode.alpha = 0.0
} else {
self.animationNode.alpha = 1.0
}
self.progressTextNode.frame = progressTextFrame
self.descriptionTextNode.frame = descriptionTextFrame
}
}