Storage Usage section improvements

This commit is contained in:
Ilya Laktyushin 2019-11-05 14:56:02 +04:00
parent ca685ce3d2
commit c51e3e8c40
18 changed files with 4731 additions and 3885 deletions

View File

@ -5067,3 +5067,11 @@ Any member of this group will be able to see messages in the channel.";
"Widget.ApplicationLocked" = "Unlock the app to use the widget";
"Group.ErrorSupergroupConversionNotPossible" = "Sorry, you are a member of too many groups and channels. Please leave some before creating a new one.";
"ClearCache.StorageTitle" = "%@ STORAGE";
"ClearCache.StorageCache" = "Telegram Cache";
"ClearCache.StorageServiceFiles" = "Telegram Service Files";
"ClearCache.StorageOtherApps" = "Other Apps";
"ClearCache.StorageFree" = "Free";
"ClearCache.ClearCache" = "Clear Telegram Cache";
"ClearCache.Clear" = "Clear";

View File

@ -763,7 +763,7 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
strongSelf.labelArrowNode = updatedLabelArrowNode
strongSelf.containerNode.addSubnode(updatedLabelArrowNode)
if let image = updatedLabelArrowNode.image {
let labelArrowNodeFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightLabelInset - image.size.width, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
let labelArrowNodeFrame = CGRect(origin: CGPoint(x: params.width - params.rightInset - rightLabelInset - image.size.width + 8.0, y: floor((contentSize.height - image.size.height) / 2.0)), size: image.size)
transition.updateFrame(node: updatedLabelArrowNode, frame: labelArrowNodeFrame)
rightLabelInset += 19.0
}
@ -775,10 +775,10 @@ public class ItemListPeerItemNode: ItemListRevealOptionsItemNode, ItemListItemNo
let badgeWidth = max(badgeDiameter, labelLayout.size.width + 10.0)
let labelFrame: CGRect
if case .badge = item.label {
labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - rightLabelInset - badgeWidth + (badgeWidth - labelLayout.size.width) / 2.0, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
strongSelf.labelNode.frame = labelFrame
} else {
labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset - rightInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
labelFrame = CGRect(origin: CGPoint(x: revealOffset + params.width - labelLayout.size.width - rightLabelInset - rightInset, y: floor((contentSize.height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
transition.updateFrame(node: strongSelf.labelNode, frame: labelFrame)
}

View File

@ -148,33 +148,35 @@ class ItemListCallListItemNode: ListViewItemNode {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let currentItem = self.item
return { item, params, neighbors in
if self.callNodes.count != item.messages.count {
for pair in self.callNodes {
return { [weak self] item, params, neighbors in
if let strongSelf = self, strongSelf.callNodes.count != item.messages.count {
for pair in strongSelf.callNodes {
pair.0.removeFromSupernode()
pair.1.removeFromSupernode()
}
self.callNodes = []
strongSelf.callNodes = []
for _ in item.messages {
let timeNode = TextNode()
timeNode.isUserInteractionEnabled = false
self.addSubnode(timeNode)
strongSelf.addSubnode(timeNode)
let typeNode = TextNode()
typeNode.isUserInteractionEnabled = false
self.addSubnode(typeNode)
strongSelf.addSubnode(typeNode)
self.callNodes.append((timeNode, typeNode))
strongSelf.callNodes.append((timeNode, typeNode))
}
}
var makeNodesLayout: [((TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode), (TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode))] = []
for nodes in self.callNodes {
let makeTimeLayout = TextNode.asyncLayout(nodes.0)
let makeTypeLayout = TextNode.asyncLayout(nodes.1)
makeNodesLayout.append((makeTimeLayout, makeTypeLayout))
if let strongSelf = self {
for nodes in strongSelf.callNodes {
let makeTimeLayout = TextNode.asyncLayout(nodes.0)
let makeTypeLayout = TextNode.asyncLayout(nodes.1)
makeNodesLayout.append((makeTimeLayout, makeTypeLayout))
}
}
var updatedTheme: PresentationTheme?

View File

@ -31,7 +31,7 @@ enum AutomaticDownloadDataUsage: Int {
}
}
class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
final class AutodownloadDataUsagePickerItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: AutomaticDownloadDataUsage
@ -93,7 +93,7 @@ private func generateKnobImage() -> UIImage? {
})
}
class AutodownloadDataUsagePickerItemNode: ListViewItemNode {
private final class AutodownloadDataUsagePickerItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode

View File

@ -49,7 +49,7 @@ private func sizeValue(for sliderValue: CGFloat) -> Int32 {
return 0
}
class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
final class AutodownloadSizeLimitItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let decimalSeparator: String
let text: String
@ -109,7 +109,7 @@ private func generateKnobImage() -> UIImage? {
})
}
class AutodownloadSizeLimitItemNode: ListViewItemNode {
private final class AutodownloadSizeLimitItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode

View File

@ -8,7 +8,7 @@ import ItemListUI
import PresentationDataUtils
import ActivityIndicator
class CalculatingCacheSizeItem: ListViewItem, ItemListItem {
final class CalculatingCacheSizeItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let title: String
let sectionId: ItemListSectionId
@ -57,7 +57,7 @@ class CalculatingCacheSizeItem: ListViewItem, ItemListItem {
private let titleFont = Font.regular(14.0)
class CalculatingCacheSizeItemNode: ListViewItemNode {
private final class CalculatingCacheSizeItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode

View File

@ -485,6 +485,9 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt
let actionsDisposable = DisposableSet()
let cacheUsagePromise = Promise<CacheUsageStatsResult?>()
cacheUsagePromise.set(cacheUsageStats(context: context))
let dataAndStorageDataPromise = Promise<DataAndStorageData>()
dataAndStorageDataPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.autodownloadSettings, ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings, ApplicationSpecificSharedDataKeys.generatedMediaStoreSettings, ApplicationSpecificSharedDataKeys.voiceCallSettings, SharedDataKeys.proxySettings])
|> map { sharedData -> DataAndStorageData in
@ -526,7 +529,7 @@ func dataAndStorageController(context: AccountContext, focusOnItemTag: DataAndSt
})
let arguments = DataAndStorageControllerArguments(openStorageUsage: {
pushControllerImpl?(storageUsageController(context: context))
pushControllerImpl?(storageUsageController(context: context, cacheUsagePromise: cacheUsagePromise))
}, openNetworkUsage: {
pushControllerImpl?(networkUsageStatsController(context: context))
}, openProxy: {

View File

@ -0,0 +1,315 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramUIPreferences
import TelegramPresentationData
import LegacyComponents
import ItemListUI
import PresentationDataUtils
private func stringForKeepMediaTimeout(strings: PresentationStrings, timeout: Int32) -> String {
if timeout > 1 * 31 * 24 * 60 * 60 {
return strings.MessageTimer_Forever
} else {
return timeIntervalString(strings: strings, value: timeout)
}
}
private let keepMediaTimeoutValues: [Int32] = [
3 * 24 * 60 * 60,
7 * 24 * 60 * 60,
1 * 31 * 24 * 60 * 60,
Int32.max
]
final class KeepMediaDurationPickerItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let strings: PresentationStrings
let value: Int32
let sectionId: ItemListSectionId
let updated: (Int32) -> Void
init(theme: PresentationTheme, strings: PresentationStrings, value: Int32, sectionId: ItemListSectionId, updated: @escaping (Int32) -> Void) {
self.theme = theme
self.strings = strings
self.value = value
self.sectionId = sectionId
self.updated = updated
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = KeepMediaDurationPickerItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? KeepMediaDurationPickerItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func generateKnobImage() -> UIImage? {
return generateImage(CGSize(width: 40.0, height: 40.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(width: 0.0, height: -2.0), blur: 3.5, color: UIColor(white: 0.0, alpha: 0.35).cgColor)
context.setFillColor(UIColor.white.cgColor)
context.fillEllipse(in: CGRect(origin: CGPoint(x: 6.0, y: 6.0), size: CGSize(width: 28.0, height: 28.0)))
})
}
private final class KeepMediaDurationPickerItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let textNodes: [TextNode]
private var sliderView: TGPhotoEditorSliderView?
private var item: KeepMediaDurationPickerItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
var textNodes: [TextNode] = []
for i in 0 ..< 4 {
let textNode = TextNode()
textNode.isUserInteractionEnabled = false
textNode.displaysAsynchronously = false
textNodes.append(textNode)
}
self.textNodes = textNodes
super.init(layerBacked: false, dynamicBounce: false)
for textNode in textNodes {
self.addSubnode(textNode)
}
}
func updateSliderView() {
if let sliderView = self.sliderView, let item = self.item {
sliderView.maximumValue = 3.0
sliderView.positionsCount = 4
let value = keepMediaTimeoutValues.firstIndex(where: { $0 == item.value }) ?? 0
sliderView.value = CGFloat(value)
}
}
override func didLoad() {
super.didLoad()
let sliderView = TGPhotoEditorSliderView()
sliderView.enablePanHandling = true
sliderView.trackCornerRadius = 1.0
sliderView.lineSize = 2.0
sliderView.dotSize = 5.0
sliderView.minimumValue = 0.0
sliderView.maximumValue = 3.0
sliderView.startValue = 0.0
sliderView.disablesInteractiveTransitionGestureRecognizer = true
sliderView.positionsCount = 4
sliderView.useLinesForPositions = true
if let item = self.item, let params = self.layoutParams {
let value = keepMediaTimeoutValues.firstIndex(where: { $0 == item.value }) ?? 0
sliderView.value = CGFloat(value)
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.startColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
}
self.view.addSubview(sliderView)
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
self.sliderView = sliderView
self.updateSliderView()
}
func asyncLayout() -> (_ item: KeepMediaDurationPickerItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
var makeTextLayouts: [(TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = []
for textNode in self.textNodes {
makeTextLayouts.append(TextNode.asyncLayout(textNode))
}
return { item, params, neighbors in
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var textLayouts: [TextNodeLayout] = []
var textApplies: [() -> TextNode] = []
for i in 0 ..< makeTextLayouts.count {
let makeTextLayout = makeTextLayouts[i]
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: stringForKeepMediaTimeout(strings: item.strings, timeout: keepMediaTimeoutValues[i]), font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .center, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
textLayouts.append(textLayout)
textApplies.append(textApply)
}
contentSize = CGSize(width: params.width, height: 88.0)
insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
for apply in textApplies {
let _ = apply()
}
var textNodes: [(TextNode, CGSize)] = []
for (node, size) in zip(strongSelf.textNodes, textLayouts.map { $0.size }) {
textNodes.append((node, size))
}
let delta = (params.width - params.leftInset - params.rightInset - 18.0 * 2.0) / CGFloat(textNodes.count - 1)
for i in 0 ..< textNodes.count {
let (textNode, textSize) = textNodes[i]
var position = params.leftInset + 18.0 + delta * CGFloat(i)
if i == textNodes.count - 1 {
position -= textSize.width
} else if i > 0 {
position -= textSize.width / 2.0
}
textNode.frame = CGRect(origin: CGPoint(x: position, y: 15.0), size: textSize)
}
if let sliderView = strongSelf.sliderView {
if themeUpdated {
sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor
sliderView.backColor = item.theme.list.disclosureArrowColor
sliderView.trackColor = item.theme.list.itemAccentColor
sliderView.knobImage = generateKnobImage()
}
sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 37.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 15.0 * 2.0, height: 44.0))
sliderView.hitTestEdgeInsets = UIEdgeInsets(top: -sliderView.frame.minX, left: 0.0, bottom: 0.0, right: -sliderView.frame.minX)
strongSelf.updateSliderView()
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
@objc private func sliderValueChanged() {
guard let sliderView = self.sliderView else {
return
}
let position = Int(sliderView.value)
let value = keepMediaTimeoutValues[position]
self.item?.updated(value)
}
}

View File

@ -12,7 +12,7 @@ enum ProxySettingsActionIcon {
case add
}
class ProxySettingsActionItem: ListViewItem, ItemListItem {
final class ProxySettingsActionItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let title: String
let icon: ProxySettingsActionIcon
@ -77,7 +77,7 @@ class ProxySettingsActionItem: ListViewItem, ItemListItem {
private let titleFont = Font.regular(17.0)
class ProxySettingsActionItemNode: ListViewItemNode {
private final class ProxySettingsActionItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode

View File

@ -101,7 +101,7 @@ final class ProxySettingsServerItem: ListViewItem, ItemListItem {
private let titleFont = Font.regular(17.0)
private let statusFont = Font.regular(14.0)
class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
private final class ProxySettingsServerItemNode: ItemListRevealOptionsItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode

View File

@ -7,6 +7,7 @@ import TelegramCore
import SyncCore
import TelegramPresentationData
import TelegramUIPreferences
import TelegramStringFormatting
import ItemListUI
import PresentationDataUtils
import OverlayStatusController
@ -15,48 +16,67 @@ import ItemListPeerItem
import DeleteChatPeerActionSheetItem
import UndoUI
private func totalDiskSpace() -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
} catch {
return 0
}
}
private func freeDiskSpace() -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
return (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
} catch {
return 0
}
}
private final class StorageUsageControllerArguments {
let account: Account
let updateKeepMedia: () -> Void
let updateKeepMediaTimeout: (Int32) -> Void
let openClearAll: () -> Void
let openPeerMedia: (PeerId) -> Void
let clearPeerMedia: (PeerId) -> Void
let setPeerIdWithRevealedOptions: (PeerId?, PeerId?) -> Void
init(account: Account, updateKeepMedia: @escaping () -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void) {
init(account: Account, updateKeepMediaTimeout: @escaping (Int32) -> Void, openClearAll: @escaping () -> Void, openPeerMedia: @escaping (PeerId) -> Void, clearPeerMedia: @escaping (PeerId) -> Void, setPeerIdWithRevealedOptions: @escaping (PeerId?, PeerId?) -> Void) {
self.account = account
self.updateKeepMedia = updateKeepMedia
self.updateKeepMediaTimeout = updateKeepMediaTimeout
self.openClearAll = openClearAll
self.openPeerMedia = openPeerMedia
self.clearPeerMedia = clearPeerMedia
self.setPeerIdWithRevealedOptions = setPeerIdWithRevealedOptions
}
}
private enum StorageUsageSection: Int32 {
case keepMedia
case immutableSize
case all
case storage
case peers
}
private enum StorageUsageEntry: ItemListNodeEntry {
case keepMedia(PresentationTheme, String, String)
case keepMediaHeader(PresentationTheme, String)
case keepMedia(PresentationTheme, PresentationStrings, Int32)
case keepMediaInfo(PresentationTheme, String)
case storageHeader(PresentationTheme, String)
case storageUsage(PresentationTheme, PresentationDateTimeFormat, [StorageUsageCategory])
case collecting(PresentationTheme, String)
case immutableSize(PresentationTheme, String, String)
case clearAll(PresentationTheme, String, String, Bool)
case clearAll(PresentationTheme, String, Bool)
case peersHeader(PresentationTheme, String)
case peer(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, Peer?, String)
case peer(Int32, PresentationTheme, PresentationStrings, PresentationDateTimeFormat, PresentationPersonNameOrder, Peer, Peer?, String, Bool)
var section: ItemListSectionId {
switch self {
case .keepMedia, .keepMediaInfo:
case .keepMediaHeader, .keepMedia, .keepMediaInfo:
return StorageUsageSection.keepMedia.rawValue
case .immutableSize:
return StorageUsageSection.immutableSize.rawValue
case .collecting, .clearAll:
return StorageUsageSection.all.rawValue
case .storageHeader, .storageUsage, .collecting, .clearAll:
return StorageUsageSection.storage.rawValue
case .peersHeader, .peer:
return StorageUsageSection.peers.rawValue
}
@ -64,27 +84,37 @@ private enum StorageUsageEntry: ItemListNodeEntry {
var stableId: Int32 {
switch self {
case .keepMedia:
case .keepMediaHeader:
return 0
case .keepMediaInfo:
case .keepMedia:
return 1
case .collecting:
case .keepMediaInfo:
return 2
case .immutableSize:
case .storageHeader:
return 3
case .clearAll:
case .storageUsage:
return 4
case .peersHeader:
case .collecting:
return 5
case let .peer(index, _, _, _, _, _, _, _):
return 6 + index
case .clearAll:
return 6
case .peersHeader:
return 7
case let .peer(index, _, _, _, _, _, _, _, _):
return 8 + index
}
}
static func ==(lhs: StorageUsageEntry, rhs: StorageUsageEntry) -> Bool {
switch lhs {
case let .keepMedia(lhsTheme, lhsText, lhsValue):
if case let .keepMedia(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
case let .keepMediaHeader(lhsTheme, lhsText):
if case let .keepMediaHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .keepMedia(lhsTheme, lhsStrings, lhsValue):
if case let .keepMedia(rhsTheme, rhsStrings, rhsValue) = rhs, lhsTheme === rhsTheme, lhsStrings === rhsStrings, lhsValue == rhsValue {
return true
} else {
return false
@ -95,20 +125,26 @@ private enum StorageUsageEntry: ItemListNodeEntry {
} else {
return false
}
case let .storageHeader(lhsTheme, lhsText):
if case let .storageHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .storageUsage(lhsTheme, lhsDateTimeFormat, lhsCategories):
if case let .storageUsage(rhsTheme, rhsDateTimeFormat, rhsCategories) = rhs, lhsTheme === rhsTheme, lhsDateTimeFormat == rhsDateTimeFormat, lhsCategories == rhsCategories {
return true
} else {
return false
}
case let .collecting(lhsTheme, lhsText):
if case let .collecting(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .immutableSize(lhsTheme, lhsText, lhsValue):
if case let .immutableSize(rhsTheme, rhsText, rhsValue) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue {
return true
} else {
return false
}
case let .clearAll(lhsTheme, lhsText, lhsValue, lhsEnabled):
if case let .clearAll(rhsTheme, rhsText, rhsValue, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsValue == rhsValue, lhsEnabled == rhsEnabled {
case let .clearAll(lhsTheme, lhsText, lhsEnabled):
if case let .clearAll(rhsTheme, rhsText, rhsEnabled) = rhs, lhsTheme === rhsTheme, lhsText == rhsText, lhsEnabled == rhsEnabled {
return true
} else {
return false
@ -119,8 +155,8 @@ private enum StorageUsageEntry: ItemListNodeEntry {
} else {
return false
}
case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsChatPeer, lhsValue):
if case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsChatPeer, rhsValue) = rhs {
case let .peer(lhsIndex, lhsTheme, lhsStrings, lhsDateTimeFormat, lhsNameOrder, lhsPeer, lhsChatPeer, lhsValue, lhsRevealed):
if case let .peer(rhsIndex, rhsTheme, rhsStrings, rhsDateTimeFormat, rhsNameOrder, rhsPeer, rhsChatPeer, rhsValue, rhsRevealed) = rhs {
if lhsIndex != rhsIndex {
return false
}
@ -145,6 +181,9 @@ private enum StorageUsageEntry: ItemListNodeEntry {
if lhsValue != rhsValue {
return false
}
if lhsRevealed != rhsRevealed {
return false
}
return true
} else {
return false
@ -159,28 +198,35 @@ private enum StorageUsageEntry: ItemListNodeEntry {
func item(_ arguments: Any) -> ListViewItem {
let arguments = arguments as! StorageUsageControllerArguments
switch self {
case let .keepMedia(theme, text, value):
return ItemListDisclosureItem(theme: theme, title: text, label: value, sectionId: self.section, style: .blocks, action: {
arguments.updateKeepMedia()
})
case let .keepMediaHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .keepMedia(theme, strings, value):
return KeepMediaDurationPickerItem(theme: theme, strings: strings, value: value, sectionId: self.section, updated: { updatedValue in
arguments.updateKeepMediaTimeout(updatedValue)
})
case let .keepMediaInfo(theme, text):
return ItemListTextItem(theme: theme, text: .markdown(text), sectionId: self.section)
case let .storageHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .storageUsage(theme, dateTimeFormat, categories):
return StorageUsageItem(theme: theme, dateTimeFormat: dateTimeFormat, categories: categories, sectionId: self.section)
case let .collecting(theme, text):
return CalculatingCacheSizeItem(theme: theme, title: text, sectionId: self.section, style: .blocks)
case let .immutableSize(theme, title, value):
return ItemListDisclosureItem(theme: theme, icon: nil, title: title, enabled: false, titleColor: .primary, label: value, labelStyle: .text, sectionId: self.section, style: .blocks, disclosureStyle: .none, action: nil)
case let .peersHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .clearAll(theme, text, value, enabled):
return ItemListDisclosureItem(theme: theme, icon: nil, title: text, enabled: enabled, label: value, sectionId: self.section, style: .blocks, disclosureStyle: .arrow, action: {
case let .clearAll(theme, text, enabled):
return ItemListActionItem(theme: theme, title: text, kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: {
arguments.openClearAll()
})
case let .peer(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, chatPeer, value):
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: false, editing: false, revealed: false), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
case let .peersHeader(theme, text):
return ItemListSectionHeaderItem(theme: theme, text: text, sectionId: self.section)
case let .peer(_, theme, strings, dateTimeFormat, nameDisplayOrder, peer, chatPeer, value, revealed):
var options: [ItemListPeerItemRevealOption] = [ItemListPeerItemRevealOption(type: .destructive, title: strings.ClearCache_Clear, action: {
arguments.clearPeerMedia(peer.id)
})]
return ItemListPeerItem(theme: theme, strings: strings, dateTimeFormat: dateTimeFormat, nameDisplayOrder: nameDisplayOrder, account: arguments.account, peer: peer, aliasHandling: .threatSelfAsSaved, nameColor: chatPeer == nil ? .primary : .secret, presence: nil, text: .none, label: .disclosure(value), editing: ItemListPeerItemEditing(editable: true, editing: false, revealed: revealed), revealOptions: ItemListPeerItemRevealOptions(options: options), switchValue: nil, enabled: true, selectable: true, sectionId: self.section, action: {
let resolvedPeer = chatPeer ?? peer
arguments.openPeerMedia(resolvedPeer.id)
}, setPeerIdWithRevealedOptions: { previousId, id in
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
arguments.setPeerIdWithRevealedOptions(peerId, fromPeerId)
}, removePeer: { _ in
})
@ -188,24 +234,26 @@ private enum StorageUsageEntry: ItemListNodeEntry {
}
}
private func stringForKeepMediaTimeout(strings: PresentationStrings, timeout: Int32) -> String {
if timeout > 1 * 31 * 24 * 60 * 60 {
return strings.MessageTimer_Forever
} else {
return timeIntervalString(strings: strings, value: timeout)
private struct StoragUsageState: Equatable {
let peerIdWithRevealedOptions: PeerId?
func withUpdatedPeerIdWithRevealedOptions(_ peerIdWithRevealedOptions: PeerId?) -> StoragUsageState {
return StoragUsageState(peerIdWithRevealedOptions: peerIdWithRevealedOptions)
}
}
private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?) -> [StorageUsageEntry] {
private func storageUsageControllerEntries(presentationData: PresentationData, cacheSettings: CacheStorageSettings, cacheStats: CacheUsageStatsResult?, state: StoragUsageState) -> [StorageUsageEntry] {
var entries: [StorageUsageEntry] = []
entries.append(.keepMedia(presentationData.theme, presentationData.strings.Cache_KeepMedia, stringForKeepMediaTimeout(strings: presentationData.strings, timeout: cacheSettings.defaultCacheStorageTimeout)))
entries.append(.keepMediaHeader(presentationData.theme, presentationData.strings.Cache_KeepMedia.uppercased()))
entries.append(.keepMedia(presentationData.theme, presentationData.strings, cacheSettings.defaultCacheStorageTimeout))
entries.append(.keepMediaInfo(presentationData.theme, presentationData.strings.Cache_Help))
var addedHeader = false
entries.append(.storageHeader(presentationData.theme, presentationData.strings.ClearCache_StorageTitle(stringForDeviceType().uppercased()).0))
if let cacheStats = cacheStats, case let .result(stats) = cacheStats {
entries.append(.immutableSize(presentationData.theme, presentationData.strings.Cache_ServiceFiles, dataSizeString(stats.immutableSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)))
//entries.append(.immutableSize(presentationData.theme, presentationData.strings.Cache_ServiceFiles, dataSizeString(stats.immutableSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)))
var peerSizes: Int64 = 0
var statsByPeerId: [(PeerId, Int64)] = []
@ -230,9 +278,27 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c
peerSizes += combinedSize
}
let totalSize = Int64(peerSizes + stats.otherSize + stats.cacheSize + stats.tempSize)
let telegramCacheSize = Int64(peerSizes + stats.otherSize + stats.cacheSize + stats.tempSize)
let totalTelegramSize = telegramCacheSize + stats.immutableSize
entries.append(.clearAll(presentationData.theme, presentationData.strings.Cache_ClearCache, totalSize > 0 ? dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator) : presentationData.strings.Cache_ClearEmpty, totalSize > 0))
var categories: [StorageUsageCategory] = []
let totalSpace = max(totalDiskSpace(), 1)
let freeSpace = freeDiskSpace()
let otherAppsSpace = totalSpace - freeSpace - totalTelegramSize
let totalSpaceValue = CGFloat(totalSpace)
if telegramCacheSize > 0 {
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageCache, size: totalTelegramSize, fraction: CGFloat(totalTelegramSize) / totalSpaceValue, color: presentationData.theme.list.itemAccentColor))
} else {
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageServiceFiles, size: totalTelegramSize, fraction: CGFloat(totalTelegramSize) / totalSpaceValue, color: presentationData.theme.list.itemAccentColor))
}
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageOtherApps, size: otherAppsSpace, fraction: CGFloat(otherAppsSpace) / totalSpaceValue, color: presentationData.theme.list.itemBlocksSeparatorColor))
categories.append(StorageUsageCategory(title: presentationData.strings.ClearCache_StorageFree, size: freeSpace, fraction: CGFloat(freeSpace) / totalSpaceValue, color: UIColor(rgb: 0xf2f1f7)))
entries.append(.storageUsage(presentationData.theme, presentationData.dateTimeFormat, categories))
entries.append(.clearAll(presentationData.theme, presentationData.strings.ClearCache_ClearCache, telegramCacheSize > 0))
var index: Int32 = 0
for (peerId, size) in statsByPeerId.sorted(by: { $0.1 > $1.1 }) {
@ -248,7 +314,7 @@ private func storageUsageControllerEntries(presentationData: PresentationData, c
chatPeer = mainPeer
mainPeer = associatedPeer
}
entries.append(.peer(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, mainPeer, chatPeer, dataSizeString(size, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator)))
entries.append(.peer(index, presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, presentationData.nameDisplayOrder, mainPeer, chatPeer, dataSizeString(size, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator), state.peerIdWithRevealedOptions == peer.id))
index += 1
}
}
@ -273,7 +339,28 @@ private func stringForCategory(strings: PresentationStrings, category: PeerCache
}
}
public func storageUsageController(context: AccountContext, isModal: Bool = false) -> ViewController {
func cacheUsageStats(context: AccountContext) -> Signal<CacheUsageStatsResult?, NoError> {
let containerPath = context.sharedContext.applicationBindings.containerPath
let additionalPaths: [String] = [
NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0],
containerPath + "/Documents/files",
containerPath + "/Documents/video",
containerPath + "/Documents/audio",
containerPath + "/Documents/mediacache",
containerPath + "/Documents/tempcache_v1/store",
]
return .single(nil)
|> then(collectCacheUsageStats(account: context.account, additionalCachePaths: additionalPaths, logFilesPath: context.sharedContext.applicationBindings.containerPath + "/telegram-data/logs")
|> map(Optional.init))
}
public func storageUsageController(context: AccountContext, cacheUsagePromise: Promise<CacheUsageStatsResult?>? = nil, isModal: Bool = false) -> ViewController {
let statePromise = ValuePromise(StoragUsageState(peerIdWithRevealedOptions: nil))
let stateValue = Atomic(value: StoragUsageState(peerIdWithRevealedOptions: nil))
let updateState: ((StoragUsageState) -> StoragUsageState) -> Void = { f in
statePromise.set(stateValue.modify { f($0) })
}
let cacheSettingsPromise = Promise<CacheStorageSettings>()
cacheSettingsPromise.set(context.sharedContext.accountManager.sharedData(keys: [SharedDataKeys.cacheStorageSettings])
|> map { sharedData -> CacheStorageSettings in
@ -289,59 +376,27 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals
var presentControllerImpl: ((ViewController, PresentationContextType, Any?) -> Void)?
let statsPromise = Promise<CacheUsageStatsResult?>()
let resetStats: () -> Void = {
let containerPath = context.sharedContext.applicationBindings.containerPath
let additionalPaths: [String] = [
NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0],
containerPath + "/Documents/files",
containerPath + "/Documents/video",
containerPath + "/Documents/audio",
containerPath + "/Documents/mediacache",
containerPath + "/Documents/tempcache_v1/store",
]
statsPromise.set(.single(nil)
|> then(collectCacheUsageStats(account: context.account, additionalCachePaths: additionalPaths, logFilesPath: context.sharedContext.applicationBindings.containerPath + "/telegram-data/logs")
|> map(Optional.init)))
var statsPromise: Promise<CacheUsageStatsResult?>
if let cacheUsagePromise = cacheUsagePromise {
statsPromise = cacheUsagePromise
} else {
statsPromise = Promise<CacheUsageStatsResult?>()
statsPromise.set(cacheUsageStats(context: context))
}
let resetStats: () -> Void = {
statsPromise.set(cacheUsageStats(context: context))
}
resetStats()
let actionDisposables = DisposableSet()
let clearDisposable = MetaDisposable()
actionDisposables.add(clearDisposable)
let arguments = StorageUsageControllerArguments(account: context.account, updateKeepMedia: {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = ActionSheetController(presentationTheme: presentationData.theme)
let dismissAction: () -> Void = { [weak controller] in
controller?.dismissAnimated()
}
let timeoutAction: (Int32) -> Void = { timeout in
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedDefaultCacheStorageTimeout(timeout)
}).start()
}
var values: [Int32] = [
3 * 24 * 60 * 60,
7 * 24 * 60 * 60,
1 * 31 * 24 * 60 * 60,
Int32.max
]
#if DEBUG
values.insert(60 * 60, at: 0)
#endif
let timeoutItems: [ActionSheetItem] = values.map { value in
return ActionSheetButtonItem(title: stringForKeepMediaTimeout(strings: presentationData.strings, timeout: value), action: {
dismissAction()
timeoutAction(value)
})
}
controller.setItemGroups([
ActionSheetItemGroup(items: timeoutItems),
ActionSheetItemGroup(items: [ActionSheetButtonItem(title: presentationData.strings.Common_Cancel, action: { dismissAction() })])
])
presentControllerImpl?(controller, .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
let arguments = StorageUsageControllerArguments(account: context.account, updateKeepMediaTimeout: { value in
let _ = updateCacheStorageSettingsInteractively(accountManager: context.sharedContext.accountManager, { current in
return current.withUpdatedDefaultCacheStorageTimeout(value)
}).start()
}, openClearAll: {
let _ = (statsPromise.get()
|> take(1)
@ -535,8 +590,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals
clearDisposable.set((signal
|> deliverOnMainQueue).start(completed: {
statsPromise.set(.single(.result(resultStats)))
let deviceName = UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone"
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", deviceName).0), elevatedLayout: false, action: { _ in }), .current, nil)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in }), .current, nil)
}))
}
@ -717,8 +771,7 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals
clearDisposable.set((signal
|> deliverOnMainQueue).start(completed: {
statsPromise.set(.single(.result(resultStats)))
let deviceName = UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone"
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", deviceName).0), elevatedLayout: false, action: { _ in }), .current, nil)
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in }), .current, nil)
}))
}
@ -734,18 +787,147 @@ public func storageUsageController(context: AccountContext, isModal: Bool = fals
}
}
})
}, clearPeerMedia: { peerId in
let _ = (statsPromise.get() |> take(1) |> deliverOnMainQueue).start(next: { [weak statsPromise] result in
if let result = result, case let .result(stats) = result {
var additionalPeerId: PeerId?
if var categories = stats.media[peerId], let peer = stats.peers[peerId] {
if let channel = peer as? TelegramChannel, case .group = channel.info {
for (_, peer) in stats.peers {
if let group = peer as? TelegramGroup, let migrationReference = group.migrationReference, migrationReference.peerId == peerId {
if let additionalCategories = stats.media[group.id] {
additionalPeerId = group.id
categories.merge(additionalCategories, uniquingKeysWith: { lhs, rhs in
return lhs.merging(rhs, uniquingKeysWith: { lhs, rhs in
return lhs + rhs
})
})
}
}
}
}
var sizeIndex: [PeerCacheUsageCategory: (Bool, Int64)] = [:]
let validCategories: [PeerCacheUsageCategory] = [.image, .video, .audio, .file]
var totalSize: Int64 = 0
for categoryId in validCategories {
if let media = categories[categoryId] {
var categorySize: Int64 = 0
for (_, size) in media {
categorySize += size
}
sizeIndex[categoryId] = (true, categorySize)
totalSize += categorySize
}
}
if let statsPromise = statsPromise {
let clearCategories = sizeIndex.keys.filter({ sizeIndex[$0]!.0 })
var clearMediaIds = Set<MediaId>()
var media = stats.media
if var categories = media[peerId] {
for category in clearCategories {
if let contents = categories[category] {
for (mediaId, _) in contents {
clearMediaIds.insert(mediaId)
}
}
categories.removeValue(forKey: category)
}
media[peerId] = categories
}
if let additionalPeerId = additionalPeerId {
if var categories = media[additionalPeerId] {
for category in clearCategories {
if let contents = categories[category] {
for (mediaId, _) in contents {
clearMediaIds.insert(mediaId)
}
}
categories.removeValue(forKey: category)
}
media[additionalPeerId] = categories
}
}
var clearResourceIds = Set<WrappedMediaResourceId>()
for id in clearMediaIds {
if let ids = stats.mediaResourceIds[id] {
for resourceId in ids {
clearResourceIds.insert(WrappedMediaResourceId(resourceId))
}
}
}
var signal = clearCachedMediaResources(account: context.account, mediaResourceIds: clearResourceIds)
let resultStats = CacheUsageStats(media: media, mediaResourceIds: stats.mediaResourceIds, peers: stats.peers, otherSize: stats.otherSize, otherPaths: stats.otherPaths, cacheSize: stats.cacheSize, tempPaths: stats.tempPaths, tempSize: stats.tempSize, immutableSize: stats.immutableSize)
var cancelImpl: (() -> Void)?
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let progressSignal = Signal<Never, NoError> { subscriber in
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
cancelImpl?()
}))
presentControllerImpl?(controller, .window(.root), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
return ActionDisposable { [weak controller] in
Queue.mainQueue().async() {
controller?.dismiss()
}
}
}
|> runOn(Queue.mainQueue())
|> delay(0.15, queue: Queue.mainQueue())
let progressDisposable = progressSignal.start()
signal = signal
|> afterDisposed {
Queue.mainQueue().async {
progressDisposable.dispose()
}
}
cancelImpl = {
clearDisposable.set(nil)
resetStats()
}
clearDisposable.set((signal
|> deliverOnMainQueue).start(completed: {
statsPromise.set(.single(.result(resultStats)))
presentControllerImpl?(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(totalSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: false, action: { _ in }), .current, nil)
}))
}
}
}
})
updateState { state in
return state.withUpdatedPeerIdWithRevealedOptions(nil)
}
}, setPeerIdWithRevealedOptions: { peerId, fromPeerId in
updateState { state in
if (peerId == nil && fromPeerId == state.peerIdWithRevealedOptions) || (peerId != nil && fromPeerId == nil) {
return state.withUpdatedPeerIdWithRevealedOptions(peerId)
} else {
return state
}
}
})
var dismissImpl: (() -> Void)?
let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), statsPromise.get()) |> deliverOnMainQueue
|> map { presentationData, cacheSettings, cacheStats -> (ItemListControllerState, (ItemListNodeState, Any)) in
let signal = combineLatest(context.sharedContext.presentationData, cacheSettingsPromise.get(), statsPromise.get(), statePromise.get()) |> deliverOnMainQueue
|> map { presentationData, cacheSettings, cacheStats, state -> (ItemListControllerState, (ItemListNodeState, Any)) in
let leftNavigationButton = isModal ? ItemListNavigationButton(content: .text(presentationData.strings.Common_Cancel), style: .regular, enabled: true, action: {
dismissImpl?()
}) : nil
let controllerState = ItemListControllerState(theme: presentationData.theme, title: .text(presentationData.strings.Cache_Title), leftNavigationButton: leftNavigationButton, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: false)
let listState = ItemListNodeState(entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats), style: .blocks, emptyStateItem: nil, animateChanges: false)
let listState = ItemListNodeState(entries: storageUsageControllerEntries(presentationData: presentationData, cacheSettings: cacheSettings, cacheStats: cacheStats, state: state), style: .blocks, emptyStateItem: nil, animateChanges: false)
return (controllerState, (listState, arguments))
} |> afterDisposed {

View File

@ -0,0 +1,318 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramCore
import SyncCore
import TelegramUIPreferences
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
struct StorageUsageCategory: Equatable {
let title: String
let size: Int64
let fraction: CGFloat
let color: UIColor
}
final class StorageUsageItem: ListViewItem, ItemListItem {
let theme: PresentationTheme
let dateTimeFormat: PresentationDateTimeFormat
let categories: [StorageUsageCategory]
let sectionId: ItemListSectionId
init(theme: PresentationTheme, dateTimeFormat: PresentationDateTimeFormat, categories: [StorageUsageCategory], sectionId: ItemListSectionId) {
self.theme = theme
self.dateTimeFormat = dateTimeFormat
self.categories = categories
self.sectionId = sectionId
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = StorageUsageItemNode()
let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? StorageUsageItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem))
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
}
private func generateDotImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 8.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.clear(bounds)
context.setFillColor(color.cgColor)
context.fillEllipse(in: bounds)
})
}
private func generateLineMaskImage(color: UIColor) -> UIImage? {
return generateImage(CGSize(width: 8.0, height: 8.0), rotatedContext: { size, context in
let bounds = CGRect(origin: CGPoint(), size: size)
context.setFillColor(color.cgColor)
context.fill(bounds)
context.setBlendMode(.clear)
context.setFillColor(UIColor.clear.cgColor)
context.fillEllipse(in: bounds)
})?.stretchableImage(withLeftCapWidth: 4, topCapHeight: 4)
}
private final class StorageUsageItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let maskNode: ASImageNode
private let lineMaskNode: ASImageNode
private var lineNodes: [ASDisplayNode]
private var descriptionNodes: [(ASImageNode, TextNode)]
private var item: StorageUsageItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.maskNode = ASImageNode()
self.lineMaskNode = ASImageNode()
self.lineMaskNode.displaysAsynchronously = false
self.lineMaskNode.displayWithoutProcessing = true
self.lineMaskNode.contentMode = .scaleToFill
self.lineNodes = []
self.descriptionNodes = []
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.lineMaskNode)
}
func asyncLayout() -> (_ item: StorageUsageItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
let currentItem = self.item
return { [weak self] item, params, neighbors in
if let strongSelf = self, strongSelf.lineNodes.count != item.categories.count {
for node in strongSelf.lineNodes {
node.removeFromSupernode()
}
strongSelf.lineNodes = []
for pair in strongSelf.descriptionNodes {
pair.0.removeFromSupernode()
pair.1.removeFromSupernode()
}
strongSelf.descriptionNodes = []
for _ in item.categories {
let lineNode = ASDisplayNode()
strongSelf.insertSubnode(lineNode, belowSubnode: strongSelf.lineMaskNode)
strongSelf.lineNodes.append(lineNode)
let dotNode = ASImageNode()
dotNode.displaysAsynchronously = false
dotNode.displayWithoutProcessing = true
strongSelf.addSubnode(dotNode)
let textNode = TextNode()
strongSelf.addSubnode(textNode)
strongSelf.descriptionNodes.append((dotNode, textNode))
}
}
var makeNodesLayout: [(TextNodeLayoutArguments) -> (TextNodeLayout, () -> TextNode)] = []
if let strongSelf = self {
for nodes in strongSelf.descriptionNodes {
let makeTextLayout = TextNode.asyncLayout(nodes.1)
makeNodesLayout.append(makeTextLayout)
}
}
var themeUpdated = false
if currentItem?.theme !== item.theme {
themeUpdated = true
}
let contentSize: CGSize
let insets: UIEdgeInsets
let separatorHeight = UIScreenPixel
var textFramesApplies: [(CGRect, () -> TextNode)] = []
let inset: CGFloat = 16.0
let horizontalSpacing: CGFloat = 32.0
let verticalSpacing: CGFloat = 22.0
var textOrigin: CGPoint = CGPoint(x: horizontalSpacing, y: 52.0)
for i in 0 ..< item.categories.count {
let makeTextLayout = makeNodesLayout[i]
let category = item.categories[i]
let attributedString = NSMutableAttributedString(string: category.title, font: Font.regular(14.0), textColor: item.theme.list.itemPrimaryTextColor, paragraphAlignment: .natural)
attributedString.append(NSAttributedString(string: "\(dataSizeString(category.size, forceDecimal: true, decimalSeparator: item.dateTimeFormat.decimalSeparator))", font: Font.bold(14.0), textColor: item.theme.list.itemPrimaryTextColor))
let (textLayout, textApply) = makeTextLayout(TextNodeLayoutArguments(attributedString: attributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 60.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
var textFrame = CGRect(origin: textOrigin, size: textLayout.size)
if textFrame.maxX > params.width - params.rightInset - inset {
textFrame.origin = CGPoint(x: horizontalSpacing, y: textOrigin.y + verticalSpacing)
}
textOrigin = CGPoint(x: textFrame.maxX + horizontalSpacing, y: textFrame.minY)
textFramesApplies.append((textFrame, textApply))
}
contentSize = CGSize(width: params.width, height: textOrigin.y + 34.0)
insets = itemListNeighborsGroupedInsets(neighbors)
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
let layoutSize = layout.size
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor
strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor
if themeUpdated {
strongSelf.lineMaskNode.image = generateLineMaskImage(color: item.theme.list.itemBlocksBackgroundColor)
}
for (_, textApply) in textFramesApplies {
let _ = textApply()
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
if strongSelf.maskNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
}
let hasCorners = itemListHasRoundedBlockLayout(params)
var hasTopCorners = false
var hasBottomCorners = false
switch neighbors.top {
case .sameSection(false):
strongSelf.topStripeNode.isHidden = true
default:
hasTopCorners = true
strongSelf.topStripeNode.isHidden = hasCorners
}
let bottomStripeInset: CGFloat
let bottomStripeOffset: CGFloat
switch neighbors.bottom {
case .sameSection(false):
bottomStripeInset = 0.0
bottomStripeOffset = -separatorHeight
default:
bottomStripeInset = 0.0
bottomStripeOffset = 0.0
hasBottomCorners = true
strongSelf.bottomStripeNode.isHidden = hasCorners
}
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0)
strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight))
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight))
let lineInset: CGFloat = params.leftInset + 12.0
var lineOrigin = CGPoint(x: lineInset, y: 16.0)
let lineWidth = params.width - lineOrigin.x * 2.0
strongSelf.lineMaskNode.frame = CGRect(origin: lineOrigin, size: CGSize(width: lineWidth, height: 21.0))
for i in 0 ..< strongSelf.lineNodes.count {
let lineNode = strongSelf.lineNodes[i]
let category = item.categories[i]
lineNode.backgroundColor = category.color
var categoryWidth = max(floor(lineWidth * category.fraction), 6.0)
if i == strongSelf.lineNodes.count - 1 {
categoryWidth = lineWidth - (lineOrigin.x - lineInset)
}
let lineRect = CGRect(origin: lineOrigin, size: CGSize(width: categoryWidth, height: 21.0))
lineNode.frame = lineRect
lineOrigin.x += lineRect.width + 1.0
}
for i in 0 ..< strongSelf.descriptionNodes.count {
let dotNode = strongSelf.descriptionNodes[i].0
let textNode = strongSelf.descriptionNodes[i].1
let textFrame = textFramesApplies[i].0
let category = item.categories[i]
if dotNode.image == nil || themeUpdated {
dotNode.image = generateDotImage(color: category.color)
}
dotNode.frame = CGRect(x: textFrame.minX - 16.0, y: textFrame.minY + 4.0, width: 8.0, height: 8.0)
textNode.frame = textFrame
}
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
}

View File

@ -9,7 +9,7 @@ import ItemListUI
import PhotoResources
import OpenInExternalAppUI
public class WebBrowserItem: ListViewItem, ItemListItem {
class WebBrowserItem: ListViewItem, ItemListItem {
let account: Account
let theme: PresentationTheme
let title: String
@ -71,7 +71,7 @@ public class WebBrowserItem: ListViewItem, ItemListItem {
private let titleFont = Font.regular(17.0)
public class WebBrowserItemNode: ListViewItemNode {
private final class WebBrowserItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode

View File

@ -0,0 +1,12 @@
import UIKit
public func stringForDeviceType() -> String {
let model = UIDevice.current.model.lowercased()
if model.contains("ipad") {
return "iPad"
} else if model.contains("ipod") {
return "iPod touch"
} else {
return "iPhone"
}
}

View File

@ -3454,8 +3454,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
guard let strongSelf = self, strongSelf.beginMediaRecordingRequestId == requestId else {
return
}
guard checkAvailableDiskSpace(context: strongSelf.context, present: { [weak self] c, a in
self?.present(c, in: .window(.root), with: a)
guard checkAvailableDiskSpace(context: strongSelf.context, push: { [weak self] c in
self?.push(c)
}) else {
return
}
@ -5292,8 +5292,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
disposable.set((signal
|> deliverOnMainQueue).start(completed: { [weak self] in
if let strongSelf = self, let layout = strongSelf.validLayout {
let deviceName = UIDevice.current.userInterfaceIdiom == .pad ? "iPad" : "iPhone"
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", deviceName).0), elevatedLayout: true, action: { _ in }), in: .current)
strongSelf.present(UndoOverlayController(presentationData: presentationData, content: .succeed(text: presentationData.strings.ClearCache_Success("\(dataSizeString(selectedSize, decimalSeparator: presentationData.dateTimeFormat.decimalSeparator))", stringForDeviceType()).0), elevatedLayout: true, action: { _ in }), in: .current)
}
}))

View File

@ -7,7 +7,7 @@ import AlertUI
import PresentationDataUtils
import SettingsUI
func totalDiskSpace() -> Int64 {
private func totalDiskSpace() -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
return (systemAttributes[FileAttributeKey.systemSize] as? NSNumber)?.int64Value ?? 0
@ -16,7 +16,7 @@ func totalDiskSpace() -> Int64 {
}
}
func freeDiskSpace() -> Int64 {
private func freeDiskSpace() -> Int64 {
do {
let systemAttributes = try FileManager.default.attributesOfFileSystem(forPath: NSHomeDirectory() as String)
return (systemAttributes[FileAttributeKey.systemFreeSize] as? NSNumber)?.int64Value ?? 0
@ -25,18 +25,16 @@ func freeDiskSpace() -> Int64 {
}
}
func checkAvailableDiskSpace(context: AccountContext, threshold: Int64 = 100 * 1024 * 1024, present: @escaping (ViewController, Any?) -> Void) -> Bool {
func checkAvailableDiskSpace(context: AccountContext, threshold: Int64 = 100 * 1024 * 1024, push: @escaping (ViewController) -> Void) -> Bool {
guard freeDiskSpace() < threshold else {
return true
}
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let controller = textAlertController(context: context, title: nil, text: presentationData.strings.Cache_LowDiskSpaceText, actions: [TextAlertAction(type: .genericAction, title: presentationData.strings.AccessDenied_Settings, action: {
let controller = storageUsageController(context: context, isModal: true)
present(controller, ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
push(storageUsageController(context: context, isModal: true))
}), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_OK, action: {})])
present(controller, nil)
push(controller)
return false
}