diff --git a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift index 00aa1b8386..2f6a3adb8b 100644 --- a/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift +++ b/submodules/TelegramUI/Components/StorageUsageScreen/Sources/PieChartComponent.swift @@ -181,77 +181,76 @@ final class PieChartComponent: Component { return true } - private final class ChartDataView: UIView { - private(set) var theme: PresentationTheme? - private(set) var data: ChartData? - private(set) var selectedKey: AnyHashable? + private struct CalculatedLabel { + var image: UIImage + var alpha: CGFloat + var angle: CGFloat + var radius: CGFloat + var scale: CGFloat - private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)? - private var animator: DisplayLinkAnimator? + init( + image: UIImage, + alpha: CGFloat, + angle: CGFloat, + radius: CGFloat, + scale: CGFloat + ) { + self.image = image + self.alpha = alpha + self.angle = angle + self.radius = radius + self.scale = scale + } + } + + private struct CalculatedSection { + var id: StorageUsageScreenComponent.Category + var color: UIColor + var innerAngle: Range + var outerAngle: Range + var innerRadius: CGFloat + var outerRadius: CGFloat + var label: CalculatedLabel? - private var labels: [AnyHashable: ChartLabel] = [:] + init( + id: StorageUsageScreenComponent.Category, + color: UIColor, + innerAngle: Range, + outerAngle: Range, + innerRadius: CGFloat, + outerRadius: CGFloat, + label: CalculatedLabel? + ) { + self.id = id + self.color = color + self.innerAngle = innerAngle + self.outerAngle = outerAngle + self.innerRadius = innerRadius + self.outerRadius = outerRadius + self.label = label + } + } + + private struct ItemAngleData { + var angleValue: CGFloat + var startAngle: CGFloat + var endAngle: CGFloat + } + + private struct CalculatedLayout { + var size: CGSize + var sections: [CalculatedSection] - override init(frame: CGRect) { - super.init(frame: frame) + init(size: CGSize, sections: [CalculatedSection]) { + self.size = size + self.sections = sections + } + + init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?) { + self.size = size + self.sections = [] - self.backgroundColor = nil - self.isOpaque = false - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - deinit { - self.animator?.invalidate() - } - - func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { - let data = processChartData(data: data) - - if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { - self.theme = theme - self.selectedKey = selectedKey - - if animated, let previous = self.data { - var initialState = previous - if let currentAnimation = self.currentAnimation { - initialState = currentAnimation.current - } - self.currentAnimation = (initialState, data, initialState, 0.0) - self.animator?.invalidate() - self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in - guard let self else { - return - } - let progress = listViewAnimationCurveSystem(progress) - if let currentAnimationValue = self.currentAnimation { - self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress), progress) - self.setNeedsDisplay() - } - }, completion: { [weak self] in - guard let self else { - return - } - self.currentAnimation = nil - self.setNeedsDisplay() - }) - } - - self.data = data - - self.setNeedsDisplay() - } - } - - override func draw(_ rect: CGRect) { - guard let context = UIGraphicsGetCurrentContext() else { - return - } - guard let _ = self.theme, let data = self.currentAnimation?.current ?? self.data else { - return - } - if data.items.isEmpty { + if items.isEmpty { return } @@ -260,8 +259,8 @@ final class PieChartComponent: Component { let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5) var angles: [Double] = [] - for i in 0 ..< data.items.count { - let item = data.items[i] + for i in 0 ..< items.count { + let item = items[i] let angle = item.value * CGFloat.pi * 2.0 angles.append(angle) } @@ -269,22 +268,14 @@ final class PieChartComponent: Component { let diameter: CGFloat = 200.0 let reducedDiameter: CGFloat = 170.0 - let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter)) - - struct ItemAngleData { - var angleValue: CGFloat - var startAngle: CGFloat - var endAngle: CGFloat - } - var anglesData: [ItemAngleData] = [] var startAngle: CGFloat = 0.0 - for i in 0 ..< data.items.count { - let item = data.items[i] + for i in 0 ..< items.count { + let item = items[i] let itemOuterDiameter: CGFloat - if let selectedKey = self.selectedKey { + if let selectedKey { if selectedKey == AnyHashable(item.id) { itemOuterDiameter = diameter } else { @@ -303,16 +294,16 @@ final class PieChartComponent: Component { if item.mergeable { let previousItem: ChartData.Item if i == 0 { - previousItem = data.items[data.items.count - 1] + previousItem = items[items.count - 1] } else { - previousItem = data.items[i - 1] + previousItem = items[i - 1] } let nextItem: ChartData.Item - if i == data.items.count - 1 { - nextItem = data.items[0] + if i == items.count - 1 { + nextItem = items[0] } else { - nextItem = data.items[i + 1] + nextItem = items[i + 1] } if previousItem.mergeable { @@ -338,243 +329,34 @@ final class PieChartComponent: Component { var arcOuterEndAngle = startAngle + angleValue - angleSpacing * 0.5 * afterSpacingFraction arcOuterEndAngle = max(arcOuterEndAngle, arcOuterStartAngle) - let path = CGMutablePath() - - path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: arcInnerEndAngle, endAngle: arcInnerStartAngle, clockwise: true) - path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: arcOuterStartAngle, endAngle: arcOuterEndAngle, clockwise: false) - - context.addPath(path) - context.setFillColor(item.color.cgColor) - context.fillPath() + self.sections.append(CalculatedSection( + id: item.id, + color: item.color, + innerAngle: arcInnerStartAngle ..< arcInnerEndAngle, + outerAngle: arcOuterStartAngle ..< arcOuterEndAngle, + innerRadius: innerDiameter * 0.5, + outerRadius: itemOuterDiameter * 0.5, + label: nil + )) startAngle += angleValue anglesData.append(ItemAngleData(angleValue: angleValue, startAngle: innerStartAngle, endAngle: innerEndAngle)) } - func updateItemLabel(id: AnyHashable, displayValue: Double, mergeFactor: CGFloat, angleData: ItemAngleData) { - let fractionValue: Double = floor(displayValue * 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)" - } - - let label: ChartLabel - if let current = self.labels[id] { - label = current - } else { - label = ChartLabel() - self.labels[id] = label - } - let labelSize = label.update(text: "\(fractionString)%") - - var labelFrame: CGRect? - - let angleValue = angleData.angleValue - let innerStartAngle = angleData.startAngle - let innerEndAngle = angleData.endAngle - - if angleValue >= 0.001 { - for step in 0 ... 20 { - let stepFraction: CGFloat = CGFloat(step) / 20.0 - let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction - - let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 - let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset) - - let relLabelCenter = CGPoint( - x: cos(midAngle) * centerDistance, - y: sin(midAngle) * centerDistance - ) - - let labelCenter = CGPoint( - x: shapeLayerFrame.midX + relLabelCenter.x, - y: shapeLayerFrame.midY + relLabelCenter.y - ) - - func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat { - let dx: CGFloat = p2.x - p1.x - let dy: CGFloat = p2.y - p1.y - let dr: CGFloat = sqrt(dx * dx + dy * dy) - let D: CGFloat = p1.x * p2.y - p2.x * p1.y - - var minDistance: CGFloat = 10000.0 - - for i in 0 ..< 2 { - let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0) - let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0 - let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr) - let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr) - let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0)) - minDistance = min(minDistance, distance) - } - - return minDistance - } - - func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat { - let x1 = p1.x - let y1 = p1.y - let x2 = p2.x - let y2 = p2.y - let x3 = p3.x - let y3 = p3.y - let x4 = p4.x - let y4 = p4.y - - let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) - if abs(d) <= 0.00001 { - return 10000.0 - } - - let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d - let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d - - let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0)) - return distance - } - - let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5) - let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5) - let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5) - let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5) - - let horizontalInset: CGFloat = 2.0 - let intersectionOuterLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), diameter * 0.5) - horizontalInset - let intersectionInnerLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), innerDiameter * 0.5) - horizontalInset - - let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle))) - let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle))) - let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle))) - let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle))) - - var distances: [CGFloat] = [ - intersectionOuterTopRight, - intersectionInnerTopRight, - intersectionOuterBottomRight, - intersectionInnerBottomRight, - intersectionOuterLeft, - intersectionInnerLeft - ] - - if angleValue < CGFloat.pi / 2.0 { - distances.append(contentsOf: [ - intersectionLine1TopRight, - intersectionLine1BottomRight, - intersectionLine2TopRight, - intersectionLine2BottomRight - ] as [CGFloat]) - } - - var minDistance: CGFloat = 1000.0 - for distance in distances { - minDistance = min(minDistance, max(distance, 1.0)) - } - - let diagonalAngle = atan2(labelSize.height, labelSize.width) - - let maxHalfWidth = cos(diagonalAngle) * minDistance - let maxHalfHeight = sin(diagonalAngle) * minDistance - - let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0) - let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height)) - - let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize) - - if finalSize.width >= labelSize.width { - labelFrame = currentFrame - break - } - if let labelFrame { - if labelFrame.width > finalSize.width { - continue - } - } - labelFrame = currentFrame - } - } else { - let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5 - let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * 0.5) - - let relLabelCenter = CGPoint( - x: cos(midAngle) * centerDistance, - y: sin(midAngle) * centerDistance - ) - - let labelCenter = CGPoint( - x: shapeLayerFrame.midX + relLabelCenter.x, - y: shapeLayerFrame.midY + relLabelCenter.y - ) - - let minSize = labelSize.aspectFitted(CGSize(width: 4.0, height: 4.0)) - labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - minSize.width * 0.5, y: labelCenter.y - minSize.height * 0.5), size: minSize) - } - - let labelView = label - if let labelFrame { - var animateIn: Bool = false - if labelView.superview == nil { - animateIn = true - self.addSubview(labelView) - } - - var labelScale = labelFrame.width / labelSize.width - - var normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0 - normalAlpha *= max(0.0, mergeFactor) - - var relLabelCenter = CGPoint( - x: labelFrame.midX - shapeLayerFrame.midX, - y: labelFrame.midY - shapeLayerFrame.midY - ) - - let labelAlpha: CGFloat - if let selectedKey = self.selectedKey { - if selectedKey == id { - labelAlpha = normalAlpha - } else { - labelAlpha = 0.0 - - let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter) - let reducedDiameterFactor: CGFloat = reducedDiameter / diameter - - labelScale *= reducedFactor - - relLabelCenter.x *= reducedDiameterFactor - relLabelCenter.y *= reducedDiameterFactor - } - } else { - labelAlpha = normalAlpha - } - if labelView.alpha != labelAlpha { - let transition: Transition - if animateIn || "".isEmpty { - transition = .immediate - } else { - transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut)) - } - transition.setAlpha(view: labelView, alpha: labelAlpha) - } - - let labelCenter = CGPoint( - x: shapeLayerFrame.midX + relLabelCenter.x, - y: shapeLayerFrame.midY + relLabelCenter.y - ) - - labelView.center = labelCenter - labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale) - } - } - var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)? - for i in 0 ..< data.items.count { - let item = data.items[i] + for i in 0 ..< items.count { + let item = items[i] let angleData = anglesData[i] - updateItemLabel(id: item.id, displayValue: item.displayValue, mergeFactor: item.mergeFactor, angleData: angleData) + self.updateLabel( + index: i, + displayValue: item.displayValue, + mergeFactor: item.mergeFactor, + innerAngle: self.sections[i].innerAngle, + outerAngle: self.sections[i].outerAngle, + innerRadius: self.sections[i].innerRadius, + outerRadius: self.sections[i].outerRadius + ) if item.mergeable { if var currentMergedItem = mergedItem { @@ -589,17 +371,622 @@ final class PieChartComponent: Component { } } - if let mergedItem { + /*if let mergedItem { updateItemLabel(id: "merged", displayValue: mergedItem.displayValue, mergeFactor: mergedItem.mergeFactor, angleData: mergedItem.angleData) } else { if let label = self.labels["merged"] { self.labels.removeValue(forKey: "merged") label.removeFromSuperview() } + }*/ + } + + private mutating func updateLabel( + index: Int, + displayValue: Double, + mergeFactor: CGFloat, + innerAngle: Range, + outerAngle: Range, + innerRadius: CGFloat, + outerRadius: CGFloat + ) { + let fractionValue: Double = floor(displayValue * 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)" + } + + let labelString = NSAttributedString(string: "\(fractionString)%", font: chartLabelFont, textColor: .white) + let labelBounds = labelString.boundingRect(with: CGSize(width: 100.0, height: 100.0), options: [.usesLineFragmentOrigin], context: nil) + let labelSize = CGSize(width: ceil(labelBounds.width), height: ceil(labelBounds.height)) + guard let labelImage = generateImage(labelSize, rotatedContext: { size, context in + context.clear(CGRect(origin: CGPoint(), size: size)) + UIGraphicsPushContext(context) + labelString.draw(in: labelBounds) + UIGraphicsPopContext() + }) else { + return + } + + var resultLabel: CalculatedLabel? + + if innerAngle.upperBound - innerAngle.lowerBound >= 0.001 { + for step in 0 ... 10 { + let stepFraction: CGFloat = CGFloat(step) / 10.0 + let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction + + let midAngle: CGFloat = (innerAngle.lowerBound + innerAngle.upperBound) * 0.5 + let centerDistance: CGFloat = (innerRadius + (outerRadius - innerRadius) * centerOffset) + + let relLabelCenter = CGPoint( + x: cos(midAngle) * centerDistance, + y: sin(midAngle) * centerDistance + ) + + func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat { + let dx: CGFloat = p2.x - p1.x + let dy: CGFloat = p2.y - p1.y + let dr: CGFloat = sqrt(dx * dx + dy * dy) + let D: CGFloat = p1.x * p2.y - p2.x * p1.y + + var minDistance: CGFloat = 10000.0 + + for i in 0 ..< 2 { + let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0) + let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0 + let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr) + let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr) + let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0)) + minDistance = min(minDistance, distance) + } + + return minDistance + } + + func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat { + let x1 = p1.x + let y1 = p1.y + let x2 = p2.x + let y2 = p2.y + let x3 = p3.x + let y3 = p3.y + let x4 = p4.x + let y4 = p4.y + + let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4) + if abs(d) <= 0.00001 { + return 10000.0 + } + + let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d + let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d + + let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0)) + return distance + } + + let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), outerRadius) + let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerRadius) + let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), outerRadius) + let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerRadius) + + let horizontalInset: CGFloat = 2.0 + let intersectionOuterLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), outerRadius) - horizontalInset + let intersectionInnerLeft = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y), innerRadius) - horizontalInset + + let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.lowerBound), y: sin(innerAngle.lowerBound))) + let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.lowerBound), y: sin(innerAngle.lowerBound))) + let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.upperBound), y: sin(innerAngle.upperBound))) + let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerAngle.upperBound), y: sin(innerAngle.upperBound))) + + var distances: [CGFloat] = [ + intersectionOuterTopRight, + intersectionInnerTopRight, + intersectionOuterBottomRight, + intersectionInnerBottomRight, + intersectionOuterLeft, + intersectionInnerLeft + ] + + if innerAngle.upperBound - innerAngle.lowerBound < CGFloat.pi / 2.0 { + distances.append(contentsOf: [ + intersectionLine1TopRight, + intersectionLine1BottomRight, + intersectionLine2TopRight, + intersectionLine2BottomRight + ] as [CGFloat]) + } + + var minDistance: CGFloat = 1000.0 + for distance in distances { + minDistance = min(minDistance, max(distance, 1.0)) + } + + let diagonalAngle = atan2(labelSize.height, labelSize.width) + + let maxHalfWidth = cos(diagonalAngle) * minDistance + let maxHalfHeight = sin(diagonalAngle) * minDistance + + let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0) + let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height)) + + let currentScale = finalSize.width / labelSize.width + + if currentScale >= 1.0 - 0.001 { + resultLabel = CalculatedLabel( + image: labelImage, + alpha: 1.0, + angle: midAngle, + radius: centerDistance, + scale: 1.0 + ) + break + } + if let resultLabel { + if resultLabel.scale > currentScale { + continue + } + } + resultLabel = CalculatedLabel( + image: labelImage, + alpha: currentScale >= 0.2 ? 1.0 : 0.0, + angle: midAngle, + radius: centerDistance, + scale: currentScale + ) + } + } else { + let midAngle: CGFloat = (innerAngle.lowerBound + innerAngle.upperBound) * 0.5 + let centerDistance: CGFloat = (innerRadius + (outerRadius - innerRadius) * 0.5) + + resultLabel = CalculatedLabel( + image: labelImage, + alpha: 0.0, + angle: midAngle, + radius: centerDistance, + scale: 0.001 + ) + } + + if let resultLabel { + self.sections[index].label = resultLabel } } } + private struct Particle { + var trackIndex: Int + var position: CGPoint + var scale: CGFloat + var alpha: CGFloat + var direction: CGPoint + var velocity: CGFloat + + init( + trackIndex: Int, + position: CGPoint, + scale: CGFloat, + alpha: CGFloat, + direction: CGPoint, + velocity: CGFloat + ) { + self.trackIndex = trackIndex + self.position = position + self.scale = scale + self.alpha = alpha + self.direction = direction + self.velocity = velocity + } + + mutating func update(deltaTime: CGFloat) { + var position = self.position + position.x += self.direction.x * self.velocity * deltaTime + position.y += self.direction.y * self.velocity * deltaTime + self.position = position + } + } + + private final class ParticleSet { + private(set) var particles: [Particle] = [] + + init() { + self.generateParticles(preAdvance: true) + } + + private func generateParticles(preAdvance: Bool) { + let maxDirections = 24 + + if self.particles.count < maxDirections { + var allTrackIndices: [Int] = Array(repeating: 0, count: maxDirections) + for i in 0 ..< maxDirections { + allTrackIndices[i] = i + } + var takenIndexCount = 0 + for particle in self.particles { + allTrackIndices[particle.trackIndex] = -1 + takenIndexCount += 1 + } + var availableTrackIndices: [Int] = [] + availableTrackIndices.reserveCapacity(maxDirections - takenIndexCount) + for index in allTrackIndices { + if index != -1 { + availableTrackIndices.append(index) + } + } + + if !availableTrackIndices.isEmpty { + availableTrackIndices.shuffle() + + for takeIndex in availableTrackIndices { + let directionIndex = takeIndex + let angle = (CGFloat(directionIndex % maxDirections) / CGFloat(maxDirections)) * CGFloat.pi * 2.0 + + let direction = CGPoint(x: cos(angle), y: sin(angle)) + let velocity = CGFloat.random(in: 20.0 ..< 40.0) + let alpha = CGFloat.random(in: 0.1 ..< 0.4) + let scale = CGFloat.random(in: 0.5 ... 1.0) * 0.22 + + var position = CGPoint(x: 100.0, y: 100.0) + var initialOffset: CGFloat = 0.4 + if preAdvance { + initialOffset = CGFloat.random(in: initialOffset ... 1.0) + } + position.x += direction.x * initialOffset * 105.0 + position.y += direction.y * initialOffset * 105.0 + + let particle = Particle( + trackIndex: directionIndex, + position: position, + scale: scale, + alpha: alpha, + direction: direction, + velocity: velocity + ) + self.particles.append(particle) + } + } + } + } + + func update(deltaTime: CGFloat) { + let size = CGSize(width: 200.0, height: 200.0) + let radius2 = pow(size.width * 0.5 + 10.0, 2.0) + for i in (0 ..< self.particles.count).reversed() { + self.particles[i].update(deltaTime: deltaTime) + let position = self.particles[i].position + + if pow(position.x - size.width * 0.5, 2.0) + pow(position.y - size.height * 0.5, 2.0) > radius2 { + self.particles.remove(at: i) + } + } + + self.generateParticles(preAdvance: false) + } + } + + private final class SectionLayer: SimpleLayer { + private let maskLayer: SimpleShapeLayer + private let gradientLayer: SimpleGradientLayer + private let labelLayer: SimpleLayer + + private var currentLabelImage: UIImage? + + private var particleImage: UIImage? + private var particleLayers: [SimpleLayer] = [] + + init(category: StorageUsageScreenComponent.Category) { + self.maskLayer = SimpleShapeLayer() + self.maskLayer.fillColor = UIColor.white.cgColor + + self.gradientLayer = SimpleGradientLayer() + self.gradientLayer.type = .radial + self.gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5) + self.gradientLayer.endPoint = CGPoint(x: 1.0, y: 1.0) + + self.labelLayer = SimpleLayer() + + super.init() + + self.mask = self.maskLayer + self.addSublayer(self.gradientLayer) + self.addSublayer(self.labelLayer) + + switch category { + case .photos: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticlePhotos")?.precomposed() + case .videos: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleVideos")?.precomposed() + case .files: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleDocuments")?.precomposed() + case .music: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleMusic")?.precomposed() + case .other: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleOther")?.precomposed() + case .stickers: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleStickers")?.precomposed() + case .avatars: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleAvatars")?.precomposed() + case .misc: + self.particleImage = UIImage(bundleImageName: "Settings/Storage/ParticleOther")?.precomposed() + } + } + + override init(layer: Any) { + self.maskLayer = SimpleShapeLayer() + self.gradientLayer = SimpleGradientLayer() + self.labelLayer = SimpleLayer() + + super.init(layer: layer) + } + + required init(coder: NSCoder) { + preconditionFailure() + } + + func update(size: CGSize, section: CalculatedSection) { + self.maskLayer.frame = CGRect(origin: CGPoint(), size: size) + self.gradientLayer.frame = CGRect(origin: CGPoint(), size: size) + + let normalColor = section.color.cgColor + let darkerColor = section.color.withMultipliedBrightnessBy(0.96).cgColor + let colors: [CGColor] = [ + darkerColor, + normalColor, + normalColor, + normalColor, + darkerColor + ] + self.gradientLayer.colors = colors + + let locations: [CGFloat] = [ + 0.0, + 0.3, + 0.5, + 0.7, + 1.0 + ] + self.gradientLayer.locations = locations.map { location in + let location = location * 0.5 + 0.5 + return location as NSNumber + } + + let path = CGMutablePath() + path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.innerRadius, startAngle: section.innerAngle.upperBound, endAngle: section.innerAngle.lowerBound, clockwise: true) + path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.outerRadius, startAngle: section.outerAngle.lowerBound, endAngle: section.outerAngle.upperBound, clockwise: false) + self.maskLayer.path = path + + if let label = section.label { + if self.currentLabelImage !== label.image { + self.currentLabelImage = label.image + self.labelLayer.contents = label.image.cgImage + } + + let position = CGPoint(x: size.width * 0.5 + cos(label.angle) * label.radius, y: size.height * 0.5 + sin(label.angle) * label.radius) + let labelSize = CGSize(width: label.image.size.width * label.scale, height: label.image.size.height * label.scale) + let labelFrame = CGRect(origin: CGPoint(x: position.x - labelSize.width * 0.5, y: position.y - labelSize.height * 0.5), size: labelSize) + self.labelLayer.frame = labelFrame + self.labelLayer.opacity = Float(label.alpha) + } else { + self.currentLabelImage = nil + self.labelLayer.contents = nil + } + } + + func updateParticles(particleSet: ParticleSet) { + guard let particleImage = self.particleImage else { + return + } + for i in 0 ..< particleSet.particles.count { + let particle = particleSet.particles[i] + + let particleLayer: SimpleLayer + if i < self.particleLayers.count { + particleLayer = self.particleLayers[i] + particleLayer.isHidden = false + } else { + particleLayer = SimpleLayer() + particleLayer.contents = particleImage.cgImage + particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size) + self.particleLayers.append(particleLayer) + self.insertSublayer(particleLayer, above: self.gradientLayer) + } + + particleLayer.position = particle.position + particleLayer.transform = CATransform3DMakeScale(particle.scale, particle.scale, 1.0) + particleLayer.opacity = Float(particle.alpha) + } + if particleSet.particles.count < self.particleLayers.count { + for i in particleSet.particles.count ..< self.particleLayers.count { + self.particleLayers[i].isHidden = true + } + } + } + } + + private final class ChartDataView: UIView { + private(set) var theme: PresentationTheme? + private(set) var data: ChartData? + private(set) var selectedKey: AnyHashable? + + private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)? + private var currentLayout: CalculatedLayout? + private var animator: DisplayLinkAnimator? + + private var displayLink: SharedDisplayLinkDriver.Link? + + private var sectionLayers: [AnyHashable: SectionLayer] = [:] + private let particleSet: ParticleSet + private var labels: [AnyHashable: ChartLabel] = [:] + + override init(frame: CGRect) { + self.particleSet = ParticleSet() + + super.init(frame: frame) + + self.backgroundColor = nil + self.isOpaque = false + + self.displayLink = SharedDisplayLinkDriver.shared.add(needsHighestFramerate: true, { [weak self] in + self?.update() + }) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + deinit { + self.animator?.invalidate() + } + + func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) { + let data = processChartData(data: data) + + if self.theme !== theme || self.data != data || self.selectedKey != selectedKey { + self.theme = theme + self.selectedKey = selectedKey + + if animated, let previous = self.data { + var initialState = previous + if let currentAnimation = self.currentAnimation { + initialState = currentAnimation.current + } + self.currentAnimation = (initialState, data, initialState, 0.0) + self.currentLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: initialState.items, + selectedKey: self.selectedKey + ) + self.animator?.invalidate() + self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in + guard let self else { + return + } + let progress = listViewAnimationCurveSystem(progress) + if let currentAnimationValue = self.currentAnimation { + let interpolatedValue = interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress) + self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolatedValue, progress) + self.currentLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: interpolatedValue.items, + selectedKey: self.selectedKey + ) + self.update() + } + }, completion: { [weak self] in + guard let self else { + return + } + self.currentAnimation = nil + self.update() + }) + } else { + self.currentLayout = CalculatedLayout( + size: CGSize(width: 200.0, height: 200.0), + items: data.items, + selectedKey: self.selectedKey + ) + } + + self.data = data + + self.update() + } + } + + private func update() { + self.particleSet.update(deltaTime: 1.0 / 60.0) + + var validIds: [AnyHashable] = [] + if let currentLayout = self.currentLayout { + for section in currentLayout.sections { + validIds.append(section.id) + + let sectionLayer: SectionLayer + if let current = self.sectionLayers[section.id] { + sectionLayer = current + } else { + sectionLayer = SectionLayer(category: section.id) + self.sectionLayers[section.id] = sectionLayer + self.layer.addSublayer(sectionLayer) + } + + sectionLayer.frame = CGRect(origin: CGPoint(), size: CGSize(width: 200.0, height: 200.0)) + sectionLayer.update(size: sectionLayer.bounds.size, section: section) + sectionLayer.updateParticles(particleSet: self.particleSet) + } + } + + var removeIds: [AnyHashable] = [] + for (id, sectionLayer) in self.sectionLayers { + if !validIds.contains(id) { + removeIds.append(id) + sectionLayer.removeFromSuperlayer() + } + } + for id in removeIds { + self.sectionLayers.removeValue(forKey: id) + } + } + + /*override func draw(_ rect: CGRect) { + guard let context = UIGraphicsGetCurrentContext() else { + return + } + guard let currentLayout = self.currentLayout else { + return + } + + let size = CGSize(width: rect.width, height: rect.height) + + for section in currentLayout.sections { + if section.innerAngle.lowerBound == section.innerAngle.upperBound { + continue + } + let path = CGMutablePath() + path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.innerRadius, startAngle: section.innerAngle.upperBound, endAngle: section.innerAngle.lowerBound, clockwise: true) + path.addArc(center: CGPoint(x: size.width * 0.5, y: size.height * 0.5), radius: section.outerRadius, startAngle: section.outerAngle.lowerBound, endAngle: section.outerAngle.upperBound, clockwise: false) + + context.addPath(path) + context.clip() + + let colors: [CGColor] = [ + section.color.withMultipliedBrightnessBy(0.9).cgColor, + section.color.cgColor, + section.color.cgColor, + section.color.cgColor, + section.color.withMultipliedBrightnessBy(0.9).cgColor + ] + var locations: [CGFloat] = [ + 1.0, + 0.9, + 0.5, + 0.1, + 0.0 + ] + if let gradient = CGGradient(colorsSpace: nil, colors: colors as CFArray, locations: &locations) { + context.drawRadialGradient(gradient, startCenter: CGPoint(x: size.width * 0.5, y: size.height * 0.5), startRadius: section.innerRadius, endCenter: CGPoint(x: size.width * 0.5, y: size.height * 0.5), endRadius: section.outerRadius, options: []) + } + + context.resetClip() + + //context.setFillColor(section.color.cgColor) + //context.fillPath() + + if let label = section.label { + let position = CGPoint(x: size.width * 0.5 + cos(label.angle) * label.radius, y: size.height * 0.5 + sin(label.angle) * label.radius) + let labelSize = CGSize(width: label.image.size.width * label.scale, height: label.image.size.height * label.scale) + let labelFrame = CGRect(origin: CGPoint(x: position.x - labelSize.width * 0.5, y: position.y - labelSize.height * 0.5), size: labelSize) + label.image.draw(in: labelFrame, blendMode: .normal, alpha: label.alpha) + } + } + }*/ + } + class View: UIView { private let dataView: ChartDataView private var labels: [StorageUsageScreenComponent.Category: ComponentView] = [:] diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/Contents.json new file mode 100644 index 0000000000..6e965652df --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/Contents.json @@ -0,0 +1,9 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "provides-namespace" : true + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Contents.json new file mode 100644 index 0000000000..bcc0438465 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Profile Photos.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Profile Photos.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Profile Photos.svg new file mode 100644 index 0000000000..60a180f4fd --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Profile Photos.svg @@ -0,0 +1,7 @@ + + + Icon / Profile Photos + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Contents.json new file mode 100644 index 0000000000..120a59fd74 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Documents.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Documents.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Documents.svg new file mode 100644 index 0000000000..f69d052835 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Documents.svg @@ -0,0 +1,7 @@ + + + Icon / Documents + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Contents.json new file mode 100644 index 0000000000..1b4b2c3beb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Music.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Music.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Music.svg new file mode 100644 index 0000000000..de6c5abbe3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Music.svg @@ -0,0 +1,7 @@ + + + Icon / Music + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Contents.json new file mode 100644 index 0000000000..3387c50e5b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Other.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Other.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Other.svg new file mode 100644 index 0000000000..436a2ef442 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Other.svg @@ -0,0 +1,7 @@ + + + Icon / Other + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Contents.json new file mode 100644 index 0000000000..393e403fd4 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Photos.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Photos.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Photos.svg new file mode 100644 index 0000000000..5b5fa61b20 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Photos.svg @@ -0,0 +1,7 @@ + + + Icon / Photos + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Contents.json new file mode 100644 index 0000000000..b84d8c2123 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Stickers.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Stickers.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Stickers.svg new file mode 100644 index 0000000000..4d2ce8eb11 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Stickers.svg @@ -0,0 +1,7 @@ + + + Icon / Stickers + + + + \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Contents.json new file mode 100644 index 0000000000..12a3baa92e --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "Videos.svg", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Videos.svg b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Videos.svg new file mode 100644 index 0000000000..2432eee397 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Videos.svg @@ -0,0 +1,7 @@ + + + Icon / Videos + + + + \ No newline at end of file