mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
527 lines
21 KiB
Swift
527 lines
21 KiB
Swift
import Foundation
|
|
import UIKit
|
|
import AsyncDisplayKit
|
|
import SwiftSignalKit
|
|
import Display
|
|
import TelegramCore
|
|
import SyncCore
|
|
import TelegramPresentationData
|
|
import LegacyComponents
|
|
import AccountContext
|
|
import MergeLists
|
|
import Postbox
|
|
|
|
private let itemSize = CGSize(width: 88.0, height: 88.0)
|
|
private let inset: CGFloat = 12.0
|
|
|
|
private func intensityToSliderValue(_ value: Int32, allowDark: Bool) -> CGFloat {
|
|
if allowDark {
|
|
if value < 0 {
|
|
return max(0.0, min(100.0, CGFloat(abs(value))))
|
|
} else {
|
|
return 100.0 + max(0.0, min(100.0, CGFloat(value)))
|
|
}
|
|
} else {
|
|
return CGFloat(max(value, 0)) * 2.0
|
|
}
|
|
}
|
|
|
|
private func sliderValueToIntensity(_ value: CGFloat, allowDark: Bool) -> Int32 {
|
|
if allowDark {
|
|
if value < 100.0 {
|
|
return -Int32(max(1.0, value))
|
|
} else {
|
|
return Int32(value - 100.0)
|
|
}
|
|
} else {
|
|
return Int32(value / 2.0)
|
|
}
|
|
}
|
|
|
|
private struct WallpaperPatternEntry: Comparable, Identifiable {
|
|
let index: Int
|
|
let wallpaper: TelegramWallpaper
|
|
let selected: Bool
|
|
|
|
var stableId: Int64 {
|
|
if case let .file(file) = self.wallpaper {
|
|
return file.id
|
|
} else {
|
|
return Int64(self.index)
|
|
}
|
|
}
|
|
|
|
static func ==(lhs: WallpaperPatternEntry, rhs: WallpaperPatternEntry) -> Bool {
|
|
if lhs.index != rhs.index {
|
|
return false
|
|
}
|
|
if lhs.wallpaper != rhs.wallpaper {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
static func <(lhs: WallpaperPatternEntry, rhs: WallpaperPatternEntry) -> Bool {
|
|
return lhs.index < rhs.index
|
|
}
|
|
|
|
func item(context: AccountContext, action: @escaping (TelegramWallpaper) -> Void) -> ListViewItem {
|
|
return WallpaperPatternItem(context: context, wallpaper: self.wallpaper, selected: self.selected, action: action)
|
|
}
|
|
}
|
|
|
|
private class WallpaperPatternItem: ListViewItem {
|
|
let context: AccountContext
|
|
let wallpaper: TelegramWallpaper
|
|
let selected: Bool
|
|
let action: (TelegramWallpaper) -> Void
|
|
|
|
public init(context: AccountContext, wallpaper: TelegramWallpaper, selected: Bool, action: @escaping (TelegramWallpaper) -> Void) {
|
|
self.context = context
|
|
self.wallpaper = wallpaper
|
|
self.selected = selected
|
|
self.action = action
|
|
}
|
|
|
|
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
|
|
async {
|
|
let node = WallpaperPatternItemNode()
|
|
let (nodeLayout, apply) = node.asyncLayout()(self, params)
|
|
node.insets = nodeLayout.insets
|
|
node.contentSize = nodeLayout.contentSize
|
|
|
|
Queue.mainQueue().async {
|
|
completion(node, {
|
|
return (nil, { _ in
|
|
apply(false)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
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 {
|
|
assert(node() is WallpaperPatternItemNode)
|
|
if let nodeValue = node() as? WallpaperPatternItemNode {
|
|
let layout = nodeValue.asyncLayout()
|
|
async {
|
|
let (nodeLayout, apply) = layout(self, params)
|
|
Queue.mainQueue().async {
|
|
completion(nodeLayout, { _ in
|
|
apply(animation.isAnimated)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public var selectable = true
|
|
public func selected(listView: ListView) {
|
|
self.action(self.wallpaper)
|
|
}
|
|
}
|
|
|
|
private final class WallpaperPatternItemNode : ListViewItemNode {
|
|
private let wallpaperNode: SettingsThemeWallpaperNode
|
|
|
|
var item: WallpaperPatternItem?
|
|
|
|
init() {
|
|
self.wallpaperNode = SettingsThemeWallpaperNode(displayLoading: true)
|
|
|
|
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
|
|
|
|
self.addSubnode(self.wallpaperNode)
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.layer.sublayerTransform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
|
}
|
|
|
|
func asyncLayout() -> (WallpaperPatternItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
|
let currentItem = self.item
|
|
|
|
return { [weak self] item, params in
|
|
var updatedWallpaper = false
|
|
var updatedSelected = false
|
|
|
|
if currentItem?.wallpaper != item.wallpaper {
|
|
updatedWallpaper = true
|
|
}
|
|
if currentItem?.selected != item.selected {
|
|
updatedSelected = true
|
|
}
|
|
|
|
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 112.0, height: 112.0), insets: UIEdgeInsets())
|
|
return (itemLayout, { animated in
|
|
if let strongSelf = self {
|
|
strongSelf.item = item
|
|
strongSelf.wallpaperNode.frame = CGRect(x: 0.0, y: 12.0, width: itemSize.width, height: itemSize.height)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
|
|
super.animateInsertion(currentTimestamp, duration: duration, short: short)
|
|
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
|
|
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateRemoved(currentTimestamp, duration: duration)
|
|
|
|
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
|
|
}
|
|
|
|
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
|
|
super.animateAdded(currentTimestamp, duration: duration)
|
|
|
|
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
|
}
|
|
}
|
|
|
|
final class WallpaperPatternPanelNode: ASDisplayNode {
|
|
private let context: AccountContext
|
|
private var theme: PresentationTheme
|
|
|
|
private let backgroundNode: NavigationBackgroundNode
|
|
private let topSeparatorNode: ASDisplayNode
|
|
|
|
let scrollNode: ASScrollNode
|
|
|
|
private let titleNode: ImmediateTextNode
|
|
private let labelNode: ImmediateTextNode
|
|
private var sliderView: TGPhotoEditorSliderView?
|
|
|
|
private var disposable: Disposable?
|
|
var wallpapers: [TelegramWallpaper] = []
|
|
private var currentWallpaper: TelegramWallpaper?
|
|
|
|
var serviceBackgroundColor: UIColor = UIColor(rgb: 0x748698) {
|
|
didSet {
|
|
guard let nodes = self.scrollNode.subnodes else {
|
|
return
|
|
}
|
|
for case let node as SettingsThemeWallpaperNode in nodes {
|
|
node.setOverlayBackgroundColor(self.serviceBackgroundColor.withAlphaComponent(0.4))
|
|
}
|
|
}
|
|
}
|
|
|
|
var backgroundColors: ([UInt32], Int32?, Int32?)? = nil {
|
|
didSet {
|
|
var updated = false
|
|
if oldValue?.0 != self.backgroundColors?.0 || oldValue?.1 != self.backgroundColors?.1 {
|
|
updated = true
|
|
} else if oldValue?.2 != self.backgroundColors?.2 {
|
|
if let oldIntensity = oldValue?.2, let newIntensity = self.backgroundColors?.2 {
|
|
if (oldIntensity < 0) != (newIntensity < 0) {
|
|
updated = true
|
|
}
|
|
} else if (oldValue?.2 != nil) != (self.backgroundColors?.2 != nil) {
|
|
updated = true
|
|
}
|
|
}
|
|
|
|
if updated {
|
|
self.updateWallpapers()
|
|
}
|
|
}
|
|
}
|
|
|
|
private var validLayout: CGSize?
|
|
|
|
var patternChanged: ((TelegramWallpaper?, Int32?, Bool) -> Void)?
|
|
|
|
private let allowDark: Bool
|
|
|
|
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
|
|
self.context = context
|
|
self.theme = theme
|
|
self.allowDark = theme.overallDarkAppearance
|
|
|
|
self.backgroundNode = NavigationBackgroundNode(color: theme.chat.inputPanel.panelBackgroundColor)
|
|
|
|
self.topSeparatorNode = ASDisplayNode()
|
|
self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor
|
|
|
|
self.scrollNode = ASScrollNode()
|
|
|
|
self.titleNode = ImmediateTextNode()
|
|
self.titleNode.attributedText = NSAttributedString(string: strings.WallpaperPreview_PatternTitle, font: Font.bold(17.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
|
|
|
self.labelNode = ImmediateTextNode()
|
|
self.labelNode.attributedText = NSAttributedString(string: strings.WallpaperPreview_PatternIntensity, font: Font.regular(14.0), textColor: theme.rootController.navigationBar.primaryTextColor)
|
|
|
|
super.init()
|
|
|
|
self.allowsGroupOpacity = true
|
|
|
|
self.addSubnode(self.backgroundNode)
|
|
self.addSubnode(self.topSeparatorNode)
|
|
self.addSubnode(self.scrollNode)
|
|
|
|
self.addSubnode(self.titleNode)
|
|
self.addSubnode(self.labelNode)
|
|
self.disposable = ((telegramWallpapers(postbox: context.account.postbox, network: context.account.network)
|
|
|> map { wallpapers -> [TelegramWallpaper] in
|
|
var existingIds = Set<MediaId>()
|
|
|
|
return wallpapers.filter { wallpaper in
|
|
if case let .file(file) = wallpaper, wallpaper.isPattern, file.file.mimeType != "image/webp" {
|
|
if file.id == 0 {
|
|
return true
|
|
}
|
|
if existingIds.contains(file.file.fileId) {
|
|
return false
|
|
} else {
|
|
existingIds.insert(file.file.fileId)
|
|
return true
|
|
}
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|> deliverOnMainQueue).start(next: { [weak self] wallpapers in
|
|
if let strongSelf = self {
|
|
strongSelf.wallpapers = wallpapers
|
|
strongSelf.updateWallpapers()
|
|
}
|
|
}))
|
|
}
|
|
|
|
deinit {
|
|
self.disposable?.dispose()
|
|
}
|
|
|
|
override func didLoad() {
|
|
super.didLoad()
|
|
|
|
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
|
self.scrollNode.view.showsVerticalScrollIndicator = false
|
|
self.scrollNode.view.alwaysBounceHorizontal = true
|
|
|
|
let sliderView = TGPhotoEditorSliderView()
|
|
sliderView.disableSnapToPositions = true
|
|
sliderView.trackCornerRadius = 1.0
|
|
sliderView.lineSize = 2.0
|
|
sliderView.startValue = 0.0
|
|
sliderView.minimumValue = 0.0
|
|
sliderView.maximumValue = 200.0
|
|
if self.allowDark {
|
|
sliderView.positionsCount = 3
|
|
}
|
|
sliderView.useLinesForPositions = true
|
|
sliderView.value = intensityToSliderValue(50, allowDark: self.allowDark)
|
|
sliderView.disablesInteractiveTransitionGestureRecognizer = true
|
|
sliderView.backgroundColor = .clear
|
|
sliderView.backColor = self.theme.list.disclosureArrowColor
|
|
if self.allowDark {
|
|
sliderView.trackColor = self.theme.list.disclosureArrowColor
|
|
} else {
|
|
sliderView.trackColor = self.theme.list.itemAccentColor
|
|
}
|
|
|
|
self.view.addSubview(sliderView)
|
|
sliderView.addTarget(self, action: #selector(self.sliderValueChanged), for: .valueChanged)
|
|
self.sliderView = sliderView
|
|
}
|
|
|
|
func updateWallpapers() {
|
|
guard let subnodes = self.scrollNode.subnodes else {
|
|
return
|
|
}
|
|
|
|
for node in subnodes {
|
|
node.removeFromSupernode()
|
|
}
|
|
|
|
let backgroundColors = self.backgroundColors ?? ([0xd6e2ee], nil, nil)
|
|
let intensity: Int32 = backgroundColors.2.flatMap { value in
|
|
if value < 0 {
|
|
return -80
|
|
} else {
|
|
return 80
|
|
}
|
|
} ?? 80
|
|
|
|
var selectedFileId: Int64?
|
|
var selectedSlug: String?
|
|
if let currentWallpaper = self.currentWallpaper, case let .file(file) = currentWallpaper {
|
|
selectedFileId = file.id
|
|
selectedSlug = file.slug
|
|
}
|
|
|
|
for wallpaper in self.wallpapers {
|
|
let node = SettingsThemeWallpaperNode(displayLoading: true, overlayBackgroundColor: self.serviceBackgroundColor.withAlphaComponent(0.4))
|
|
node.clipsToBounds = true
|
|
node.cornerRadius = 5.0
|
|
|
|
var updatedWallpaper = wallpaper
|
|
if case let .file(file) = updatedWallpaper {
|
|
let settings = WallpaperSettings(colors: backgroundColors.0, intensity: intensity, rotation: backgroundColors.1)
|
|
updatedWallpaper = .file(id: file.id, accessHash: file.accessHash, isCreator: file.isCreator, isDefault: file.isDefault, isPattern: updatedWallpaper.isPattern, isDark: file.isDark, slug: file.slug, file: file.file, settings: settings)
|
|
}
|
|
|
|
var selected = false
|
|
if case let .file(file) = wallpaper, (file.id == selectedFileId || file.slug == selectedSlug) {
|
|
selected = true
|
|
}
|
|
|
|
node.setWallpaper(context: self.context, wallpaper: updatedWallpaper, selected: selected, size: itemSize)
|
|
node.pressed = { [weak self, weak node] in
|
|
if let strongSelf = self {
|
|
strongSelf.currentWallpaper = updatedWallpaper
|
|
if let sliderView = strongSelf.sliderView {
|
|
strongSelf.patternChanged?(updatedWallpaper, sliderValueToIntensity(sliderView.value, allowDark: strongSelf.allowDark), false)
|
|
}
|
|
if let subnodes = strongSelf.scrollNode.subnodes {
|
|
for case let subnode as SettingsThemeWallpaperNode in subnodes {
|
|
let selected = node === subnode
|
|
subnode.setSelected(selected, animated: true)
|
|
if selected {
|
|
strongSelf.scrollToNode(subnode, animated: true)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
self.scrollNode.addSubnode(node)
|
|
}
|
|
|
|
self.scrollNode.view.contentSize = CGSize(width: (itemSize.width + inset) * CGFloat(wallpapers.count) + inset, height: 112.0)
|
|
self.layoutItemNodes(transition: .immediate)
|
|
}
|
|
|
|
func updateTheme(_ theme: PresentationTheme) {
|
|
self.theme = theme
|
|
|
|
self.backgroundNode.updateColor(color: self.theme.chat.inputPanel.panelBackgroundColor, transition: .immediate)
|
|
self.topSeparatorNode.backgroundColor = self.theme.chat.inputPanel.panelSeparatorColor
|
|
|
|
self.sliderView?.backColor = self.theme.list.disclosureArrowColor
|
|
if self.allowDark {
|
|
self.sliderView?.trackColor = self.theme.list.disclosureArrowColor
|
|
} else {
|
|
self.sliderView?.trackColor = self.theme.list.itemAccentColor
|
|
}
|
|
self.titleNode.attributedText = NSAttributedString(string: self.labelNode.attributedText?.string ?? "", font: Font.bold(17.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
|
self.labelNode.attributedText = NSAttributedString(string: self.labelNode.attributedText?.string ?? "", font: Font.regular(14.0), textColor: self.theme.rootController.navigationBar.primaryTextColor)
|
|
|
|
if let size = self.validLayout {
|
|
self.updateLayout(size: size, transition: .immediate)
|
|
}
|
|
}
|
|
|
|
@objc func sliderValueChanged() {
|
|
guard let sliderView = self.sliderView else {
|
|
return
|
|
}
|
|
|
|
if let wallpaper = self.currentWallpaper {
|
|
self.patternChanged?(wallpaper, sliderValueToIntensity(sliderView.value, allowDark: self.allowDark), sliderView.isTracking)
|
|
}
|
|
}
|
|
|
|
func didAppear(initialWallpaper: TelegramWallpaper? = nil, intensity: Int32? = nil) {
|
|
let wallpaper: TelegramWallpaper?
|
|
|
|
switch initialWallpaper {
|
|
case let .file(id, accessHash, isCreator, isDefault, isPattern, isDark, slug, file, _):
|
|
wallpaper = .file(id: id, accessHash: accessHash, isCreator: isCreator, isDefault: isDefault, isPattern: isPattern, isDark: isDark, slug: slug, file: file, settings: self.wallpapers[0].settings ?? WallpaperSettings())
|
|
default:
|
|
wallpaper = self.wallpapers.first
|
|
}
|
|
|
|
if let wallpaper = wallpaper {
|
|
var selectedFileId: Int64?
|
|
if case let .file(file) = wallpaper {
|
|
selectedFileId = file.id
|
|
}
|
|
|
|
self.currentWallpaper = wallpaper
|
|
self.sliderView?.value = intensity.flatMap { intensityToSliderValue($0, allowDark: self.allowDark) } ?? intensityToSliderValue(50, allowDark: self.allowDark)
|
|
|
|
self.scrollNode.view.contentOffset = CGPoint()
|
|
|
|
var selectedNode: SettingsThemeWallpaperNode?
|
|
if let subnodes = self.scrollNode.subnodes {
|
|
for case let subnode as SettingsThemeWallpaperNode in subnodes {
|
|
var selected = false
|
|
if case let .file(file) = subnode.wallpaper, file.id == selectedFileId {
|
|
selected = true
|
|
selectedNode = subnode
|
|
}
|
|
subnode.setSelected(selected, animated: false)
|
|
}
|
|
}
|
|
|
|
if let wallpaper = self.currentWallpaper, let sliderView = self.sliderView {
|
|
self.patternChanged?(wallpaper, sliderValueToIntensity(sliderView.value, allowDark: self.allowDark), false)
|
|
}
|
|
|
|
if let selectedNode = selectedNode {
|
|
self.scrollToNode(selectedNode)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func scrollToNode(_ node: SettingsThemeWallpaperNode, animated: Bool = false) {
|
|
let bounds = self.scrollNode.view.bounds
|
|
let frame = node.frame.insetBy(dx: -48.0, dy: 0.0)
|
|
|
|
if frame.minX < bounds.minX || frame.maxX > bounds.maxX {
|
|
let transition: ContainedViewLayoutTransition = animated ? .animated(duration: 0.3, curve: .easeInOut) : .immediate
|
|
|
|
var origin = CGPoint()
|
|
if frame.minX < bounds.minX {
|
|
origin.x = max(0.0, frame.minX)
|
|
} else if frame.maxX > bounds.maxX {
|
|
origin.x = min(self.scrollNode.view.contentSize.width - bounds.width, frame.maxX - bounds.width)
|
|
}
|
|
|
|
transition.updateBounds(node: self.scrollNode, bounds: CGRect(origin: origin, size: self.scrollNode.frame.size))
|
|
}
|
|
}
|
|
|
|
func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
|
|
self.validLayout = size
|
|
|
|
transition.updateFrame(node: self.backgroundNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height))
|
|
self.backgroundNode.update(size: self.backgroundNode.bounds.size, transition: transition)
|
|
transition.updateFrame(node: self.topSeparatorNode, frame: CGRect(x: 0.0, y: 0.0, width: size.width, height: UIScreenPixel))
|
|
|
|
let titleSize = self.titleNode.updateLayout(self.bounds.size)
|
|
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: floor((self.bounds.width - titleSize.width) / 2.0), y: 19.0), size: titleSize))
|
|
|
|
let scrollViewFrame = CGRect(x: 0.0, y: 52.0, width: size.width, height: 114.0)
|
|
transition.updateFrame(node: self.scrollNode, frame: scrollViewFrame)
|
|
|
|
let labelSize = self.labelNode.updateLayout(self.bounds.size)
|
|
var combinedHeight = labelSize.height + 34.0
|
|
|
|
var originY: CGFloat = scrollViewFrame.maxY + floor((size.height - scrollViewFrame.maxY - combinedHeight) / 2.0)
|
|
transition.updateFrame(node: self.labelNode, frame: CGRect(origin: CGPoint(x: 14.0, y: originY), size: labelSize))
|
|
|
|
self.sliderView?.frame = CGRect(origin: CGPoint(x: 15.0, y: originY + 8.0), size: CGSize(width: size.width - 15.0 * 2.0, height: 44.0))
|
|
|
|
self.layoutItemNodes(transition: transition)
|
|
}
|
|
|
|
private func layoutItemNodes(transition: ContainedViewLayoutTransition) {
|
|
var offset: CGFloat = 12.0
|
|
if let subnodes = self.scrollNode.subnodes {
|
|
for node in subnodes {
|
|
transition.updateFrame(node: node, frame: CGRect(x: offset, y: 12.0, width: itemSize.width, height: itemSize.height))
|
|
offset += inset + itemSize.width
|
|
}
|
|
}
|
|
}
|
|
}
|