import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import TelegramCore import TelegramPresentationData import LegacyComponents import ItemListUI import PresentationDataUtils private let textFont = Font.with(size: 17.0, traits: .monospacedNumbers) private let smallTextFont = Font.with(size: 13.0, traits: .monospacedNumbers) public final class MessagePriceItem: ListViewItem, ItemListItem { let theme: PresentationTheme let strings: PresentationStrings let minValue: Int64 let maxValue: Int64 let value: Int64 let price: String public let sectionId: ItemListSectionId let updated: (Int64) -> Void public init(theme: PresentationTheme, strings: PresentationStrings, minValue: Int64, maxValue: Int64, value: Int64, price: String, sectionId: ItemListSectionId, updated: @escaping (Int64) -> Void) { self.theme = theme self.strings = strings self.minValue = minValue self.maxValue = maxValue self.value = value self.price = price self.sectionId = sectionId self.updated = updated } public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal?, (ListViewItemApply) -> Void)) -> Void) { async { let node = MessagePriceItemNode() let (layout, apply) = node.asyncLayout()(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) node.contentSize = layout.contentSize node.insets = layout.insets Queue.mainQueue().async { completion(node, { return (nil, { _ in apply() }) }) } } } public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) { Queue.mainQueue().async { if let nodeValue = node() as? MessagePriceItemNode { let makeLayout = nodeValue.asyncLayout() async { let (layout, apply) = makeLayout(self, params, itemListNeighbors(item: self, topItem: previousItem as? ItemListItem, bottomItem: nextItem as? ItemListItem)) Queue.mainQueue().async { completion(layout, { _ in apply() }) } } } } } } private class MessagePriceItemNode: ListViewItemNode { private struct Amount: Equatable { private let sliderSteps: [Int] private let maxRealValue: Int let maxSliderValue: Int private let isLogarithmic: Bool private(set) var realValue: Int private(set) var sliderValue: Int private static func makeSliderSteps(maxRealValue: Int, isLogarithmic: Bool) -> [Int] { if isLogarithmic { var sliderSteps: [Int] = [ 1, 10, 50, 100, 500, 1_000, 2_000, 5_000, 7_500, 10_000 ] sliderSteps.removeAll(where: { $0 >= maxRealValue }) sliderSteps.append(maxRealValue) return sliderSteps } else { return [1, maxRealValue] } } private static func remapValueToSlider(realValue: Int, maxSliderValue: Int, steps: [Int]) -> Int { guard realValue >= steps.first!, realValue <= steps.last! else { return 0 } for i in 0 ..< steps.count - 1 { if realValue >= steps[i] && realValue <= steps[i + 1] { let range = steps[i + 1] - steps[i] let relativeValue = realValue - steps[i] let stepFraction = Float(relativeValue) / Float(range) return Int(Float(i) * Float(maxSliderValue) / Float(steps.count - 1)) + Int(stepFraction * Float(maxSliderValue) / Float(steps.count - 1)) } } return maxSliderValue } private static func remapSliderToValue(sliderValue: Int, maxSliderValue: Int, steps: [Int]) -> Int { guard sliderValue >= 0, sliderValue <= maxSliderValue else { return steps.first! } let stepIndex = Int(Float(sliderValue) / Float(maxSliderValue) * Float(steps.count - 1)) let fraction = Float(sliderValue) / Float(maxSliderValue) * Float(steps.count - 1) - Float(stepIndex) if stepIndex >= steps.count - 1 { return steps.last! } else { let range = steps[stepIndex + 1] - steps[stepIndex] return steps[stepIndex] + Int(fraction * Float(range)) } } init(realValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) { self.sliderSteps = Amount.makeSliderSteps(maxRealValue: maxRealValue, isLogarithmic: isLogarithmic) self.maxRealValue = maxRealValue self.maxSliderValue = maxSliderValue self.isLogarithmic = isLogarithmic self.realValue = realValue self.sliderValue = Amount.remapValueToSlider(realValue: self.realValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps) } init(sliderValue: Int, maxRealValue: Int, maxSliderValue: Int, isLogarithmic: Bool) { self.sliderSteps = Amount.makeSliderSteps(maxRealValue: maxRealValue, isLogarithmic: isLogarithmic) self.maxRealValue = maxRealValue self.maxSliderValue = maxSliderValue self.isLogarithmic = isLogarithmic self.sliderValue = sliderValue self.realValue = Amount.remapSliderToValue(sliderValue: self.sliderValue, maxSliderValue: self.maxSliderValue, steps: self.sliderSteps) } func withRealValue(_ realValue: Int) -> Amount { return Amount(realValue: realValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) } func withSliderValue(_ sliderValue: Int) -> Amount { return Amount(sliderValue: sliderValue, maxRealValue: self.maxRealValue, maxSliderValue: self.maxSliderValue, isLogarithmic: self.isLogarithmic) } } private let backgroundNode: ASDisplayNode private let topStripeNode: ASDisplayNode private let bottomStripeNode: ASDisplayNode private let maskNode: ASImageNode private var sliderView: TGPhotoEditorSliderView? private let leftTextNode: ImmediateTextNode private let rightTextNode: ImmediateTextNode private let centerLeftTextNode: ImmediateTextNode private let centerRightTextNode: ImmediateTextNode private var amount: Amount = Amount(realValue: 1, maxRealValue: 1000, maxSliderValue: 1000, isLogarithmic: true) private var item: MessagePriceItem? private var layoutParams: ListViewItemLayoutParams? init() { self.backgroundNode = ASDisplayNode() self.backgroundNode.isLayerBacked = true self.topStripeNode = ASDisplayNode() self.topStripeNode.isLayerBacked = true self.bottomStripeNode = ASDisplayNode() self.bottomStripeNode.isLayerBacked = true self.maskNode = ASImageNode() self.leftTextNode = ImmediateTextNode() self.rightTextNode = ImmediateTextNode() self.centerLeftTextNode = ImmediateTextNode() self.centerRightTextNode = ImmediateTextNode() super.init(layerBacked: false, dynamicBounce: false) self.addSubnode(self.leftTextNode) self.addSubnode(self.rightTextNode) self.addSubnode(self.centerLeftTextNode) self.addSubnode(self.centerRightTextNode) } override func didLoad() { super.didLoad() let sliderView = TGPhotoEditorSliderView() sliderView.enableEdgeTap = true sliderView.enablePanHandling = true sliderView.trackCornerRadius = 1.0 sliderView.lineSize = 4.0 sliderView.disablesInteractiveTransitionGestureRecognizer = true if let item = self.item, let params = self.layoutParams { self.amount = Amount(realValue: Int(item.value), maxRealValue: Int(item.maxValue), maxSliderValue: 999, isLogarithmic: true) sliderView.minimumValue = 0 sliderView.startValue = 0 sliderView.maximumValue = CGFloat(self.amount.maxSliderValue) sliderView.displayEdges = true sliderView.value = CGFloat(self.amount.sliderValue) sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor sliderView.backColor = item.theme.list.itemSwitchColors.frameColor sliderView.trackColor = item.theme.list.itemAccentColor sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) } self.view.addSubview(sliderView) sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged) self.sliderView = sliderView } func asyncLayout() -> (_ item: MessagePriceItem, _ params: ListViewItemLayoutParams, _ neighbors: ItemListNeighbors) -> (ListViewItemNodeLayout, () -> Void) { let currentItem = self.item return { item, params, neighbors in var themeUpdated = false if currentItem?.theme !== item.theme { themeUpdated = true } let contentSize: CGSize let insets: UIEdgeInsets let separatorHeight = UIScreenPixel contentSize = CGSize(width: params.width, height: 88.0) insets = itemListNeighborsGroupedInsets(neighbors, params) let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets) let layoutSize = layout.size return (layout, { [weak self] in if let strongSelf = self { strongSelf.item = item strongSelf.layoutParams = params strongSelf.backgroundNode.backgroundColor = item.theme.list.itemBlocksBackgroundColor strongSelf.topStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor strongSelf.bottomStripeNode.backgroundColor = item.theme.list.itemBlocksSeparatorColor if strongSelf.backgroundNode.supernode == nil { strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0) } if strongSelf.topStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1) } if strongSelf.bottomStripeNode.supernode == nil { strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2) } if strongSelf.maskNode.supernode == nil { strongSelf.insertSubnode(strongSelf.maskNode, at: 3) } let hasCorners = itemListHasRoundedBlockLayout(params) var hasTopCorners = false var hasBottomCorners = false switch neighbors.top { case .sameSection(false): strongSelf.topStripeNode.isHidden = true default: hasTopCorners = true strongSelf.topStripeNode.isHidden = hasCorners } let bottomStripeInset: CGFloat let bottomStripeOffset: CGFloat switch neighbors.bottom { case .sameSection(false): bottomStripeInset = params.leftInset + 16.0 bottomStripeOffset = -separatorHeight strongSelf.bottomStripeNode.isHidden = false default: bottomStripeInset = 0.0 bottomStripeOffset = 0.0 hasBottomCorners = true strongSelf.bottomStripeNode.isHidden = hasCorners } strongSelf.maskNode.image = hasCorners ? PresentationResourcesItemList.cornersImage(item.theme, top: hasTopCorners, bottom: hasBottomCorners) : nil strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight))) strongSelf.maskNode.frame = strongSelf.backgroundNode.frame.insetBy(dx: params.leftInset, dy: 0.0) strongSelf.topStripeNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)) strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset, height: separatorHeight)) strongSelf.leftTextNode.attributedText = NSAttributedString(string: "\(item.minValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) strongSelf.rightTextNode.attributedText = NSAttributedString(string: "\(item.maxValue)", font: Font.regular(13.0), textColor: item.theme.list.itemSecondaryTextColor) //TODO:localize let centralLeftText = "\(item.value) Stars" strongSelf.centerLeftTextNode.attributedText = NSAttributedString(string: centralLeftText, font: textFont, textColor: item.theme.list.itemPrimaryTextColor) strongSelf.centerRightTextNode.attributedText = NSAttributedString(string: item.price, font: smallTextFont, textColor: item.theme.list.itemSecondaryTextColor) let leftTextSize = strongSelf.leftTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) let rightTextSize = strongSelf.rightTextNode.updateLayout(CGSize(width: 100.0, height: 100.0)) let centerLeftTextSize = strongSelf.centerLeftTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) let centerRightTextSize = strongSelf.centerRightTextNode.updateLayout(CGSize(width: 200.0, height: 100.0)) let centerSpacing: CGFloat = 6.0 let sideInset: CGFloat = 18.0 strongSelf.leftTextNode.frame = CGRect(origin: CGPoint(x: params.leftInset + sideInset, y: 15.0), size: leftTextSize) strongSelf.rightTextNode.frame = CGRect(origin: CGPoint(x: params.width - params.leftInset - sideInset - rightTextSize.width, y: 15.0), size: rightTextSize) let totalCenterWidth = centerLeftTextSize.width + centerSpacing + centerRightTextSize.width let centerLeftFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalCenterWidth) / 2.0), y: 11.0), size: centerLeftTextSize) strongSelf.centerLeftTextNode.frame = centerLeftFrame let centerRightFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((params.width - totalCenterWidth) / 2.0) + totalCenterWidth - centerRightTextSize.width, y: 14.0 - UIScreenPixel), size: centerRightTextSize) strongSelf.centerRightTextNode.frame = centerRightFrame if let sliderView = strongSelf.sliderView { if themeUpdated { sliderView.backgroundColor = item.theme.list.itemBlocksBackgroundColor sliderView.backColor = item.theme.list.itemSecondaryTextColor sliderView.trackColor = item.theme.list.itemAccentColor.withAlphaComponent(0.45) sliderView.knobImage = PresentationResourcesItemList.knobImage(item.theme) } sliderView.frame = CGRect(origin: CGPoint(x: params.leftInset + 18.0, y: 36.0), size: CGSize(width: params.width - params.leftInset - params.rightInset - 18.0 * 2.0, height: 44.0)) } } }) } } override func animateInsertion(_ currentTimestamp: Double, duration: Double, options: ListViewItemAnimationOptions) { self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4) } override func animateRemoved(_ currentTimestamp: Double, duration: Double) { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) } @objc func sliderValueChanged() { guard let sliderView = self.sliderView else { return } var updatedAmount = self.amount.withSliderValue(Int(sliderView.value)) if updatedAmount.realValue > 50 { if updatedAmount.realValue < 100 { updatedAmount = updatedAmount.withRealValue(Int(round(Double(updatedAmount.realValue) / 1.0) * 1.0)) } else if updatedAmount.realValue < 500 { updatedAmount = updatedAmount.withRealValue(Int(round(Double(updatedAmount.realValue) / 5.0) * 5.0)) } else if updatedAmount.realValue < 1000 { updatedAmount = updatedAmount.withRealValue(Int(round(Double(updatedAmount.realValue) / 10.0) * 10.0)) } else if updatedAmount.realValue < 5000 { updatedAmount = updatedAmount.withRealValue(Int(round(Double(updatedAmount.realValue) / 25.0) * 25.0)) } else { updatedAmount = updatedAmount.withRealValue(Int(round(Double(updatedAmount.realValue) / 50.0) * 50.0)) } } self.amount = updatedAmount self.item?.updated(Int64(self.amount.realValue)) } }