mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-11-07 01:10:09 +00:00
Charts improvements
This commit is contained in:
parent
c5d39df2b3
commit
d6f0a02fc7
BIN
Telegram/Telegram-iOS/Resources/Charts.tgs
Normal file
BIN
Telegram/Telegram-iOS/Resources/Charts.tgs
Normal file
Binary file not shown.
@ -5368,6 +5368,7 @@ Any member of this group will be able to see messages in the channel.";
|
|||||||
"Stats.InteractionsTitle" = "INTERACTIONS";
|
"Stats.InteractionsTitle" = "INTERACTIONS";
|
||||||
"Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS";
|
"Stats.InstantViewInteractionsTitle" = "INSTANT VIEW INTERACTIONS";
|
||||||
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
|
"Stats.ViewsBySourceTitle" = "VIEWS BY SOURCE";
|
||||||
|
"Stats.ViewsByHoursTitle" = "VIEWS BY HOURS";
|
||||||
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
|
"Stats.FollowersBySourceTitle" = "FOLLOWERS BY SOURCE";
|
||||||
"Stats.LanguagesTitle" = "LANGUAGES";
|
"Stats.LanguagesTitle" = "LANGUAGES";
|
||||||
"Stats.PostsTitle" = "RECENT POSTS";
|
"Stats.PostsTitle" = "RECENT POSTS";
|
||||||
@ -5386,6 +5387,9 @@ Any member of this group will be able to see messages in the channel.";
|
|||||||
"Stats.MessageForwards_many" = "%@ forwards";
|
"Stats.MessageForwards_many" = "%@ forwards";
|
||||||
"Stats.MessageForwards_any" = "%@ forwards";
|
"Stats.MessageForwards_any" = "%@ forwards";
|
||||||
|
|
||||||
|
"Stats.LoadingTitle" = "Preparing stats";
|
||||||
|
"Stats.LoadingText" = "Please wait a few moments while\nwe generate your stats";
|
||||||
|
|
||||||
"InstantPage.Views_0" = "%@ views";
|
"InstantPage.Views_0" = "%@ views";
|
||||||
"InstantPage.Views_1" = "%@ view";
|
"InstantPage.Views_1" = "%@ view";
|
||||||
"InstantPage.Views_2" = "%@ views";
|
"InstantPage.Views_2" = "%@ views";
|
||||||
|
|||||||
@ -38,7 +38,7 @@ open class OverlayMediaItemNode: ASDisplayNode {
|
|||||||
open func setShouldAcquireContext(_ value: Bool) {
|
open func setShouldAcquireContext(_ value: Bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
open func preferredSizeForOverlayDisplay() -> CGSize {
|
open func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
|
||||||
return CGSize(width: 50.0, height: 50.0)
|
return CGSize(width: 50.0, height: 50.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1881,8 +1881,7 @@ class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func foldLineBreaks(_ text: String) -> String {
|
private func foldLineBreaks(_ text: String) -> String {
|
||||||
var lines = text.split { $0.isNewline }
|
let lines = text.split { $0.isNewline }
|
||||||
var startedBothLines = false
|
|
||||||
var result = ""
|
var result = ""
|
||||||
for line in lines {
|
for line in lines {
|
||||||
if line.isEmpty {
|
if line.isEmpty {
|
||||||
|
|||||||
@ -67,7 +67,15 @@ public extension ChartsCollection {
|
|||||||
}
|
}
|
||||||
switch type {
|
switch type {
|
||||||
case .axix:
|
case .axix:
|
||||||
axixValuesToSetup = try column.dropFirst().map { Date(timeIntervalSince1970: try Convert.doubleFrom($0) / 1000) }
|
axixValuesToSetup = try column.dropFirst().map { value in
|
||||||
|
let numberValue = try Convert.doubleFrom(value)
|
||||||
|
if numberValue < 24.0 {
|
||||||
|
return Date(timeIntervalSince1970: numberValue)
|
||||||
|
} else {
|
||||||
|
return Date(timeIntervalSince1970: numberValue / 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
case .chart, .bar, .area, .step:
|
case .chart, .bar, .area, .step:
|
||||||
guard let colorString = colors[columnId],
|
guard let colorString = colors[columnId],
|
||||||
let color = GColor(hexString: colorString) else {
|
let color = GColor(hexString: colorString) else {
|
||||||
|
|||||||
@ -245,8 +245,14 @@ class GeneralChartComponentController: ChartThemeContainer {
|
|||||||
var labels: [LinesChartLabel] = []
|
var labels: [LinesChartLabel] = []
|
||||||
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
for index in stride(from: chartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||||
let date = chartsCollection.axisValues[index]
|
let date = chartsCollection.axisValues[index]
|
||||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
let timestamp = date.timeIntervalSince1970
|
||||||
text: scaleType.dateFormatter.string(from: date)))
|
if timestamp <= 24 {
|
||||||
|
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||||
|
text: "\(Int(timestamp)):00"))
|
||||||
|
} else {
|
||||||
|
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||||
|
text: scaleType.dateFormatter.string(from: date)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
prevoiusHorizontalStrideInterval = strideInterval
|
prevoiusHorizontalStrideInterval = strideInterval
|
||||||
horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||||
@ -318,8 +324,9 @@ class GeneralChartComponentController: ChartThemeContainer {
|
|||||||
tapAction: { [weak self] in
|
tapAction: { [weak self] in
|
||||||
self?.zoomInOnDateClosure?(closestDate) },
|
self?.zoomInOnDateClosure?(closestDate) },
|
||||||
hideAction: { [weak self] in
|
hideAction: { [weak self] in
|
||||||
|
self?.setDetailsChartVisibleClosure?(false, true)
|
||||||
|
|
||||||
})
|
})
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -157,8 +157,14 @@ public class BaseLinesChartController: BaseChartController {
|
|||||||
var labels: [LinesChartLabel] = []
|
var labels: [LinesChartLabel] = []
|
||||||
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
for index in stride(from: initialChartsCollection.axisValues.count - 1, to: -1, by: -strideInterval).reversed() {
|
||||||
let date = initialChartsCollection.axisValues[index]
|
let date = initialChartsCollection.axisValues[index]
|
||||||
labels.append(LinesChartLabel(value: CGFloat(date.timeIntervalSince1970),
|
let timestamp = date.timeIntervalSince1970
|
||||||
text: scaleType.dateFormatter.string(from: date)))
|
if timestamp <= 24 {
|
||||||
|
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||||
|
text: "\(Int(timestamp)):00"))
|
||||||
|
} else {
|
||||||
|
labels.append(LinesChartLabel(value: CGFloat(timestamp),
|
||||||
|
text: scaleType.dateFormatter.string(from: date)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return (strideInterval, labels)
|
return (strideInterval, labels)
|
||||||
}
|
}
|
||||||
@ -179,14 +185,14 @@ public class BaseLinesChartController: BaseChartController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
func verticalLimitsLabels(verticalRange: ClosedRange<CGFloat>) -> (ClosedRange<CGFloat>, [LinesChartLabel]) {
|
||||||
let ditance = verticalRange.distance
|
let distance = verticalRange.distance
|
||||||
let chartHeight = chartFrame().height
|
let chartHeight = chartFrame().height
|
||||||
|
|
||||||
guard ditance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
guard distance > 0, chartHeight > 0 else { return (BaseConstants.defaultRange, []) }
|
||||||
|
|
||||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||||
|
|
||||||
var numberOfOffsetsPerItem = ditance / approximateNumberOfChartValues
|
var numberOfOffsetsPerItem = distance / approximateNumberOfChartValues
|
||||||
var multiplier: CGFloat = 1.0
|
var multiplier: CGFloat = 1.0
|
||||||
while numberOfOffsetsPerItem > 10 {
|
while numberOfOffsetsPerItem > 10 {
|
||||||
numberOfOffsetsPerItem /= 10
|
numberOfOffsetsPerItem /= 10
|
||||||
|
|||||||
@ -237,7 +237,7 @@ public class TwoAxisLinesChartController: BaseLinesChartController {
|
|||||||
let chartHeight = chartFrame().height
|
let chartHeight = chartFrame().height
|
||||||
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||||
|
|
||||||
var dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
let dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||||
let (index, controller) = arg
|
let (index, controller) = arg
|
||||||
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
|
let verticalRange = LinesChartRenderer.LineData.verticalRange(lines: controller.chartLines,
|
||||||
calculatingRange: horizontalRange,
|
calculatingRange: horizontalRange,
|
||||||
|
|||||||
@ -138,7 +138,7 @@ class PercentChartComponentController: GeneralChartComponentController {
|
|||||||
|
|
||||||
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
let values: [ChartDetailsViewModel.Value] = chartsCollection.chartValues.enumerated().map { arg in
|
||||||
let (index, component) = arg
|
let (index, component) = arg
|
||||||
return ChartDetailsViewModel.Value(prefix: PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100),
|
return ChartDetailsViewModel.Value(prefix: total > 0 ? PercentConstants.percentValueFormatter.string(from: component.values[pointIndex] / total * 100) : "0%",
|
||||||
title: component.name,
|
title: component.name,
|
||||||
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
value: BaseConstants.detailsNumberFormatter.string(from: component.values[pointIndex]),
|
||||||
color: component.color,
|
color: component.color,
|
||||||
@ -151,16 +151,16 @@ class PercentChartComponentController: GeneralChartComponentController {
|
|||||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
||||||
}
|
}
|
||||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
let viewModel = ChartDetailsViewModel(title: dateString,
|
||||||
showArrow: self.isZoomable && !self.isZoomed,
|
showArrow: total > 0 && self.isZoomable && !self.isZoomed,
|
||||||
showPrefixes: true,
|
showPrefixes: true,
|
||||||
values: values,
|
values: values,
|
||||||
totalValue: nil,
|
totalValue: nil,
|
||||||
tapAction: { [weak self] in
|
tapAction: { [weak self] in
|
||||||
self?.hideDetailsView(animated: true)
|
self?.hideDetailsView(animated: true)
|
||||||
self?.zoomInOnDateClosure?(closestDate) },
|
self?.zoomInOnDateClosure?(closestDate) },
|
||||||
hideAction: { [weak self] in
|
hideAction: { [weak self] in
|
||||||
self?.hideDetailsView(animated: true)
|
self?.hideDetailsView(animated: true)
|
||||||
})
|
})
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,9 +95,11 @@ public class PercentPieChartController: BaseChartController {
|
|||||||
totalVerticalRange: BaseConstants.defaultRange)
|
totalVerticalRange: BaseConstants.defaultRange)
|
||||||
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
|
switchToChart(chartsCollection: percentController.chartsCollection, isZoomed: false, animated: false)
|
||||||
|
|
||||||
TimeInterval.animationDurationMultipler = 0.00001
|
if let lastDate = initialChartsCollection.axisValues.last {
|
||||||
self.didTapZoomIn(date: Date(timeIntervalSinceReferenceDate: 603849600.0), animated: false)
|
TimeInterval.animationDurationMultipler = 0.00001
|
||||||
TimeInterval.animationDurationMultipler = 1
|
self.didTapZoomIn(date: lastDate, animated: false)
|
||||||
|
TimeInterval.animationDurationMultipler = 1.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
func switchToChart(chartsCollection: ChartsCollection, isZoomed: Bool, animated: Bool) {
|
||||||
|
|||||||
@ -17,25 +17,25 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
let mainBarsRenderer: BarChartRenderer
|
let mainBarsRenderer: BarChartRenderer
|
||||||
let horizontalScalesRenderer: HorizontalScalesRenderer
|
let horizontalScalesRenderer: HorizontalScalesRenderer
|
||||||
let verticalScalesRenderer: VerticalScalesRenderer
|
let verticalScalesRenderer: VerticalScalesRenderer
|
||||||
let secondVerticalScalesRenderer: VerticalScalesRenderer?
|
|
||||||
|
|
||||||
let previewBarsChartRenderer: BarChartRenderer
|
let previewBarsChartRenderer: BarChartRenderer
|
||||||
private(set) var barsWidth: CGFloat = 1
|
private(set) var barsWidth: CGFloat = 1
|
||||||
|
|
||||||
private (set) var chartBars: BarChartRenderer.BarsData = .blank
|
private (set) var chartBars: BarChartRenderer.BarsData = .blank
|
||||||
|
|
||||||
|
private var step: Bool
|
||||||
|
|
||||||
init(isZoomed: Bool,
|
init(isZoomed: Bool,
|
||||||
mainBarsRenderer: BarChartRenderer,
|
mainBarsRenderer: BarChartRenderer,
|
||||||
horizontalScalesRenderer: HorizontalScalesRenderer,
|
horizontalScalesRenderer: HorizontalScalesRenderer,
|
||||||
verticalScalesRenderer: VerticalScalesRenderer,
|
verticalScalesRenderer: VerticalScalesRenderer,
|
||||||
previewBarsChartRenderer: BarChartRenderer) {
|
previewBarsChartRenderer: BarChartRenderer,
|
||||||
|
step: Bool = false) {
|
||||||
self.mainBarsRenderer = mainBarsRenderer
|
self.mainBarsRenderer = mainBarsRenderer
|
||||||
self.horizontalScalesRenderer = horizontalScalesRenderer
|
self.horizontalScalesRenderer = horizontalScalesRenderer
|
||||||
self.verticalScalesRenderer = verticalScalesRenderer
|
self.verticalScalesRenderer = verticalScalesRenderer
|
||||||
self.previewBarsChartRenderer = previewBarsChartRenderer
|
self.previewBarsChartRenderer = previewBarsChartRenderer
|
||||||
|
self.step = step
|
||||||
self.secondVerticalScalesRenderer = VerticalScalesRenderer()
|
|
||||||
self.secondVerticalScalesRenderer?.isRightAligned = true
|
|
||||||
|
|
||||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||||
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
self.previewBarsChartRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||||
@ -44,7 +44,7 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
override func initialize(chartsCollection: ChartsCollection, initialDate: Date, totalHorizontalRange _: ClosedRange<CGFloat>, totalVerticalRange _: ClosedRange<CGFloat>) {
|
||||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection, separate: self.step)
|
||||||
self.chartBars = chartBars
|
self.chartBars = chartBars
|
||||||
self.barsWidth = width
|
self.barsWidth = width
|
||||||
|
|
||||||
@ -76,10 +76,10 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
mainBarsRenderer.bars = self.chartBars
|
mainBarsRenderer.bars = self.chartBars
|
||||||
previewBarsChartRenderer.bars = self.chartBars
|
previewBarsChartRenderer.bars = self.chartBars
|
||||||
|
|
||||||
previewBarsChartRenderer.setup(verticalRange: 0...117278, animated: animated)
|
previewBarsChartRenderer.setup(verticalRange: totalVerticalRange, animated: animated)
|
||||||
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
previewBarsChartRenderer.setup(horizontalRange: totalHorizontalRange, animated: animated)
|
||||||
|
|
||||||
setupMainChart(verticalRange: 0...117278, animated: animated)
|
setupMainChart(verticalRange: initialVerticalRange, animated: animated)
|
||||||
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
setupMainChart(horizontalRange: initialHorizontalRange, animated: animated)
|
||||||
|
|
||||||
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
updateChartVerticalRanges(horizontalRange: initialHorizontalRange, animated: animated)
|
||||||
@ -119,16 +119,12 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
horizontalScalesRenderer.setVisible(visible, animated: animated)
|
||||||
verticalScalesRenderer.setVisible(visible, animated: animated)
|
verticalScalesRenderer.setVisible(visible, animated: animated)
|
||||||
previewBarsChartRenderer.setVisible(visible, animated: animated)
|
previewBarsChartRenderer.setVisible(visible, animated: animated)
|
||||||
|
|
||||||
secondVerticalScalesRenderer?.setVisible(visible, animated: animated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
func setupMainChart(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
mainBarsRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||||
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
horizontalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||||
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
verticalScalesRenderer.setup(horizontalRange: horizontalRange, animated: animated)
|
||||||
|
|
||||||
secondVerticalScalesRenderer?.setup(horizontalRange: horizontalRange, animated: animated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var visibleBars: BarChartRenderer.BarsData {
|
var visibleBars: BarChartRenderer.BarsData {
|
||||||
@ -142,6 +138,7 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
|
|
||||||
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
func updateChartVerticalRanges(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars,
|
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars,
|
||||||
|
separate: self.step,
|
||||||
calculatingRange: horizontalRange,
|
calculatingRange: horizontalRange,
|
||||||
addBounds: true) {
|
addBounds: true) {
|
||||||
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
let (range, labels) = verticalLimitsLabels(verticalRange: range)
|
||||||
@ -151,31 +148,19 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
verticalScalesRenderer.setVisible(true, animated: animated)
|
verticalScalesRenderer.setVisible(true, animated: animated)
|
||||||
|
|
||||||
setupMainChart(verticalRange: range, animated: animated)
|
setupMainChart(verticalRange: range, animated: animated)
|
||||||
|
|
||||||
let (secondRange, secondLabels) = verticalLimitsLabels(verticalRange: range)
|
|
||||||
if secondVerticalScalesRenderer?.verticalRange.end != secondRange {
|
|
||||||
secondVerticalScalesRenderer?.setup(verticalLimitsLabels: secondLabels, animated: animated)
|
|
||||||
}
|
|
||||||
secondVerticalScalesRenderer?.setVisible(true, animated: animated)
|
|
||||||
} else {
|
} else {
|
||||||
verticalScalesRenderer.setVisible(false, animated: animated)
|
verticalScalesRenderer.setVisible(false, animated: animated)
|
||||||
secondVerticalScalesRenderer?.setVisible(false, animated: animated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars) {
|
if let range = BarChartRenderer.BarsData.verticalRange(bars: visibleBars, separate: self.step) {
|
||||||
previewBarsChartRenderer.setup(verticalRange: range, animated: animated)
|
previewBarsChartRenderer.setup(verticalRange: range, animated: animated)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
func setupMainChart(verticalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
var verticalRange = verticalRange
|
|
||||||
if verticalRange.upperBound > 2000 && verticalRange.upperBound < 10000 {
|
|
||||||
verticalRange = 0...117278
|
|
||||||
}
|
|
||||||
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
|
mainBarsRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||||
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
horizontalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||||
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
verticalScalesRenderer.setup(verticalRange: verticalRange, animated: animated)
|
||||||
secondVerticalScalesRenderer?.setup(verticalRange: verticalRange, animated: animated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||||
@ -198,12 +183,15 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
||||||
let visibleChartValues = self.visibleChartValues
|
let visibleChartValues = self.visibleChartValues
|
||||||
let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
||||||
|
if !self.step {
|
||||||
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
||||||
title: "Total",
|
title: "Total",
|
||||||
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
||||||
color: .white,
|
color: .white,
|
||||||
visible: visibleChartValues.count > 1)
|
visible: visibleChartValues.count > 1)
|
||||||
|
} else {
|
||||||
|
viewModel.title = ""
|
||||||
|
}
|
||||||
return viewModel
|
return viewModel
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -235,10 +223,6 @@ class BarsComponentController: GeneralChartComponentController {
|
|||||||
verticalScalesRenderer.horizontalLinesColor = theme.barChartStrongLinesColor
|
verticalScalesRenderer.horizontalLinesColor = theme.barChartStrongLinesColor
|
||||||
mainBarsRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
|
mainBarsRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
|
||||||
previewBarsChartRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
|
previewBarsChartRenderer.update(backgroundColor: theme.chartBackgroundColor, animated: false)
|
||||||
|
|
||||||
secondVerticalScalesRenderer?.labelsColor = theme.chartLabelsColor
|
|
||||||
secondVerticalScalesRenderer?.axisXColor = theme.barChartStrongLinesColor
|
|
||||||
secondVerticalScalesRenderer?.horizontalLinesColor = theme.barChartStrongLinesColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func updateChartRangeTitle(animated: Bool) {
|
override func updateChartRangeTitle(animated: Bool) {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
//
|
//
|
||||||
// DailyBarsChartController.swift
|
// StackedBarsChartController.swift
|
||||||
// GraphTest
|
// GraphTest
|
||||||
//
|
//
|
||||||
// Created by Andrei Salavei on 4/7/19.
|
// Created by Andrei Salavei on 4/7/19.
|
||||||
@ -17,6 +17,12 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
let barsController: BarsComponentController
|
let barsController: BarsComponentController
|
||||||
let zoomedBarsController: BarsComponentController
|
let zoomedBarsController: BarsComponentController
|
||||||
|
|
||||||
|
override public var isZoomable: Bool {
|
||||||
|
didSet {
|
||||||
|
barsController.isZoomable = self.isZoomable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override public init(chartsCollection: ChartsCollection) {
|
override public init(chartsCollection: ChartsCollection) {
|
||||||
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||||
@ -24,13 +30,13 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
mainBarsRenderer: BarChartRenderer(step: true),
|
mainBarsRenderer: BarChartRenderer(step: true),
|
||||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||||
verticalScalesRenderer: verticalScalesRenderer,
|
verticalScalesRenderer: verticalScalesRenderer,
|
||||||
previewBarsChartRenderer: BarChartRenderer())
|
previewBarsChartRenderer: BarChartRenderer(step: true), step: true)
|
||||||
zoomedBarsController = BarsComponentController(isZoomed: true,
|
zoomedBarsController = BarsComponentController(isZoomed: true,
|
||||||
mainBarsRenderer: BarChartRenderer(step: true),
|
mainBarsRenderer: BarChartRenderer(),
|
||||||
horizontalScalesRenderer: horizontalScalesRenderer,
|
horizontalScalesRenderer: horizontalScalesRenderer,
|
||||||
verticalScalesRenderer: verticalScalesRenderer,
|
verticalScalesRenderer: verticalScalesRenderer,
|
||||||
previewBarsChartRenderer: BarChartRenderer())
|
previewBarsChartRenderer: BarChartRenderer(), step: true)
|
||||||
|
|
||||||
super.init(chartsCollection: chartsCollection)
|
super.init(chartsCollection: chartsCollection)
|
||||||
|
|
||||||
[barsController, zoomedBarsController].forEach { controller in
|
[barsController, zoomedBarsController].forEach { controller in
|
||||||
@ -40,7 +46,7 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
self.didTapZoomIn(date: date)
|
self.didTapZoomIn(date: date)
|
||||||
}
|
}
|
||||||
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
||||||
self.setChartTitleClosure?(title, animated)
|
self.setChartTitleClosure?("", animated)
|
||||||
}
|
}
|
||||||
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
||||||
self.setDetailsViewPositionClosure?(position)
|
self.setDetailsViewPositionClosure?(position)
|
||||||
@ -60,12 +66,18 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var hourly: Bool = false
|
||||||
|
|
||||||
|
public convenience init(chartsCollection: ChartsCollection, hourly: Bool) {
|
||||||
|
self.init(chartsCollection: chartsCollection)
|
||||||
|
self.hourly = hourly
|
||||||
|
}
|
||||||
|
|
||||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||||
return [barsController.mainBarsRenderer,
|
return [barsController.mainBarsRenderer,
|
||||||
zoomedBarsController.mainBarsRenderer,
|
zoomedBarsController.mainBarsRenderer,
|
||||||
barsController.horizontalScalesRenderer,
|
barsController.horizontalScalesRenderer,
|
||||||
barsController.verticalScalesRenderer,
|
barsController.verticalScalesRenderer,
|
||||||
barsController.secondVerticalScalesRenderer!
|
|
||||||
// performanceRenderer
|
// performanceRenderer
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -90,57 +102,65 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
TimeInterval.setDefaultSuration(.osXDuration)
|
TimeInterval.setDefaultSuration(.osXDuration)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
super.isZoomed = isZoomed
|
super.isZoomed = isZoomed
|
||||||
if isZoomed {
|
if isZoomed {
|
||||||
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
|
let toHorizontalRange = zoomedBarsController.initialHorizontalRange
|
||||||
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
let destinationHorizontalRange = (toHorizontalRange.lowerBound - barsController.barsWidth)...(toHorizontalRange.upperBound - barsController.barsWidth)
|
||||||
// let initialChartVerticalRange = lineProportionAnimationRange()
|
let verticalVisibleRange = barsController.currentVerticalMainChartRange
|
||||||
|
let initialVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||||
// let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: zoomedBarsController.visibleBars,
|
|
||||||
// calculatingRange: zoomedBarsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
zoomedBarsController.mainBarsRenderer.setup(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
||||||
zoomedBarsController.mainBarsRenderer.setup(verticalRange: 0...117278, animated: false)
|
|
||||||
|
|
||||||
zoomedBarsController.setupMainChart(horizontalRange: barsController.currentHorizontalMainChartRange, animated: false)
|
|
||||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.currentPreviewHorizontalRange, animated: false)
|
||||||
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
zoomedBarsController.mainBarsRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||||
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: initialChartVerticalRange, animated: false)
|
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: initialVerticalRange, animated: false)
|
||||||
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
zoomedBarsController.mainBarsRenderer.setVisible(true, animated: false)
|
||||||
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
zoomedBarsController.previewBarsChartRenderer.setVisible(true, animated: false)
|
||||||
|
|
||||||
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
barsController.setupMainChart(horizontalRange: destinationHorizontalRange, animated: animated)
|
||||||
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
|
barsController.previewBarsChartRenderer.setup(horizontalRange: zoomedBarsController.totalHorizontalRange, animated: animated)
|
||||||
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
barsController.mainBarsRenderer.setVisible(false, animated: animated)
|
||||||
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
barsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
||||||
|
|
||||||
zoomedBarsController.willAppear(animated: animated)
|
zoomedBarsController.willAppear(animated: animated)
|
||||||
barsController.willDisappear(animated: animated)
|
barsController.willDisappear(animated: animated)
|
||||||
|
|
||||||
zoomedBarsController.updateChartsVisibility(visibility: zoomedBarsController.chartBars.components.map { _ in true }, animated: false)
|
zoomedBarsController.updateChartsVisibility(visibility: barsController.chartVisibility, animated: false)
|
||||||
|
zoomedBarsController.mainBarsRenderer.setup(verticalRange: zoomedBarsController.currentVerticalMainChartRange, animated: animated, timeFunction: .easeOut)
|
||||||
|
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: zoomedBarsController.currentPreviewVerticalRange, animated: animated, timeFunction: .easeOut)
|
||||||
} else {
|
} else {
|
||||||
if !zoomedBarsController.chartsCollection.isBlank {
|
if !zoomedBarsController.chartsCollection.isBlank {
|
||||||
barsController.hideDetailsView(animated: false)
|
barsController.hideDetailsView(animated: false)
|
||||||
|
barsController.chartVisibility = zoomedBarsController.chartVisibility
|
||||||
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
let visibleVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: barsController.visibleBars,
|
||||||
|
separate: true,
|
||||||
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
calculatingRange: barsController.initialHorizontalRange) ?? BaseConstants.defaultRange
|
||||||
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
barsController.mainBarsRenderer.setup(verticalRange: visibleVerticalRange, animated: false)
|
||||||
|
|
||||||
let toHorizontalRange = barsController.initialHorizontalRange
|
let toHorizontalRange = barsController.initialHorizontalRange
|
||||||
// let destinationChartVerticalRange = lineProportionAnimationRange()
|
|
||||||
|
let verticalVisibleRange = barsController.initialVerticalRange
|
||||||
|
let targetVerticalRange = verticalVisibleRange.lowerBound...(verticalVisibleRange.upperBound + verticalVisibleRange.distance * 10)
|
||||||
|
|
||||||
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
zoomedBarsController.setupMainChart(horizontalRange: toHorizontalRange, animated: animated)
|
||||||
// zoomedBarsController.mainLinesRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
zoomedBarsController.mainBarsRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||||
// zoomedBarsController.previewLinesChartRenderer.setup(verticalRange: destinationChartVerticalRange, animated: animated)
|
zoomedBarsController.previewBarsChartRenderer.setup(verticalRange: targetVerticalRange, animated: animated, timeFunction: .easeIn)
|
||||||
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
zoomedBarsController.previewBarsChartRenderer.setup(horizontalRange: barsController.totalHorizontalRange, animated: animated)
|
||||||
zoomedBarsController.mainBarsRenderer.setVisible(false, animated: animated)
|
DispatchQueue.main.asyncAfter(deadline: .now() + .defaultDuration) { [weak self] in
|
||||||
zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: animated)
|
self?.zoomedBarsController.mainBarsRenderer.setVisible(false, animated: false)
|
||||||
|
self?.zoomedBarsController.previewBarsChartRenderer.setVisible(false, animated: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
barsController.willAppear(animated: animated)
|
barsController.willAppear(animated: animated)
|
||||||
zoomedBarsController.willDisappear(animated: animated)
|
zoomedBarsController.willDisappear(animated: animated)
|
||||||
|
|
||||||
|
if !zoomedBarsController.chartsCollection.isBlank {
|
||||||
|
barsController.updateChartsVisibility(visibility: zoomedBarsController.chartVisibility, animated: false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
||||||
self.refreshChartToolsClosure?(animated)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||||
@ -166,13 +186,12 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
|
|
||||||
public override var actualChartsCollection: ChartsCollection {
|
public override var actualChartsCollection: ChartsCollection {
|
||||||
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
let collection = isZoomed ? zoomedBarsController.chartsCollection : barsController.chartsCollection
|
||||||
|
|
||||||
if collection.isBlank {
|
if collection.isBlank {
|
||||||
return self.initialChartsCollection
|
return self.initialChartsCollection
|
||||||
}
|
}
|
||||||
return collection
|
return collection
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||||
if isZoomed {
|
if isZoomed {
|
||||||
zoomedBarsController.chartInteractionDidBegin(point: point)
|
zoomedBarsController.chartInteractionDidBegin(point: point)
|
||||||
@ -189,6 +208,10 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public override var drawChartVisibity: Bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||||
if isZoomed {
|
if isZoomed {
|
||||||
return zoomedBarsController.currentChartHorizontalRangeFraction
|
return zoomedBarsController.currentChartHorizontalRangeFraction
|
||||||
@ -221,15 +244,6 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// func lineProportionAnimationRange() -> ClosedRange<CGFloat> {
|
|
||||||
// let visibleLines = self.barsController.chartVisibility.enumerated().compactMap { $0.element ? self.zoomedBarsController.chartLines[$0.offset] : nil }
|
|
||||||
// let linesRange = LinesChartRenderer.LineData.verticalRange(lines: visibleLines) ?? BaseConstants.defaultRange
|
|
||||||
// let barsRange = BarChartRenderer.BarsData.verticalRange(bars: self.barsController.visibleBars,
|
|
||||||
// calculatingRange: self.zoomedBarsController.totalHorizontalRange) ?? BaseConstants.defaultRange
|
|
||||||
// let range = 0...(linesRange.upperBound / barsRange.distance * self.barsController.currentVerticalMainChartRange.distance)
|
|
||||||
// return range
|
|
||||||
// }
|
|
||||||
|
|
||||||
public override func didTapZoomOut() {
|
public override func didTapZoomOut() {
|
||||||
cancelChartInteraction()
|
cancelChartInteraction()
|
||||||
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
switchToChart(chartsCollection: barsController.chartsCollection, isZoomed: false, animated: true)
|
||||||
@ -243,17 +257,10 @@ public class StepBarsChartController: BaseChartController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func apply(theme: ChartTheme, animated: Bool) {
|
public override func apply(theme: ChartTheme, animated: Bool) {
|
||||||
super.apply(theme: theme, animated: animated)
|
super.apply(theme: theme, animated: animated)
|
||||||
|
|
||||||
zoomedBarsController.apply(theme: theme, animated: animated)
|
zoomedBarsController.apply(theme: theme, animated: animated)
|
||||||
barsController.apply(theme: theme, animated: animated)
|
barsController.apply(theme: theme, animated: animated)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override var drawChartVisibity: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
|
||||||
//TODO: Добавить ховеры на кнопки
|
|
||||||
|
|||||||
@ -1,383 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
#if os(macOS)
|
|
||||||
import Cocoa
|
|
||||||
#else
|
|
||||||
import UIKit
|
|
||||||
#endif
|
|
||||||
|
|
||||||
public class StepBarsChartController2: BaseChartController {
|
|
||||||
class GraphController {
|
|
||||||
let mainBarsRenderer: BarChartRenderer
|
|
||||||
let verticalScalesRenderer = VerticalScalesRenderer()
|
|
||||||
let lineBulletsRenderer = LineBulletsRenderer()
|
|
||||||
let previewBarsRenderer: BarChartRenderer
|
|
||||||
|
|
||||||
var chartBars: BarChartRenderer.BarsData = .blank
|
|
||||||
var barsWidth: CGFloat = 1
|
|
||||||
|
|
||||||
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
|
||||||
|
|
||||||
init(isZoomed: Bool,
|
|
||||||
mainBarsRenderer: BarChartRenderer,
|
|
||||||
previewBarsRenderer: BarChartRenderer) {
|
|
||||||
self.mainBarsRenderer = mainBarsRenderer
|
|
||||||
self.previewBarsRenderer = previewBarsRenderer
|
|
||||||
|
|
||||||
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
|
||||||
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private var graphControllers: [GraphController] = []
|
|
||||||
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
|
||||||
|
|
||||||
private let verticalLineRenderer = VerticalLinesRenderer()
|
|
||||||
|
|
||||||
var chartVisibility: [Bool] = []
|
|
||||||
var zoomChartVisibility: [Bool] = []
|
|
||||||
|
|
||||||
private let initialChartCollection: ChartsCollection
|
|
||||||
var initialChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
|
||||||
var zoomedChartRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
|
||||||
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
|
||||||
|
|
||||||
var lastChartInteractionPoint: CGPoint = .zero
|
|
||||||
var isChartInteractionBegun: Bool = false
|
|
||||||
|
|
||||||
override public init(chartsCollection: ChartsCollection) {
|
|
||||||
self.initialChartCollection = chartsCollection
|
|
||||||
|
|
||||||
self.graphControllers = chartsCollection.chartValues.map { _ in GraphController(isZoomed: false, mainBarsRenderer: BarChartRenderer(step: true), previewBarsRenderer: BarChartRenderer(step: true))
|
|
||||||
}
|
|
||||||
|
|
||||||
super.init(chartsCollection: chartsCollection)
|
|
||||||
|
|
||||||
self.chartVisibility = Array(repeating: true, count: chartsCollection.chartValues.count)
|
|
||||||
self.zoomChartVisibility = self.chartVisibility
|
|
||||||
|
|
||||||
// self.graphControllers.map({ $0.barsController }).forEach { controller in
|
|
||||||
// controller.chartFrame = { [unowned self] in self.chartFrame() }
|
|
||||||
// controller.cartViewBounds = { [unowned self] in self.cartViewBounds() }
|
|
||||||
// controller.zoomInOnDateClosure = { [unowned self] date in
|
|
||||||
// self.didTapZoomIn(date: date)
|
|
||||||
// }
|
|
||||||
// controller.setChartTitleClosure = { [unowned self] (title, animated) in
|
|
||||||
// self.setChartTitleClosure?(title, animated)
|
|
||||||
// }
|
|
||||||
// controller.setDetailsViewPositionClosure = { [unowned self] (position) in
|
|
||||||
// self.setDetailsViewPositionClosure?(position)
|
|
||||||
// }
|
|
||||||
// controller.setDetailsChartVisibleClosure = { [unowned self] (visible, animated) in
|
|
||||||
// self.setDetailsChartVisibleClosure?(visible, animated)
|
|
||||||
// }
|
|
||||||
// controller.setDetailsViewModel = { [unowned self] (viewModel, animated) in
|
|
||||||
// self.setDetailsViewModel?(viewModel, animated)
|
|
||||||
// }
|
|
||||||
// controller.updatePreviewRangeClosure = { [unowned self] (fraction, animated) in
|
|
||||||
// self.chartRangeUpdatedClosure?(fraction, animated)
|
|
||||||
// }
|
|
||||||
// controller.chartRangePagingClosure = { [unowned self] (isEnabled, pageSize) in
|
|
||||||
// self.setChartRangePagingEnabled(isEnabled: isEnabled, minimumSelectionSize: pageSize)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var mainChartRenderers: [ChartViewRenderer] {
|
|
||||||
var renderers: [ChartViewRenderer] = []
|
|
||||||
self.graphControllers.forEach { controller in
|
|
||||||
renderers.append(controller.mainBarsRenderer)
|
|
||||||
}
|
|
||||||
renderers.append(self.horizontalScalesRenderer)
|
|
||||||
self.graphControllers.forEach { controller in
|
|
||||||
renderers.append(controller.verticalScalesRenderer)
|
|
||||||
renderers.append(controller.lineBulletsRenderer)
|
|
||||||
}
|
|
||||||
renderers.append(self.verticalLineRenderer)
|
|
||||||
return renderers
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var navigationRenderers: [ChartViewRenderer] {
|
|
||||||
return graphControllers.map { $0.previewBarsRenderer }
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func initializeChart() {
|
|
||||||
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
|
||||||
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
|
||||||
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
|
||||||
}
|
|
||||||
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
|
||||||
self.chartVisibility = visibility
|
|
||||||
self.zoomChartVisibility = visibility
|
|
||||||
let firstIndex = visibility.firstIndex(where: { $0 })
|
|
||||||
for (index, isVisible) in visibility.enumerated() {
|
|
||||||
let graph = graphControllers[index]
|
|
||||||
for graphIndex in graph.chartBars.components.indices {
|
|
||||||
graph.mainBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
|
|
||||||
graph.previewBarsRenderer.setComponentVisible(isVisible, at: graphIndex, animated: animated)
|
|
||||||
graph.lineBulletsRenderer.setLineVisible(isVisible, at: graphIndex, animated: animated)
|
|
||||||
}
|
|
||||||
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
|
||||||
if let firstIndex = firstIndex {
|
|
||||||
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
|
||||||
|
|
||||||
if isChartInteractionBegun {
|
|
||||||
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func findClosestDateTo(dateToFind: Date) -> (Date, Int)? {
|
|
||||||
guard self.initialChartCollection.axisValues.count > 0 else { return nil }
|
|
||||||
var closestDate = self.initialChartCollection.axisValues[0]
|
|
||||||
var minIndex = 0
|
|
||||||
for (index, date) in self.initialChartCollection.axisValues.enumerated() {
|
|
||||||
if abs(dateToFind.timeIntervalSince(date)) < abs(dateToFind.timeIntervalSince(closestDate)) {
|
|
||||||
closestDate = date
|
|
||||||
minIndex = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (closestDate, minIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func chartInteractionDidBegin(point: CGPoint) {
|
|
||||||
let horizontalRange = currentHorizontalRange
|
|
||||||
let chartFrame = self.chartFrame()
|
|
||||||
guard chartFrame.width > 0 else { return }
|
|
||||||
|
|
||||||
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
|
||||||
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
|
||||||
|
|
||||||
let chartInteractionWasBegin = isChartInteractionBegun
|
|
||||||
super.chartInteractionDidBegin(point: point)
|
|
||||||
|
|
||||||
// for graphController in graphControllers {
|
|
||||||
// graphController.lineBulletsRenderer.bullets = graphController.chartBars.components.map { component in
|
|
||||||
// LineBulletsRenderer.Bullet(coordinate: component.values[minIndex], color: component.color)
|
|
||||||
// }
|
|
||||||
// graphController.lineBulletsRenderer.isEnabled = true
|
|
||||||
// }
|
|
||||||
|
|
||||||
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
|
||||||
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
|
||||||
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
|
||||||
self.setDetailsChartVisibleClosure?(true, true)
|
|
||||||
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
|
||||||
self.verticalLineRenderer.values = [chartValue]
|
|
||||||
}
|
|
||||||
|
|
||||||
// func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
|
||||||
// var viewModel = super.chartDetailsViewModel(closestDate: closestDate, pointIndex: pointIndex)
|
|
||||||
// let visibleChartValues = self.visibleChartValues
|
|
||||||
// let totalSumm: CGFloat = visibleChartValues.map { CGFloat($0.values[pointIndex]) }.reduce(0, +)
|
|
||||||
//
|
|
||||||
// viewModel.totalValue = ChartDetailsViewModel.Value(prefix: nil,
|
|
||||||
// title: "Total",
|
|
||||||
// value: BaseConstants.detailsNumberFormatter.string(from: totalSumm),
|
|
||||||
// color: .white,
|
|
||||||
// visible: visibleChartValues.count > 1)
|
|
||||||
// return viewModel
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
func chartDetailsViewModel(closestDate: Date, pointIndex: Int) -> ChartDetailsViewModel {
|
|
||||||
let values: [ChartDetailsViewModel.Value] = initialChartCollection.chartValues.enumerated().map { arg in
|
|
||||||
let (index, component) = arg
|
|
||||||
return ChartDetailsViewModel.Value(prefix: nil,
|
|
||||||
title: component.name,
|
|
||||||
value: BaseConstants.detailsNumberFormatter.string(from: NSNumber(value: component.values[pointIndex])) ?? "",
|
|
||||||
color: component.color,
|
|
||||||
visible: chartVisibility[index])
|
|
||||||
}
|
|
||||||
let dateString: String
|
|
||||||
if isZoomed {
|
|
||||||
dateString = BaseConstants.timeDateFormatter.string(from: closestDate)
|
|
||||||
} else {
|
|
||||||
dateString = BaseConstants.headerMediumRangeFormatter.string(from: closestDate)
|
|
||||||
}
|
|
||||||
let viewModel = ChartDetailsViewModel(title: dateString,
|
|
||||||
showArrow: self.isZoomable && !self.isZoomed,
|
|
||||||
showPrefixes: false,
|
|
||||||
values: values,
|
|
||||||
totalValue: nil,
|
|
||||||
tapAction: { [weak self] in },
|
|
||||||
hideAction: { [weak self] in
|
|
||||||
self?.setDetailsChartVisibleClosure?(false, true)
|
|
||||||
})
|
|
||||||
return viewModel
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func chartInteractionDidEnd() {
|
|
||||||
self.isChartInteractionBegun = false
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
|
||||||
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
|
||||||
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
|
||||||
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
|
||||||
return lowerPercent...upperPercent
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func cancelChartInteraction() {
|
|
||||||
super.cancelChartInteraction()
|
|
||||||
self.graphControllers.forEach { controller in
|
|
||||||
controller.lineBulletsRenderer.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
self.setDetailsChartVisibleClosure?(false, true)
|
|
||||||
self.verticalLineRenderer.values = []
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
|
||||||
for (index, controller) in self.graphControllers.enumerated() {
|
|
||||||
let chart = chartsCollection.chartValues[index]
|
|
||||||
let points = chart.values.enumerated().map({ (arg) -> CGPoint in
|
|
||||||
return CGPoint(x: chartsCollection.axisValues[arg.offset].timeIntervalSince1970,
|
|
||||||
y: arg.element)
|
|
||||||
})
|
|
||||||
|
|
||||||
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection)
|
|
||||||
controller.chartBars = chartBars
|
|
||||||
controller.barsWidth = width
|
|
||||||
|
|
||||||
controller.verticalScalesRenderer.labelsColor = chart.color
|
|
||||||
|
|
||||||
controller.totalVerticalRange = totalVerticalRange
|
|
||||||
self.totalHorizontalRange = totalHorizontalRange
|
|
||||||
// controller.lineBulletsRenderer.bullets = chartBars.components.map { LineBulletsRenderer.Bullet(coordinate: $0.values.first ?? .zero,
|
|
||||||
// color: $0.color) }
|
|
||||||
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
|
||||||
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
|
||||||
|
|
||||||
controller.mainBarsRenderer.bars = chartBars
|
|
||||||
controller.previewBarsRenderer.bars = chartBars
|
|
||||||
|
|
||||||
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
|
||||||
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
let chartRange: ClosedRange<CGFloat>
|
|
||||||
if isZoomed {
|
|
||||||
chartRange = zoomedChartRange
|
|
||||||
} else {
|
|
||||||
chartRange = initialChartRange
|
|
||||||
}
|
|
||||||
|
|
||||||
// updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
|
||||||
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
|
||||||
updateMainChartVerticalRange(range: chartRange, animated: animated)
|
|
||||||
// updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
|
||||||
|
|
||||||
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
// func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
|
||||||
// if animated {
|
|
||||||
// TimeInterval.setDefaultSuration(.expandAnimationDuration)
|
|
||||||
// DispatchQueue.main.asyncAfter(deadline: .now() + .expandAnimationDuration) {
|
|
||||||
// TimeInterval.setDefaultSuration(.osXDuration)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// self.initialChartsCollection = chartsCollection
|
|
||||||
// self.isZoomed = isZoomed
|
|
||||||
//
|
|
||||||
// self.setBackButtonVisibilityClosure?(isZoomed, animated)
|
|
||||||
//
|
|
||||||
// self.graphControllers.forEach { controller in
|
|
||||||
// controller.barsController.willAppear(animated: animated)
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// self.refreshChartToolsClosure?(animated)
|
|
||||||
// }
|
|
||||||
|
|
||||||
public override func didTapZoomIn(date: Date) {
|
|
||||||
guard isZoomed == false else { return }
|
|
||||||
cancelChartInteraction()
|
|
||||||
self.getDetailsData?(date, { updatedCollection in
|
|
||||||
if let updatedCollection = updatedCollection {
|
|
||||||
self.initialChartRange = self.currentHorizontalRange
|
|
||||||
if let startDate = updatedCollection.axisValues.first,
|
|
||||||
let endDate = updatedCollection.axisValues.last {
|
|
||||||
self.zoomedChartRange = CGFloat(max(date.timeIntervalSince1970, startDate.timeIntervalSince1970))...CGFloat(min(date.timeIntervalSince1970 + .day - .hour, endDate.timeIntervalSince1970))
|
|
||||||
} else {
|
|
||||||
self.zoomedChartRange = CGFloat(date.timeIntervalSince1970)...CGFloat(date.timeIntervalSince1970 + .day - 1)
|
|
||||||
}
|
|
||||||
self.setupChartCollection(chartsCollection: updatedCollection, animated: true, isZoomed: true)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func didTapZoomOut() {
|
|
||||||
cancelChartInteraction()
|
|
||||||
self.setupChartCollection(chartsCollection: self.initialChartCollection, animated: true, isZoomed: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool) {
|
|
||||||
cancelChartInteraction()
|
|
||||||
|
|
||||||
let horizontalRange = ClosedRange(uncheckedBounds:
|
|
||||||
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
|
||||||
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
|
||||||
|
|
||||||
zoomedChartRange = horizontalRange
|
|
||||||
// updateChartRangeTitle(animated: true)
|
|
||||||
|
|
||||||
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
|
||||||
// updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
|
||||||
// updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
|
||||||
|
|
||||||
// barsController.chartRangeFractionDidUpdated(rangeFraction)
|
|
||||||
//
|
|
||||||
// let totalHorizontalRange = barsController.totalHorizontalRange
|
|
||||||
// let horizontalRange = ClosedRange(uncheckedBounds:
|
|
||||||
// (lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
|
||||||
// upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
|
||||||
//
|
|
||||||
// updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
|
||||||
self.graphControllers.forEach { controller in
|
|
||||||
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
|
|
||||||
// controller.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
|
||||||
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
|
||||||
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
|
||||||
}
|
|
||||||
self.horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
|
||||||
self.verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
|
||||||
}
|
|
||||||
|
|
||||||
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
|
||||||
self.verticalLineRenderer.setup(verticalRange: range, animated: animated)
|
|
||||||
|
|
||||||
self.graphControllers.forEach { controller in
|
|
||||||
controller.lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override public func apply(theme: ChartTheme, animated: Bool) {
|
|
||||||
super.apply(theme: theme, animated: animated)
|
|
||||||
|
|
||||||
self.graphControllers.forEach { controller in
|
|
||||||
controller.verticalScalesRenderer.horizontalLinesColor = theme.chartHelperLinesColor
|
|
||||||
controller.lineBulletsRenderer.setInnerColor(theme.chartBackgroundColor, animated: animated)
|
|
||||||
controller.verticalScalesRenderer.axisXColor = theme.chartStrongLinesColor
|
|
||||||
}
|
|
||||||
verticalLineRenderer.linesColor = theme.chartStrongLinesColor
|
|
||||||
}
|
|
||||||
|
|
||||||
public override var drawChartVisibity: Bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//TODO: Убрать Performance полоски сверзу чартов (Не забыть)
|
|
||||||
//TODO: Добавить ховеры на кнопки
|
|
||||||
@ -0,0 +1,309 @@
|
|||||||
|
import Foundation
|
||||||
|
#if os(macOS)
|
||||||
|
import Cocoa
|
||||||
|
#else
|
||||||
|
import UIKit
|
||||||
|
#endif
|
||||||
|
|
||||||
|
private enum Constants {
|
||||||
|
static let verticalBaseAnchors: [CGFloat] = [8, 5, 4, 2.5, 2, 1]
|
||||||
|
}
|
||||||
|
|
||||||
|
public class TwoAxisStepBarsChartController: BaseLinesChartController {
|
||||||
|
class GraphController {
|
||||||
|
let mainBarsRenderer = BarChartRenderer(step: true)
|
||||||
|
let verticalScalesRenderer = VerticalScalesRenderer()
|
||||||
|
let lineBulletsRenderer = LineBulletsRenderer()
|
||||||
|
let previewBarsRenderer = BarChartRenderer(step: true, lineWidth: 1.0)
|
||||||
|
|
||||||
|
var chartBars: BarChartRenderer.BarsData = .blank
|
||||||
|
var barsWidth: CGFloat = 1
|
||||||
|
|
||||||
|
var totalVerticalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.lineBulletsRenderer.isEnabled = false
|
||||||
|
|
||||||
|
self.mainBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||||
|
self.previewBarsRenderer.optimizationLevel = BaseConstants.barsChartOptimizationLevel
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMainChartVerticalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
|
mainBarsRenderer.setup(verticalRange: range, animated: animated)
|
||||||
|
verticalScalesRenderer.setup(verticalRange: range, animated: animated)
|
||||||
|
lineBulletsRenderer.setup(verticalRange: range, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var graphControllers: [GraphController] = []
|
||||||
|
private let verticalLineRenderer = VerticalLinesRenderer()
|
||||||
|
private let horizontalScalesRenderer = HorizontalScalesRenderer()
|
||||||
|
|
||||||
|
var totalHorizontalRange: ClosedRange<CGFloat> = BaseConstants.defaultRange
|
||||||
|
|
||||||
|
private let initialChartCollection: ChartsCollection
|
||||||
|
|
||||||
|
private var prevoiusHorizontalStrideInterval: Int = 1
|
||||||
|
|
||||||
|
override public init(chartsCollection: ChartsCollection) {
|
||||||
|
self.initialChartCollection = chartsCollection
|
||||||
|
graphControllers = chartsCollection.chartValues.map { _ in GraphController() }
|
||||||
|
|
||||||
|
super.init(chartsCollection: chartsCollection)
|
||||||
|
self.zoomChartVisibility = chartVisibility
|
||||||
|
}
|
||||||
|
|
||||||
|
override func setupChartCollection(chartsCollection: ChartsCollection, animated: Bool, isZoomed: Bool) {
|
||||||
|
super.setupChartCollection(chartsCollection: chartsCollection, animated: animated, isZoomed: isZoomed)
|
||||||
|
|
||||||
|
for (index, controller) in self.graphControllers.enumerated() {
|
||||||
|
let chart = chartsCollection.chartValues[index]
|
||||||
|
let initialComponents = [BarChartRenderer.BarsData.Component(color: chart.color,
|
||||||
|
values: chart.values.map { CGFloat($0) })]
|
||||||
|
let (width, chartBars, totalHorizontalRange, totalVerticalRange) = BarChartRenderer.BarsData.initialComponents(chartsCollection: chartsCollection, separate: true, initialComponents: initialComponents)
|
||||||
|
controller.chartBars = chartBars
|
||||||
|
controller.verticalScalesRenderer.labelsColor = chart.color
|
||||||
|
controller.barsWidth = width
|
||||||
|
controller.totalVerticalRange = totalVerticalRange
|
||||||
|
self.totalHorizontalRange = totalHorizontalRange
|
||||||
|
|
||||||
|
var bullets: [LineBulletsRenderer.Bullet] = []
|
||||||
|
if let component = chartBars.components.first {
|
||||||
|
for i in 0 ..< chartBars.locations.count {
|
||||||
|
let location = chartBars.locations[i]
|
||||||
|
let value = component.values[i]
|
||||||
|
bullets.append(LineBulletsRenderer.Bullet(coordinate: CGPoint(x: location, y: value), color: component.color))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.lineBulletsRenderer.bullets = bullets
|
||||||
|
controller.previewBarsRenderer.setup(horizontalRange: self.totalHorizontalRange, animated: animated)
|
||||||
|
controller.previewBarsRenderer.setup(verticalRange: controller.totalVerticalRange, animated: animated)
|
||||||
|
controller.mainBarsRenderer.bars = chartBars
|
||||||
|
controller.previewBarsRenderer.bars = chartBars
|
||||||
|
|
||||||
|
controller.verticalScalesRenderer.setHorizontalLinesVisible((index == 0), animated: animated)
|
||||||
|
controller.verticalScalesRenderer.isRightAligned = (index != 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.prevoiusHorizontalStrideInterval = -1
|
||||||
|
|
||||||
|
let chartRange: ClosedRange<CGFloat>
|
||||||
|
if isZoomed {
|
||||||
|
chartRange = zoomedChartRange
|
||||||
|
} else {
|
||||||
|
chartRange = initialChartRange
|
||||||
|
}
|
||||||
|
|
||||||
|
updateHorizontalLimits(horizontalRange: chartRange, animated: animated)
|
||||||
|
updateMainChartHorizontalRange(range: chartRange, animated: animated)
|
||||||
|
updateVerticalLimitsAndRange(horizontalRange: chartRange, animated: animated)
|
||||||
|
|
||||||
|
self.chartRangeUpdatedClosure?(currentChartHorizontalRangeFraction, animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func initializeChart() {
|
||||||
|
if let first = initialChartCollection.axisValues.first?.timeIntervalSince1970,
|
||||||
|
let last = initialChartCollection.axisValues.last?.timeIntervalSince1970 {
|
||||||
|
initialChartRange = CGFloat(max(first, last - BaseConstants.defaultRangePresetLength))...CGFloat(last)
|
||||||
|
}
|
||||||
|
setupChartCollection(chartsCollection: initialChartCollection, animated: false, isZoomed: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var mainChartRenderers: [ChartViewRenderer] {
|
||||||
|
return graphControllers.map { $0.mainBarsRenderer } +
|
||||||
|
graphControllers.flatMap { [$0.verticalScalesRenderer, $0.lineBulletsRenderer] } +
|
||||||
|
[horizontalScalesRenderer, verticalLineRenderer,
|
||||||
|
// performanceRenderer
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var navigationRenderers: [ChartViewRenderer] {
|
||||||
|
return graphControllers.map { $0.previewBarsRenderer }
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func updateChartsVisibility(visibility: [Bool], animated: Bool) {
|
||||||
|
chartVisibility = visibility
|
||||||
|
zoomChartVisibility = visibility
|
||||||
|
let firstIndex = visibility.firstIndex(where: { $0 })
|
||||||
|
for (index, isVisible) in visibility.enumerated() {
|
||||||
|
let graph = graphControllers[index]
|
||||||
|
graph.mainBarsRenderer.setVisible(isVisible, animated: animated)
|
||||||
|
graph.previewBarsRenderer.setVisible(isVisible, animated: animated)
|
||||||
|
graph.lineBulletsRenderer.setLineVisible(isVisible, at: 0, animated: animated)
|
||||||
|
graph.verticalScalesRenderer.setVisible(isVisible, animated: animated)
|
||||||
|
if let firstIndex = firstIndex {
|
||||||
|
graph.verticalScalesRenderer.setHorizontalLinesVisible(index == firstIndex, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVerticalLimitsAndRange(horizontalRange: currentHorizontalRange, animated: true)
|
||||||
|
|
||||||
|
if isChartInteractionBegun {
|
||||||
|
chartInteractionDidBegin(point: lastChartInteractionPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func chartInteractionDidBegin(point: CGPoint) {
|
||||||
|
let horizontalRange = currentHorizontalRange
|
||||||
|
let chartFrame = self.chartFrame()
|
||||||
|
guard chartFrame.width > 0 else { return }
|
||||||
|
|
||||||
|
let dateToFind = Date(timeIntervalSince1970: TimeInterval(horizontalRange.distance * point.x + horizontalRange.lowerBound))
|
||||||
|
guard let (closestDate, minIndex) = findClosestDateTo(dateToFind: dateToFind) else { return }
|
||||||
|
|
||||||
|
let chartInteractionWasBegin = isChartInteractionBegun
|
||||||
|
super.chartInteractionDidBegin(point: point)
|
||||||
|
|
||||||
|
for graphController in graphControllers {
|
||||||
|
|
||||||
|
var bullets: [LineBulletsRenderer.Bullet] = []
|
||||||
|
if let component = graphController.chartBars.components.first {
|
||||||
|
let location = graphController.chartBars.locations[minIndex]
|
||||||
|
let value = component.values[minIndex]
|
||||||
|
bullets.append(LineBulletsRenderer.Bullet(coordinate: CGPoint(x: location, y: value), color: component.color))
|
||||||
|
}
|
||||||
|
graphController.lineBulletsRenderer.bullets = bullets
|
||||||
|
graphController.lineBulletsRenderer.isEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let chartValue: CGFloat = CGFloat(closestDate.timeIntervalSince1970)
|
||||||
|
let detailsViewPosition = (chartValue - horizontalRange.lowerBound) / horizontalRange.distance * chartFrame.width + chartFrame.minX
|
||||||
|
self.setDetailsViewModel?(chartDetailsViewModel(closestDate: closestDate, pointIndex: minIndex), chartInteractionWasBegin)
|
||||||
|
self.setDetailsChartVisibleClosure?(true, true)
|
||||||
|
self.setDetailsViewPositionClosure?(detailsViewPosition)
|
||||||
|
self.verticalLineRenderer.values = [chartValue]
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var currentChartHorizontalRangeFraction: ClosedRange<CGFloat> {
|
||||||
|
let lowerPercent = (currentHorizontalRange.lowerBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||||
|
let upperPercent = (currentHorizontalRange.upperBound - totalHorizontalRange.lowerBound) / totalHorizontalRange.distance
|
||||||
|
return lowerPercent...upperPercent
|
||||||
|
}
|
||||||
|
|
||||||
|
public override var currentHorizontalRange: ClosedRange<CGFloat> {
|
||||||
|
return graphControllers.first?.mainBarsRenderer.horizontalRange.end ?? BaseConstants.defaultRange
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func cancelChartInteraction() {
|
||||||
|
super.cancelChartInteraction()
|
||||||
|
for graphController in graphControllers {
|
||||||
|
graphController.lineBulletsRenderer.isEnabled = false
|
||||||
|
}
|
||||||
|
|
||||||
|
self.setDetailsChartVisibleClosure?(false, true)
|
||||||
|
self.verticalLineRenderer.values = []
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func didTapZoomOut() {
|
||||||
|
cancelChartInteraction()
|
||||||
|
self.setupChartCollection(chartsCollection: initialChartCollection, animated: true, isZoomed: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func updateChartRange(_ rangeFraction: ClosedRange<CGFloat>, animated: Bool = true) {
|
||||||
|
cancelChartInteraction()
|
||||||
|
|
||||||
|
let horizontalRange = ClosedRange(uncheckedBounds:
|
||||||
|
(lower: totalHorizontalRange.lowerBound + rangeFraction.lowerBound * totalHorizontalRange.distance,
|
||||||
|
upper: totalHorizontalRange.lowerBound + rangeFraction.upperBound * totalHorizontalRange.distance))
|
||||||
|
|
||||||
|
zoomedChartRange = horizontalRange
|
||||||
|
updateChartRangeTitle(animated: true)
|
||||||
|
|
||||||
|
updateMainChartHorizontalRange(range: horizontalRange, animated: false)
|
||||||
|
updateHorizontalLimits(horizontalRange: horizontalRange, animated: true)
|
||||||
|
updateVerticalLimitsAndRange(horizontalRange: horizontalRange, animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateMainChartHorizontalRange(range: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
|
for controller in graphControllers {
|
||||||
|
controller.mainBarsRenderer.setup(horizontalRange: range, animated: animated)
|
||||||
|
controller.verticalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||||
|
controller.lineBulletsRenderer.setup(horizontalRange: range, animated: animated)
|
||||||
|
}
|
||||||
|
horizontalScalesRenderer.setup(horizontalRange: range, animated: animated)
|
||||||
|
verticalLineRenderer.setup(horizontalRange: range, animated: animated)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateHorizontalLimits(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
|
if let (stride, labels) = horizontalLimitsLabels(horizontalRange: horizontalRange,
|
||||||
|
scaleType: isZoomed ? .minutes5 : .day,
|
||||||
|
prevoiusHorizontalStrideInterval: prevoiusHorizontalStrideInterval) {
|
||||||
|
self.horizontalScalesRenderer.setup(labels: labels, animated: animated)
|
||||||
|
self.prevoiusHorizontalStrideInterval = stride
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateVerticalLimitsAndRange(horizontalRange: ClosedRange<CGFloat>, animated: Bool) {
|
||||||
|
let chartHeight = chartFrame().height
|
||||||
|
let approximateNumberOfChartValues = (chartHeight / BaseConstants.minimumAxisYLabelsDistance)
|
||||||
|
|
||||||
|
let dividorsAndMultiplers: [(startValue: CGFloat, base: CGFloat, count: Int, maximumNumberOfDecimals: Int)] = graphControllers.enumerated().map { arg in
|
||||||
|
let (index, controller) = arg
|
||||||
|
let verticalRange = BarChartRenderer.BarsData.verticalRange(bars: controller.chartBars, separate: true, calculatingRange: horizontalRange, addBounds: true) ?? controller.totalVerticalRange
|
||||||
|
|
||||||
|
var numberOfOffsetsPerItem = verticalRange.distance / approximateNumberOfChartValues
|
||||||
|
|
||||||
|
var multiplier: CGFloat = 1.0
|
||||||
|
while numberOfOffsetsPerItem > 10 {
|
||||||
|
numberOfOffsetsPerItem /= 10
|
||||||
|
multiplier *= 10
|
||||||
|
}
|
||||||
|
var dividor: CGFloat = 1.0
|
||||||
|
var maximumNumberOfDecimals = 2
|
||||||
|
while numberOfOffsetsPerItem < 1 {
|
||||||
|
numberOfOffsetsPerItem *= 10
|
||||||
|
dividor *= 10
|
||||||
|
maximumNumberOfDecimals += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
let generalBase = Constants.verticalBaseAnchors.first { numberOfOffsetsPerItem > $0 } ?? BaseConstants.defaultVerticalBaseAnchor
|
||||||
|
let base = generalBase * multiplier / dividor
|
||||||
|
|
||||||
|
var verticalValue = (verticalRange.lowerBound / base).rounded(.down) * base
|
||||||
|
let startValue = verticalValue
|
||||||
|
var count = 0
|
||||||
|
if chartVisibility[index] {
|
||||||
|
while verticalValue < verticalRange.upperBound {
|
||||||
|
count += 1
|
||||||
|
verticalValue += base
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (startValue: startValue, base: base, count: count, maximumNumberOfDecimals: maximumNumberOfDecimals)
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalCount = dividorsAndMultiplers.map { $0.count }.max() ?? 0
|
||||||
|
guard totalCount > 0 else { return }
|
||||||
|
|
||||||
|
let numberFormatter = BaseConstants.chartNumberFormatter
|
||||||
|
for (index, controller) in graphControllers.enumerated() {
|
||||||
|
let (startValue, base, _, maximumNumberOfDecimals) = dividorsAndMultiplers[index]
|
||||||
|
|
||||||
|
let updatedRange = startValue...(startValue + base * CGFloat(totalCount))
|
||||||
|
if controller.verticalScalesRenderer.verticalRange.end != updatedRange {
|
||||||
|
numberFormatter.maximumFractionDigits = maximumNumberOfDecimals
|
||||||
|
|
||||||
|
var verticalLabels: [LinesChartLabel] = []
|
||||||
|
for multipler in 0...(totalCount - 1) {
|
||||||
|
let verticalValue = startValue + base * CGFloat(multipler)
|
||||||
|
let text: String = numberFormatter.string(from: NSNumber(value: Double(verticalValue))) ?? ""
|
||||||
|
verticalLabels.append(LinesChartLabel(value: verticalValue, text: text))
|
||||||
|
}
|
||||||
|
|
||||||
|
controller.verticalScalesRenderer.setup(verticalLimitsLabels: verticalLabels, animated: animated)
|
||||||
|
controller.updateMainChartVerticalRange(range: updatedRange, animated: animated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override func apply(theme: ChartTheme, animated: Bool) {
|
||||||
|
horizontalScalesRenderer.labelsColor = theme.chartLabelsColor
|
||||||
|
verticalLineRenderer.linesColor = theme.chartStrongLinesColor
|
||||||
|
|
||||||
|
for controller in graphControllers {
|
||||||
|
controller.verticalScalesRenderer.horizontalLinesColor = theme.chartHelperLinesColor
|
||||||
|
controller.lineBulletsRenderer.setInnerColor(theme.chartBackgroundColor, animated: animated)
|
||||||
|
controller.verticalScalesRenderer.axisXColor = theme.chartStrongLinesColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -27,9 +27,11 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var step = false
|
private var step = false
|
||||||
|
private var lineWidth: CGFloat = 2.0
|
||||||
|
|
||||||
init(step: Bool = false) {
|
init(step: Bool = false, lineWidth: CGFloat = 2.0) {
|
||||||
self.step = step
|
self.step = step
|
||||||
|
self.lineWidth = lineWidth
|
||||||
|
|
||||||
super.init()
|
super.init()
|
||||||
}
|
}
|
||||||
@ -158,14 +160,10 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
|
var leftX = transform(toChartCoordinateHorizontal: currentLocation - bars.barWidth, chartFrame: chartFrame)
|
||||||
var rightX: CGFloat = 0
|
var rightX: CGFloat = 0
|
||||||
|
|
||||||
let startPoint = CGPoint(x: leftX,
|
var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
|
||||||
|
|
||||||
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
|
||||||
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
||||||
for path in backgourndPaths.indices {
|
for path in backgroundPaths.indices {
|
||||||
backgourndPaths[path].reserveCapacity(itemsCount)
|
backgroundPaths[path].reserveCapacity(itemsCount)
|
||||||
backgourndPaths[path].append(startPoint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
||||||
@ -173,18 +171,13 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
currentLocation = bars.locations[barIndex]
|
currentLocation = bars.locations[barIndex]
|
||||||
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
|
rightX = transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame)
|
||||||
|
|
||||||
var stackedValue: CGFloat = 0
|
let bottomY: CGFloat = transform(toChartCoordinateVertical: 0.0, chartFrame: chartFrame)
|
||||||
var bottomY: CGFloat = transform(toChartCoordinateVertical: stackedValue, chartFrame: chartFrame)
|
|
||||||
for (index, component) in bars.components.enumerated() {
|
for (index, component) in bars.components.enumerated() {
|
||||||
let visibilityPercent = componentsAnimators[index].current
|
let visibilityPercent = componentsAnimators[index].current
|
||||||
if visibilityPercent == 0 { continue }
|
if visibilityPercent == 0 { continue }
|
||||||
|
|
||||||
var value = component.values[barIndex]
|
let value = component.values[barIndex]
|
||||||
if value < 5000 {
|
|
||||||
value *= 40
|
|
||||||
}
|
|
||||||
let height = value * visibilityPercent
|
let height = value * visibilityPercent
|
||||||
// stackedValue += height
|
|
||||||
let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame)
|
let topY = transform(toChartCoordinateVertical: height, chartFrame: chartFrame)
|
||||||
let componentHeight = (bottomY - topY)
|
let componentHeight = (bottomY - topY)
|
||||||
maxValues[index] = max(maxValues[index], componentHeight)
|
maxValues[index] = max(maxValues[index], componentHeight)
|
||||||
@ -195,9 +188,8 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
height: componentHeight)
|
height: componentHeight)
|
||||||
selectedPaths[index].append(rect)
|
selectedPaths[index].append(rect)
|
||||||
}
|
}
|
||||||
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
|
backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||||
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
|
backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||||
// bottomY = topY
|
|
||||||
}
|
}
|
||||||
if currentLocation > range.upperBound {
|
if currentLocation > range.upperBound {
|
||||||
break
|
break
|
||||||
@ -206,8 +198,6 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
barIndex += 1
|
barIndex += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
let endPoint = CGPoint(x: transform(toChartCoordinateHorizontal: currentLocation, chartFrame: chartFrame).roundedUpToPixelGrid(),
|
|
||||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
|
||||||
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
|
let colorOffset = Double((1.0 - (1.0 - generalUnselectedAlpha) * selectedIndexAnimator.current) * chartsAlpha)
|
||||||
|
|
||||||
for (index, component) in bars.components.enumerated().reversed() {
|
for (index, component) in bars.components.enumerated().reversed() {
|
||||||
@ -216,12 +206,12 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
}
|
}
|
||||||
context.saveGState()
|
context.saveGState()
|
||||||
|
|
||||||
context.setLineWidth(2.0)
|
context.setLineWidth(self.lineWidth)
|
||||||
context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
|
context.setStrokeColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
|
||||||
end: component.color,
|
end: component.color,
|
||||||
offset: colorOffset).cgColor)
|
offset: colorOffset).cgColor)
|
||||||
context.beginPath()
|
context.beginPath()
|
||||||
context.addLines(between: backgourndPaths[index])
|
context.addLines(between: backgroundPaths[index])
|
||||||
context.strokePath()
|
context.strokePath()
|
||||||
context.restoreGState()
|
context.restoreGState()
|
||||||
}
|
}
|
||||||
@ -236,11 +226,11 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
let startPoint = CGPoint(x: leftX,
|
let startPoint = CGPoint(x: leftX,
|
||||||
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
y: transform(toChartCoordinateVertical: verticalRange.current.lowerBound, chartFrame: chartFrame))
|
||||||
|
|
||||||
var backgourndPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
var backgroundPaths: [[CGPoint]] = bars.components.map { _ in Array() }
|
||||||
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
let itemsCount = ((bars.locations.count - barIndex) * 2) + 4
|
||||||
for path in backgourndPaths.indices {
|
for path in backgroundPaths.indices {
|
||||||
backgourndPaths[path].reserveCapacity(itemsCount)
|
backgroundPaths[path].reserveCapacity(itemsCount)
|
||||||
backgourndPaths[path].append(startPoint)
|
backgroundPaths[path].append(startPoint)
|
||||||
}
|
}
|
||||||
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
var maxValues: [CGFloat] = bars.components.map { _ in 0 }
|
||||||
while barIndex < bars.locations.count {
|
while barIndex < bars.locations.count {
|
||||||
@ -265,8 +255,8 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
height: componentHeight)
|
height: componentHeight)
|
||||||
selectedPaths[index].append(rect)
|
selectedPaths[index].append(rect)
|
||||||
}
|
}
|
||||||
backgourndPaths[index].append(CGPoint(x: leftX, y: topY))
|
backgroundPaths[index].append(CGPoint(x: leftX, y: topY))
|
||||||
backgourndPaths[index].append(CGPoint(x: rightX, y: topY))
|
backgroundPaths[index].append(CGPoint(x: rightX, y: topY))
|
||||||
bottomY = topY
|
bottomY = topY
|
||||||
}
|
}
|
||||||
if currentLocation > range.upperBound {
|
if currentLocation > range.upperBound {
|
||||||
@ -285,13 +275,13 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
context.saveGState()
|
context.saveGState()
|
||||||
backgourndPaths[index].append(endPoint)
|
backgroundPaths[index].append(endPoint)
|
||||||
|
|
||||||
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
|
context.setFillColor(GColor.valueBetween(start: backgroundColorAnimator.current.color,
|
||||||
end: component.color,
|
end: component.color,
|
||||||
offset: colorOffset).cgColor)
|
offset: colorOffset).cgColor)
|
||||||
context.beginPath()
|
context.beginPath()
|
||||||
context.addLines(between: backgourndPaths[index])
|
context.addLines(between: backgroundPaths[index])
|
||||||
context.closePath()
|
context.closePath()
|
||||||
context.fillPath()
|
context.fillPath()
|
||||||
context.restoreGState()
|
context.restoreGState()
|
||||||
@ -308,7 +298,7 @@ class BarChartRenderer: BaseChartRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension BarChartRenderer.BarsData {
|
extension BarChartRenderer.BarsData {
|
||||||
static func initialComponents(chartsCollection: ChartsCollection) ->
|
static func initialComponents(chartsCollection: ChartsCollection, separate: Bool = false, initialComponents: [BarChartRenderer.BarsData.Component]? = nil) ->
|
||||||
(width: CGFloat,
|
(width: CGFloat,
|
||||||
chartBars: BarChartRenderer.BarsData,
|
chartBars: BarChartRenderer.BarsData,
|
||||||
totalHorizontalRange: ClosedRange<CGFloat>,
|
totalHorizontalRange: ClosedRange<CGFloat>,
|
||||||
@ -319,15 +309,13 @@ extension BarChartRenderer.BarsData {
|
|||||||
} else {
|
} else {
|
||||||
width = 1
|
width = 1
|
||||||
}
|
}
|
||||||
let components = chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
|
let components = initialComponents ?? chartsCollection.chartValues.map { BarChartRenderer.BarsData.Component(color: $0.color,
|
||||||
values: $0.values.map { CGFloat($0) }) }
|
values: $0.values.map { CGFloat($0) }) }
|
||||||
let chartBars = BarChartRenderer.BarsData(barWidth: width,
|
let chartBars = BarChartRenderer.BarsData(barWidth: width,
|
||||||
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
|
locations: chartsCollection.axisValues.map { CGFloat($0.timeIntervalSince1970) },
|
||||||
components: components)
|
components: components)
|
||||||
|
|
||||||
|
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars, separate: separate) ?? 0...1
|
||||||
|
|
||||||
let totalVerticalRange = BarChartRenderer.BarsData.verticalRange(bars: chartBars) ?? 0...1
|
|
||||||
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
|
let totalHorizontalRange = BarChartRenderer.BarsData.visibleHorizontalRange(bars: chartBars, width: width) ?? 0...1
|
||||||
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
|
return (width: width, chartBars: chartBars, totalHorizontalRange: totalHorizontalRange, totalVerticalRange: totalVerticalRange)
|
||||||
}
|
}
|
||||||
@ -342,7 +330,7 @@ extension BarChartRenderer.BarsData {
|
|||||||
return (firstPoint - width)...lastPoint
|
return (firstPoint - width)...lastPoint
|
||||||
}
|
}
|
||||||
|
|
||||||
static func verticalRange(bars: BarChartRenderer.BarsData, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
static func verticalRange(bars: BarChartRenderer.BarsData, separate: Bool = false, calculatingRange: ClosedRange<CGFloat>? = nil, addBounds: Bool = false) -> ClosedRange<CGFloat>? {
|
||||||
guard bars.components.count > 0 else {
|
guard bars.components.count > 0 else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -353,11 +341,17 @@ extension BarChartRenderer.BarsData {
|
|||||||
|
|
||||||
var vMax: CGFloat = bars.components[0].values[index]
|
var vMax: CGFloat = bars.components[0].values[index]
|
||||||
while index < bars.locations.count {
|
while index < bars.locations.count {
|
||||||
var summ: CGFloat = 0
|
if separate {
|
||||||
for component in bars.components {
|
for component in bars.components {
|
||||||
summ += component.values[index]
|
vMax = max(vMax, component.values[index])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var summ: CGFloat = 0
|
||||||
|
for component in bars.components {
|
||||||
|
summ += component.values[index]
|
||||||
|
}
|
||||||
|
vMax = max(vMax, summ)
|
||||||
}
|
}
|
||||||
vMax = max(vMax, summ)
|
|
||||||
|
|
||||||
if bars.locations[index] > calculatingRange.upperBound {
|
if bars.locations[index] > calculatingRange.upperBound {
|
||||||
break
|
break
|
||||||
@ -370,11 +364,18 @@ extension BarChartRenderer.BarsData {
|
|||||||
|
|
||||||
var vMax: CGFloat = bars.components[0].values[index]
|
var vMax: CGFloat = bars.components[0].values[index]
|
||||||
while index < bars.locations.count {
|
while index < bars.locations.count {
|
||||||
var summ: CGFloat = 0
|
if separate {
|
||||||
for component in bars.components {
|
for component in bars.components {
|
||||||
summ += component.values[index]
|
vMax = max(vMax, component.values[index])
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var summ: CGFloat = 0
|
||||||
|
for component in bars.components {
|
||||||
|
summ += component.values[index]
|
||||||
|
}
|
||||||
|
vMax = max(vMax, summ)
|
||||||
}
|
}
|
||||||
vMax = max(vMax, summ)
|
|
||||||
index += 1
|
index += 1
|
||||||
}
|
}
|
||||||
return 0...vMax
|
return 0...vMax
|
||||||
|
|||||||
@ -48,7 +48,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
|||||||
linesShapeAnimator.set(current: 1)
|
linesShapeAnimator.set(current: 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
func setLineVisible(_ isVisible: Bool, at index: Int, animated: Bool) {
|
||||||
linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
linesAlphaAnimators[index].animate(to: isVisible ? 1 : 0, duration: animated ? .defaultDuration : 0)
|
||||||
}
|
}
|
||||||
@ -62,9 +62,10 @@ class LinesChartRenderer: BaseChartRenderer {
|
|||||||
for (index, toLine) in toLines.enumerated() {
|
for (index, toLine) in toLines.enumerated() {
|
||||||
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
||||||
if alpha == 0 { continue }
|
if alpha == 0 { continue }
|
||||||
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
context.setAlpha(alpha)
|
||||||
|
context.setStrokeColor(toLine.color.cgColor)
|
||||||
context.setLineWidth(lineWidth)
|
context.setLineWidth(lineWidth)
|
||||||
|
|
||||||
if linesShapeAnimator.isAnimating {
|
if linesShapeAnimator.isAnimating {
|
||||||
let animationOffset = linesShapeAnimator.current
|
let animationOffset = linesShapeAnimator.current
|
||||||
|
|
||||||
@ -93,7 +94,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
|||||||
var previousToPoint: CGPoint
|
var previousToPoint: CGPoint
|
||||||
let startFromPoint: CGPoint?
|
let startFromPoint: CGPoint?
|
||||||
let startToPoint: CGPoint?
|
let startToPoint: CGPoint?
|
||||||
|
|
||||||
if let validFrom = fromIndex {
|
if let validFrom = fromIndex {
|
||||||
previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)])
|
previousFromPoint = convertFromPoint(fromPoints[max(0, validFrom - 1)])
|
||||||
startFromPoint = previousFromPoint
|
startFromPoint = previousFromPoint
|
||||||
@ -110,7 +111,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var combinedPoints: [CGPoint] = []
|
var combinedPoints: [CGPoint] = []
|
||||||
|
|
||||||
func add(pointToDraw: CGPoint) {
|
func add(pointToDraw: CGPoint) {
|
||||||
if let startFromPoint = startFromPoint,
|
if let startFromPoint = startFromPoint,
|
||||||
pointToDraw.x < startFromPoint.x {
|
pointToDraw.x < startFromPoint.x {
|
||||||
@ -312,13 +313,7 @@ class LinesChartRenderer: BaseChartRenderer {
|
|||||||
|
|
||||||
context.setLineCap(.round)
|
context.setLineCap(.round)
|
||||||
context.strokeLineSegments(between: lines)
|
context.strokeLineSegments(between: lines)
|
||||||
|
} else {
|
||||||
} else {
|
|
||||||
let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
|
||||||
if alpha == 0 { continue }
|
|
||||||
context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
|
||||||
context.setLineWidth(lineWidth)
|
|
||||||
|
|
||||||
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
|
if var index = toLine.points.firstIndex(where: { $0.x >= range.lowerBound }) {
|
||||||
var lines: [CGPoint] = []
|
var lines: [CGPoint] = []
|
||||||
index = max(0, index - 1)
|
index = max(0, index - 1)
|
||||||
@ -436,33 +431,8 @@ class LinesChartRenderer: BaseChartRenderer {
|
|||||||
context.setLineCap(.round)
|
context.setLineCap(.round)
|
||||||
context.strokeLineSegments(between: lines)
|
context.strokeLineSegments(between: lines)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if var start = toLine.points.firstIndex(where: { $0.x > range.lowerBound }) {
|
|
||||||
// let alpha = linesAlphaAnimators[index].current * chartsAlpha
|
|
||||||
// if alpha == 0 { continue }
|
|
||||||
// context.setStrokeColor(toLine.color.withAlphaComponent(alpha).cgColor)
|
|
||||||
// context.setLineWidth(lineWidth)
|
|
||||||
//
|
|
||||||
// context.setLineCap(.round)
|
|
||||||
// start = max(0, start - 1)
|
|
||||||
// let startPoint = toLine.points[start]
|
|
||||||
// var lines: [CGPoint] = []
|
|
||||||
// var pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: startPoint.x, chartFrame: chartFrame),
|
|
||||||
// y: transform(toChartCoordinateVertical: startPoint.y, chartFrame: chartFrame))
|
|
||||||
// for index in (start + 1)..<toLine.points.count {
|
|
||||||
// lines.append(pointToDraw)
|
|
||||||
// let point = toLine.points[index]
|
|
||||||
// pointToDraw = CGPoint(x: transform(toChartCoordinateHorizontal: point.x, chartFrame: chartFrame),
|
|
||||||
// y: transform(toChartCoordinateVertical: point.y, chartFrame: chartFrame))
|
|
||||||
// lines.append(pointToDraw)
|
|
||||||
// if point.x > range.upperBound {
|
|
||||||
// break
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// context.strokeLineSegments(between: lines)
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
context.setAlpha(1.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,21 @@ extension GColor {
|
|||||||
case "lightblue":
|
case "lightblue":
|
||||||
self.init(hexString: "#5ac8fa")
|
self.init(hexString: "#5ac8fa")
|
||||||
return
|
return
|
||||||
|
case "seablue":
|
||||||
|
self.init(hexString: "#35afdc")
|
||||||
|
return
|
||||||
|
case "orange":
|
||||||
|
self.init(hexString: "#fd7a32")
|
||||||
|
return
|
||||||
|
case "violet":
|
||||||
|
self.init(hexString: "#9968f7")
|
||||||
|
return
|
||||||
|
case "emerald":
|
||||||
|
self.init(hexString: "#37cca3")
|
||||||
|
return
|
||||||
|
case "pink":
|
||||||
|
self.init(hexString: "#ff4f79")
|
||||||
|
return
|
||||||
default:
|
default:
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,7 @@ private let cornerRadius: CGFloat = 5
|
|||||||
private let verticalMargins: CGFloat = 8
|
private let verticalMargins: CGFloat = 8
|
||||||
private var labelHeight: CGFloat = 18
|
private var labelHeight: CGFloat = 18
|
||||||
private var margin: CGFloat = 10
|
private var margin: CGFloat = 10
|
||||||
private var prefixLabelWidth: CGFloat = 27
|
private var prefixLabelWidth: CGFloat = 29
|
||||||
private var textLabelWidth: CGFloat = 96
|
private var textLabelWidth: CGFloat = 96
|
||||||
private var valueLabelWidth: CGFloat = 65
|
private var valueLabelWidth: CGFloat = 65
|
||||||
|
|
||||||
@ -56,9 +56,10 @@ class ChartDetailsView: UIControl {
|
|||||||
func setup(viewModel: ChartDetailsViewModel, animated: Bool) {
|
func setup(viewModel: ChartDetailsViewModel, animated: Bool) {
|
||||||
self.viewModel = viewModel
|
self.viewModel = viewModel
|
||||||
|
|
||||||
titleLabel.setText(viewModel.title, animated: animated)
|
titleLabel.setText(viewModel.title, animated: false)
|
||||||
titleLabel.setVisible(!viewModel.title.isEmpty, animated: animated)
|
titleLabel.setVisible(!viewModel.title.isEmpty, animated: animated)
|
||||||
arrowView.setVisible(viewModel.showArrow, animated: animated)
|
arrowView.setVisible(viewModel.showArrow, animated: animated)
|
||||||
|
arrowButton.isUserInteractionEnabled = viewModel.showArrow
|
||||||
|
|
||||||
let width: CGFloat = margin * 2 + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + textLabelWidth + valueLabelWidth
|
let width: CGFloat = margin * 2 + (viewModel.showPrefixes ? (prefixLabelWidth + margin) : 0) + textLabelWidth + valueLabelWidth
|
||||||
var y: CGFloat = verticalMargins
|
var y: CGFloat = verticalMargins
|
||||||
|
|||||||
@ -13,6 +13,8 @@ public enum ChartType {
|
|||||||
case pie
|
case pie
|
||||||
case bars
|
case bars
|
||||||
case step
|
case step
|
||||||
|
case twoAxisStep
|
||||||
|
case hourlyStep
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension ChartTheme {
|
public extension ChartTheme {
|
||||||
@ -24,6 +26,54 @@ public extension ChartTheme {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func createChartController(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) -> BaseChartController? {
|
||||||
|
var resultController: BaseChartController?
|
||||||
|
if let data = data.data(using: .utf8) {
|
||||||
|
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
|
||||||
|
let controller: BaseChartController
|
||||||
|
switch type {
|
||||||
|
case .lines:
|
||||||
|
controller = GeneralLinesChartController(chartsCollection: collection)
|
||||||
|
controller.isZoomable = false
|
||||||
|
case .twoAxis:
|
||||||
|
controller = TwoAxisLinesChartController(chartsCollection: collection)
|
||||||
|
controller.isZoomable = false
|
||||||
|
case .pie:
|
||||||
|
controller = PercentPieChartController(chartsCollection: collection)
|
||||||
|
case .bars:
|
||||||
|
controller = StackedBarsChartController(chartsCollection: collection)
|
||||||
|
controller.isZoomable = false
|
||||||
|
case .step:
|
||||||
|
controller = StepBarsChartController(chartsCollection: collection)
|
||||||
|
case .twoAxisStep:
|
||||||
|
controller = TwoAxisStepBarsChartController(chartsCollection: collection)
|
||||||
|
case .hourlyStep:
|
||||||
|
controller = StepBarsChartController(chartsCollection: collection, hourly: true)
|
||||||
|
controller.isZoomable = false
|
||||||
|
}
|
||||||
|
controller.getDetailsData = { date, completion in
|
||||||
|
getDetailsData(date, { detailsData in
|
||||||
|
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
|
||||||
|
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
|
||||||
|
Queue.mainQueue().async {
|
||||||
|
completion(collection)
|
||||||
|
}
|
||||||
|
}) { error in
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
completion(nil)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
resultController = controller
|
||||||
|
}) { error in
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resultController
|
||||||
|
}
|
||||||
|
|
||||||
public final class ChartNode: ASDisplayNode {
|
public final class ChartNode: ASDisplayNode {
|
||||||
private var chartView: ChartStackSection {
|
private var chartView: ChartStackSection {
|
||||||
return self.view as! ChartStackSection
|
return self.view as! ChartStackSection
|
||||||
@ -36,64 +86,21 @@ public final class ChartNode: ASDisplayNode {
|
|||||||
return ChartStackSection()
|
return ChartStackSection()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func didLoad() {
|
|
||||||
super.didLoad()
|
|
||||||
|
|
||||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
|
||||||
}
|
|
||||||
|
|
||||||
public func setupTheme(_ theme: ChartTheme) {
|
public func setupTheme(_ theme: ChartTheme) {
|
||||||
self.chartView.apply(theme: ChartTheme.defaultDayTheme, animated: false)
|
self.chartView.apply(theme: ChartTheme.defaultDayTheme, animated: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
public override func layout() {
|
public func setup(controller: BaseChartController) {
|
||||||
super.layout()
|
var displayRange = true
|
||||||
|
if let controller = controller as? StepBarsChartController {
|
||||||
self.chartView.setNeedsDisplay()
|
displayRange = !controller.hourly
|
||||||
|
}
|
||||||
|
self.chartView.setup(controller: controller, displayRange: displayRange)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setup(_ data: String, type: ChartType, getDetailsData: @escaping (Date, @escaping (String?) -> Void) -> Void) {
|
public override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
if let data = data.data(using: .utf8) {
|
return super.hitTest(point, with: event)
|
||||||
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { [weak self] collection in
|
|
||||||
let controller: BaseChartController
|
|
||||||
switch type {
|
|
||||||
case .lines:
|
|
||||||
controller = GeneralLinesChartController(chartsCollection: collection)
|
|
||||||
controller.isZoomable = false
|
|
||||||
case .twoAxis:
|
|
||||||
controller = TwoAxisLinesChartController(chartsCollection: collection)
|
|
||||||
controller.isZoomable = false
|
|
||||||
case .pie:
|
|
||||||
controller = PercentPieChartController(chartsCollection: collection)
|
|
||||||
case .bars:
|
|
||||||
controller = StackedBarsChartController(chartsCollection: collection)
|
|
||||||
controller.isZoomable = false
|
|
||||||
case .step:
|
|
||||||
controller = StepBarsChartController(chartsCollection: collection)
|
|
||||||
}
|
|
||||||
controller.getDetailsData = { date, completion in
|
|
||||||
getDetailsData(date, { detailsData in
|
|
||||||
if let detailsData = detailsData, let data = detailsData.data(using: .utf8) {
|
|
||||||
ChartsDataManager.readChart(data: data, extraCopiesCount: 0, sync: true, success: { collection in
|
|
||||||
Queue.mainQueue().async {
|
|
||||||
completion(collection)
|
|
||||||
}
|
|
||||||
}) { error in
|
|
||||||
completion(nil)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
completion(nil)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.chartView.setup(controller: controller, title: "")
|
|
||||||
}
|
|
||||||
}) { error in
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
required public init?(coder aDecoder: NSCoder) {
|
required public init?(coder aDecoder: NSCoder) {
|
||||||
|
|||||||
@ -35,6 +35,8 @@ class ChartStackSection: UIView, ChartThemeContainer {
|
|||||||
var controller: BaseChartController?
|
var controller: BaseChartController?
|
||||||
var theme: ChartTheme?
|
var theme: ChartTheme?
|
||||||
|
|
||||||
|
var displayRange: Bool = true
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
sectionContainerView = UIView()
|
sectionContainerView = UIView()
|
||||||
chartView = ChartView()
|
chartView = ChartView()
|
||||||
@ -157,13 +159,20 @@ class ChartStackSection: UIView, ChartThemeContainer {
|
|||||||
self.titleLabel.frame = CGRect(origin: CGPoint(x: backButton.alpha > 0.0 ? 36.0 : 0.0, y: 5.0), size: CGSize(width: bounds.width, height: 28.0))
|
self.titleLabel.frame = CGRect(origin: CGPoint(x: backButton.alpha > 0.0 ? 36.0 : 0.0, y: 5.0), size: CGSize(width: bounds.width, height: 28.0))
|
||||||
self.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 750.0))
|
self.sectionContainerView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 750.0))
|
||||||
self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 250.0))
|
self.chartView.frame = CGRect(origin: CGPoint(), size: CGSize(width: bounds.width, height: 250.0))
|
||||||
|
|
||||||
|
self.rangeView.isHidden = !self.displayRange
|
||||||
|
|
||||||
self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 250.0), size: CGSize(width: bounds.width, height: 42.0))
|
self.rangeView.frame = CGRect(origin: CGPoint(x: 0.0, y: 250.0), size: CGSize(width: bounds.width, height: 42.0))
|
||||||
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: 308.0), size: CGSize(width: bounds.width, height: 350.0))
|
self.visibilityView.frame = CGRect(origin: CGPoint(x: 0.0, y: self.displayRange ? 308.0 : 266.0), size: CGSize(width: bounds.width, height: 350.0))
|
||||||
self.backButton.frame = CGRect(x: 0.0, y: 0.0, width: 96.0, height: 38.0)
|
self.backButton.frame = CGRect(x: 0.0, y: 0.0, width: 96.0, height: 38.0)
|
||||||
|
|
||||||
|
self.chartView.setNeedsDisplay()
|
||||||
}
|
}
|
||||||
|
|
||||||
func setup(controller: BaseChartController, title: String) {
|
func setup(controller: BaseChartController, displayRange: Bool = true) {
|
||||||
self.controller = controller
|
self.controller = controller
|
||||||
|
self.displayRange = displayRange
|
||||||
|
|
||||||
if let theme = self.theme {
|
if let theme = self.theme {
|
||||||
controller.apply(theme: theme, animated: false)
|
controller.apply(theme: theme, animated: false)
|
||||||
}
|
}
|
||||||
@ -222,7 +231,10 @@ class ChartStackSection: UIView, ChartThemeContainer {
|
|||||||
controller.initializeChart()
|
controller.initializeChart()
|
||||||
updateToolViews(animated: false)
|
updateToolViews(animated: false)
|
||||||
|
|
||||||
rangeView.setRange(0.8...1.0, animated: false)
|
let range: ClosedRange<CGFloat> = displayRange ? 0.8 ... 1.0 : 0.0 ... 1.0
|
||||||
controller.updateChartRange(0.8...1.0, animated: false)
|
rangeView.setRange(range, animated: false)
|
||||||
|
controller.updateChartRange(range, animated: false)
|
||||||
|
|
||||||
|
self.setNeedsLayout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,7 +75,9 @@ class ChartView: UIControl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
|
||||||
if let point = touches.first?.location(in: self) {
|
if var point = touches.first?.location(in: self) {
|
||||||
|
point.x = max(0.0, min(self.frame.width, point.x))
|
||||||
|
point.y = max(0.0, min(self.frame.height, point.y))
|
||||||
let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
|
let fractionPoint = CGPoint(x: (point.x - chartFrame.origin.x) / chartFrame.width,
|
||||||
y: (point.y - chartFrame.origin.y) / chartFrame.height)
|
y: (point.y - chartFrame.origin.y) / chartFrame.height)
|
||||||
userDidSelectCoordinateClosure?(fractionPoint)
|
userDidSelectCoordinateClosure?(fractionPoint)
|
||||||
|
|||||||
@ -17,9 +17,42 @@ private enum Constants {
|
|||||||
static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
|
static let insets = UIEdgeInsets(top: 0, left: 16, bottom: 16, right: 16)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ChartVisibilityItem {
|
public struct ChartVisibilityItem {
|
||||||
var title: String
|
var title: String
|
||||||
var color: UIColor
|
var color: UIColor
|
||||||
|
|
||||||
|
public init(title: String, color: UIColor) {
|
||||||
|
self.title = title
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func calculateVisiblityHeight(width: CGFloat, items: [ChartVisibilityItem]) -> CGFloat {
|
||||||
|
let frames = generateItemsFrames(frame: CGRect(origin: CGPoint(), size: CGSize(width: width, height: CGFloat.greatestFiniteMagnitude)), items: items)
|
||||||
|
guard let lastFrame = frames.last else { return .zero }
|
||||||
|
return lastFrame.maxY + Constants.insets.bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateItemsFrames(frame: CGRect, items: [ChartVisibilityItem]) -> [CGRect] {
|
||||||
|
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
|
||||||
|
var frames: [CGRect] = []
|
||||||
|
|
||||||
|
for item in items {
|
||||||
|
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
|
||||||
|
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
|
||||||
|
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
|
||||||
|
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||||
|
} else if previousPoint.x <= Constants.insets.left {
|
||||||
|
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||||
|
} else {
|
||||||
|
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
|
||||||
|
previousPoint.x = Constants.insets.left
|
||||||
|
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
||||||
|
}
|
||||||
|
previousPoint.x += width + Constants.itemSpacing
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChartVisibilityView: UIView {
|
class ChartVisibilityView: UIView {
|
||||||
@ -80,29 +113,7 @@ class ChartVisibilityView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var selectionViews: [ChartVisibilityItemView] = []
|
private var selectionViews: [ChartVisibilityItemView] = []
|
||||||
|
|
||||||
private func generateItemsFrames(frame: CGRect) -> [CGRect] {
|
|
||||||
var previousPoint = CGPoint(x: Constants.insets.left, y: Constants.insets.top)
|
|
||||||
var frames: [CGRect] = []
|
|
||||||
|
|
||||||
for item in items {
|
|
||||||
let labelSize = (item.title as NSString).size(withAttributes: [.font: ChartVisibilityItemView.textFont])
|
|
||||||
let width = (labelSize.width + Constants.labelTextApproxInsets).rounded(.up)
|
|
||||||
if previousPoint.x + width < (frame.width - Constants.insets.left - Constants.insets.right) {
|
|
||||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
|
||||||
} else if previousPoint.x <= Constants.insets.left {
|
|
||||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
|
||||||
} else {
|
|
||||||
previousPoint.y += Constants.itemHeight + Constants.itemSpacing
|
|
||||||
previousPoint.x = Constants.insets.left
|
|
||||||
frames.append(CGRect(origin: previousPoint, size: CGSize(width: width, height: Constants.itemHeight)))
|
|
||||||
}
|
|
||||||
previousPoint.x += width + Constants.itemSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
return frames
|
|
||||||
}
|
|
||||||
|
|
||||||
var selectionCallbackClosure: (([Bool]) -> Void)?
|
var selectionCallbackClosure: (([Bool]) -> Void)?
|
||||||
|
|
||||||
func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) {
|
func setItemSelected(_ selected: Bool, at index: Int, animated: Bool) {
|
||||||
@ -129,7 +140,7 @@ class ChartVisibilityView: UIView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func updateFrames() {
|
private func updateFrames() {
|
||||||
for (index, frame) in generateItemsFrames(frame: bounds).enumerated() {
|
for (index, frame) in generateItemsFrames(frame: bounds, items: self.items).enumerated() {
|
||||||
selectionViews[index].frame = frame
|
selectionViews[index].frame = frame
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -140,7 +151,7 @@ class ChartVisibilityView: UIView {
|
|||||||
size.height = 0
|
size.height = 0
|
||||||
return size
|
return size
|
||||||
}
|
}
|
||||||
let frames = generateItemsFrames(frame: UIScreen.main.bounds)
|
let frames = generateItemsFrames(frame: UIScreen.main.bounds, items: self.items)
|
||||||
guard let lastFrame = frames.last else { return .zero }
|
guard let lastFrame = frames.last else { return .zero }
|
||||||
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
|
let size = CGSize(width: frame.width, height: lastFrame.maxY + Constants.insets.bottom)
|
||||||
return size
|
return size
|
||||||
|
|||||||
@ -293,7 +293,11 @@ typedef enum
|
|||||||
[_wrapperView addSubview:_fadeView];
|
[_wrapperView addSubview:_fadeView];
|
||||||
}
|
}
|
||||||
|
|
||||||
CGFloat circleWrapperViewLength = 216.0f + 38.0f;
|
CGFloat minSide = MIN(_wrapperView.frame.size.width, _wrapperView.frame.size.height);
|
||||||
|
CGFloat diameter = minSide > 320.0f ? 240.0f : 216.0f;
|
||||||
|
CGFloat shadowSize = minSide > 320.0f ? 21.0f : 19.0f;
|
||||||
|
|
||||||
|
CGFloat circleWrapperViewLength = diameter + shadowSize * 2.0;
|
||||||
_circleWrapperView = [[UIView alloc] initWithFrame:(CGRect){
|
_circleWrapperView = [[UIView alloc] initWithFrame:(CGRect){
|
||||||
.origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f,
|
.origin.x = (_wrapperView.bounds.size.width - circleWrapperViewLength) / 2.0f,
|
||||||
.origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f,
|
.origin.y = _wrapperView.bounds.size.height + circleWrapperViewLength * 0.3f,
|
||||||
@ -309,7 +313,7 @@ typedef enum
|
|||||||
_shadowView.frame = _circleWrapperView.bounds;
|
_shadowView.frame = _circleWrapperView.bounds;
|
||||||
[_circleWrapperView addSubview:_shadowView];
|
[_circleWrapperView addSubview:_shadowView];
|
||||||
|
|
||||||
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, 19.0f, 19.0f)];
|
_circleView = [[UIView alloc] initWithFrame:CGRectInset(_circleWrapperView.bounds, shadowSize, shadowSize)];
|
||||||
_circleView.clipsToBounds = true;
|
_circleView.clipsToBounds = true;
|
||||||
_circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f;
|
_circleView.layer.cornerRadius = _circleView.frame.size.width / 2.0f;
|
||||||
[_circleWrapperView addSubview:_circleView];
|
[_circleWrapperView addSubview:_circleView];
|
||||||
@ -325,7 +329,7 @@ typedef enum
|
|||||||
_placeholderView.accessibilityIgnoresInvertColors = true;
|
_placeholderView.accessibilityIgnoresInvertColors = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
CGFloat ringViewLength = 234.0f;
|
CGFloat ringViewLength = minSide > 320.0f ? 260.0f : 234.0f;
|
||||||
_ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){
|
_ringView = [[TGVideoMessageRingView alloc] initWithFrame:(CGRect){
|
||||||
.origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f,
|
.origin.x = (_circleWrapperView.bounds.size.width - ringViewLength) / 2.0f,
|
||||||
.origin.y = (_circleWrapperView.bounds.size.height - ringViewLength) / 2.0f,
|
.origin.y = (_circleWrapperView.bounds.size.height - ringViewLength) / 2.0f,
|
||||||
|
|||||||
18
submodules/ManagedAnimationNode/BUCK
Normal file
18
submodules/ManagedAnimationNode/BUCK
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
load("//Config:buck_rule_macros.bzl", "static_library")
|
||||||
|
|
||||||
|
static_library(
|
||||||
|
name = "ManagedAnimationNode",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display:Display#shared",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit#shared",
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit#shared",
|
||||||
|
"//submodules/rlottie:RLottieBinding",
|
||||||
|
],
|
||||||
|
frameworks = [
|
||||||
|
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||||
|
"$SDKROOT/System/Library/Frameworks/UIKit.framework",
|
||||||
|
],
|
||||||
|
)
|
||||||
21
submodules/ManagedAnimationNode/BUILD
Normal file
21
submodules/ManagedAnimationNode/BUILD
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "ManagedAnimationNode",
|
||||||
|
module_name = "ManagedAnimationNode",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
deps = [
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/Postbox:Postbox",
|
||||||
|
"//submodules/GZip:GZip",
|
||||||
|
"//submodules/rlottie:RLottieBinding",
|
||||||
|
"//submodules/AppBundle:AppBundle",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
||||||
22
submodules/ManagedAnimationNode/Info.plist
Normal file
22
submodules/ManagedAnimationNode/Info.plist
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
#import <Cocoa/Cocoa.h>
|
||||||
|
|
||||||
|
//! Project version number for ManagedAnimationNode.
|
||||||
|
FOUNDATION_EXPORT double ManagedAnimationNodeVersionNumber;
|
||||||
|
|
||||||
|
//! Project version string for ManagedAnimationNode.
|
||||||
|
FOUNDATION_EXPORT const unsigned char ManagedAnimationNodeVersionString[];
|
||||||
|
|
||||||
|
// In this header, you should import all the public headers of your framework using statements like #import <ManagedAnimationNode/PublicHeader.h>
|
||||||
|
|
||||||
|
|
||||||
@ -0,0 +1,231 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Postbox
|
||||||
|
import RLottieBinding
|
||||||
|
import AppBundle
|
||||||
|
import GZip
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
public final class ManagedAnimationState {
|
||||||
|
public let item: ManagedAnimationItem
|
||||||
|
|
||||||
|
private let instance: LottieInstance
|
||||||
|
|
||||||
|
let frameCount: Int
|
||||||
|
let fps: Double
|
||||||
|
|
||||||
|
var relativeTime: Double = 0.0
|
||||||
|
public var frameIndex: Int?
|
||||||
|
|
||||||
|
private let renderContext: DrawingContext
|
||||||
|
|
||||||
|
public init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
|
||||||
|
let resolvedInstance: LottieInstance
|
||||||
|
let renderContext: DrawingContext
|
||||||
|
|
||||||
|
if let current = current {
|
||||||
|
resolvedInstance = current.instance
|
||||||
|
renderContext = current.renderContext
|
||||||
|
} else {
|
||||||
|
guard let path = item.source.path else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.source.cacheKey) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resolvedInstance = instance
|
||||||
|
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.item = item
|
||||||
|
self.instance = resolvedInstance
|
||||||
|
self.renderContext = renderContext
|
||||||
|
|
||||||
|
self.frameCount = Int(self.instance.frameCount)
|
||||||
|
self.fps = Double(self.instance.frameRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func draw() -> UIImage? {
|
||||||
|
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
|
||||||
|
return self.renderContext.generateImage()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ManagedAnimationFrameRange: Equatable {
|
||||||
|
var startFrame: Int
|
||||||
|
var endFrame: Int
|
||||||
|
|
||||||
|
public init(startFrame: Int, endFrame: Int) {
|
||||||
|
self.startFrame = startFrame
|
||||||
|
self.endFrame = endFrame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ManagedAnimationSource: Equatable {
|
||||||
|
case local(String)
|
||||||
|
case resource(MediaBox, MediaResource)
|
||||||
|
|
||||||
|
var cacheKey: String {
|
||||||
|
switch self {
|
||||||
|
case let .local(name):
|
||||||
|
return name
|
||||||
|
case let .resource(_, resource):
|
||||||
|
return resource.id.uniqueId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var path: String? {
|
||||||
|
switch self {
|
||||||
|
case let .local(name):
|
||||||
|
return getAppBundle().path(forResource: name, ofType: "tgs")
|
||||||
|
case let .resource(mediaBox, resource):
|
||||||
|
return mediaBox.completedResourcePath(resource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static func == (lhs: ManagedAnimationSource, rhs: ManagedAnimationSource) -> Bool {
|
||||||
|
switch lhs {
|
||||||
|
case let .local(lhsPath):
|
||||||
|
if case let .local(rhsPath) = rhs, lhsPath == rhsPath {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
case let .resource(lhsMediaBox, lhsResource):
|
||||||
|
if case let .resource(rhsMediaBox, rhsResource) = rhs, lhsMediaBox === rhsMediaBox, lhsResource.isEqual(to: rhsResource) {
|
||||||
|
return true
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct ManagedAnimationItem: Equatable {
|
||||||
|
public let source: ManagedAnimationSource
|
||||||
|
var frames: ManagedAnimationFrameRange
|
||||||
|
var duration: Double
|
||||||
|
|
||||||
|
public init(source: ManagedAnimationSource, frames: ManagedAnimationFrameRange, duration: Double) {
|
||||||
|
self.source = source
|
||||||
|
self.frames = frames
|
||||||
|
self.duration = duration
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open class ManagedAnimationNode: ASDisplayNode {
|
||||||
|
public let intrinsicSize: CGSize
|
||||||
|
|
||||||
|
private let imageNode: ASImageNode
|
||||||
|
private let displayLink: CADisplayLink
|
||||||
|
|
||||||
|
public var state: ManagedAnimationState?
|
||||||
|
public var trackStack: [ManagedAnimationItem] = []
|
||||||
|
public var didTryAdvancingState = false
|
||||||
|
|
||||||
|
public init(size: CGSize) {
|
||||||
|
self.intrinsicSize = size
|
||||||
|
|
||||||
|
self.imageNode = ASImageNode()
|
||||||
|
self.imageNode.displayWithoutProcessing = true
|
||||||
|
self.imageNode.displaysAsynchronously = false
|
||||||
|
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
||||||
|
|
||||||
|
final class DisplayLinkTarget: NSObject {
|
||||||
|
private let f: () -> Void
|
||||||
|
|
||||||
|
init(_ f: @escaping () -> Void) {
|
||||||
|
self.f = f
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func event() {
|
||||||
|
self.f()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var displayLinkUpdate: (() -> Void)?
|
||||||
|
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
||||||
|
displayLinkUpdate?()
|
||||||
|
}, selector: #selector(DisplayLinkTarget.event))
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.imageNode)
|
||||||
|
|
||||||
|
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
||||||
|
|
||||||
|
displayLinkUpdate = { [weak self] in
|
||||||
|
self?.updateAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open func advanceState() {
|
||||||
|
guard !self.trackStack.isEmpty else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let item = self.trackStack.removeFirst()
|
||||||
|
|
||||||
|
if let state = self.state, state.item.source == item.source {
|
||||||
|
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||||
|
} else {
|
||||||
|
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.didTryAdvancingState = false
|
||||||
|
}
|
||||||
|
|
||||||
|
public func updateAnimation() {
|
||||||
|
if self.state == nil {
|
||||||
|
self.advanceState()
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let state = self.state else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let timestamp = CACurrentMediaTime()
|
||||||
|
|
||||||
|
let fps = state.fps
|
||||||
|
let frameRange = state.item.frames
|
||||||
|
|
||||||
|
let duration: Double = state.item.duration
|
||||||
|
var t = state.relativeTime / duration
|
||||||
|
t = max(0.0, t)
|
||||||
|
t = min(1.0, t)
|
||||||
|
//print("\(t) \(state.item.name)")
|
||||||
|
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
|
||||||
|
let lowerBound: Int = 0
|
||||||
|
let upperBound = state.frameCount - 1
|
||||||
|
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
||||||
|
|
||||||
|
if state.frameIndex != frameIndex {
|
||||||
|
state.frameIndex = frameIndex
|
||||||
|
if let image = state.draw() {
|
||||||
|
self.imageNode.image = image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var animationAdvancement: Double = 1.0 / 60.0
|
||||||
|
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
|
||||||
|
|
||||||
|
state.relativeTime += animationAdvancement
|
||||||
|
|
||||||
|
if state.relativeTime >= duration && !self.didTryAdvancingState {
|
||||||
|
self.didTryAdvancingState = true
|
||||||
|
self.advanceState()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func trackTo(item: ManagedAnimationItem) {
|
||||||
|
self.trackStack.append(item)
|
||||||
|
self.didTryAdvancingState = false
|
||||||
|
self.updateAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -40,6 +40,7 @@ static_library(
|
|||||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||||
"//submodules/rlottie:RLottieBinding",
|
"//submodules/rlottie:RLottieBinding",
|
||||||
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
],
|
],
|
||||||
frameworks = [
|
frameworks = [
|
||||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||||
|
|||||||
@ -39,6 +39,7 @@ swift_library(
|
|||||||
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
"//submodules/SolidRoundedButtonNode:SolidRoundedButtonNode",
|
||||||
"//submodules/OverlayStatusController:OverlayStatusController",
|
"//submodules/OverlayStatusController:OverlayStatusController",
|
||||||
"//submodules/rlottie:RLottieBinding",
|
"//submodules/rlottie:RLottieBinding",
|
||||||
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -1,340 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
import Display
|
|
||||||
import AsyncDisplayKit
|
|
||||||
import RLottieBinding
|
|
||||||
import AppBundle
|
|
||||||
import GZip
|
|
||||||
import SwiftSignalKit
|
|
||||||
|
|
||||||
private final class ManagedAnimationState {
|
|
||||||
let item: ManagedAnimationItem
|
|
||||||
|
|
||||||
private let instance: LottieInstance
|
|
||||||
|
|
||||||
let frameCount: Int
|
|
||||||
let fps: Double
|
|
||||||
|
|
||||||
var relativeTime: Double = 0.0
|
|
||||||
var frameIndex: Int?
|
|
||||||
|
|
||||||
private let renderContext: DrawingContext
|
|
||||||
|
|
||||||
init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
|
|
||||||
let resolvedInstance: LottieInstance
|
|
||||||
let renderContext: DrawingContext
|
|
||||||
|
|
||||||
if let current = current {
|
|
||||||
resolvedInstance = current.instance
|
|
||||||
renderContext = current.renderContext
|
|
||||||
} else {
|
|
||||||
guard let path = getAppBundle().path(forResource: item.name, ofType: "tgs") else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.name) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resolvedInstance = instance
|
|
||||||
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.item = item
|
|
||||||
self.instance = resolvedInstance
|
|
||||||
self.renderContext = renderContext
|
|
||||||
|
|
||||||
self.frameCount = Int(self.instance.frameCount)
|
|
||||||
self.fps = Double(self.instance.frameRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func draw() -> UIImage? {
|
|
||||||
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
|
|
||||||
return self.renderContext.generateImage()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ManagedAnimationFrameRange: Equatable {
|
|
||||||
var startFrame: Int
|
|
||||||
var endFrame: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ManagedAnimationItem: Equatable {
|
|
||||||
let name: String
|
|
||||||
var frames: ManagedAnimationFrameRange
|
|
||||||
var duration: Double
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManagedAnimationNode: ASDisplayNode {
|
|
||||||
let intrinsicSize: CGSize
|
|
||||||
|
|
||||||
private let imageNode: ASImageNode
|
|
||||||
private let displayLink: CADisplayLink
|
|
||||||
|
|
||||||
fileprivate var state: ManagedAnimationState?
|
|
||||||
fileprivate var trackStack: [ManagedAnimationItem] = []
|
|
||||||
fileprivate var didTryAdvancingState = false
|
|
||||||
|
|
||||||
init(size: CGSize) {
|
|
||||||
self.intrinsicSize = size
|
|
||||||
|
|
||||||
self.imageNode = ASImageNode()
|
|
||||||
self.imageNode.displayWithoutProcessing = true
|
|
||||||
self.imageNode.displaysAsynchronously = false
|
|
||||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
|
||||||
|
|
||||||
final class DisplayLinkTarget: NSObject {
|
|
||||||
private let f: () -> Void
|
|
||||||
|
|
||||||
init(_ f: @escaping () -> Void) {
|
|
||||||
self.f = f
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func event() {
|
|
||||||
self.f()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var displayLinkUpdate: (() -> Void)?
|
|
||||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
|
||||||
displayLinkUpdate?()
|
|
||||||
}, selector: #selector(DisplayLinkTarget.event))
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.imageNode)
|
|
||||||
|
|
||||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
|
||||||
|
|
||||||
displayLinkUpdate = { [weak self] in
|
|
||||||
self?.updateAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func advanceState() {
|
|
||||||
guard !self.trackStack.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let item = self.trackStack.removeFirst()
|
|
||||||
|
|
||||||
if let state = self.state, state.item.name == item.name {
|
|
||||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
|
||||||
} else {
|
|
||||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.didTryAdvancingState = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func updateAnimation() {
|
|
||||||
if self.state == nil {
|
|
||||||
self.advanceState()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let state = self.state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
let fps = state.fps
|
|
||||||
let frameRange = state.item.frames
|
|
||||||
|
|
||||||
let duration: Double = state.item.duration
|
|
||||||
var t = state.relativeTime / duration
|
|
||||||
t = max(0.0, t)
|
|
||||||
t = min(1.0, t)
|
|
||||||
//print("\(t) \(state.item.name)")
|
|
||||||
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
|
|
||||||
let lowerBound: Int = 0
|
|
||||||
let upperBound = state.frameCount - 1
|
|
||||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
|
||||||
|
|
||||||
if state.frameIndex != frameIndex {
|
|
||||||
state.frameIndex = frameIndex
|
|
||||||
if let image = state.draw() {
|
|
||||||
self.imageNode.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var animationAdvancement: Double = 1.0 / 60.0
|
|
||||||
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
|
|
||||||
|
|
||||||
state.relativeTime += animationAdvancement
|
|
||||||
|
|
||||||
if state.relativeTime >= duration && !self.didTryAdvancingState {
|
|
||||||
self.didTryAdvancingState = true
|
|
||||||
self.advanceState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func trackTo(item: ManagedAnimationItem) {
|
|
||||||
self.trackStack.append(item)
|
|
||||||
self.didTryAdvancingState = false
|
|
||||||
self.updateAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ManagedMonkeyAnimationIdle: CaseIterable {
|
|
||||||
case blink
|
|
||||||
case ear
|
|
||||||
case still
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ManagedMonkeyAnimationState: Equatable {
|
|
||||||
case idle(ManagedMonkeyAnimationIdle)
|
|
||||||
case eyesClosed
|
|
||||||
case peeking
|
|
||||||
case tracking(CGFloat)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
|
|
||||||
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
|
|
||||||
private var timer: SwiftSignalKit.Timer?
|
|
||||||
|
|
||||||
init() {
|
|
||||||
super.init(size: CGSize(width: 136.0, height: 136.0))
|
|
||||||
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.timer?.invalidate()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func startIdleTimer() {
|
|
||||||
self.timer?.invalidate()
|
|
||||||
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
|
|
||||||
guard let strongSelf = self else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
switch strongSelf.monkeyState {
|
|
||||||
case .idle:
|
|
||||||
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
|
|
||||||
strongSelf.setState(.idle(idle))
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}, queue: .mainQueue())
|
|
||||||
self.timer = timer
|
|
||||||
timer.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
override func advanceState() {
|
|
||||||
super.advanceState()
|
|
||||||
|
|
||||||
self.timer?.invalidate()
|
|
||||||
self.timer = nil
|
|
||||||
|
|
||||||
if self.trackStack.isEmpty, case .idle = self.monkeyState {
|
|
||||||
self.startIdleTimer()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
|
|
||||||
switch idle {
|
|
||||||
case .still:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
|
||||||
case .blink:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle1", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
|
||||||
case .ear:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle2", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
|
||||||
//self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
|
|
||||||
let previousState = self.monkeyState
|
|
||||||
self.monkeyState = monkeyState
|
|
||||||
|
|
||||||
self.timer?.invalidate()
|
|
||||||
self.timer = nil
|
|
||||||
|
|
||||||
func enqueueTracking(_ value: CGFloat) {
|
|
||||||
let lowerBound = 18
|
|
||||||
let upperBound = 160
|
|
||||||
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
|
||||||
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
|
|
||||||
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
|
|
||||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
|
||||||
self.didTryAdvancingState = false
|
|
||||||
self.updateAnimation()
|
|
||||||
} else {
|
|
||||||
self.trackStack = self.trackStack.filter {
|
|
||||||
$0.name != "TwoFactorSetupMonkeyTracking"
|
|
||||||
}
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func enqueueClearTracking() {
|
|
||||||
if let state = self.state, state.item.name == "TwoFactorSetupMonkeyTracking" {
|
|
||||||
let item = ManagedAnimationItem(name: "TwoFactorSetupMonkeyTracking", frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
|
|
||||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
|
||||||
self.didTryAdvancingState = false
|
|
||||||
self.updateAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch previousState {
|
|
||||||
case let .idle(previousIdle):
|
|
||||||
switch monkeyState {
|
|
||||||
case let .idle(idle):
|
|
||||||
self.enqueueIdle(idle)
|
|
||||||
case .eyesClosed:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
|
||||||
case .peeking:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
|
||||||
case let .tracking(value):
|
|
||||||
enqueueTracking(value)
|
|
||||||
}
|
|
||||||
case .eyesClosed:
|
|
||||||
switch monkeyState {
|
|
||||||
case let .idle(idle):
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
|
||||||
self.enqueueIdle(idle)
|
|
||||||
case .eyesClosed:
|
|
||||||
break
|
|
||||||
case .peeking:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3))
|
|
||||||
case let .tracking(value):
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
|
||||||
enqueueTracking(value)
|
|
||||||
}
|
|
||||||
case .peeking:
|
|
||||||
switch monkeyState {
|
|
||||||
case let .idle(idle):
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
|
||||||
self.enqueueIdle(idle)
|
|
||||||
case .eyesClosed:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyPeek", frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3))
|
|
||||||
case .peeking:
|
|
||||||
break
|
|
||||||
case let .tracking(value):
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
|
||||||
enqueueTracking(value)
|
|
||||||
}
|
|
||||||
case let .tracking(currentValue):
|
|
||||||
switch monkeyState {
|
|
||||||
case let .idle(idle):
|
|
||||||
enqueueClearTracking()
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyIdle", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
|
||||||
self.enqueueIdle(idle)
|
|
||||||
case .eyesClosed:
|
|
||||||
enqueueClearTracking()
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyClose", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
|
||||||
case .peeking:
|
|
||||||
enqueueClearTracking()
|
|
||||||
self.trackTo(item: ManagedAnimationItem(name: "TwoFactorSetupMonkeyCloseAndPeek", frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
|
||||||
case let .tracking(value):
|
|
||||||
if abs(currentValue - value) > CGFloat.ulpOfOne {
|
|
||||||
enqueueTracking(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,170 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import RLottieBinding
|
||||||
|
import AppBundle
|
||||||
|
import GZip
|
||||||
|
import SwiftSignalKit
|
||||||
|
import ManagedAnimationNode
|
||||||
|
|
||||||
|
enum ManagedMonkeyAnimationIdle: CaseIterable {
|
||||||
|
case blink
|
||||||
|
case ear
|
||||||
|
case still
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ManagedMonkeyAnimationState: Equatable {
|
||||||
|
case idle(ManagedMonkeyAnimationIdle)
|
||||||
|
case eyesClosed
|
||||||
|
case peeking
|
||||||
|
case tracking(CGFloat)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ManagedMonkeyAnimationNode: ManagedAnimationNode {
|
||||||
|
private var monkeyState: ManagedMonkeyAnimationState = .idle(.blink)
|
||||||
|
private var timer: SwiftSignalKit.Timer?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
super.init(size: CGSize(width: 136.0, height: 136.0))
|
||||||
|
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.timer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startIdleTimer() {
|
||||||
|
self.timer?.invalidate()
|
||||||
|
let timer = SwiftSignalKit.Timer(timeout: Double.random(in: 1.0 ..< 1.5), repeat: false, completion: { [weak self] in
|
||||||
|
guard let strongSelf = self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch strongSelf.monkeyState {
|
||||||
|
case .idle:
|
||||||
|
if let idle = ManagedMonkeyAnimationIdle.allCases.randomElement() {
|
||||||
|
strongSelf.setState(.idle(idle))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}, queue: .mainQueue())
|
||||||
|
self.timer = timer
|
||||||
|
timer.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
override func advanceState() {
|
||||||
|
super.advanceState()
|
||||||
|
|
||||||
|
self.timer?.invalidate()
|
||||||
|
self.timer = nil
|
||||||
|
|
||||||
|
if self.trackStack.isEmpty, case .idle = self.monkeyState {
|
||||||
|
self.startIdleTimer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func enqueueIdle(_ idle: ManagedMonkeyAnimationIdle) {
|
||||||
|
switch idle {
|
||||||
|
case .still:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
case .blink:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle1"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
||||||
|
case .ear:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle2"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 30), duration: 0.3))
|
||||||
|
//self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 179), duration: 3.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setState(_ monkeyState: ManagedMonkeyAnimationState) {
|
||||||
|
let previousState = self.monkeyState
|
||||||
|
self.monkeyState = monkeyState
|
||||||
|
|
||||||
|
self.timer?.invalidate()
|
||||||
|
self.timer = nil
|
||||||
|
|
||||||
|
func enqueueTracking(_ value: CGFloat) {
|
||||||
|
let lowerBound = 18
|
||||||
|
let upperBound = 160
|
||||||
|
let frameIndex = lowerBound + Int(value * CGFloat(upperBound - lowerBound))
|
||||||
|
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
|
||||||
|
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: frameIndex), duration: 0.3)
|
||||||
|
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||||
|
self.didTryAdvancingState = false
|
||||||
|
self.updateAnimation()
|
||||||
|
} else {
|
||||||
|
self.trackStack = self.trackStack.filter {
|
||||||
|
$0.source != .local("TwoFactorSetupMonkeyTracking")
|
||||||
|
}
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: frameIndex), duration: 0.3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func enqueueClearTracking() {
|
||||||
|
if let state = self.state, state.item.source == .local("TwoFactorSetupMonkeyTracking") {
|
||||||
|
let item = ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyTracking"), frames: ManagedAnimationFrameRange(startFrame: state.frameIndex ?? 0, endFrame: 0), duration: 0.3)
|
||||||
|
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
||||||
|
self.didTryAdvancingState = false
|
||||||
|
self.updateAnimation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch previousState {
|
||||||
|
case let .idle(previousIdle):
|
||||||
|
switch monkeyState {
|
||||||
|
case let .idle(idle):
|
||||||
|
self.enqueueIdle(idle)
|
||||||
|
case .eyesClosed:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||||
|
case .peeking:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||||
|
case let .tracking(value):
|
||||||
|
enqueueTracking(value)
|
||||||
|
}
|
||||||
|
case .eyesClosed:
|
||||||
|
switch monkeyState {
|
||||||
|
case let .idle(idle):
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||||
|
self.enqueueIdle(idle)
|
||||||
|
case .eyesClosed:
|
||||||
|
break
|
||||||
|
case .peeking:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 14), duration: 0.3))
|
||||||
|
case let .tracking(value):
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||||
|
enqueueTracking(value)
|
||||||
|
}
|
||||||
|
case .peeking:
|
||||||
|
switch monkeyState {
|
||||||
|
case let .idle(idle):
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||||
|
self.enqueueIdle(idle)
|
||||||
|
case .eyesClosed:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyPeek"), frames: ManagedAnimationFrameRange(startFrame: 14, endFrame: 0), duration: 0.3))
|
||||||
|
case .peeking:
|
||||||
|
break
|
||||||
|
case let .tracking(value):
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 41, endFrame: 0), duration: 0.3))
|
||||||
|
enqueueTracking(value)
|
||||||
|
}
|
||||||
|
case let .tracking(currentValue):
|
||||||
|
switch monkeyState {
|
||||||
|
case let .idle(idle):
|
||||||
|
enqueueClearTracking()
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyIdle"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
self.enqueueIdle(idle)
|
||||||
|
case .eyesClosed:
|
||||||
|
enqueueClearTracking()
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyClose"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||||
|
case .peeking:
|
||||||
|
enqueueClearTracking()
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("TwoFactorSetupMonkeyCloseAndPeek"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 41), duration: 0.3))
|
||||||
|
case let .tracking(value):
|
||||||
|
if abs(currentValue - value) > CGFloat.ulpOfOne {
|
||||||
|
enqueueTracking(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,6 +24,7 @@ static_library(
|
|||||||
"//submodules/PhotoResources:PhotoResources",
|
"//submodules/PhotoResources:PhotoResources",
|
||||||
"//submodules/GraphCore:GraphCore",
|
"//submodules/GraphCore:GraphCore",
|
||||||
"//submodules/GraphUI:GraphUI",
|
"//submodules/GraphUI:GraphUI",
|
||||||
|
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||||
],
|
],
|
||||||
frameworks = [
|
frameworks = [
|
||||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||||
|
|||||||
@ -25,6 +25,7 @@ swift_library(
|
|||||||
"//submodules/PhotoResources:PhotoResources",
|
"//submodules/PhotoResources:PhotoResources",
|
||||||
"//submodules/GraphCore:GraphCore",
|
"//submodules/GraphCore:GraphCore",
|
||||||
"//submodules/GraphUI:GraphUI",
|
"//submodules/GraphUI:GraphUI",
|
||||||
|
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
@ -296,36 +296,36 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
func item(presentationData: ItemListPresentationData, arguments: Any) -> ListViewItem {
|
||||||
let arguments = arguments as! StatsControllerArguments
|
let arguments = arguments as! StatsControllerArguments
|
||||||
switch self {
|
switch self {
|
||||||
case let .overviewHeader(theme, text, dates):
|
case let .overviewHeader(_, text, dates):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, accessoryText: ItemListSectionHeaderAccessoryText(value: dates, color: .generic), sectionId: self.section)
|
||||||
case let .growthTitle(theme, text),
|
case let .growthTitle(_, text),
|
||||||
let .followersTitle(theme, text),
|
let .followersTitle(_, text),
|
||||||
let .notificationsTitle(theme, text),
|
let .notificationsTitle(_, text),
|
||||||
let .viewsByHourTitle(theme, text),
|
let .viewsByHourTitle(_, text),
|
||||||
let .viewsBySourceTitle(theme, text),
|
let .viewsBySourceTitle(_, text),
|
||||||
let .followersBySourceTitle(theme, text),
|
let .followersBySourceTitle(_, text),
|
||||||
let .languagesTitle(theme, text),
|
let .languagesTitle(_, text),
|
||||||
let .postInteractionsTitle(theme, text),
|
let .postInteractionsTitle(_, text),
|
||||||
let .postsTitle(theme, text),
|
let .postsTitle(_, text),
|
||||||
let .instantPageInteractionsTitle(theme, text):
|
let .instantPageInteractionsTitle(_, text):
|
||||||
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
return ItemListSectionHeaderItem(presentationData: presentationData, text: text, sectionId: self.section)
|
||||||
case let .overview(theme, stats):
|
case let .overview(_, stats):
|
||||||
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
|
return StatsOverviewItem(presentationData: presentationData, stats: stats, sectionId: self.section, style: .blocks)
|
||||||
case let .growthGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .growthGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .followersGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .followersGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .notificationsGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .notificationsGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .viewsByHourGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .viewsByHourGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .viewsBySourceGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .viewsBySourceGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .followersBySourceGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .followersBySourceGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 160.0, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .languagesGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .languagesGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, height: 100.0, sectionId: self.section, style: .blocks)
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, sectionId: self.section, style: .blocks)
|
||||||
case let .postInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
|
case let .postInteractionsGraph(_, _, _, graph, type):
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||||
@ -333,18 +333,18 @@ private enum StatsEntry: ItemListNodeEntry {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, sectionId: self.section, style: .blocks)
|
}, sectionId: self.section, style: .blocks)
|
||||||
case let .post(index, theme, strings, dateTimeFormat, message, interactions):
|
case let .instantPageInteractionsGraph(_, _, _, graph, type):
|
||||||
|
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
||||||
|
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
||||||
|
if let graph = graph, case let .Loaded(_, data) = graph {
|
||||||
|
completion(data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, sectionId: self.section, style: .blocks)
|
||||||
|
case let .post(_, _, _, _, message, interactions):
|
||||||
return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: interactions.views, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
|
return StatsMessageItem(context: arguments.context, presentationData: presentationData, message: message, views: interactions.views, forwards: interactions.forwards, sectionId: self.section, style: .blocks, action: {
|
||||||
arguments.openMessage(message.id)
|
arguments.openMessage(message.id)
|
||||||
})
|
})
|
||||||
case let .instantPageInteractionsGraph(theme, strings, dateTimeFormat, graph, type):
|
|
||||||
return StatsGraphItem(presentationData: presentationData, graph: graph, type: type, getDetailsData: { date, completion in
|
|
||||||
let _ = arguments.loadDetailedGraph(graph, Int64(date.timeIntervalSince1970) * 1000).start(next: { graph in
|
|
||||||
if let graph = graph, case let .Loaded(_, data) = graph {
|
|
||||||
completion(data)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}, sectionId: self.section, style: .blocks)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -368,32 +368,37 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
|
|||||||
entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle))
|
entries.append(.followersTitle(presentationData.theme, presentationData.strings.Stats_FollowersTitle))
|
||||||
entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines))
|
entries.append(.followersGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.followersGraph, .lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.muteGraph.isEmpty {
|
if !data.muteGraph.isEmpty {
|
||||||
entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle))
|
entries.append(.notificationsTitle(presentationData.theme, presentationData.strings.Stats_NotificationsTitle))
|
||||||
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines))
|
entries.append(.notificationsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.muteGraph, .lines))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !data.topHoursGraph.isEmpty {
|
||||||
|
entries.append(.viewsByHourTitle(presentationData.theme, presentationData.strings.Stats_ViewsByHoursTitle))
|
||||||
|
entries.append(.viewsByHourGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.topHoursGraph, .hourlyStep))
|
||||||
|
}
|
||||||
|
|
||||||
if !data.viewsBySourceGraph.isEmpty {
|
if !data.viewsBySourceGraph.isEmpty {
|
||||||
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
|
entries.append(.viewsBySourceTitle(presentationData.theme, presentationData.strings.Stats_ViewsBySourceTitle))
|
||||||
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
|
entries.append(.viewsBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.viewsBySourceGraph, .bars))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.newFollowersBySourceGraph.isEmpty {
|
if !data.newFollowersBySourceGraph.isEmpty {
|
||||||
entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle))
|
entries.append(.followersBySourceTitle(presentationData.theme, presentationData.strings.Stats_FollowersBySourceTitle))
|
||||||
entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars))
|
entries.append(.followersBySourceGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.newFollowersBySourceGraph, .bars))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.languagesGraph.isEmpty {
|
if !data.languagesGraph.isEmpty {
|
||||||
entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle))
|
entries.append(.languagesTitle(presentationData.theme, presentationData.strings.Stats_LanguagesTitle))
|
||||||
entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie))
|
entries.append(.languagesGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.languagesGraph, .pie))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.interactionsGraph.isEmpty {
|
if !data.interactionsGraph.isEmpty {
|
||||||
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle))
|
entries.append(.postInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InteractionsTitle))
|
||||||
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .step))
|
entries.append(.postInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.interactionsGraph, .twoAxisStep))
|
||||||
}
|
}
|
||||||
|
|
||||||
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
|
if let messages = messages, !messages.isEmpty, let interactions = interactions, !interactions.isEmpty {
|
||||||
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
|
entries.append(.postsTitle(presentationData.theme, presentationData.strings.Stats_PostsTitle))
|
||||||
var index: Int32 = 0
|
var index: Int32 = 0
|
||||||
@ -404,7 +409,7 @@ private func statsControllerEntries(data: ChannelStats?, messages: [Message]?, i
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !data.instantPageInteractionsGraph.isEmpty {
|
if !data.instantPageInteractionsGraph.isEmpty {
|
||||||
entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle))
|
entries.append(.instantPageInteractionsTitle(presentationData.theme, presentationData.strings.Stats_InstantViewInteractionsTitle))
|
||||||
entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .step))
|
entries.append(.instantPageInteractionsGraph(presentationData.theme, presentationData.strings, presentationData.dateTimeFormat, data.instantPageInteractionsGraph, .step))
|
||||||
@ -439,6 +444,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
|
|||||||
if let statsContext = statsContext, let stats = stats {
|
if let statsContext = statsContext, let stats = stats {
|
||||||
if case .OnDemand = stats.interactionsGraph {
|
if case .OnDemand = stats.interactionsGraph {
|
||||||
statsContext.loadInteractionsGraph()
|
statsContext.loadInteractionsGraph()
|
||||||
|
statsContext.loadTopHoursGraph()
|
||||||
statsContext.loadNewFollowersBySourceGraph()
|
statsContext.loadNewFollowersBySourceGraph()
|
||||||
statsContext.loadViewsBySourceGraph()
|
statsContext.loadViewsBySourceGraph()
|
||||||
statsContext.loadLanguagesGraph()
|
statsContext.loadLanguagesGraph()
|
||||||
@ -460,12 +466,21 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
|
|||||||
}
|
}
|
||||||
messagesPromise.set(.single(nil) |> then(messageView))
|
messagesPromise.set(.single(nil) |> then(messageView))
|
||||||
|
|
||||||
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get())
|
let longLoadingSignal: Signal<Bool, NoError> = .single(false) |> then(.single(true) |> delay(1.5, queue: Queue.mainQueue()))
|
||||||
|
|
||||||
|
let previousData = Atomic<ChannelStats?>(value: nil)
|
||||||
|
|
||||||
|
let signal = combineLatest(context.sharedContext.presentationData, dataPromise.get(), messagesPromise.get(), longLoadingSignal)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
|> map { presentationData, data, messageView -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
|> map { presentationData, data, messageView, longLoading -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||||
|
let previous = previousData.swap(data)
|
||||||
var emptyStateItem: ItemListControllerEmptyStateItem?
|
var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||||
if data == nil {
|
if data == nil {
|
||||||
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
if longLoading {
|
||||||
|
emptyStateItem = StatsEmptyStateItem(theme: presentationData.theme, strings: presentationData.strings)
|
||||||
|
} else {
|
||||||
|
emptyStateItem = ItemListLoadingIndicatorEmptyStateItem(theme: presentationData.theme)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let messages = messageView?.entries.map { $0.message }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in
|
let messages = messageView?.entries.map { $0.message }.sorted(by: { (lhsMessage, rhsMessage) -> Bool in
|
||||||
@ -478,7 +493,7 @@ public func channelStatsController(context: AccountContext, peerId: PeerId, cach
|
|||||||
}
|
}
|
||||||
|
|
||||||
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
let controllerState = ItemListControllerState(presentationData: ItemListPresentationData(presentationData), title: .text(presentationData.strings.ChannelInfo_Stats), leftNavigationButton: nil, rightNavigationButton: nil, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
|
||||||
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: false, animateChanges: false)
|
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: statsControllerEntries(data: data, messages: messages, interactions: interactions, presentationData: presentationData), style: .blocks, emptyStateItem: emptyStateItem, crossfadeState: previous == nil, animateChanges: false)
|
||||||
|
|
||||||
return (controllerState, (listState, arguments))
|
return (controllerState, (listState, arguments))
|
||||||
}
|
}
|
||||||
|
|||||||
107
submodules/StatisticsUI/Sources/StatsEmptyItem.swift
Normal file
107
submodules/StatisticsUI/Sources/StatsEmptyItem.swift
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import Display
|
||||||
|
import TelegramPresentationData
|
||||||
|
import ItemListUI
|
||||||
|
import PresentationDataUtils
|
||||||
|
import AnimatedStickerNode
|
||||||
|
import AppBundle
|
||||||
|
|
||||||
|
final class StatsEmptyStateItem: ItemListControllerEmptyStateItem {
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
|
||||||
|
init(theme: PresentationTheme, strings: PresentationStrings) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
|
||||||
|
if let item = to as? StatsEmptyStateItem {
|
||||||
|
return self.theme === item.theme && self.strings === item.strings
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func node(current: ItemListControllerEmptyStateItemNode?) -> ItemListControllerEmptyStateItemNode {
|
||||||
|
if let current = current as? StatsEmptyStateItemNode {
|
||||||
|
current.item = self
|
||||||
|
return current
|
||||||
|
} else {
|
||||||
|
return StatsEmptyStateItemNode(item: self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final class StatsEmptyStateItemNode: ItemListControllerEmptyStateItemNode {
|
||||||
|
private var animationNode: AnimatedStickerNode
|
||||||
|
private let titleNode: ASTextNode
|
||||||
|
private let textNode: ASTextNode
|
||||||
|
private var validLayout: (ContainerViewLayout, CGFloat)?
|
||||||
|
|
||||||
|
var item: StatsEmptyStateItem {
|
||||||
|
didSet {
|
||||||
|
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
|
||||||
|
if let (layout, navigationHeight) = self.validLayout {
|
||||||
|
self.updateLayout(layout: layout, navigationBarHeight: navigationHeight, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init(item: StatsEmptyStateItem) {
|
||||||
|
self.item = item
|
||||||
|
|
||||||
|
self.animationNode = AnimatedStickerNode()
|
||||||
|
if let path = getAppBundle().path(forResource: "Charts", ofType: "tgs") {
|
||||||
|
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(path: path), width: 192, height: 192, playbackMode: .once, mode: .direct)
|
||||||
|
self.animationNode.visibility = true
|
||||||
|
}
|
||||||
|
|
||||||
|
self.titleNode = ASTextNode()
|
||||||
|
self.titleNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
self.textNode = ASTextNode()
|
||||||
|
self.textNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
super.init()
|
||||||
|
|
||||||
|
self.addSubnode(self.animationNode)
|
||||||
|
self.addSubnode(self.titleNode)
|
||||||
|
self.addSubnode(self.textNode)
|
||||||
|
|
||||||
|
self.updateThemeAndStrings(theme: self.item.theme, strings: self.item.strings)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
|
||||||
|
self.titleNode.attributedText = NSAttributedString(string: strings.Stats_LoadingTitle, font: Font.bold(17.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
|
||||||
|
self.textNode.attributedText = NSAttributedString(string: strings.Stats_LoadingText, font: Font.regular(14.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
|
||||||
|
self.validLayout = (layout, navigationBarHeight)
|
||||||
|
var insets = layout.insets(options: [])
|
||||||
|
insets.top += navigationBarHeight
|
||||||
|
|
||||||
|
let imageSpacing: CGFloat = 20.0
|
||||||
|
let textSpacing: CGFloat = 8.0
|
||||||
|
|
||||||
|
let imageSize = CGSize(width: 96.0, height: 96.0)
|
||||||
|
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
|
||||||
|
|
||||||
|
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
|
||||||
|
self.animationNode.updateLayout(size: imageSize)
|
||||||
|
|
||||||
|
let titleSize = self.titleNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
|
||||||
|
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right - 50.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
|
||||||
|
|
||||||
|
let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height
|
||||||
|
let topOffset = insets.top + floor((layout.size.height - insets.top - insets.bottom - totalHeight) / 2.0)
|
||||||
|
|
||||||
|
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
|
||||||
|
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
|
||||||
|
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize))
|
||||||
|
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right - layout.intrinsicInsets.left - layout.intrinsicInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -16,16 +16,14 @@ class StatsGraphItem: ListViewItem, ItemListItem {
|
|||||||
let presentationData: ItemListPresentationData
|
let presentationData: ItemListPresentationData
|
||||||
let graph: ChannelStatsGraph
|
let graph: ChannelStatsGraph
|
||||||
let type: ChartType
|
let type: ChartType
|
||||||
let height: CGFloat
|
|
||||||
let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)?
|
let getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)?
|
||||||
let sectionId: ItemListSectionId
|
let sectionId: ItemListSectionId
|
||||||
let style: ItemListStyle
|
let style: ItemListStyle
|
||||||
|
|
||||||
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, height: CGFloat = 0.0, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
init(presentationData: ItemListPresentationData, graph: ChannelStatsGraph, type: ChartType, getDetailsData: ((Date, @escaping (String?) -> Void) -> Void)? = nil, sectionId: ItemListSectionId, style: ItemListStyle) {
|
||||||
self.presentationData = presentationData
|
self.presentationData = presentationData
|
||||||
self.graph = graph
|
self.graph = graph
|
||||||
self.type = type
|
self.type = type
|
||||||
self.height = height
|
|
||||||
self.getDetailsData = getDetailsData
|
self.getDetailsData = getDetailsData
|
||||||
self.sectionId = sectionId
|
self.sectionId = sectionId
|
||||||
self.style = style
|
self.style = style
|
||||||
@ -72,11 +70,13 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
private let topStripeNode: ASDisplayNode
|
private let topStripeNode: ASDisplayNode
|
||||||
private let bottomStripeNode: ASDisplayNode
|
private let bottomStripeNode: ASDisplayNode
|
||||||
private let maskNode: ASImageNode
|
private let maskNode: ASImageNode
|
||||||
|
private let chartContainerNode: ASDisplayNode
|
||||||
|
|
||||||
let chartNode: ChartNode
|
let chartNode: ChartNode
|
||||||
private let activityIndicator: ActivityIndicator
|
private let activityIndicator: ActivityIndicator
|
||||||
|
|
||||||
private var item: StatsGraphItem?
|
private var item: StatsGraphItem?
|
||||||
|
private var visibilityHeight: CGFloat?
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
self.backgroundNode = ASDisplayNode()
|
self.backgroundNode = ASDisplayNode()
|
||||||
@ -84,41 +84,46 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
self.backgroundNode.backgroundColor = .white
|
self.backgroundNode.backgroundColor = .white
|
||||||
|
|
||||||
self.maskNode = ASImageNode()
|
self.maskNode = ASImageNode()
|
||||||
|
self.maskNode.isUserInteractionEnabled = false
|
||||||
|
|
||||||
self.topStripeNode = ASDisplayNode()
|
self.topStripeNode = ASDisplayNode()
|
||||||
self.topStripeNode.isLayerBacked = true
|
self.topStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
self.bottomStripeNode = ASDisplayNode()
|
self.bottomStripeNode = ASDisplayNode()
|
||||||
self.bottomStripeNode.isLayerBacked = true
|
self.bottomStripeNode.isLayerBacked = true
|
||||||
|
|
||||||
|
self.chartContainerNode = ASDisplayNode()
|
||||||
|
self.chartContainerNode.clipsToBounds = true
|
||||||
|
self.chartContainerNode.isUserInteractionEnabled = true
|
||||||
|
|
||||||
self.chartNode = ChartNode()
|
self.chartNode = ChartNode()
|
||||||
self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false))
|
self.activityIndicator = ActivityIndicator(type: ActivityIndicatorType.custom(.black, 16.0, 2.0, false))
|
||||||
self.activityIndicator.isHidden = true
|
self.activityIndicator.isHidden = true
|
||||||
|
|
||||||
super.init(layerBacked: false, dynamicBounce: false)
|
super.init(layerBacked: false, dynamicBounce: false)
|
||||||
|
|
||||||
self.clipsToBounds = true
|
self.chartContainerNode.addSubnode(self.chartNode)
|
||||||
|
self.chartContainerNode.addSubnode(self.activityIndicator)
|
||||||
self.addSubnode(self.chartNode)
|
|
||||||
self.addSubnode(self.activityIndicator)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func didLoad() {
|
override func didLoad() {
|
||||||
super.didLoad()
|
super.didLoad()
|
||||||
|
|
||||||
self.chartNode.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
self.view.interactiveTransitionGestureRecognizerTest = { point -> Bool in
|
||||||
return point.x > 30.0
|
return point.x > 30.0 || (point.y > 250.0 && point.y < 295.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
func asyncLayout() -> (_ item: StatsGraphItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) {
|
||||||
let currentItem = self.item
|
let currentItem = self.item
|
||||||
|
let currentVisibilityHeight = self.visibilityHeight
|
||||||
|
|
||||||
return { item, params, neighbors in
|
return { item, params, neighbors in
|
||||||
let leftInset = params.leftInset
|
let leftInset = params.leftInset
|
||||||
let rightInset: CGFloat = params.rightInset
|
let rightInset: CGFloat = params.rightInset
|
||||||
var updatedTheme: PresentationTheme?
|
var updatedTheme: PresentationTheme?
|
||||||
var updatedGraph: ChannelStatsGraph?
|
var updatedGraph: ChannelStatsGraph?
|
||||||
|
var updatedController: BaseChartController?
|
||||||
|
|
||||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||||
updatedTheme = item.presentationData.theme
|
updatedTheme = item.presentationData.theme
|
||||||
@ -126,9 +131,16 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
if currentItem?.graph != item.graph {
|
if currentItem?.graph != item.graph {
|
||||||
updatedGraph = item.graph
|
updatedGraph = item.graph
|
||||||
|
if case let .Loaded(_, data) = updatedGraph {
|
||||||
|
updatedController = createChartController(data, type: item.type, getDetailsData: { [weak self] date, completion in
|
||||||
|
if let strongSelf = self, let item = strongSelf.item {
|
||||||
|
item.getDetailsData?(date, completion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let contentSize: CGSize
|
var contentSize: CGSize
|
||||||
let insets: UIEdgeInsets
|
let insets: UIEdgeInsets
|
||||||
let separatorHeight = UIScreenPixel
|
let separatorHeight = UIScreenPixel
|
||||||
let itemBackgroundColor: UIColor
|
let itemBackgroundColor: UIColor
|
||||||
@ -138,20 +150,39 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
case .plain:
|
case .plain:
|
||||||
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
itemBackgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||||
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
itemSeparatorColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||||
contentSize = CGSize(width: params.width, height: 350.0 + item.height)
|
contentSize = CGSize(width: params.width, height: 301.0)
|
||||||
insets = itemListNeighborsPlainInsets(neighbors)
|
insets = itemListNeighborsPlainInsets(neighbors)
|
||||||
case .blocks:
|
case .blocks:
|
||||||
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
itemBackgroundColor = item.presentationData.theme.list.itemBlocksBackgroundColor
|
||||||
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
itemSeparatorColor = item.presentationData.theme.list.itemBlocksSeparatorColor
|
||||||
contentSize = CGSize(width: params.width, height: 350.0 + item.height)
|
contentSize = CGSize(width: params.width, height: 301.0)
|
||||||
insets = itemListNeighborsGroupedInsets(neighbors)
|
insets = itemListNeighborsGroupedInsets(neighbors)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var visibilityHeight = currentVisibilityHeight
|
||||||
|
if let updatedController = updatedController {
|
||||||
|
var height: CGFloat = 0.0
|
||||||
|
var items: [ChartVisibilityItem] = []
|
||||||
|
for item in updatedController.actualChartsCollection.chartValues {
|
||||||
|
items.append(ChartVisibilityItem(title: item.name, color: .black))
|
||||||
|
}
|
||||||
|
if items.count > 1 {
|
||||||
|
height = calculateVisiblityHeight(width: params.width - params.leftInset - params.rightInset, items: items)
|
||||||
|
}
|
||||||
|
if item.type == .hourlyStep {
|
||||||
|
height -= 42.0
|
||||||
|
}
|
||||||
|
visibilityHeight = height
|
||||||
|
}
|
||||||
|
if let visibilityHeight = visibilityHeight {
|
||||||
|
contentSize.height += visibilityHeight
|
||||||
|
}
|
||||||
|
|
||||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||||
|
|
||||||
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
return (ListViewItemNodeLayout(contentSize: contentSize, insets: insets), { [weak self] in
|
||||||
if let strongSelf = self {
|
if let strongSelf = self {
|
||||||
strongSelf.item = item
|
strongSelf.item = item
|
||||||
|
strongSelf.visibilityHeight = visibilityHeight
|
||||||
|
|
||||||
if let _ = updatedTheme {
|
if let _ = updatedTheme {
|
||||||
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
strongSelf.topStripeNode.backgroundColor = itemSeparatorColor
|
||||||
@ -178,14 +209,17 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
if strongSelf.backgroundNode.supernode == nil {
|
if strongSelf.backgroundNode.supernode == nil {
|
||||||
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
|
||||||
}
|
}
|
||||||
|
if strongSelf.chartContainerNode.supernode == nil {
|
||||||
|
strongSelf.insertSubnode(strongSelf.chartContainerNode, at: 1)
|
||||||
|
}
|
||||||
if strongSelf.topStripeNode.supernode == nil {
|
if strongSelf.topStripeNode.supernode == nil {
|
||||||
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
|
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 2)
|
||||||
}
|
}
|
||||||
if strongSelf.bottomStripeNode.supernode == nil {
|
if strongSelf.bottomStripeNode.supernode == nil {
|
||||||
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
|
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 3)
|
||||||
}
|
}
|
||||||
if strongSelf.maskNode.supernode == nil {
|
if strongSelf.maskNode.supernode == nil {
|
||||||
strongSelf.insertSubnode(strongSelf.maskNode, at: 3)
|
strongSelf.insertSubnode(strongSelf.maskNode, at: 4)
|
||||||
}
|
}
|
||||||
let hasCorners = itemListHasRoundedBlockLayout(params)
|
let hasCorners = itemListHasRoundedBlockLayout(params)
|
||||||
var hasTopCorners = false
|
var hasTopCorners = false
|
||||||
@ -207,7 +241,8 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
strongSelf.bottomStripeNode.isHidden = hasCorners
|
strongSelf.bottomStripeNode.isHidden = hasCorners
|
||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.chartNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 750.0))
|
strongSelf.chartContainerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: 0.0), size: CGSize(width: layout.size.width - leftInset - rightInset, height: contentSize.height))
|
||||||
|
strongSelf.chartNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width - leftInset - rightInset, height: 750.0))
|
||||||
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.presentationData.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil
|
||||||
|
|
||||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
|
||||||
@ -220,13 +255,13 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
|
|
||||||
strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false)
|
strongSelf.activityIndicator.type = .custom(item.presentationData.theme.list.itemSecondaryTextColor, 16.0, 2.0, false)
|
||||||
|
|
||||||
|
if let updatedTheme = updatedTheme {
|
||||||
|
strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
|
||||||
|
}
|
||||||
|
|
||||||
if let updatedGraph = updatedGraph {
|
if let updatedGraph = updatedGraph {
|
||||||
if case let .Loaded(_, data) = updatedGraph {
|
if case let .Loaded(_, data) = updatedGraph, let updatedController = updatedController {
|
||||||
strongSelf.chartNode.setup(data, type: item.type, getDetailsData: { [weak self] date, completion in
|
strongSelf.chartNode.setup(controller: updatedController)
|
||||||
if let strongSelf = self, let item = strongSelf.item {
|
|
||||||
item.getDetailsData?(date, completion)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
strongSelf.activityIndicator.isHidden = true
|
strongSelf.activityIndicator.isHidden = true
|
||||||
strongSelf.chartNode.isHidden = false
|
strongSelf.chartNode.isHidden = false
|
||||||
} else if case .OnDemand = updatedGraph {
|
} else if case .OnDemand = updatedGraph {
|
||||||
@ -234,10 +269,6 @@ class StatsGraphItemNode: ListViewItemNode {
|
|||||||
strongSelf.chartNode.isHidden = true
|
strongSelf.chartNode.isHidden = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let updatedTheme = updatedTheme {
|
|
||||||
strongSelf.chartNode.setupTheme(ChartTheme(presentationTheme: updatedTheme))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -170,15 +170,16 @@ public class StatsMessageItemNode: ListViewItemNode, ItemListItemNode {
|
|||||||
let itemBackgroundColor: UIColor
|
let itemBackgroundColor: UIColor
|
||||||
let itemSeparatorColor: UIColor
|
let itemSeparatorColor: UIColor
|
||||||
|
|
||||||
var leftInset = 16.0 + params.leftInset
|
let leftInset = 16.0 + params.leftInset
|
||||||
var rightInset = 16.0 + params.rightInset
|
let rightInset = 16.0 + params.rightInset
|
||||||
var totalLeftInset = leftInset
|
var totalLeftInset = leftInset
|
||||||
var additionalRightInset: CGFloat = 93.0
|
let additionalRightInset: CGFloat = 93.0
|
||||||
|
|
||||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseFontSize)
|
||||||
|
|
||||||
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId)
|
let contentKind = messageContentKind(contentSettings: item.context.currentContentSettings.with { $0 }, message: item.message, strings: item.presentationData.strings, nameDisplayOrder: .firstLast, accountPeerId: item.context.account.peerId)
|
||||||
let text = stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
var text = stringForMediaKind(contentKind, strings: item.presentationData.strings).0
|
||||||
|
text = foldLineBreaks(text)
|
||||||
|
|
||||||
var contentImageMedia: Media?
|
var contentImageMedia: Media?
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
|
|||||||
@ -156,26 +156,26 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||||||
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
let titleFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||||
let deltaFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
let deltaFont = Font.regular(item.presentationData.fontSize.itemListBaseHeaderFontSize)
|
||||||
|
|
||||||
let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (followersValueLabelLayout, followersValueLabelApply) = makeFollowersValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.followers.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.viewsPerPost.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (viewsPerPostValueLabelLayout, viewsPerPostValueLabelApply) = makeViewsPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.viewsPerPost.current > 0 ? compactNumericCountString(Int(item.stats.viewsPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: compactNumericCountString(Int(item.stats.sharesPerPost.current)), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (sharesPerPostValueLabelLayout, sharesPerPostValueLabelApply) = makeSharesPerPostValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.sharesPerPost.current > 0 ? compactNumericCountString(Int(item.stats.sharesPerPost.current)) : "", font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
var enabledNotifications: Double = 0.0
|
var enabledNotifications: Double = 0.0
|
||||||
if item.stats.enabledNotifications.total > 0 {
|
if item.stats.enabledNotifications.total > 0 {
|
||||||
enabledNotifications = item.stats.enabledNotifications.value / item.stats.enabledNotifications.total
|
enabledNotifications = item.stats.enabledNotifications.value / item.stats.enabledNotifications.total
|
||||||
}
|
}
|
||||||
|
|
||||||
let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 100.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (enabledNotificationsValueLabelLayout, enabledNotificationsValueLabelApply) = makeEnabledNotificationsValueLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%.02f%%", enabledNotifications * 100.0), font: valueFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (followersTitleLabelLayout, followersTitleLabelApply) = makeFollowersTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_Followers, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_ViewsPerPost, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (viewsPerPostTitleLabelLayout, viewsPerPostTitleLabelApply) = makeViewsPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.viewsPerPost.current > 0 ? item.presentationData.strings.Stats_ViewsPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_SharesPerPost, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (sharesPerPostTitleLabelLayout, sharesPerPostTitleLabelApply) = makeSharesPerPostTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.stats.sharesPerPost.current > 0 ? item.presentationData.strings.Stats_SharesPerPost : "", font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let (enabledNotificationsTitleLabelLayout, enabledNotificationsTitleLabelApply) = makeEnabledNotificationsTitleLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.Stats_EnabledNotifications, font: titleFont, textColor: item.presentationData.theme.list.sectionHeaderTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let followersDeltaValue = item.stats.followers.current - item.stats.followers.previous
|
let followersDeltaValue = item.stats.followers.current - item.stats.followers.previous
|
||||||
let followersDeltaCompact = compactNumericCountString(abs(Int(followersDeltaValue)))
|
let followersDeltaCompact = compactNumericCountString(abs(Int(followersDeltaValue)))
|
||||||
@ -185,7 +185,9 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||||||
followersDeltaPercentage = abs(followersDeltaValue / item.stats.followers.previous)
|
followersDeltaPercentage = abs(followersDeltaValue / item.stats.followers.previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", followersDelta, followersDeltaPercentage * 100.0), font: deltaFont, textColor: followersDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let followersDeltaText = abs(followersDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", followersDelta, followersDeltaPercentage * 100.0) : ""
|
||||||
|
|
||||||
|
let (followersDeltaLabelLayout, followersDeltaLabelApply) = makeFollowersDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: followersDeltaText, font: deltaFont, textColor: followersDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let viewsPerPostDeltaValue = item.stats.viewsPerPost.current - item.stats.viewsPerPost.previous
|
let viewsPerPostDeltaValue = item.stats.viewsPerPost.current - item.stats.viewsPerPost.previous
|
||||||
let viewsPerPostDeltaCompact = compactNumericCountString(abs(Int(viewsPerPostDeltaValue)))
|
let viewsPerPostDeltaCompact = compactNumericCountString(abs(Int(viewsPerPostDeltaValue)))
|
||||||
@ -195,7 +197,9 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||||||
viewsPerPostDeltaPercentage = abs(viewsPerPostDeltaValue / item.stats.viewsPerPost.previous)
|
viewsPerPostDeltaPercentage = abs(viewsPerPostDeltaValue / item.stats.viewsPerPost.previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", viewsPerPostDelta, viewsPerPostDeltaPercentage * 100.0), font: deltaFont, textColor: viewsPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let viewsPerPostDeltaText = abs(viewsPerPostDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", viewsPerPostDelta, viewsPerPostDeltaPercentage * 100.0) : ""
|
||||||
|
|
||||||
|
let (viewsPerPostDeltaLabelLayout, viewsPerPostDeltaLabelApply) = makeViewsPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: viewsPerPostDeltaText, font: deltaFont, textColor: viewsPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let sharesPerPostDeltaValue = item.stats.sharesPerPost.current - item.stats.sharesPerPost.previous
|
let sharesPerPostDeltaValue = item.stats.sharesPerPost.current - item.stats.sharesPerPost.previous
|
||||||
let sharesPerPostDeltaCompact = compactNumericCountString(abs(Int(sharesPerPostDeltaValue)))
|
let sharesPerPostDeltaCompact = compactNumericCountString(abs(Int(sharesPerPostDeltaValue)))
|
||||||
@ -205,7 +209,9 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||||||
sharesPerPostDeltaPercentage = abs(sharesPerPostDeltaValue / item.stats.sharesPerPost.previous)
|
sharesPerPostDeltaPercentage = abs(sharesPerPostDeltaValue / item.stats.sharesPerPost.previous)
|
||||||
}
|
}
|
||||||
|
|
||||||
let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: String(format: "%@ (%.02f%%)", sharesPerPostDelta, sharesPerPostDeltaPercentage * 100.0), font: deltaFont, textColor: sharesPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 140.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
let sharesPerPostDeltaText = abs(sharesPerPostDeltaPercentage) > 0.0 ? String(format: "%@ (%.02f%%)", sharesPerPostDelta, sharesPerPostDeltaPercentage * 100.0) : ""
|
||||||
|
|
||||||
|
let (sharesPerPostDeltaLabelLayout, sharesPerPostDeltaLabelApply) = makeSharesPerPostDeltaLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: sharesPerPostDeltaText, font: deltaFont, textColor: sharesPerPostDeltaValue > 0.0 ? item.presentationData.theme.list.freeTextSuccessColor : item.presentationData.theme.list.freeTextErrorColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||||
|
|
||||||
let contentSize: CGSize
|
let contentSize: CGSize
|
||||||
let insets: UIEdgeInsets
|
let insets: UIEdgeInsets
|
||||||
@ -213,11 +219,35 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||||||
let itemBackgroundColor: UIColor
|
let itemBackgroundColor: UIColor
|
||||||
let itemSeparatorColor: UIColor
|
let itemSeparatorColor: UIColor
|
||||||
|
|
||||||
let height: CGFloat
|
let horizontalSpacing: CGFloat = 4.0
|
||||||
if item.stats.viewsPerPost.current.isZero && item.stats.sharesPerPost.current.isZero {
|
let verticalSpacing: CGFloat = 18.0
|
||||||
height = 64.0
|
let topInset: CGFloat = 14.0
|
||||||
|
let sideInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
var height: CGFloat = topInset * 2.0
|
||||||
|
height += enabledNotificationsValueLabelLayout.size.height + enabledNotificationsTitleLabelLayout.size.height
|
||||||
|
|
||||||
|
var twoColumnLayout = true
|
||||||
|
if max(followersValueLabelLayout.size.width + followersDeltaLabelLayout.size.width + horizontalSpacing + enabledNotificationsValueLabelLayout.size.width, viewsPerPostValueLabelLayout.size.width + viewsPerPostDeltaLabelLayout.size.width + horizontalSpacing + sharesPerPostValueLabelLayout.size.width + sharesPerPostDeltaLabelLayout.size.width) > params.width - leftInset - rightInset {
|
||||||
|
twoColumnLayout = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if twoColumnLayout {
|
||||||
|
if !item.stats.viewsPerPost.current.isZero || !item.stats.sharesPerPost.current.isZero {
|
||||||
|
height += verticalSpacing
|
||||||
|
height += sharesPerPostValueLabelLayout.size.height + sharesPerPostTitleLabelLayout.size.height
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
height = 120.0
|
height += verticalSpacing
|
||||||
|
height += enabledNotificationsValueLabelLayout.size.height + enabledNotificationsTitleLabelLayout.size.height
|
||||||
|
if !item.stats.viewsPerPost.current.isZero {
|
||||||
|
height += verticalSpacing
|
||||||
|
height += viewsPerPostValueLabelLayout.size.height + viewsPerPostTitleLabelLayout.size.height
|
||||||
|
}
|
||||||
|
if !item.stats.sharesPerPost.current.isZero {
|
||||||
|
height += verticalSpacing
|
||||||
|
height += sharesPerPostValueLabelLayout.size.height + sharesPerPostTitleLabelLayout.size.height
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch item.style {
|
switch item.style {
|
||||||
@ -316,28 +346,25 @@ class StatsOverviewItemNode: ListViewItemNode {
|
|||||||
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - bottomStripeInset, height: separatorHeight))
|
||||||
}
|
}
|
||||||
|
|
||||||
let horizontalSpacing: CGFloat = 4.0
|
|
||||||
let verticalSpacing: CGFloat = 70.0
|
|
||||||
let topInset: CGFloat = 14.0
|
|
||||||
let sideInset: CGFloat = 16.0
|
|
||||||
|
|
||||||
strongSelf.followersValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: topInset), size: followersValueLabelLayout.size)
|
strongSelf.followersValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: topInset), size: followersValueLabelLayout.size)
|
||||||
strongSelf.followersTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.followersValueLabel.frame.maxY), size: followersTitleLabelLayout.size)
|
strongSelf.followersTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.followersValueLabel.frame.maxY), size: followersTitleLabelLayout.size)
|
||||||
strongSelf.followersDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.followersValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.followersValueLabel.frame.minY + 4.0), size: followersDeltaLabelLayout.size)
|
strongSelf.followersDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.followersValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.followersValueLabel.frame.maxY - followersDeltaLabelLayout.size.height - 2.0), size: followersDeltaLabelLayout.size)
|
||||||
|
|
||||||
strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: verticalSpacing), size: viewsPerPostValueLabelLayout.size)
|
let secondColumnX = twoColumnLayout ? max(layout.size.width / 2.0, sideInset + leftInset + max(followersValueLabelLayout.size.width + followersDeltaLabelLayout.size.width, viewsPerPostValueLabelLayout.size.width + viewsPerPostDeltaLabelLayout.size.width) + horizontalSpacing) : sideInset + leftInset
|
||||||
|
|
||||||
|
let enabledNotificationsY = twoColumnLayout ? topInset : strongSelf.followersTitleLabel.frame.maxY + verticalSpacing
|
||||||
|
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: enabledNotificationsY), size: enabledNotificationsValueLabelLayout.size)
|
||||||
|
strongSelf.enabledNotificationsTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.enabledNotificationsValueLabel.frame.maxY), size: enabledNotificationsTitleLabelLayout.size)
|
||||||
|
|
||||||
|
let viewsPerPostY = twoColumnLayout ? strongSelf.followersTitleLabel.frame.maxY + verticalSpacing : strongSelf.enabledNotificationsTitleLabel.frame.maxY + verticalSpacing
|
||||||
|
strongSelf.viewsPerPostValueLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: viewsPerPostY), size: viewsPerPostValueLabelLayout.size)
|
||||||
strongSelf.viewsPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.viewsPerPostValueLabel.frame.maxY), size: viewsPerPostTitleLabelLayout.size)
|
strongSelf.viewsPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: sideInset + leftInset, y: strongSelf.viewsPerPostValueLabel.frame.maxY), size: viewsPerPostTitleLabelLayout.size)
|
||||||
strongSelf.viewsPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.viewsPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.viewsPerPostValueLabel.frame.minY + 4.0), size: viewsPerPostDeltaLabelLayout.size)
|
strongSelf.viewsPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.viewsPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.viewsPerPostValueLabel.frame.maxY - viewsPerPostDeltaLabelLayout.size.height - 2.0), size: viewsPerPostDeltaLabelLayout.size)
|
||||||
|
|
||||||
let rightColumnX = max(layout.size.width / 2.0, max(strongSelf.followersDeltaLabel.frame.maxX, strongSelf.viewsPerPostDeltaLabel.frame.maxX) + horizontalSpacing)
|
let sharesPerPostY = twoColumnLayout ? strongSelf.enabledNotificationsTitleLabel.frame.maxY + verticalSpacing : strongSelf.viewsPerPostTitleLabel.frame.maxY + verticalSpacing
|
||||||
|
strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: sharesPerPostY), size: sharesPerPostValueLabelLayout.size)
|
||||||
strongSelf.sharesPerPostValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: verticalSpacing), size: sharesPerPostValueLabelLayout.size)
|
strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: secondColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
|
||||||
strongSelf.enabledNotificationsValueLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: topInset), size: enabledNotificationsValueLabelLayout.size)
|
strongSelf.sharesPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.sharesPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.sharesPerPostValueLabel.frame.maxY - sharesPerPostDeltaLabelLayout.size.height - 2.0), size: sharesPerPostDeltaLabelLayout.size)
|
||||||
|
|
||||||
strongSelf.sharesPerPostTitleLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: strongSelf.sharesPerPostValueLabel.frame.maxY), size: sharesPerPostTitleLabelLayout.size)
|
|
||||||
strongSelf.enabledNotificationsTitleLabel.frame = CGRect(origin: CGPoint(x: rightColumnX, y: strongSelf.enabledNotificationsValueLabel.frame.maxY), size: enabledNotificationsTitleLabelLayout.size)
|
|
||||||
|
|
||||||
strongSelf.sharesPerPostDeltaLabel.frame = CGRect(origin: CGPoint(x: strongSelf.sharesPerPostValueLabel.frame.maxX + horizontalSpacing, y: strongSelf.sharesPerPostValueLabel.frame.minY + 4.0), size: sharesPerPostDeltaLabelLayout.size)
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ public enum ChannelStatsGraph: Equatable {
|
|||||||
case .Empty:
|
case .Empty:
|
||||||
return true
|
return true
|
||||||
case let .Failed(error):
|
case let .Failed(error):
|
||||||
return true
|
return error.lowercased().contains("not enough data")
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -598,8 +598,11 @@ extension ChannelStatsMessageInteractions {
|
|||||||
extension ChannelStats {
|
extension ChannelStats {
|
||||||
convenience init(apiBroadcastStats: Api.stats.BroadcastStats, peerId: PeerId) {
|
convenience init(apiBroadcastStats: Api.stats.BroadcastStats, peerId: PeerId) {
|
||||||
switch apiBroadcastStats {
|
switch apiBroadcastStats {
|
||||||
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, growthGraph, followersGraph, muteGraph, topHoursGraph, interactionsGraph, instantViewInteractionsGraph, viewsBySourceGraph, newFollowersBySourceGraph, languagesGraph, recentMessageInteractions):
|
case let .broadcastStats(period, followers, viewsPerPost, sharesPerPost, enabledNotifications, apiGrowthGraph, apiFollowersGraph, apiMuteGraph, apiTopHoursGraph, apiInteractionsGraph, apiInstantViewInteractionsGraph, apiViewsBySourceGraph, apiNewFollowersBySourceGraph, apiLanguagesGraph, recentMessageInteractions):
|
||||||
self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), growthGraph: ChannelStatsGraph(apiStatsGraph: growthGraph), followersGraph: ChannelStatsGraph(apiStatsGraph: followersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: muteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: topHoursGraph), interactionsGraph: ChannelStatsGraph(apiStatsGraph: interactionsGraph), instantPageInteractionsGraph: ChannelStatsGraph(apiStatsGraph: instantViewInteractionsGraph), viewsBySourceGraph: ChannelStatsGraph(apiStatsGraph: viewsBySourceGraph), newFollowersBySourceGraph: ChannelStatsGraph(apiStatsGraph: newFollowersBySourceGraph), languagesGraph: ChannelStatsGraph(apiStatsGraph: languagesGraph), messageInteractions: recentMessageInteractions.map { ChannelStatsMessageInteractions(apiMessageInteractionCounters: $0, peerId: peerId) })
|
let growthGraph = ChannelStatsGraph(apiStatsGraph: apiGrowthGraph)
|
||||||
|
let isEmpty = growthGraph.isEmpty
|
||||||
|
|
||||||
|
self.init(period: ChannelStatsDateRange(apiStatsDateRangeDays: period), followers: ChannelStatsValue(apiStatsAbsValueAndPrev: followers), viewsPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: viewsPerPost), sharesPerPost: ChannelStatsValue(apiStatsAbsValueAndPrev: sharesPerPost), enabledNotifications: ChannelStatsPercentValue(apiPercentValue: enabledNotifications), growthGraph: growthGraph, followersGraph: ChannelStatsGraph(apiStatsGraph: apiFollowersGraph), muteGraph: ChannelStatsGraph(apiStatsGraph: apiMuteGraph), topHoursGraph: ChannelStatsGraph(apiStatsGraph: apiTopHoursGraph), interactionsGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiInteractionsGraph), instantPageInteractionsGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiInstantViewInteractionsGraph), viewsBySourceGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiViewsBySourceGraph), newFollowersBySourceGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiNewFollowersBySourceGraph), languagesGraph: isEmpty ? .Empty : ChannelStatsGraph(apiStatsGraph: apiLanguagesGraph), messageInteractions: recentMessageInteractions.map { ChannelStatsMessageInteractions(apiMessageInteractionCounters: $0, peerId: peerId) })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -221,3 +221,19 @@ public func descriptionStringForMessage(contentSettings: ContentSettings, messag
|
|||||||
}
|
}
|
||||||
return stringForMediaKind(messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings)
|
return stringForMediaKind(messageContentKind(contentSettings: contentSettings, message: message, strings: strings, nameDisplayOrder: nameDisplayOrder, accountPeerId: accountPeerId), strings: strings)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func foldLineBreaks(_ text: String) -> String {
|
||||||
|
let lines = text.split { $0.isNewline }
|
||||||
|
var result = ""
|
||||||
|
for line in lines {
|
||||||
|
if line.isEmpty {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result.isEmpty {
|
||||||
|
result += line
|
||||||
|
} else {
|
||||||
|
result += " " + line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@ -203,6 +203,7 @@ framework(
|
|||||||
"//submodules/AccountUtils:AccountUtils",
|
"//submodules/AccountUtils:AccountUtils",
|
||||||
"//submodules/Svg:Svg",
|
"//submodules/Svg:Svg",
|
||||||
"//submodules/StatisticsUI:StatisticsUI",
|
"//submodules/StatisticsUI:StatisticsUI",
|
||||||
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
],
|
],
|
||||||
frameworks = [
|
frameworks = [
|
||||||
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
"$SDKROOT/System/Library/Frameworks/Foundation.framework",
|
||||||
|
|||||||
@ -201,6 +201,7 @@ swift_library(
|
|||||||
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
"//submodules/SemanticStatusNode:SemanticStatusNode",
|
||||||
"//submodules/AccountUtils:AccountUtils",
|
"//submodules/AccountUtils:AccountUtils",
|
||||||
"//submodules/Svg:Svg",
|
"//submodules/Svg:Svg",
|
||||||
|
"//submodules/ManagedAnimationNode:ManagedAnimationNode",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
|||||||
Binary file not shown.
@ -120,7 +120,6 @@ public final class ChatControllerInteraction {
|
|||||||
var stickerSettings: ChatInterfaceStickerSettings
|
var stickerSettings: ChatInterfaceStickerSettings
|
||||||
var searchTextHighightState: (String, [MessageIndex])?
|
var searchTextHighightState: (String, [MessageIndex])?
|
||||||
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
var seenOneTimeAnimatedMedia = Set<MessageId>()
|
||||||
var seenDicePointsValue = [MessageId: Int]()
|
|
||||||
|
|
||||||
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
init(openMessage: @escaping (Message, ChatControllerInteractionOpenMessageMode) -> Bool, openPeer: @escaping (PeerId?, ChatControllerInteractionNavigateToPeer, Message?) -> Void, openPeerMention: @escaping (String) -> Void, openMessageContextMenu: @escaping (Message, Bool, ASDisplayNode, CGRect, TapLongTapOrDoubleTapGestureRecognizer?) -> Void, openMessageContextActions: @escaping (Message, ASDisplayNode, CGRect, ContextGesture?) -> Void, navigateToMessage: @escaping (MessageId, MessageId) -> Void, tapMessage: ((Message) -> Void)?, clickThroughMessage: @escaping () -> Void, toggleMessagesSelection: @escaping ([MessageId], Bool) -> Void, sendCurrentMessage: @escaping (Bool) -> Void, sendMessage: @escaping (String) -> Void, sendSticker: @escaping (FileMediaReference, Bool, ASDisplayNode, CGRect) -> Bool, sendGif: @escaping (FileMediaReference, ASDisplayNode, CGRect) -> Bool, requestMessageActionCallback: @escaping (MessageId, MemoryBuffer?, Bool) -> Void, requestMessageActionUrlAuth: @escaping (String, MessageId, Int32) -> Void, activateSwitchInline: @escaping (PeerId?, String) -> Void, openUrl: @escaping (String, Bool, Bool?, Message?) -> Void, shareCurrentLocation: @escaping () -> Void, shareAccountContact: @escaping () -> Void, sendBotCommand: @escaping (MessageId?, String) -> Void, openInstantPage: @escaping (Message, ChatMessageItemAssociatedData?) -> Void, openWallpaper: @escaping (Message) -> Void, openTheme: @escaping (Message) -> Void, openHashtag: @escaping (String?, String) -> Void, updateInputState: @escaping ((ChatTextInputState) -> ChatTextInputState) -> Void, updateInputMode: @escaping ((ChatInputMode) -> ChatInputMode) -> Void, openMessageShareMenu: @escaping (MessageId) -> Void, presentController: @escaping (ViewController, Any?) -> Void, navigationController: @escaping () -> NavigationController?, chatControllerNode: @escaping () -> ASDisplayNode?, reactionContainerNode: @escaping () -> ReactionSelectionParentNode?, presentGlobalOverlayController: @escaping (ViewController, Any?) -> Void, callPeer: @escaping (PeerId) -> Void, longTap: @escaping (ChatControllerInteractionLongTapAction, Message?) -> Void, openCheckoutOrReceipt: @escaping (MessageId) -> Void, openSearch: @escaping () -> Void, setupReply: @escaping (MessageId) -> Void, canSetupReply: @escaping (Message) -> Bool, navigateToFirstDateMessage: @escaping(Int32) ->Void, requestRedeliveryOfFailedMessages: @escaping (MessageId) -> Void, addContact: @escaping (String) -> Void, rateCall: @escaping (Message, CallId) -> Void, requestSelectMessagePollOptions: @escaping (MessageId, [Data]) -> Void, requestOpenMessagePollResults: @escaping (MessageId, MediaId) -> Void, openAppStorePage: @escaping () -> Void, displayMessageTooltip: @escaping (MessageId, String, ASDisplayNode?, CGRect?) -> Void, seekToTimecode: @escaping (Message, Double, Bool) -> Void, scheduleCurrentMessage: @escaping () -> Void, sendScheduledMessagesNow: @escaping ([MessageId]) -> Void, editScheduledMessagesTime: @escaping ([MessageId]) -> Void, performTextSelectionAction: @escaping (UInt32, String, TextSelectionAction) -> Void, updateMessageReaction: @escaping (MessageId, String?) -> Void, openMessageReactions: @escaping (MessageId) -> Void, displaySwipeToReplyHint: @escaping () -> Void, dismissReplyMarkupMessage: @escaping (Message) -> Void, openMessagePollResults: @escaping (MessageId, Data) -> Void, openPollCreation: @escaping (Bool?) -> Void, requestMessageUpdate: @escaping (MessageId) -> Void, cancelInteractiveKeyboardGestures: @escaping () -> Void, automaticMediaDownloadSettings: MediaAutoDownloadSettings, pollActionState: ChatInterfacePollActionState, stickerSettings: ChatInterfaceStickerSettings) {
|
||||||
self.openMessage = openMessage
|
self.openMessage = openMessage
|
||||||
|
|||||||
@ -2240,35 +2240,40 @@ class ChatControllerNode: ASDisplayNode, UIScrollViewDelegate {
|
|||||||
|
|
||||||
var messages: [EnqueueMessage] = []
|
var messages: [EnqueueMessage] = []
|
||||||
|
|
||||||
let inputText = convertMarkdownToAttributes(effectivePresentationInterfaceState.interfaceState.composeInputState.inputText)
|
let effectiveInputText = effectivePresentationInterfaceState.interfaceState.composeInputState.inputText
|
||||||
|
if effectiveInputText.string.trimmingCharacters(in: .whitespacesAndNewlines) == "🎲" {
|
||||||
for text in breakChatInputText(trimChatInputText(inputText)) {
|
messages.append(.message(text: "", attributes: [], mediaReference: AnyMediaReference.standalone(media: TelegramMediaDice()), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
|
||||||
if text.length != 0 {
|
} else {
|
||||||
var attributes: [MessageAttribute] = []
|
let inputText = convertMarkdownToAttributes(effectiveInputText)
|
||||||
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
|
||||||
if !entities.isEmpty {
|
for text in breakChatInputText(trimChatInputText(inputText)) {
|
||||||
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
if text.length != 0 {
|
||||||
|
var attributes: [MessageAttribute] = []
|
||||||
|
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
|
||||||
|
if !entities.isEmpty {
|
||||||
|
attributes.append(TextEntitiesMessageAttribute(entities: entities))
|
||||||
|
}
|
||||||
|
var webpage: TelegramMediaWebpage?
|
||||||
|
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
|
||||||
|
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
|
||||||
|
} else {
|
||||||
|
webpage = self.chatPresentationInterfaceState.urlPreview?.1
|
||||||
|
}
|
||||||
|
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
|
||||||
}
|
}
|
||||||
var webpage: TelegramMediaWebpage?
|
|
||||||
if self.chatPresentationInterfaceState.interfaceState.composeDisableUrlPreview != nil {
|
|
||||||
attributes.append(OutgoingContentInfoMessageAttribute(flags: [.disableLinkPreviews]))
|
|
||||||
} else {
|
|
||||||
webpage = self.chatPresentationInterfaceState.urlPreview?.1
|
|
||||||
}
|
|
||||||
messages.append(.message(text: text.string, attributes: attributes, mediaReference: webpage.flatMap(AnyMediaReference.standalone), replyToMessageId: self.chatPresentationInterfaceState.interfaceState.replyMessageId, localGroupingKey: nil))
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
var forwardingToSameChat = false
|
var forwardingToSameChat = false
|
||||||
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
|
if case let .peer(id) = self.chatPresentationInterfaceState.chatLocation, id.namespace == Namespaces.Peer.CloudUser, id != self.context.account.peerId, let forwardMessageIds = self.chatPresentationInterfaceState.interfaceState.forwardMessageIds {
|
||||||
for messageId in forwardMessageIds {
|
for messageId in forwardMessageIds {
|
||||||
if messageId.peerId == id {
|
if messageId.peerId == id {
|
||||||
forwardingToSameChat = true
|
forwardingToSameChat = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
if !messages.isEmpty && forwardingToSameChat {
|
||||||
if !messages.isEmpty && forwardingToSameChat {
|
self.controllerInteraction.displaySwipeToReplyHint()
|
||||||
self.controllerInteraction.displaySwipeToReplyHint()
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
|
if !messages.isEmpty || self.chatPresentationInterfaceState.interfaceState.forwardMessageIds != nil {
|
||||||
|
|||||||
@ -28,7 +28,12 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
|||||||
|
|
||||||
var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = []
|
var groupBucket: [(Message, Bool, ChatHistoryMessageSelection, ChatMessageEntryAttributes)] = []
|
||||||
loop: for entry in view.entries {
|
loop: for entry in view.entries {
|
||||||
|
var contentTypeHint: ChatMessageEntryContentType = .generic
|
||||||
|
|
||||||
for media in entry.message.media {
|
for media in entry.message.media {
|
||||||
|
if media is TelegramMediaDice {
|
||||||
|
contentTypeHint = .animatedEmoji
|
||||||
|
}
|
||||||
if let action = media as? TelegramMediaAction {
|
if let action = media as? TelegramMediaAction {
|
||||||
switch action.action {
|
switch action.action {
|
||||||
case .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared:
|
case .channelMigratedFromGroup, .groupMigratedToChannel, .historyCleared:
|
||||||
@ -44,7 +49,7 @@ func chatHistoryEntriesForView(location: ChatLocation, view: MessageHistoryView,
|
|||||||
adminRank = adminRanks[author.id]
|
adminRank = adminRanks[author.id]
|
||||||
}
|
}
|
||||||
|
|
||||||
var contentTypeHint: ChatMessageEntryContentType = .generic
|
|
||||||
if presentationData.largeEmoji, entry.message.media.isEmpty {
|
if presentationData.largeEmoji, entry.message.media.isEmpty {
|
||||||
if stickersEnabled && entry.message.text.count == 1, let _ = associatedData.animatedEmojiStickers[entry.message.text.basicEmoji.0] {
|
if stickersEnabled && entry.message.text.count == 1, let _ = associatedData.animatedEmojiStickers[entry.message.text.basicEmoji.0] {
|
||||||
contentTypeHint = .animatedEmoji
|
contentTypeHint = .animatedEmoji
|
||||||
|
|||||||
@ -18,375 +18,24 @@ import AnimatedStickerNode
|
|||||||
import TelegramAnimatedStickerNode
|
import TelegramAnimatedStickerNode
|
||||||
import Emoji
|
import Emoji
|
||||||
import Markdown
|
import Markdown
|
||||||
|
import ManagedAnimationNode
|
||||||
import RLottieBinding
|
|
||||||
import AppBundle
|
|
||||||
import GZip
|
|
||||||
|
|
||||||
private let nameFont = Font.medium(14.0)
|
private let nameFont = Font.medium(14.0)
|
||||||
private let inlineBotPrefixFont = Font.regular(14.0)
|
private let inlineBotPrefixFont = Font.regular(14.0)
|
||||||
private let inlineBotNameFont = nameFont
|
private let inlineBotNameFont = nameFont
|
||||||
|
|
||||||
private final class ManagedAnimationState {
|
protocol GenericAnimatedStickerNode: ASDisplayNode {
|
||||||
let item: ManagedAnimationItem
|
|
||||||
|
|
||||||
private let instance: LottieInstance
|
|
||||||
|
|
||||||
let frameCount: Int
|
|
||||||
let fps: Double
|
|
||||||
|
|
||||||
var relativeTime: Double = 0.0
|
|
||||||
var frameIndex: Int?
|
|
||||||
|
|
||||||
private let renderContext: DrawingContext
|
|
||||||
|
|
||||||
init?(displaySize: CGSize, item: ManagedAnimationItem, current: ManagedAnimationState?) {
|
|
||||||
let resolvedInstance: LottieInstance
|
|
||||||
let renderContext: DrawingContext
|
|
||||||
|
|
||||||
if let current = current {
|
|
||||||
resolvedInstance = current.instance
|
|
||||||
renderContext = current.renderContext
|
|
||||||
} else {
|
|
||||||
guard let path = item.source.path else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let unpackedData = TGGUnzipData(data, 5 * 1024 * 1024) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
guard let instance = LottieInstance(data: unpackedData, cacheKey: item.source.cacheKey) else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resolvedInstance = instance
|
|
||||||
renderContext = DrawingContext(size: displaySize, scale: UIScreenScale, premultiplied: true, clear: true)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.item = item
|
|
||||||
self.instance = resolvedInstance
|
|
||||||
self.renderContext = renderContext
|
|
||||||
|
|
||||||
self.frameCount = Int(self.instance.frameCount)
|
|
||||||
self.fps = Double(self.instance.frameRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
func draw() -> UIImage? {
|
|
||||||
self.instance.renderFrame(with: Int32(self.frameIndex ?? 0), into: self.renderContext.bytes.assumingMemoryBound(to: UInt8.self), width: Int32(self.renderContext.size.width * self.renderContext.scale), height: Int32(self.renderContext.size.height * self.renderContext.scale), bytesPerRow: Int32(self.renderContext.bytesPerRow))
|
|
||||||
return self.renderContext.generateImage()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ManagedAnimationFrameRange: Equatable {
|
extension AnimatedStickerNode: GenericAnimatedStickerNode {
|
||||||
var startFrame: Int
|
|
||||||
var endFrame: Int
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ManagedAnimationSource: Equatable {
|
|
||||||
case local(String)
|
|
||||||
case resource(MediaBox, MediaResource)
|
|
||||||
|
|
||||||
var cacheKey: String {
|
|
||||||
switch self {
|
|
||||||
case let .local(name):
|
|
||||||
return name
|
|
||||||
case let .resource(mediaBox, resource):
|
|
||||||
return resource.id.uniqueId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var path: String? {
|
|
||||||
switch self {
|
|
||||||
case let .local(name):
|
|
||||||
return getAppBundle().path(forResource: name, ofType: "tgs")
|
|
||||||
case let .resource(mediaBox, resource):
|
|
||||||
return mediaBox.completedResourcePath(resource)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static func == (lhs: ManagedAnimationSource, rhs: ManagedAnimationSource) -> Bool {
|
|
||||||
switch lhs {
|
|
||||||
case let .local(lhsPath):
|
|
||||||
if case let .local(rhsPath) = rhs, lhsPath == rhsPath {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
case let .resource(lhsMediaBox, lhsResource):
|
|
||||||
if case let .resource(rhsMediaBox, rhsResource) = rhs, lhsMediaBox === rhsMediaBox, lhsResource.isEqual(to: rhsResource) {
|
|
||||||
return true
|
|
||||||
} else {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ManagedAnimationItem: Equatable {
|
|
||||||
let source: ManagedAnimationSource
|
|
||||||
var frames: ManagedAnimationFrameRange
|
|
||||||
var duration: Double
|
|
||||||
}
|
|
||||||
|
|
||||||
class ManagedAnimationNode: ASDisplayNode {
|
|
||||||
let intrinsicSize: CGSize
|
|
||||||
|
|
||||||
private let imageNode: ASImageNode
|
|
||||||
private let displayLink: CADisplayLink
|
|
||||||
|
|
||||||
fileprivate var state: ManagedAnimationState?
|
|
||||||
fileprivate var trackStack: [ManagedAnimationItem] = []
|
|
||||||
fileprivate var didTryAdvancingState = false
|
|
||||||
|
|
||||||
init(size: CGSize) {
|
|
||||||
self.intrinsicSize = size
|
|
||||||
|
|
||||||
self.imageNode = ASImageNode()
|
|
||||||
self.imageNode.displayWithoutProcessing = true
|
|
||||||
self.imageNode.displaysAsynchronously = false
|
|
||||||
self.imageNode.frame = CGRect(origin: CGPoint(), size: self.intrinsicSize)
|
|
||||||
|
|
||||||
final class DisplayLinkTarget: NSObject {
|
|
||||||
private let f: () -> Void
|
|
||||||
|
|
||||||
init(_ f: @escaping () -> Void) {
|
|
||||||
self.f = f
|
|
||||||
}
|
|
||||||
|
|
||||||
@objc func event() {
|
|
||||||
self.f()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var displayLinkUpdate: (() -> Void)?
|
|
||||||
self.displayLink = CADisplayLink(target: DisplayLinkTarget {
|
|
||||||
displayLinkUpdate?()
|
|
||||||
}, selector: #selector(DisplayLinkTarget.event))
|
|
||||||
|
|
||||||
super.init()
|
|
||||||
|
|
||||||
self.addSubnode(self.imageNode)
|
|
||||||
|
|
||||||
self.displayLink.add(to: RunLoop.main, forMode: .common)
|
|
||||||
|
|
||||||
displayLinkUpdate = { [weak self] in
|
|
||||||
self?.updateAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func advanceState() {
|
|
||||||
guard !self.trackStack.isEmpty else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let item = self.trackStack.removeFirst()
|
|
||||||
|
|
||||||
if let state = self.state, state.item.source == item.source {
|
|
||||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: state)
|
|
||||||
} else {
|
|
||||||
self.state = ManagedAnimationState(displaySize: self.intrinsicSize, item: item, current: nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
self.didTryAdvancingState = false
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate func updateAnimation() {
|
|
||||||
if self.state == nil {
|
|
||||||
self.advanceState()
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let state = self.state else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let timestamp = CACurrentMediaTime()
|
|
||||||
|
|
||||||
let fps = state.fps
|
|
||||||
let frameRange = state.item.frames
|
|
||||||
|
|
||||||
let duration: Double = state.item.duration
|
|
||||||
var t = state.relativeTime / duration
|
|
||||||
t = max(0.0, t)
|
|
||||||
t = min(1.0, t)
|
|
||||||
|
|
||||||
let frameOffset = Int(Double(frameRange.startFrame) * (1.0 - t) + Double(frameRange.endFrame) * t)
|
|
||||||
let lowerBound: Int = 0
|
|
||||||
let upperBound = state.frameCount - 1
|
|
||||||
let frameIndex = max(lowerBound, min(upperBound, frameOffset))
|
|
||||||
|
|
||||||
if state.frameIndex != frameIndex {
|
|
||||||
state.frameIndex = frameIndex
|
|
||||||
if let image = state.draw() {
|
|
||||||
self.imageNode.image = image
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var animationAdvancement: Double = 1.0 / 60.0
|
|
||||||
animationAdvancement *= Double(min(2, self.trackStack.count + 1))
|
|
||||||
|
|
||||||
state.relativeTime += animationAdvancement
|
|
||||||
|
|
||||||
if state.relativeTime >= duration && !self.didTryAdvancingState {
|
|
||||||
self.didTryAdvancingState = true
|
|
||||||
self.advanceState()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func trackTo(item: ManagedAnimationItem) {
|
|
||||||
self.trackStack.append(item)
|
|
||||||
self.didTryAdvancingState = false
|
|
||||||
self.updateAnimation()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum ManagedDiceAnimationState: Equatable {
|
|
||||||
case rolling
|
|
||||||
case value(Int)
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ManagedDiceAnimationNode: ManagedAnimationNode {
|
|
||||||
private let context: AccountContext
|
|
||||||
private let emojis: [TelegramMediaFile]
|
|
||||||
|
|
||||||
private var diceState: ManagedDiceAnimationState = .rolling
|
|
||||||
private let disposable = MetaDisposable()
|
|
||||||
|
|
||||||
init(context: AccountContext, emojis: [TelegramMediaFile]) {
|
|
||||||
self.context = context
|
|
||||||
self.emojis = emojis
|
|
||||||
|
|
||||||
super.init(size: CGSize(width: 136.0, height: 136.0))
|
|
||||||
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
|
||||||
}
|
|
||||||
|
|
||||||
deinit {
|
|
||||||
self.disposable.dispose()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setState(_ diceState: ManagedDiceAnimationState) {
|
|
||||||
let previousState = self.diceState
|
|
||||||
self.diceState = diceState
|
|
||||||
|
|
||||||
switch previousState {
|
|
||||||
case .rolling:
|
|
||||||
switch diceState {
|
|
||||||
case let .value(value):
|
|
||||||
// self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .standalone(media: emojiFile)).start())
|
|
||||||
//
|
|
||||||
// return chatMessageAnimationData(postbox: self.account.postbox, resource: self.resource, fitzModifier: self.fitzModifier, width: width, height: height, synchronousLoad: false)
|
|
||||||
// |> filter { data in
|
|
||||||
// return data.size != 0
|
|
||||||
// }
|
|
||||||
// |> map { data -> (String, Bool) in
|
|
||||||
// return (data.path, data.complete)
|
|
||||||
// }
|
|
||||||
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
|
||||||
case .rolling:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
case let .value(currentValue):
|
|
||||||
switch diceState {
|
|
||||||
case .rolling:
|
|
||||||
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
|
||||||
case let .value(value):
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private class ChatMessageHeartbeatHaptic {
|
|
||||||
private var hapticFeedback = HapticFeedback()
|
|
||||||
private var timer: SwiftSignalKit.Timer?
|
|
||||||
private var time: Double = 0.0
|
|
||||||
var enabled = false {
|
|
||||||
didSet {
|
|
||||||
if !self.enabled {
|
|
||||||
self.reset()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var active: Bool {
|
|
||||||
return self.timer != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
private func reset() {
|
|
||||||
if let timer = self.timer {
|
|
||||||
self.time = 0.0
|
|
||||||
timer.invalidate()
|
|
||||||
self.timer = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func beat(time: Double) {
|
|
||||||
let epsilon = 0.1
|
|
||||||
if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon {
|
|
||||||
self.hapticFeedback.impact(.medium)
|
|
||||||
} else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon {
|
|
||||||
self.hapticFeedback.impact(.light)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func start(time: Double) {
|
|
||||||
self.hapticFeedback.prepareImpact()
|
|
||||||
|
|
||||||
if time > 2.0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var startTime: Double = 0.0
|
|
||||||
var delay: Double = 0.0
|
|
||||||
|
|
||||||
if time > 0.0 {
|
|
||||||
if time <= 1.0 {
|
|
||||||
startTime = 1.0
|
|
||||||
} else if time <= 2.0 {
|
|
||||||
startTime = 2.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
delay = max(0.0, startTime - time)
|
|
||||||
|
|
||||||
let block = { [weak self] in
|
|
||||||
guard let strongSelf = self, strongSelf.enabled else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
strongSelf.time = startTime
|
|
||||||
strongSelf.beat(time: startTime)
|
|
||||||
strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
|
|
||||||
guard let strongSelf = self, strongSelf.enabled else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
strongSelf.time += 0.2
|
|
||||||
strongSelf.beat(time: strongSelf.time)
|
|
||||||
|
|
||||||
if strongSelf.time > 2.2 {
|
|
||||||
strongSelf.reset()
|
|
||||||
strongSelf.time = 0.0
|
|
||||||
strongSelf.timer?.invalidate()
|
|
||||||
strongSelf.timer = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
}, queue: Queue.mainQueue())
|
|
||||||
strongSelf.timer?.start()
|
|
||||||
}
|
|
||||||
|
|
||||||
if delay > 0.0 {
|
|
||||||
Queue.mainQueue().after(delay, block)
|
|
||||||
} else {
|
|
||||||
block()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
||||||
private let contextSourceNode: ContextExtractedContentContainingNode
|
private let contextSourceNode: ContextExtractedContentContainingNode
|
||||||
let imageNode: TransformImageNode
|
let imageNode: TransformImageNode
|
||||||
private let animationNode: AnimatedStickerNode
|
private var animationNode: GenericAnimatedStickerNode?
|
||||||
private var didSetUpAnimationNode = false
|
private var didSetUpAnimationNode = false
|
||||||
private var isPlaying = false
|
private var isPlaying = false
|
||||||
|
|
||||||
@ -399,6 +48,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
var telegramFile: TelegramMediaFile?
|
var telegramFile: TelegramMediaFile?
|
||||||
var emojiFile: TelegramMediaFile?
|
var emojiFile: TelegramMediaFile?
|
||||||
|
var telegramDice: TelegramMediaDice?
|
||||||
private let disposable = MetaDisposable()
|
private let disposable = MetaDisposable()
|
||||||
|
|
||||||
private var viaBotNode: TextNode?
|
private var viaBotNode: TextNode?
|
||||||
@ -410,34 +60,20 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
|
|
||||||
private var highlightedState: Bool = false
|
private var highlightedState: Bool = false
|
||||||
|
|
||||||
private var heartbeatHaptic: ChatMessageHeartbeatHaptic?
|
private var heartbeatHaptic: HeartbeatHaptic?
|
||||||
|
|
||||||
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
private var currentSwipeToReplyTranslation: CGFloat = 0.0
|
||||||
|
|
||||||
required init() {
|
required init() {
|
||||||
self.contextSourceNode = ContextExtractedContentContainingNode()
|
self.contextSourceNode = ContextExtractedContentContainingNode()
|
||||||
self.imageNode = TransformImageNode()
|
self.imageNode = TransformImageNode()
|
||||||
self.animationNode = AnimatedStickerNode()
|
|
||||||
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
self.dateAndStatusNode = ChatMessageDateAndStatusNode()
|
||||||
|
|
||||||
super.init(layerBacked: false)
|
super.init(layerBacked: false)
|
||||||
|
|
||||||
self.animationNode.started = { [weak self] in
|
|
||||||
if let strongSelf = self {
|
|
||||||
strongSelf.imageNode.alpha = 0.0
|
|
||||||
|
|
||||||
if let item = strongSelf.item {
|
|
||||||
if let _ = strongSelf.emojiFile {
|
|
||||||
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.imageNode.displaysAsynchronously = false
|
self.imageNode.displaysAsynchronously = false
|
||||||
self.addSubnode(self.contextSourceNode)
|
self.addSubnode(self.contextSourceNode)
|
||||||
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
self.contextSourceNode.contentNode.addSubnode(self.imageNode)
|
||||||
self.contextSourceNode.contentNode.addSubnode(self.animationNode)
|
|
||||||
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
self.contextSourceNode.contentNode.addSubnode(self.dateAndStatusNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -460,7 +96,7 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if strongSelf.telegramFile == nil {
|
if strongSelf.telegramFile == nil {
|
||||||
if strongSelf.animationNode.frame.contains(point) {
|
if let animationNode = strongSelf.animationNode, animationNode.frame.contains(point) {
|
||||||
return .waitForDoubleTap
|
return .waitForDoubleTap
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -511,9 +147,36 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setupNode(item: ChatMessageItem) {
|
||||||
|
var isDice = false
|
||||||
|
|
||||||
|
if let telegramDice = self.telegramDice, let diceEmojis = item.associatedData.animatedEmojiStickers["🎲"] {
|
||||||
|
let animationNode = ManagedDiceAnimationNode(context: item.context, emojis: diceEmojis.map { $0.file })
|
||||||
|
self.animationNode = animationNode
|
||||||
|
} else {
|
||||||
|
let animationNode = AnimatedStickerNode()
|
||||||
|
animationNode.started = { [weak self] in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.imageNode.alpha = 0.0
|
||||||
|
|
||||||
|
if let item = strongSelf.item {
|
||||||
|
if let _ = strongSelf.emojiFile {
|
||||||
|
item.controllerInteraction.seenOneTimeAnimatedMedia.insert(item.message.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.animationNode = animationNode
|
||||||
|
}
|
||||||
|
|
||||||
|
if let animationNode = self.animationNode {
|
||||||
|
self.contextSourceNode.contentNode.insertSubnode(animationNode, aboveSubnode: self.imageNode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override func setupItem(_ item: ChatMessageItem) {
|
override func setupItem(_ item: ChatMessageItem) {
|
||||||
super.setupItem(item)
|
super.setupItem(item)
|
||||||
|
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
if let telegramFile = media as? TelegramMediaFile {
|
if let telegramFile = media as? TelegramMediaFile {
|
||||||
if self.telegramFile?.id != telegramFile.id {
|
if self.telegramFile?.id != telegramFile.id {
|
||||||
@ -524,29 +187,26 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start())
|
self.disposable.set(freeMediaFileInteractiveFetched(account: item.context.account, fileReference: .message(message: MessageReference(item.message), media: telegramFile)).start())
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
} else if let telegramDice = media as? TelegramMediaDice {
|
||||||
|
self.telegramDice = telegramDice
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.setupNode(item: item)
|
||||||
|
|
||||||
let (emoji, fitz) = item.message.text.basicEmoji
|
if let telegramDice = self.telegramDice, let diceNode = self.animationNode as? ManagedDiceAnimationNode {
|
||||||
if self.telegramFile == nil {
|
if let value = telegramDice.value {
|
||||||
|
diceNode.setState(value == 0 ? .rolling : .value(value))
|
||||||
|
} else {
|
||||||
|
diceNode.setState(.rolling)
|
||||||
|
}
|
||||||
|
} else if self.telegramFile == nil {
|
||||||
|
let (emoji, fitz) = item.message.text.basicEmoji
|
||||||
var emojiFile: TelegramMediaFile?
|
var emojiFile: TelegramMediaFile?
|
||||||
|
|
||||||
if false && emoji == "🎲" {
|
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
|
||||||
var pointsValue: Int
|
if emojiFile == nil {
|
||||||
if let value = item.controllerInteraction.seenDicePointsValue[item.message.id] {
|
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
|
||||||
pointsValue = value
|
|
||||||
} else {
|
|
||||||
pointsValue = Int(arc4random_uniform(6))
|
|
||||||
item.controllerInteraction.seenDicePointsValue[item.message.id] = pointsValue
|
|
||||||
}
|
|
||||||
if let diceEmojis = item.associatedData.animatedEmojiStickers[emoji] {
|
|
||||||
emojiFile = diceEmojis[pointsValue].file
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji]?.first?.file
|
|
||||||
if emojiFile == nil {
|
|
||||||
emojiFile = item.associatedData.animatedEmojiStickers[emoji.strippedEmoji]?.first?.file
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.emojiFile?.id != emojiFile?.id {
|
if self.emojiFile?.id != emojiFile?.id {
|
||||||
@ -570,65 +230,69 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let isPlaying = self.visibilityStatus
|
if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||||
if self.isPlaying != isPlaying {
|
let isPlaying = self.visibilityStatus
|
||||||
self.isPlaying = isPlaying
|
if self.isPlaying != isPlaying {
|
||||||
|
self.isPlaying = isPlaying
|
||||||
var alreadySeen = false
|
|
||||||
if isPlaying, let _ = self.emojiFile {
|
|
||||||
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
|
||||||
alreadySeen = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.animationNode.visibility = isPlaying && !alreadySeen
|
|
||||||
|
|
||||||
if self.didSetUpAnimationNode && alreadySeen {
|
|
||||||
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
|
|
||||||
} else {
|
|
||||||
self.animationNode.seekTo(.start)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.isPlaying && !self.didSetUpAnimationNode {
|
|
||||||
self.didSetUpAnimationNode = true
|
|
||||||
|
|
||||||
var file: TelegramMediaFile?
|
var alreadySeen = false
|
||||||
var playbackMode: AnimatedStickerPlaybackMode = .loop
|
if isPlaying, let _ = self.emojiFile {
|
||||||
var isEmoji = false
|
if item.controllerInteraction.seenOneTimeAnimatedMedia.contains(item.message.id) {
|
||||||
var fitzModifier: EmojiFitzModifier?
|
alreadySeen = true
|
||||||
|
|
||||||
if let telegramFile = self.telegramFile {
|
|
||||||
file = telegramFile
|
|
||||||
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
|
|
||||||
playbackMode = .once
|
|
||||||
}
|
|
||||||
} else if let emojiFile = self.emojiFile {
|
|
||||||
isEmoji = true
|
|
||||||
file = emojiFile
|
|
||||||
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
|
|
||||||
playbackMode = .still(.end)
|
|
||||||
} else {
|
|
||||||
playbackMode = .once
|
|
||||||
}
|
|
||||||
let (_, fitz) = item.message.text.basicEmoji
|
|
||||||
if let fitz = fitz {
|
|
||||||
fitzModifier = EmojiFitzModifier(emoji: fitz)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let file = file {
|
animationNode.visibility = isPlaying && !alreadySeen
|
||||||
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
|
||||||
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
if self.didSetUpAnimationNode && alreadySeen {
|
||||||
let mode: AnimatedStickerMode
|
if let emojiFile = self.emojiFile, emojiFile.resource is LocalFileReferenceMediaResource {
|
||||||
if file.resource is LocalFileReferenceMediaResource {
|
|
||||||
mode = .direct
|
|
||||||
} else {
|
} else {
|
||||||
mode = .cached
|
animationNode.seekTo(.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.isPlaying && !self.didSetUpAnimationNode {
|
||||||
|
self.didSetUpAnimationNode = true
|
||||||
|
|
||||||
|
var file: TelegramMediaFile?
|
||||||
|
var playbackMode: AnimatedStickerPlaybackMode = .loop
|
||||||
|
var isEmoji = false
|
||||||
|
var fitzModifier: EmojiFitzModifier?
|
||||||
|
|
||||||
|
if let telegramFile = self.telegramFile {
|
||||||
|
file = telegramFile
|
||||||
|
if !item.controllerInteraction.stickerSettings.loopAnimatedStickers {
|
||||||
|
playbackMode = .once
|
||||||
|
}
|
||||||
|
} else if let emojiFile = self.emojiFile {
|
||||||
|
isEmoji = true
|
||||||
|
file = emojiFile
|
||||||
|
if alreadySeen && emojiFile.resource is LocalFileReferenceMediaResource {
|
||||||
|
playbackMode = .still(.end)
|
||||||
|
} else {
|
||||||
|
playbackMode = .once
|
||||||
|
}
|
||||||
|
let (_, fitz) = item.message.text.basicEmoji
|
||||||
|
if let fitz = fitz {
|
||||||
|
fitzModifier = EmojiFitzModifier(emoji: fitz)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let file = file {
|
||||||
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
|
let fittedSize = isEmoji ? dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0)) : dimensions.cgSize.aspectFitted(CGSize(width: 384.0, height: 384.0))
|
||||||
|
let mode: AnimatedStickerMode
|
||||||
|
if file.resource is LocalFileReferenceMediaResource {
|
||||||
|
mode = .direct
|
||||||
|
} else {
|
||||||
|
mode = .cached
|
||||||
|
}
|
||||||
|
animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
||||||
}
|
}
|
||||||
self.animationNode.setup(source: AnimatedStickerResourceSource(account: item.context.account, resource: file.resource, fitzModifier: fitzModifier), width: Int(fittedSize.width), height: Int(fittedSize.height), playbackMode: playbackMode, mode: mode)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if let animationNode = self.animationNode as? ManagedDiceAnimationNode {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -931,8 +595,10 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
strongSelf.imageNode.frame = updatedContentFrame
|
strongSelf.imageNode.frame = updatedContentFrame
|
||||||
strongSelf.animationNode.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
|
strongSelf.animationNode?.frame = updatedContentFrame.insetBy(dx: imageInset, dy: imageInset)
|
||||||
strongSelf.animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
|
if let animationNode = strongSelf.animationNode as? AnimatedStickerNode {
|
||||||
|
animationNode.updateLayout(size: updatedContentFrame.insetBy(dx: imageInset, dy: imageInset).size)
|
||||||
|
}
|
||||||
imageApply()
|
imageApply()
|
||||||
|
|
||||||
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
strongSelf.contextSourceNode.contentRect = strongSelf.imageNode.frame
|
||||||
@ -1175,13 +841,13 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
} else if let _ = self.emojiFile {
|
} else if let _ = self.emojiFile {
|
||||||
let (emoji, fitz) = item.message.text.basicEmoji
|
let (emoji, fitz) = item.message.text.basicEmoji
|
||||||
if emoji == "🎲" {
|
if emoji == "🎲" {
|
||||||
|
|
||||||
} else {
|
} else if let animationNode = self.animationNode as? AnimatedStickerNode {
|
||||||
var startTime: Signal<Double, NoError>
|
var startTime: Signal<Double, NoError>
|
||||||
if self.animationNode.playIfNeeded() {
|
if animationNode.playIfNeeded() {
|
||||||
startTime = .single(0.0)
|
startTime = .single(0.0)
|
||||||
} else {
|
} else {
|
||||||
startTime = self.animationNode.status
|
startTime = animationNode.status
|
||||||
|> map { $0.timestamp }
|
|> map { $0.timestamp }
|
||||||
|> take(1)
|
|> take(1)
|
||||||
|> deliverOnMainQueue
|
|> deliverOnMainQueue
|
||||||
@ -1195,11 +861,11 @@ class ChatMessageAnimatedStickerItemNode: ChatMessageItemView {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let heartbeatHaptic: ChatMessageHeartbeatHaptic
|
let heartbeatHaptic: HeartbeatHaptic
|
||||||
if let current = strongSelf.heartbeatHaptic {
|
if let current = strongSelf.heartbeatHaptic {
|
||||||
heartbeatHaptic = current
|
heartbeatHaptic = current
|
||||||
} else {
|
} else {
|
||||||
heartbeatHaptic = ChatMessageHeartbeatHaptic()
|
heartbeatHaptic = HeartbeatHaptic()
|
||||||
heartbeatHaptic.enabled = true
|
heartbeatHaptic.enabled = true
|
||||||
strongSelf.heartbeatHaptic = heartbeatHaptic
|
strongSelf.heartbeatHaptic = heartbeatHaptic
|
||||||
}
|
}
|
||||||
|
|||||||
@ -201,7 +201,7 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView {
|
|||||||
deliveryFailedInset += 24.0
|
deliveryFailedInset += 24.0
|
||||||
}
|
}
|
||||||
|
|
||||||
let displaySize = CGSize(width: 212.0, height: 212.0)
|
let displaySize = layoutConstants.instantVideo.dimensions
|
||||||
|
|
||||||
var automaticDownload = true
|
var automaticDownload = true
|
||||||
for media in item.message.media {
|
for media in item.message.media {
|
||||||
|
|||||||
@ -406,10 +406,12 @@ public final class ChatMessageItem: ListViewItem, CustomStringConvertible {
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let _ = media as? TelegramMediaAction {
|
} else if media is TelegramMediaAction {
|
||||||
viewClassName = ChatMessageBubbleItemNode.self
|
viewClassName = ChatMessageBubbleItemNode.self
|
||||||
} else if let _ = media as? TelegramMediaExpiredContent {
|
} else if media is TelegramMediaExpiredContent {
|
||||||
viewClassName = ChatMessageBubbleItemNode.self
|
viewClassName = ChatMessageBubbleItemNode.self
|
||||||
|
} else if media is TelegramMediaDice {
|
||||||
|
viewClassName = ChatMessageAnimatedStickerItemNode.self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -101,7 +101,7 @@ struct ChatMessageItemLayoutConstants {
|
|||||||
let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0))
|
let image = ChatMessageItemImageLayoutConstants(bubbleInsets: UIEdgeInsets(top: 2.0, left: 2.0, bottom: 2.0, right: 2.0), statusInsets: UIEdgeInsets(top: 0.0, left: 0.0, bottom: 6.0, right: 6.0), defaultCornerRadius: 16.0, mergedCornerRadius: 8.0, contentMergedCornerRadius: 5.0, maxDimensions: CGSize(width: 440.0, height: 440.0), minDimensions: CGSize(width: 170.0, height: 74.0))
|
||||||
let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
|
let video = ChatMessageItemVideoLayoutConstants(maxHorizontalHeight: 250.0, maxVerticalHeight: 360.0)
|
||||||
let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
|
let file = ChatMessageItemFileLayoutConstants(bubbleInsets: UIEdgeInsets(top: 15.0, left: 9.0, bottom: 15.0, right: 12.0))
|
||||||
let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 212.0, height: 212.0))
|
let instantVideo = ChatMessageItemInstantVideoConstants(insets: UIEdgeInsets(top: 4.0, left: 0.0, bottom: 4.0, right: 0.0), dimensions: CGSize(width: 240.0, height: 240.0))
|
||||||
let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0)
|
let wallpapers = ChatMessageItemWallpaperLayoutConstants(maxTextWidth: 180.0)
|
||||||
|
|
||||||
return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers)
|
return ChatMessageItemLayoutConstants(avatarDiameter: 37.0, timestampHeaderHeight: 34.0, bubble: bubble, image: image, video: video, text: text, file: file, instantVideo: instantVideo, wallpapers: wallpapers)
|
||||||
@ -125,6 +125,7 @@ func chatMessageItemLayoutConstants(_ constants: (ChatMessageItemLayoutConstants
|
|||||||
let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition)))
|
let textInset: CGFloat = min(maxInset, ceil(maxInset * radiusTransition + minInset * (1.0 - radiusTransition)))
|
||||||
result.text.bubbleInsets.left = textInset
|
result.text.bubbleInsets.left = textInset
|
||||||
result.text.bubbleInsets.right = textInset
|
result.text.bubbleInsets.right = textInset
|
||||||
|
result.instantVideo.dimensions = min(params.width, params.availableHeight) > 320.0 ? constants.1.instantVideo.dimensions : constants.0.instantVideo.dimensions
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,23 +13,6 @@ import StickerResources
|
|||||||
import PhotoResources
|
import PhotoResources
|
||||||
import TelegramStringFormatting
|
import TelegramStringFormatting
|
||||||
|
|
||||||
private func foldLineBreaks(_ text: String) -> String {
|
|
||||||
var lines = text.split { $0.isNewline }
|
|
||||||
var startedBothLines = false
|
|
||||||
var result = ""
|
|
||||||
for line in lines {
|
|
||||||
if line.isEmpty {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if result.isEmpty {
|
|
||||||
result += line
|
|
||||||
} else {
|
|
||||||
result += " " + line
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
final class ChatPinnedMessageTitlePanelNode: ChatTitleAccessoryPanelNode {
|
||||||
private let context: AccountContext
|
private let context: AccountContext
|
||||||
private let tapButton: HighlightTrackingButtonNode
|
private let tapButton: HighlightTrackingButtonNode
|
||||||
|
|||||||
89
submodules/TelegramUI/Sources/HeartbeatHaptic.swift
Normal file
89
submodules/TelegramUI/Sources/HeartbeatHaptic.swift
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import Foundation
|
||||||
|
import Display
|
||||||
|
import SwiftSignalKit
|
||||||
|
|
||||||
|
final class HeartbeatHaptic {
|
||||||
|
private var hapticFeedback = HapticFeedback()
|
||||||
|
private var timer: SwiftSignalKit.Timer?
|
||||||
|
private var time: Double = 0.0
|
||||||
|
var enabled = false {
|
||||||
|
didSet {
|
||||||
|
if !self.enabled {
|
||||||
|
self.reset()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var active: Bool {
|
||||||
|
return self.timer != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func reset() {
|
||||||
|
if let timer = self.timer {
|
||||||
|
self.time = 0.0
|
||||||
|
timer.invalidate()
|
||||||
|
self.timer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func beat(time: Double) {
|
||||||
|
let epsilon = 0.1
|
||||||
|
if fabs(0.0 - time) < epsilon || fabs(1.0 - time) < epsilon || fabs(2.0 - time) < epsilon {
|
||||||
|
self.hapticFeedback.impact(.medium)
|
||||||
|
} else if fabs(0.2 - time) < epsilon || fabs(1.2 - time) < epsilon || fabs(2.2 - time) < epsilon {
|
||||||
|
self.hapticFeedback.impact(.light)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func start(time: Double) {
|
||||||
|
self.hapticFeedback.prepareImpact()
|
||||||
|
|
||||||
|
if time > 2.0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var startTime: Double = 0.0
|
||||||
|
var delay: Double = 0.0
|
||||||
|
|
||||||
|
if time > 0.0 {
|
||||||
|
if time <= 1.0 {
|
||||||
|
startTime = 1.0
|
||||||
|
} else if time <= 2.0 {
|
||||||
|
startTime = 2.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delay = max(0.0, startTime - time)
|
||||||
|
|
||||||
|
let block = { [weak self] in
|
||||||
|
guard let strongSelf = self, strongSelf.enabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
strongSelf.time = startTime
|
||||||
|
strongSelf.beat(time: startTime)
|
||||||
|
strongSelf.timer = SwiftSignalKit.Timer(timeout: 0.2, repeat: true, completion: { [weak self] in
|
||||||
|
guard let strongSelf = self, strongSelf.enabled else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
strongSelf.time += 0.2
|
||||||
|
strongSelf.beat(time: strongSelf.time)
|
||||||
|
|
||||||
|
if strongSelf.time > 2.2 {
|
||||||
|
strongSelf.reset()
|
||||||
|
strongSelf.time = 0.0
|
||||||
|
strongSelf.timer?.invalidate()
|
||||||
|
strongSelf.timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
}, queue: Queue.mainQueue())
|
||||||
|
strongSelf.timer?.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
if delay > 0.0 {
|
||||||
|
Queue.mainQueue().after(delay, block)
|
||||||
|
} else {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
114
submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift
Normal file
114
submodules/TelegramUI/Sources/ManagedDiceAnimationNode.swift
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import Foundation
|
||||||
|
import Display
|
||||||
|
import AsyncDisplayKit
|
||||||
|
import SyncCore
|
||||||
|
import TelegramCore
|
||||||
|
import SwiftSignalKit
|
||||||
|
import AccountContext
|
||||||
|
import StickerResources
|
||||||
|
import ManagedAnimationNode
|
||||||
|
|
||||||
|
enum ManagedDiceAnimationState: Equatable {
|
||||||
|
case rolling
|
||||||
|
case value(Int32)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class ManagedDiceAnimationNode: ManagedAnimationNode, GenericAnimatedStickerNode {
|
||||||
|
private let context: AccountContext
|
||||||
|
private let emojis: [TelegramMediaFile]
|
||||||
|
|
||||||
|
private var diceState: ManagedDiceAnimationState? = nil
|
||||||
|
private let disposable = MetaDisposable()
|
||||||
|
|
||||||
|
init(context: AccountContext, emojis: [TelegramMediaFile]) {
|
||||||
|
self.context = context
|
||||||
|
self.emojis = emojis
|
||||||
|
|
||||||
|
super.init(size: CGSize(width: 136.0, height: 136.0))
|
||||||
|
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
self.disposable.dispose()
|
||||||
|
}
|
||||||
|
|
||||||
|
func setState(_ diceState: ManagedDiceAnimationState) {
|
||||||
|
let previousState = self.diceState
|
||||||
|
self.diceState = diceState
|
||||||
|
|
||||||
|
if let previousState = previousState {
|
||||||
|
switch previousState {
|
||||||
|
case .rolling:
|
||||||
|
switch diceState {
|
||||||
|
case let .value(value):
|
||||||
|
let file = self.emojis[Int(value) - 1]
|
||||||
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
|
let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0))
|
||||||
|
|
||||||
|
let fetched = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file))
|
||||||
|
let sticker = Signal<Void, NoError> { subscriber in
|
||||||
|
let fetchedDisposable = fetched.start()
|
||||||
|
let resourceDisposable = (chatMessageAnimationData(postbox: self.context.account.postbox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false)
|
||||||
|
|> filter { data in
|
||||||
|
return data.complete
|
||||||
|
}).start(next: { next in
|
||||||
|
subscriber.putNext(Void())
|
||||||
|
})
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
fetchedDisposable.dispose()
|
||||||
|
resourceDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.disposable.set(sticker.start(next: { [weak self] data in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.trackTo(item: ManagedAnimationItem(source: .resource(strongSelf.context.account.postbox.mediaBox, file.resource), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
case .rolling:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case let .value(currentValue):
|
||||||
|
switch diceState {
|
||||||
|
case .rolling:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
case let .value(value):
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch diceState {
|
||||||
|
case let .value(value):
|
||||||
|
let file = self.emojis[Int(value) - 1]
|
||||||
|
let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512)
|
||||||
|
let fittedSize = dimensions.cgSize.aspectFilled(CGSize(width: 384.0, height: 384.0))
|
||||||
|
|
||||||
|
let fetched = freeMediaFileInteractiveFetched(account: self.context.account, fileReference: .standalone(media: file))
|
||||||
|
let sticker = Signal<Void, NoError> { subscriber in
|
||||||
|
let fetchedDisposable = fetched.start()
|
||||||
|
let resourceDisposable = (chatMessageAnimationData(postbox: self.context.account.postbox, resource: file.resource, fitzModifier: nil, width: Int(fittedSize.width), height: Int(fittedSize.height), synchronousLoad: false)
|
||||||
|
|> filter { data in
|
||||||
|
return data.complete
|
||||||
|
}).start(next: { next in
|
||||||
|
subscriber.putNext(Void())
|
||||||
|
})
|
||||||
|
|
||||||
|
return ActionDisposable {
|
||||||
|
fetchedDisposable.dispose()
|
||||||
|
resourceDisposable.dispose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.disposable.set(sticker.start(next: { [weak self] data in
|
||||||
|
if let strongSelf = self {
|
||||||
|
strongSelf.trackTo(item: ManagedAnimationItem(source: .resource(strongSelf.context.account.postbox.mediaBox, file.resource), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
case .rolling:
|
||||||
|
self.trackTo(item: ManagedAnimationItem(source: .local("DiceRolling"), frames: ManagedAnimationFrameRange(startFrame: 0, endFrame: 0), duration: 0.3))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,7 +34,6 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
|
|||||||
|
|
||||||
self.contentContainerNode = ASDisplayNode()
|
self.contentContainerNode = ASDisplayNode()
|
||||||
self.contentContainerNode.backgroundColor = .white
|
self.contentContainerNode.backgroundColor = .white
|
||||||
self.contentContainerNode.cornerRadius = 60.0
|
|
||||||
self.contentContainerNode.clipsToBounds = true
|
self.contentContainerNode.clipsToBounds = true
|
||||||
|
|
||||||
self.foregroundContainerNode = ASDisplayNode()
|
self.foregroundContainerNode = ASDisplayNode()
|
||||||
@ -74,7 +73,7 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
|
|||||||
|
|
||||||
if let snapshot = snapshot {
|
if let snapshot = snapshot {
|
||||||
self.contentContainerNode.view.addSubview(snapshot)
|
self.contentContainerNode.view.addSubview(snapshot)
|
||||||
if let validLayoutSize = self.validLayoutSize {
|
if let _ = self.validLayoutSize {
|
||||||
snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size)
|
snapshot.frame = CGRect(origin: CGPoint(), size: snapshot.frame.size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -84,6 +83,8 @@ final class OverlayInstantVideoDecoration: UniversalVideoDecoration {
|
|||||||
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
||||||
self.validLayoutSize = size
|
self.validLayoutSize = size
|
||||||
|
|
||||||
|
self.contentContainerNode.cornerRadius = size.width / 2.0
|
||||||
|
|
||||||
let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0)
|
let shadowInsets = UIEdgeInsets(top: 2.0, left: 3.0, bottom: 4.0, right: 3.0)
|
||||||
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom)))
|
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: -shadowInsets.left, y: -shadowInsets.top), size: CGSize(width: size.width + shadowInsets.left + shadowInsets.right, height: size.height + shadowInsets.top + shadowInsets.bottom)))
|
||||||
|
|
||||||
|
|||||||
@ -83,8 +83,12 @@ final class OverlayInstantVideoNode: OverlayMediaItemNode {
|
|||||||
self.updateLayout(self.bounds.size)
|
self.updateLayout(self.bounds.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override func preferredSizeForOverlayDisplay() -> CGSize {
|
override func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
|
||||||
return CGSize(width: 120.0, height: 120.0)
|
if min(boundingSize.width, boundingSize.height) > 320.0 {
|
||||||
|
return CGSize(width: 150.0, height: 150.0)
|
||||||
|
} else {
|
||||||
|
return CGSize(width: 120.0, height: 120.0)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override func dismiss() {
|
override func dismiss() {
|
||||||
|
|||||||
@ -239,7 +239,7 @@ final class OverlayMediaControllerNode: ASDisplayNode, UIGestureRecognizerDelega
|
|||||||
location = groupLocation
|
location = groupLocation
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay())
|
let nodeData = OverlayMediaVideoNodeData(node: node, location: location, isMinimized: false, currentSize: node.preferredSizeForOverlayDisplay(boundingSize: self.frame.size))
|
||||||
self.videoNodes.append(nodeData)
|
self.videoNodes.append(nodeData)
|
||||||
self.addSubnode(node)
|
self.addSubnode(node)
|
||||||
if let validLayout = self.validLayout {
|
if let validLayout = self.validLayout {
|
||||||
|
|||||||
@ -98,7 +98,7 @@ public final class OverlayUniversalVideoNode: OverlayMediaItemNode {
|
|||||||
self.updateLayout(self.bounds.size)
|
self.updateLayout(self.bounds.size)
|
||||||
}
|
}
|
||||||
|
|
||||||
override public func preferredSizeForOverlayDisplay() -> CGSize {
|
override public func preferredSizeForOverlayDisplay(boundingSize: CGSize) -> CGSize {
|
||||||
return self.content.dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0))
|
return self.content.dimensions.aspectFitted(CGSize(width: 300.0, height: 300.0))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user