import Foundation import UIKit import Display import AsyncDisplayKit import SwiftSignalKit import CheckNode import AnimationUI public enum WallpaperOptionButtonValue { case check(Bool) case color(Bool, UIColor) case colors(Bool, [UIColor]) } private func generateColorsImage(diameter: CGFloat, colors: [UIColor]) -> UIImage? { return generateImage(CGSize(width: diameter, height: diameter), rotatedContext: { size, context in context.clear(CGRect(origin: CGPoint(), size: size)) if !colors.isEmpty { let center = CGPoint(x: size.width / 2.0, y: size.height / 2.0) var startAngle = -CGFloat.pi * 0.5 for i in 0 ..< colors.count { context.setFillColor(colors[i].cgColor) let endAngle = startAngle + 2.0 * CGFloat.pi * (1.0 / CGFloat(colors.count)) context.move(to: center) context.addArc(center: center, radius: size.width / 2.0, startAngle: startAngle, endAngle: endAngle, clockwise: false) context.fillPath() startAngle = endAngle } } }) } final class WallpaperLightButtonBackgroundNode: ASDisplayNode { private let backgroundNode: NavigationBackgroundNode private let overlayNode: ASDisplayNode private let lightNode: ASDisplayNode override init() { self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: false) self.overlayNode = ASDisplayNode() self.overlayNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.75) self.overlayNode.layer.compositingFilter = "overlayBlendMode" self.lightNode = ASDisplayNode() self.lightNode.backgroundColor = UIColor(rgb: 0xf2f2f2, alpha: 0.2) super.init() self.clipsToBounds = true self.addSubnode(self.backgroundNode) self.addSubnode(self.overlayNode) self.addSubnode(self.lightNode) } func updateLayout(size: CGSize) { let frame = CGRect(origin: .zero, size: size) self.backgroundNode.frame = frame self.overlayNode.frame = frame self.lightNode.frame = frame self.backgroundNode.update(size: size, transition: .immediate) } } final class WallpaperOptionBackgroundNode: ASDisplayNode { private let backgroundNode: NavigationBackgroundNode var enableSaturation: Bool { didSet { self.backgroundNode.updateColor(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: self.enableSaturation, transition: .immediate) } } init(enableSaturation: Bool = false) { self.enableSaturation = enableSaturation self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: enableSaturation) super.init() self.clipsToBounds = true self.isUserInteractionEnabled = false self.addSubnode(self.backgroundNode) } func updateLayout(size: CGSize) { let frame = CGRect(origin: .zero, size: size) self.backgroundNode.frame = frame self.backgroundNode.update(size: size, transition: .immediate) } } final class WallpaperNavigationButtonNode: HighlightTrackingButtonNode { enum Content { case icon(image: UIImage?, size: CGSize) case text(String) case dayNight(isNight: Bool) } var enableSaturation: Bool = false { didSet { if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode { backgroundNode.enableSaturation = self.enableSaturation } } } private let content: Content var dark: Bool { didSet { if self.dark != oldValue { self.backgroundNode.removeFromSupernode() if self.dark { self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation) } else { self.backgroundNode = WallpaperLightButtonBackgroundNode() } self.insertSubnode(self.backgroundNode, at: 0) } } } private var backgroundNode: ASDisplayNode private let iconNode: ASImageNode private let textNode: ImmediateTextNode private var animationNode: AnimationNode? func setIcon(_ image: UIImage?) { self.iconNode.image = generateTintedImage(image: image, color: .white) } init(content: Content, dark: Bool) { self.content = content self.dark = dark if dark { self.backgroundNode = WallpaperOptionBackgroundNode(enableSaturation: self.enableSaturation) } else { self.backgroundNode = WallpaperLightButtonBackgroundNode() } self.iconNode = ASImageNode() self.iconNode.displaysAsynchronously = false self.iconNode.contentMode = .center var title: String switch content { case let .text(text): title = text case let .icon(icon, _): title = "" self.iconNode.image = generateTintedImage(image: icon, color: .white) case let .dayNight(isNight): title = "" let animationNode = AnimationNode(animation: isNight ? "anim_sun_reverse" : "anim_sun", colors: [:], scale: 1.0) animationNode.speed = 1.66 animationNode.isUserInteractionEnabled = false self.animationNode = animationNode } self.textNode = ImmediateTextNode() self.textNode.attributedText = NSAttributedString(string: title, font: Font.semibold(15.0), textColor: .white) super.init() self.isExclusiveTouch = true self.addSubnode(self.backgroundNode) self.addSubnode(self.iconNode) self.addSubnode(self.textNode) if let animationNode = self.animationNode { self.addSubnode(animationNode) } self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.backgroundNode.alpha = 0.4 strongSelf.iconNode.layer.removeAnimation(forKey: "opacity") strongSelf.iconNode.alpha = 0.4 strongSelf.textNode.layer.removeAnimation(forKey: "opacity") strongSelf.textNode.alpha = 0.4 // if let animationNode = strongSelf.animationNode { // animationNode.layer.removeAnimation(forKey: "opacity") // animationNode.alpha = 0.4 // } } else { strongSelf.backgroundNode.alpha = 1.0 strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.iconNode.alpha = 1.0 strongSelf.iconNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.textNode.alpha = 1.0 strongSelf.textNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) // if let animationNode = strongSelf.animationNode { // animationNode.alpha = 1.0 // animationNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) // } } } } } func setIsNight(_ isNight: Bool) { self.animationNode?.setAnimation(name: !isNight ? "anim_sun_reverse" : "anim_sun", colors: [:]) self.animationNode?.speed = 1.66 self.animationNode?.playOnce() self.isUserInteractionEnabled = false Queue.mainQueue().after(0.4) { self.isUserInteractionEnabled = true } } var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { didSet { } } private var textSize: CGSize? override func measure(_ constrainedSize: CGSize) -> CGSize { switch self.content { case .text: let size = self.textNode.updateLayout(constrainedSize) self.textSize = size return CGSize(width: ceil(size.width) + 16.0, height: 28.0) case let .icon(_, size): return size case .dayNight: return CGSize(width: 28.0, height: 28.0) } } override func layout() { super.layout() let size = self.bounds.size self.backgroundNode.frame = self.bounds if let backgroundNode = self.backgroundNode as? WallpaperOptionBackgroundNode { backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) } else if let backgroundNode = self.backgroundNode as? WallpaperLightButtonBackgroundNode { backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) } self.backgroundNode.cornerRadius = size.height / 2.0 self.iconNode.frame = self.bounds if let textSize = self.textSize { self.textNode.frame = CGRect(x: floorToScreenPixels((size.width - textSize.width) / 2.0), y: floorToScreenPixels((size.height - textSize.height) / 2.0), width: textSize.width, height: textSize.height) } if let animationNode = self.animationNode { animationNode.bounds = CGRect(origin: .zero, size: CGSize(width: 24.0, height: 24.0)) animationNode.position = CGPoint(x: 14.0, y: 14.0) } } } public final class WallpaperOptionButtonNode: HighlightTrackingButtonNode { let backgroundNode: WallpaperOptionBackgroundNode private let checkNode: CheckNode private let colorNode: ASImageNode private let textNode: ImmediateTextNode private var textSize: CGSize? private var _value: WallpaperOptionButtonValue public override var isSelected: Bool { get { switch self._value { case let .check(selected), let .color(selected, _), let .colors(selected, _): return selected } } set { switch self._value { case .check: self._value = .check(newValue) case let .color(_, color): self._value = .color(newValue, color) case let .colors(_, colors): self._value = .colors(newValue, colors) } self.checkNode.setSelected(newValue, animated: false) } } public var title: String { didSet { self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white) } } public init(title: String, value: WallpaperOptionButtonValue) { self._value = value self.title = title self.backgroundNode = WallpaperOptionBackgroundNode() self.checkNode = CheckNode(theme: CheckNodeTheme(backgroundColor: .white, strokeColor: .clear, borderColor: .white, overlayBorder: false, hasInset: false, hasShadow: false, borderWidth: 1.5)) self.checkNode.isUserInteractionEnabled = false self.colorNode = ASImageNode() self.textNode = ImmediateTextNode() self.textNode.displaysAsynchronously = false self.textNode.attributedText = NSAttributedString(string: title, font: Font.medium(13), textColor: .white) super.init() self.clipsToBounds = true self.cornerRadius = 14.0 self.isExclusiveTouch = true switch value { case let .check(selected): self.checkNode.isHidden = false self.colorNode.isHidden = true self.checkNode.selected = selected case let .color(_, color): self.checkNode.isHidden = true self.colorNode.isHidden = false self.colorNode.image = generateFilledCircleImage(diameter: 18.0, color: color) case let .colors(_, colors): self.checkNode.isHidden = true self.colorNode.isHidden = false self.colorNode.image = generateColorsImage(diameter: 18.0, colors: colors) } self.addSubnode(self.backgroundNode) self.addSubnode(self.checkNode) self.addSubnode(self.textNode) self.addSubnode(self.colorNode) self.highligthedChanged = { [weak self] highlighted in if let strongSelf = self { if highlighted { strongSelf.backgroundNode.layer.removeAnimation(forKey: "opacity") strongSelf.backgroundNode.alpha = 0.4 strongSelf.colorNode.layer.removeAnimation(forKey: "opacity") strongSelf.colorNode.alpha = 0.4 } else { strongSelf.backgroundNode.alpha = 1.0 strongSelf.backgroundNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) strongSelf.colorNode.alpha = 1.0 strongSelf.colorNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2) } } } } public var buttonColor: UIColor = UIColor(rgb: 0x000000, alpha: 0.3) { didSet { } } public var color: UIColor? { get { switch self._value { case let .color(_, color): return color default: return nil } } set { if let color = newValue { switch self._value { case let .color(selected, _): self._value = .color(selected, color) self.colorNode.image = generateFilledCircleImage(diameter: 18.0, color: color) default: break } } } } public var colors: [UIColor]? { get { switch self._value { case let .colors(_, colors): return colors default: return nil } } set { if let colors = newValue { switch self._value { case let .colors(selected, current): if current.count == colors.count { var updated = false for i in 0 ..< current.count { if !current[i].isEqual(colors[i]) { updated = true break } } if !updated { return } } self._value = .colors(selected, colors) self.colorNode.image = generateColorsImage(diameter: 18.0, colors: colors) default: break } } } } public func setSelected(_ selected: Bool, animated: Bool = false) { switch self._value { case .check: self._value = .check(selected) case let .color(_, color): self._value = .color(selected, color) case let .colors(_, colors): self._value = .colors(selected, colors) } self.checkNode.setSelected(selected, animated: animated) } public func setEnabled(_ enabled: Bool) { let alpha: CGFloat = enabled ? 1.0 : 0.4 self.checkNode.alpha = alpha self.colorNode.alpha = alpha self.textNode.alpha = alpha self.isUserInteractionEnabled = enabled } public override func measure(_ constrainedSize: CGSize) -> CGSize { let size = self.textNode.updateLayout(constrainedSize) self.textSize = size return CGSize(width: ceil(size.width) + 48.0, height: 30.0) } public override func layout() { super.layout() self.backgroundNode.frame = self.bounds self.backgroundNode.updateLayout(size: self.backgroundNode.bounds.size) guard let _ = self.textSize else { return } let padding: CGFloat = 6.0 let spacing: CGFloat = 9.0 let checkSize = CGSize(width: 18.0, height: 18.0) let checkFrame = CGRect(origin: CGPoint(x: padding, y: padding), size: checkSize) self.checkNode.frame = checkFrame self.colorNode.frame = checkFrame if let textSize = self.textSize { self.textNode.frame = CGRect(x: max(padding + checkSize.width + spacing, padding + checkSize.width + floor((self.bounds.width - padding - checkSize.width - textSize.width) / 2.0) - 2.0), y: floorToScreenPixels((self.bounds.height - textSize.height) / 2.0), width: textSize.width, height: textSize.height) } } } final class WallpaperSliderNode: ASDisplayNode { let minValue: CGFloat let maxValue: CGFloat var value: CGFloat = 1.0 { didSet { if let size = self.validLayout { self.updateLayout(size: size) } } } private let backgroundNode: NavigationBackgroundNode private let foregroundNode: ASDisplayNode private let foregroundLightNode: ASDisplayNode private let leftIconNode: ASImageNode private let rightIconNode: ASImageNode private let valueChanged: (CGFloat, Bool) -> Void private let hapticFeedback = HapticFeedback() private var validLayout: CGSize? init(minValue: CGFloat, maxValue: CGFloat, value: CGFloat, valueChanged: @escaping (CGFloat, Bool) -> Void) { self.minValue = minValue self.maxValue = maxValue self.value = value self.valueChanged = valueChanged self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x333333, alpha: 0.35), enableBlur: true, enableSaturation: false) self.foregroundNode = ASDisplayNode() self.foregroundNode.clipsToBounds = true self.foregroundNode.cornerRadius = 3.0 self.foregroundNode.isAccessibilityElement = false self.foregroundNode.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.75) self.foregroundNode.layer.compositingFilter = "overlayBlendMode" self.foregroundNode.isUserInteractionEnabled = false self.foregroundLightNode = ASDisplayNode() self.foregroundLightNode.clipsToBounds = true self.foregroundLightNode.cornerRadius = 3.0 self.foregroundLightNode.backgroundColor = UIColor(rgb: 0xf2f2f2, alpha: 0.2) self.leftIconNode = ASImageNode() self.leftIconNode.displaysAsynchronously = false self.leftIconNode.image = UIImage(bundleImageName: "Settings/WallpaperBrightnessMin") self.leftIconNode.contentMode = .center self.rightIconNode = ASImageNode() self.rightIconNode.displaysAsynchronously = false self.rightIconNode.image = UIImage(bundleImageName: "Settings/WallpaperBrightnessMax") self.rightIconNode.contentMode = .center super.init() self.clipsToBounds = true self.cornerRadius = 15.0 self.isUserInteractionEnabled = true self.addSubnode(self.backgroundNode) self.addSubnode(self.foregroundNode) self.addSubnode(self.foregroundLightNode) self.addSubnode(self.leftIconNode) self.addSubnode(self.rightIconNode) } override func didLoad() { super.didLoad() let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:))) self.view.addGestureRecognizer(panGestureRecognizer) let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))) self.view.addGestureRecognizer(tapGestureRecognizer) } var ignoreUpdates = false func animateValue(from: CGFloat, to: CGFloat, transition: ContainedViewLayoutTransition = .immediate) { guard let size = self.validLayout else { return } self.internalUpdateLayout(size: size, value: from) self.internalUpdateLayout(size: size, value: to, transition: transition) } func internalUpdateLayout(size: CGSize, value: CGFloat, transition: ContainedViewLayoutTransition = .immediate) { self.validLayout = size transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: .zero, size: size)) self.backgroundNode.update(size: size, transition: transition) if let icon = self.leftIconNode.image { transition.updateFrame(node: self.leftIconNode, frame: CGRect(origin: CGPoint(x: 7.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size)) } if let icon = self.rightIconNode.image { transition.updateFrame(node: self.rightIconNode, frame: CGRect(origin: CGPoint(x: size.width - icon.size.width - 6.0, y: floorToScreenPixels((size.height - icon.size.height) / 2.0)), size: icon.size)) } let range = self.maxValue - self.minValue let value = (value - self.minValue) / range let foregroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: value * size.width, height: size.height)) transition.updateFrame(node: self.foregroundNode, frame: foregroundFrame) transition.updateFrame(node: self.foregroundLightNode, frame: foregroundFrame) } func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition = .immediate) { guard !self.ignoreUpdates else { return } self.internalUpdateLayout(size: size, value: self.value, transition: transition) } @objc private func panGesture(_ gestureRecognizer: UIPanGestureRecognizer) { let range = self.maxValue - self.minValue switch gestureRecognizer.state { case .began: break case .changed: let previousValue = self.value let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x let delta = translation / self.bounds.width * range self.value = max(self.minValue, min(self.maxValue, self.value + delta)) gestureRecognizer.setTranslation(CGPoint(), in: gestureRecognizer.view) if self.value == 0.0 && previousValue != 0.0 { self.hapticFeedback.impact(.soft) } else if self.value == 1.0 && previousValue != 1.0 { self.hapticFeedback.impact(.soft) } if abs(previousValue - self.value) >= 0.001 { self.valueChanged(self.value, false) } case .ended: let translation: CGFloat = gestureRecognizer.translation(in: gestureRecognizer.view).x let delta = translation / self.bounds.width * range self.value = max(self.minValue, min(self.maxValue, self.value + delta)) self.valueChanged(self.value, true) default: break } } @objc private func tapGesture(_ gestureRecognizer: UITapGestureRecognizer) { let range = self.maxValue - self.minValue let location = gestureRecognizer.location(in: gestureRecognizer.view) self.value = max(self.minValue, min(self.maxValue, self.minValue + location.x / self.bounds.width * range)) self.valueChanged(self.value, true) } }