Piechart update
@ -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?
|
||||
|
||||
private var labels: [AnyHashable: ChartLabel] = [:]
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
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()
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return
|
||||
private struct CalculatedSection {
|
||||
var id: StorageUsageScreenComponent.Category
|
||||
var color: UIColor
|
||||
var innerAngle: Range<CGFloat>
|
||||
var outerAngle: Range<CGFloat>
|
||||
var innerRadius: CGFloat
|
||||
var outerRadius: CGFloat
|
||||
var label: CalculatedLabel?
|
||||
|
||||
init(
|
||||
id: StorageUsageScreenComponent.Category,
|
||||
color: UIColor,
|
||||
innerAngle: Range<CGFloat>,
|
||||
outerAngle: Range<CGFloat>,
|
||||
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
|
||||
}
|
||||
guard let _ = self.theme, let data = self.currentAnimation?.current ?? self.data else {
|
||||
return
|
||||
}
|
||||
if data.items.isEmpty {
|
||||
|
||||
private struct ItemAngleData {
|
||||
var angleValue: CGFloat
|
||||
var startAngle: CGFloat
|
||||
var endAngle: CGFloat
|
||||
}
|
||||
|
||||
private struct CalculatedLayout {
|
||||
var size: CGSize
|
||||
var sections: [CalculatedSection]
|
||||
|
||||
init(size: CGSize, sections: [CalculatedSection]) {
|
||||
self.size = size
|
||||
self.sections = sections
|
||||
}
|
||||
|
||||
init(size: CGSize, items: [ChartData.Item], selectedKey: AnyHashable?) {
|
||||
self.size = size
|
||||
self.sections = []
|
||||
|
||||
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,21 +329,67 @@ 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) {
|
||||
var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)?
|
||||
for i in 0 ..< items.count {
|
||||
let item = items[i]
|
||||
let angleData = anglesData[i]
|
||||
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 {
|
||||
currentMergedItem.displayValue += item.displayValue
|
||||
currentMergedItem.angleData.startAngle = min(currentMergedItem.angleData.startAngle, angleData.startAngle)
|
||||
currentMergedItem.angleData.endAngle = max(currentMergedItem.angleData.endAngle, angleData.endAngle)
|
||||
mergedItem = currentMergedItem
|
||||
} else {
|
||||
let invertedMergeFactor: CGFloat = 1.0 - max(0.0, item.mergeFactor)
|
||||
mergedItem = (item.displayValue, angleData, invertedMergeFactor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*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<CGFloat>,
|
||||
outerAngle: Range<CGFloat>,
|
||||
innerRadius: CGFloat,
|
||||
outerRadius: CGFloat
|
||||
) {
|
||||
let fractionValue: Double = floor(displayValue * 100.0 * 10.0) / 10.0
|
||||
let fractionString: String
|
||||
if fractionValue < 0.1 {
|
||||
@ -363,39 +400,33 @@ final class PieChartComponent: Component {
|
||||
fractionString = "\(fractionValue)"
|
||||
}
|
||||
|
||||
let label: ChartLabel
|
||||
if let current = self.labels[id] {
|
||||
label = current
|
||||
} else {
|
||||
label = ChartLabel()
|
||||
self.labels[id] = label
|
||||
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
|
||||
}
|
||||
let labelSize = label.update(text: "\(fractionString)%")
|
||||
|
||||
var labelFrame: CGRect?
|
||||
var resultLabel: CalculatedLabel?
|
||||
|
||||
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
|
||||
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 = (innerStartAngle + innerEndAngle) * 0.5
|
||||
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset)
|
||||
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
|
||||
)
|
||||
|
||||
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
|
||||
@ -438,19 +469,19 @@ final class PieChartComponent: Component {
|
||||
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 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), 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 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(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)))
|
||||
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,
|
||||
@ -461,7 +492,7 @@ final class PieChartComponent: Component {
|
||||
intersectionInnerLeft
|
||||
]
|
||||
|
||||
if angleValue < CGFloat.pi / 2.0 {
|
||||
if innerAngle.upperBound - innerAngle.lowerBound < CGFloat.pi / 2.0 {
|
||||
distances.append(contentsOf: [
|
||||
intersectionLine1TopRight,
|
||||
intersectionLine1BottomRight,
|
||||
@ -483,122 +514,478 @@ final class PieChartComponent: Component {
|
||||
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)
|
||||
let currentScale = finalSize.width / labelSize.width
|
||||
|
||||
if finalSize.width >= labelSize.width {
|
||||
labelFrame = currentFrame
|
||||
if currentScale >= 1.0 - 0.001 {
|
||||
resultLabel = CalculatedLabel(
|
||||
image: labelImage,
|
||||
alpha: 1.0,
|
||||
angle: midAngle,
|
||||
radius: centerDistance,
|
||||
scale: 1.0
|
||||
)
|
||||
break
|
||||
}
|
||||
if let labelFrame {
|
||||
if labelFrame.width > finalSize.width {
|
||||
if let resultLabel {
|
||||
if resultLabel.scale > currentScale {
|
||||
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
|
||||
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)
|
||||
|
||||
let labelCenter = CGPoint(
|
||||
x: shapeLayerFrame.midX + relLabelCenter.x,
|
||||
y: shapeLayerFrame.midY + relLabelCenter.y
|
||||
resultLabel = CalculatedLabel(
|
||||
image: labelImage,
|
||||
alpha: 0.0,
|
||||
angle: midAngle,
|
||||
radius: centerDistance,
|
||||
scale: 0.001
|
||||
)
|
||||
|
||||
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)
|
||||
if let resultLabel {
|
||||
self.sections[index].label = resultLabel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var labelScale = labelFrame.width / labelSize.width
|
||||
private struct Particle {
|
||||
var trackIndex: Int
|
||||
var position: CGPoint
|
||||
var scale: CGFloat
|
||||
var alpha: CGFloat
|
||||
var direction: CGPoint
|
||||
var velocity: CGFloat
|
||||
|
||||
var normalAlpha: CGFloat = labelScale < 0.4 ? 0.0 : 1.0
|
||||
normalAlpha *= max(0.0, mergeFactor)
|
||||
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
|
||||
}
|
||||
|
||||
var relLabelCenter = CGPoint(
|
||||
x: labelFrame.midX - shapeLayerFrame.midX,
|
||||
y: labelFrame.midY - shapeLayerFrame.midY
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let labelAlpha: CGFloat
|
||||
if let selectedKey = self.selectedKey {
|
||||
if selectedKey == id {
|
||||
labelAlpha = normalAlpha
|
||||
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 {
|
||||
labelAlpha = 0.0
|
||||
|
||||
let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter)
|
||||
let reducedDiameterFactor: CGFloat = reducedDiameter / diameter
|
||||
|
||||
labelScale *= reducedFactor
|
||||
|
||||
relLabelCenter.x *= reducedDiameterFactor
|
||||
relLabelCenter.y *= reducedDiameterFactor
|
||||
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 {
|
||||
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)
|
||||
particleLayer = SimpleLayer()
|
||||
particleLayer.contents = particleImage.cgImage
|
||||
particleLayer.bounds = CGRect(origin: CGPoint(), size: particleImage.size)
|
||||
self.particleLayers.append(particleLayer)
|
||||
self.insertSublayer(particleLayer, above: self.gradientLayer)
|
||||
}
|
||||
|
||||
let labelCenter = CGPoint(
|
||||
x: shapeLayerFrame.midX + relLabelCenter.x,
|
||||
y: shapeLayerFrame.midY + relLabelCenter.y
|
||||
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
|
||||
)
|
||||
|
||||
labelView.center = labelCenter
|
||||
labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale)
|
||||
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()
|
||||
}
|
||||
|
||||
var mergedItem: (displayValue: Double, angleData: ItemAngleData, mergeFactor: CGFloat)?
|
||||
for i in 0 ..< data.items.count {
|
||||
let item = data.items[i]
|
||||
let angleData = anglesData[i]
|
||||
updateItemLabel(id: item.id, displayValue: item.displayValue, mergeFactor: item.mergeFactor, angleData: angleData)
|
||||
|
||||
if item.mergeable {
|
||||
if var currentMergedItem = mergedItem {
|
||||
currentMergedItem.displayValue += item.displayValue
|
||||
currentMergedItem.angleData.startAngle = min(currentMergedItem.angleData.startAngle, angleData.startAngle)
|
||||
currentMergedItem.angleData.endAngle = max(currentMergedItem.angleData.endAngle, angleData.endAngle)
|
||||
mergedItem = currentMergedItem
|
||||
}, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentAnimation = nil
|
||||
self.update()
|
||||
})
|
||||
} else {
|
||||
let invertedMergeFactor: CGFloat = 1.0 - max(0.0, item.mergeFactor)
|
||||
mergedItem = (item.displayValue, angleData, invertedMergeFactor)
|
||||
self.currentLayout = CalculatedLayout(
|
||||
size: CGSize(width: 200.0, height: 200.0),
|
||||
items: data.items,
|
||||
selectedKey: self.selectedKey
|
||||
)
|
||||
}
|
||||
|
||||
self.data = data
|
||||
|
||||
self.update()
|
||||
}
|
||||
}
|
||||
|
||||
if let mergedItem {
|
||||
updateItemLabel(id: "merged", displayValue: mergedItem.displayValue, mergeFactor: mergedItem.mergeFactor, angleData: mergedItem.angleData)
|
||||
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 {
|
||||
if let label = self.labels["merged"] {
|
||||
self.labels.removeValue(forKey: "merged")
|
||||
label.removeFromSuperview()
|
||||
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
|
||||
|
@ -0,0 +1,9 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"provides-namespace" : true
|
||||
}
|
||||
}
|
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleAvatars.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Profile Photos.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Profile Photos</title>
|
||||
<g id="Icon-/-Profile-Photos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M36,10 C21.648,10 10,21.648 10,36 C10,50.352 21.648,62 36,62 C50.352,62 62,50.352 62,36 C62,21.648 50.352,10 36,10 Z M36,19.8 C40.316,19.8 43.8,23.284 43.8,27.6 C43.8,31.916 40.316,35.4 36,35.4 C31.684,35.4 28.2,31.916 28.2,27.6 C28.2,23.284 31.684,19.8 36,19.8 Z M36,54.72 C29.5,54.72 23.754,51.4742263 20.4,46.5548505 C20.478,42.4839792 29.826,39.72 36,39.72 C42.174,39.72 51.522,42.4839792 51.6,46.5548505 C48.246,51.4742263 42.5,54.72 36,54.72 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 860 B |
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleDocuments.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Documents.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Documents</title>
|
||||
<g id="Icon-/-Documents" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M43.15125,14.3808511 C42.24875,13.4914894 41.0375,13 39.77875,13 L24.7,13 C21.1375,13 18,16.106383 18,19.6170213 L18,50.3829787 C18,53.893617 21.11375,57 24.67625,57 L49.3,57 C52.8625,57 56,53.893617 56,50.3829787 L56,28.9851064 C56,27.7446809 55.50125,26.5510638 54.59875,25.6851064 L43.15125,14.3808511 Z M39.375,27.0425532 L39.375,16.5106383 L52.4375,29.3829787 L41.75,29.3829787 C40.44375,29.3829787 39.375,28.3297872 39.375,27.0425532 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 842 B |
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Music.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
7
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleMusic.imageset/Music.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Music</title>
|
||||
<g id="Icon-/-Music" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M36,10 C50.352,10 62,21.648 62,36 C62,50.352 50.352,62 36,62 C21.648,62 10,50.352 10,36 C10,21.648 21.648,10 36,10 Z M33.1111111,23.950754 C31.5156218,23.950754 30.2222222,25.2441536 30.2222222,26.8396429 L30.2222222,45.6296296 C30.2222222,46.2546991 30.4249583,46.8629074 30.8,47.362963 C31.7572936,48.6393544 33.568053,48.8980343 34.8444444,47.9407407 L46.5881862,39.1329344 C46.7600033,39.0040716 46.9169145,38.8564504 47.0560109,38.6928077 C48.089327,37.4771416 47.9415032,35.6539814 46.7258372,34.6206653 L34.9820954,24.6384848 C34.4597827,24.194519 33.7966155,23.950754 33.1111111,23.950754 Z" id="Shape" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 991 B |
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Other.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
7
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleOther.imageset/Other.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Other</title>
|
||||
<g id="Icon-/-Other" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M31.2684326,15 L17.9882353,15 C15.2447059,15 13.0249412,17.25 13.0249412,20 L13,49.0697674 C13,52.75 15.2447059,55 18.9294118,55 L55.0705882,55 C58.7552941,55 61,52.75 61,49.0697674 L61,25.9302326 C61,22.25 58.7552941,20.5813953 55.0705882,20.5813953 L40.0468122,20.5813953 C38.7070557,20.5813953 37.430813,20.0103436 36.5380314,19.0113998 L34.0754572,16.2559964 C33.361232,15.4568414 32.3402378,15 31.2684326,15 Z" id="Path" fill="#FFFFFF" fill-rule="nonzero"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 806 B |
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Photos.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
7
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticlePhotos.imageset/Photos.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Photos</title>
|
||||
<g id="Icon-/-Photos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M48,12 C54.627417,12 60,17.372583 60,24 L60,48 C60,54.627417 54.627417,60 48,60 L24,60 C17.372583,60 12,54.627417 12,48 L12,24 C12,17.372583 17.372583,12 24,12 L48,12 Z M44.3651844,34.6306685 C43.8183724,33.9010116 42.7247485,33.9010116 42.1779366,34.6036442 L33.7023513,45.3863509 L27.9608258,38.549196 C27.3866733,37.8735878 26.3477306,37.9006121 25.8282592,38.6032446 L19.9804504,47.2510295 C19.2695949,48.1428323 19.8984286,50.4 21.0467337,50.4 L51.9081103,50.4 C52.9904208,50.4 53.639443,48.3210078 53.0676996,47.374937 L44.3651844,34.6306685 Z" id="Shape" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 924 B |
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleStickers.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Stickers.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Stickers</title>
|
||||
<g id="Icon-/-Stickers" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M60,35.5 L59.9953966,35.670698 C59.9067791,37.309531 58.5498341,38.6111111 56.8888889,38.6111111 L50.6666667,38.6111111 L50.3912615,38.6142092 C45.9287855,38.7147025 42.0676082,41.2512307 40.0852923,44.9462311 C39.7167534,44.8455073 39.3161253,44.8387068 38.9195771,44.9463572 C37.9769648,45.202247 36.9982344,45.3333333 36,45.3333333 C33.4531941,45.3333333 29.5907927,43.2780139 28.1265456,41.3638425 L27.9058916,41.062472 C27.1856257,40.1544843 25.8744537,39.9480152 24.9057896,40.6151336 C23.8950097,41.3112571 23.6399302,42.6949756 24.3360537,43.7057556 C26.5046744,46.8546196 31.8881746,49.7777778 36,49.7777778 C36.9171651,49.7777778 37.8247038,49.6981543 38.7149707,49.541295 C38.6826363,49.8921863 38.6666667,50.2497837 38.6666667,50.6111111 L38.6666667,56.8888889 L38.6620632,57.0595868 C38.5734457,58.6984199 37.2165008,60 35.5555556,60 L24.4137931,60 C17.5578445,60 12,54.4421555 12,47.5862069 L12,24.4137931 C12,17.5578445 17.5578445,12 24.4137931,12 L47.5862069,12 C54.4421555,12 60,17.5578445 60,24.4137931 L60,35.5 Z M42.6383189,59.9550937 L42.457984,59.9664377 C42.8777423,59.0264139 43.1111111,57.9849045 43.1111111,56.8888889 L43.1111111,50.6111111 L43.1144503,50.384302 C43.2343809,46.3164026 46.569718,43.0555556 50.6666667,43.0555556 L56.8888889,43.0555556 L57.115698,43.0522164 C58.130088,43.0223099 59.0942932,42.7924609 59.9702649,42.4007181 C59.4478271,51.6700337 52.1614105,59.1320852 42.9674765,59.929458 L42.6383189,59.9550937 Z M44.4444444,28 C42.7262252,28 41.3333333,29.790861 41.3333333,32 C41.3333333,34.209139 42.7262252,36 44.4444444,36 C46.1626637,36 47.5555556,34.209139 47.5555556,32 C47.5555556,29.790861 46.1626637,28 44.4444444,28 Z M27.5555556,28 C25.8373363,28 24.4444444,29.790861 24.4444444,32 C24.4444444,34.209139 25.8373363,36 27.5555556,36 C29.2737748,36 30.6666667,34.209139 30.6666667,32 C30.6666667,29.790861 29.2737748,28 27.5555556,28 Z" id="Shape" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 2.2 KiB |
12
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "Videos.svg",
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
7
submodules/TelegramUI/Images.xcassets/Settings/Storage/ParticleVideos.imageset/Videos.svg
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="72px" height="72px" viewBox="0 0 72 72" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<title>Icon / Videos</title>
|
||||
<g id="Icon-/-Videos" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
|
||||
<path d="M18.5095398,18.85 L36.9719416,18.85 C40.5670612,18.85 43.4814815,21.7644203 43.4814815,25.3595398 L43.4814815,45.6404602 C43.4814815,49.2355797 40.5670612,52.15 36.9719416,52.15 L18.5095398,52.15 C14.9144203,52.15 12,49.2355797 12,45.6404602 L12,25.3595398 C12,21.7644203 14.9144203,18.85 18.5095398,18.85 Z M49.5847133,27.1084323 L56.6902242,21.3231792 C58.0841802,20.1882308 60.1342623,20.3981979 61.2692107,21.7921539 C61.741904,22.3727209 62,23.0984963 62,23.8471598 L62,47.6096485 C62,49.4072083 60.5427899,50.8644185 58.7452301,50.8644185 C58.0575374,50.8644185 57.3875158,50.6465978 56.831289,50.2422056 L49.8668429,45.1788626 C48.1820423,43.9539665 47.1851852,41.9967567 47.1851852,39.9137485 L47.1851852,32.1563935 C47.1851852,30.1985078 48.0664292,28.3446077 49.5847133,27.1084323 Z" id="Shape" fill="#FFFFFF"></path>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.1 KiB |