Ilya Laktyushin 2df32e47e2 Various fixes
2024-02-01 04:07:15 +04:00

396 lines
20 KiB
Swift

import Foundation
import UIKit
import AsyncDisplayKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import AccountContext
import RadialStatusNode
import WallpaperResources
import GradientBackground
import StickerResources
import AnimatedStickerNode
import TelegramAnimatedStickerNode
private func whiteColorImage(theme: PresentationTheme, color: UIColor) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
return .single({ arguments in
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
return nil
}
context.withFlippedContext { c in
c.setFillColor(color.cgColor)
c.fill(CGRect(origin: CGPoint(), size: arguments.drawingSize))
let lineWidth: CGFloat = 1.0
c.setLineWidth(lineWidth)
c.setStrokeColor(theme.list.controlSecondaryColor.cgColor)
c.stroke(CGRect(origin: CGPoint(), size: arguments.drawingSize).insetBy(dx: lineWidth / 2.0, dy: lineWidth / 2.0))
}
return context
})
}
private let blackColorImage: UIImage? = {
guard let context = DrawingContext(size: CGSize(width: 1.0, height: 1.0), scale: 1.0, opaque: true, clear: false) else {
return nil
}
context.withContext { c in
c.setFillColor(UIColor.black.cgColor)
c.fill(CGRect(origin: CGPoint(), size: CGSize(width: 1.0, height: 1.0)))
}
return context.generateImage()
}()
public final class SettingsThemeWallpaperNode: ASDisplayNode {
public var wallpaper: TelegramWallpaper?
private var arguments: PatternWallpaperArguments?
private var emojiFile: TelegramMediaFile?
public let buttonNode = HighlightTrackingButtonNode()
public let backgroundNode = ASImageNode()
public let imageNode = TransformImageNode()
private var gradientNode: GradientBackgroundNode?
private let statusNode: RadialStatusNode
private let emojiContainerNode: ASDisplayNode
private let emojiImageNode: TransformImageNode
private var animatedStickerNode: AnimatedStickerNode?
private let stickerFetchedDisposable = MetaDisposable()
public var pressed: (() -> Void)?
private let displayLoading: Bool
private var isSelected: Bool = false
private var isLoaded: Bool = false
private let isLoadedDisposable = MetaDisposable()
public init(displayLoading: Bool = false, overlayBackgroundColor: UIColor = UIColor(white: 0.0, alpha: 0.3)) {
self.displayLoading = displayLoading
self.imageNode.contentAnimations = [.subsequentUpdates]
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.2), enableBlur: true)
let progressDiameter: CGFloat = 50.0
self.statusNode.frame = CGRect(x: 0.0, y: 0.0, width: progressDiameter, height: progressDiameter)
self.statusNode.isUserInteractionEnabled = false
self.emojiContainerNode = ASDisplayNode()
self.emojiContainerNode.isUserInteractionEnabled = false
self.emojiImageNode = TransformImageNode()
super.init()
self.addSubnode(self.backgroundNode)
self.addSubnode(self.imageNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.statusNode)
self.addSubnode(self.emojiContainerNode)
// self.emojiContainerNode.addSubnode(self.emojiNode)
self.emojiContainerNode.addSubnode(self.emojiImageNode)
self.buttonNode.addTarget(self, action: #selector(self.buttonPressed), forControlEvents: .touchUpInside)
var firstTime = true
self.emojiImageNode.imageUpdated = { [weak self] image in
guard let strongSelf = self else {
return
}
if image != nil {
strongSelf.removePlaceholder(animated: !firstTime)
if firstTime {
strongSelf.emojiImageNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
firstTime = false
}
}
deinit {
self.isLoadedDisposable.dispose()
self.stickerFetchedDisposable.dispose()
}
private func removePlaceholder(animated: Bool) {
}
public func setSelected(_ selected: Bool, animated: Bool = false) {
if self.isSelected != selected {
self.isSelected = selected
self.updateStatus(animated: animated)
}
}
private func updateIsLoaded(isLoaded: Bool, animated: Bool) {
if self.isLoaded != isLoaded {
self.isLoaded = isLoaded
self.updateStatus(animated: animated)
}
}
private func updateStatus(animated: Bool) {
if self.isSelected {
if self.isLoaded || !displayLoading {
self.statusNode.transitionToState(.check(.white), animated: animated, completion: {})
} else {
self.statusNode.transitionToState(.progress(color: .white, lineWidth: nil, value: nil, cancelEnabled: false, animateRotation: true), animated: animated, completion: {})
}
} else {
self.statusNode.transitionToState(.none, animated: animated, completion: {})
}
}
public func setOverlayBackgroundColor(_ color: UIColor) {
self.statusNode.backgroundNodeColor = color
}
public func setWallpaper(context: AccountContext, theme: PresentationTheme? = nil, wallpaper: TelegramWallpaper, isEmpty: Bool = false, emojiFile: TelegramMediaFile? = nil, selected: Bool, size: CGSize, cornerRadius: CGFloat = 0.0, synchronousLoad: Bool = false) {
self.buttonNode.frame = CGRect(origin: CGPoint(), size: size)
self.backgroundNode.frame = CGRect(origin: CGPoint(), size: size)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
var colors: [UInt32] = []
var intensity: CGFloat = 0.5
if case let .gradient(gradient) = wallpaper {
colors = gradient.colors
} else if case let .file(file) = wallpaper {
colors = file.settings.colors
intensity = CGFloat(file.settings.intensity ?? 50) / 100.0
} else if case let .color(color) = wallpaper {
colors = [color]
}
let isBlack = UIColor.average(of: colors.map(UIColor.init(rgb:))).hsb.b <= 0.01
if colors.count >= 3 {
if let gradientNode = self.gradientNode {
gradientNode.updateColors(colors: colors.map { UIColor(rgb: $0) })
} else {
let gradientNode = createGradientBackgroundNode()
gradientNode.isUserInteractionEnabled = false
self.gradientNode = gradientNode
gradientNode.updateColors(colors: colors.map { UIColor(rgb: $0) })
self.insertSubnode(gradientNode, belowSubnode: self.imageNode)
}
if intensity < 0.0 {
self.imageNode.layer.compositingFilter = nil
} else {
if isBlack {
self.imageNode.layer.compositingFilter = nil
} else {
self.imageNode.layer.compositingFilter = "softLightBlendMode"
}
}
self.backgroundNode.image = nil
} else {
if let gradientNode = self.gradientNode {
self.gradientNode = nil
gradientNode.removeFromSupernode()
}
if intensity < 0.0 {
self.imageNode.layer.compositingFilter = nil
} else {
if isBlack {
self.imageNode.layer.compositingFilter = nil
} else {
self.imageNode.layer.compositingFilter = "softLightBlendMode"
}
}
if colors.count >= 2 {
self.backgroundNode.image = generateGradientImage(size: CGSize(width: 80.0, height: 80.0), colors: colors.map(UIColor.init(rgb:)), locations: [0.0, 1.0], direction: .vertical)
self.backgroundNode.backgroundColor = nil
} else if colors.count >= 1 {
self.backgroundNode.image = nil
self.backgroundNode.backgroundColor = UIColor(rgb: colors[0])
}
}
if isEmpty, let theme {
self.backgroundNode.image = nil
self.backgroundNode.backgroundColor = theme.list.mediaPlaceholderColor
}
if let gradientNode = self.gradientNode {
gradientNode.frame = CGRect(origin: CGPoint(), size: size)
gradientNode.updateLayout(size: size, transition: .immediate, extendAnimation: false, backwards: false, completion: {})
}
let progressDiameter: CGFloat = 50.0
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - progressDiameter) / 2.0), y: floorToScreenPixels((size.height - progressDiameter) / 2.0), width: progressDiameter, height: progressDiameter)
let corners = ImageCorners(radius: cornerRadius)
if self.wallpaper != wallpaper && !isEmpty {
self.wallpaper = wallpaper
switch wallpaper {
case .builtin:
self.imageNode.alpha = 1.0
self.imageNode.setSignal(settingsBuiltinWallpaperImage(account: context.account, thumbnail: true))
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
self.isLoadedDisposable.set(nil)
self.updateIsLoaded(isLoaded: true, animated: false)
case let .image(representations, _):
let convertedRepresentations: [ImageRepresentationWithReference] = representations.map({ ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: nil, resource: $0.resource)) })
self.imageNode.alpha = 1.0
self.imageNode.setSignal(wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, synchronousLoad: synchronousLoad))
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: largestImageRepresentation(representations)!.dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
self.isLoadedDisposable.set(nil)
self.updateIsLoaded(isLoaded: true, animated: false)
case let .file(file):
let convertedRepresentations : [ImageRepresentationWithReference] = file.file.previewRepresentations.map {
ImageRepresentationWithReference(representation: $0, reference: .wallpaper(wallpaper: .slug(file.slug), resource: $0.resource))
}
let fullDimensions = file.file.dimensions ?? PixelDimensions(width: 2000, height: 4000)
let convertedFullRepresentations = [ImageRepresentationWithReference(representation: .init(dimensions: fullDimensions, resource: file.file.resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false), reference: .wallpaper(wallpaper: .slug(file.slug), resource: file.file.resource))]
let imageSignal: Signal<(TransformImageArguments) -> DrawingContext?, NoError>
if wallpaper.isPattern {
var patternIntensity: CGFloat = 0.5
if !file.settings.colors.isEmpty {
if let intensity = file.settings.intensity {
patternIntensity = CGFloat(intensity) / 100.0
}
}
if patternIntensity < 0.0 {
self.imageNode.alpha = 1.0
self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: UIColor(white: 0.0, alpha: 1.0 + patternIntensity))
} else {
self.imageNode.alpha = CGFloat(file.settings.intensity ?? 50) / 100.0
let isLight = UIColor.average(of: file.settings.colors.map(UIColor.init(rgb:))).hsb.b > 0.3
self.arguments = PatternWallpaperArguments(colors: [.clear], rotation: nil, customPatternColor: isLight ? .black : .white)
}
imageSignal = patternWallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, representations: convertedRepresentations, mode: .thumbnail, autoFetchFullSize: true)
|> mapToSignal { value -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> in
if let value = value {
return .single(value)
} else {
return .complete()
}
}
let anyStatus = combineLatest(queue: .mainQueue(),
context.account.postbox.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true),
context.sharedContext.accountManager.mediaBox.resourceStatus(convertedFullRepresentations[0].reference.resource, approximateSynchronousValue: true)
)
|> map { a, b -> Bool in
switch a {
case .Local:
return true
default:
break
}
switch b {
case .Local:
return true
default:
break
}
return false
}
|> distinctUntilChanged
self.updateIsLoaded(isLoaded: false, animated: false)
self.isLoadedDisposable.set((anyStatus
|> deliverOnMainQueue).start(next: { [weak self] value in
guard let strongSelf = self else {
return
}
strongSelf.updateIsLoaded(isLoaded: value, animated: true)
}))
} else {
self.imageNode.alpha = 1.0
imageSignal = wallpaperImage(account: context.account, accountManager: context.sharedContext.accountManager, fileReference: .standalone(media: file.file), representations: convertedRepresentations, thumbnail: true, autoFetchFullSize: true, blurred: file.settings.blur, synchronousLoad: synchronousLoad)
self.updateIsLoaded(isLoaded: true, animated: false)
self.isLoadedDisposable.set(nil)
}
self.imageNode.setSignal(imageSignal, attemptSynchronously: synchronousLoad)
let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100)
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: self.arguments))
apply()
default:
break
}
} else if let wallpaper = self.wallpaper {
switch wallpaper {
case .builtin, .color, .gradient, .emoticon:
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: CGSize(), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
case let .image(representations, _):
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: largestImageRepresentation(representations)!.dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets()))
apply()
case let .file(file):
let dimensions = file.file.dimensions ?? PixelDimensions(width: 100, height: 100)
let apply = self.imageNode.asyncLayout()(TransformImageArguments(corners: corners, imageSize: dimensions.cgSize.aspectFilled(size), boundingSize: size, intrinsicInsets: UIEdgeInsets(), custom: self.arguments))
apply()
}
}
self.setSelected(selected, animated: false)
self.emojiContainerNode.frame = self.backgroundNode.frame
var emojiFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - 42.0) / 2.0), y: 98.0), size: CGSize(width: 42.0, height: 42.0))
if isEmpty {
emojiFrame = emojiFrame.insetBy(dx: 3.0, dy: 3.0)
}
if let file = emojiFile, self.emojiFile?.id != emojiFile?.id {
self.emojiFile = file
let imageApply = self.emojiImageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: emojiFrame.size, boundingSize: emojiFrame.size, intrinsicInsets: UIEdgeInsets()))
imageApply()
self.emojiImageNode.setSignal(chatMessageStickerPackThumbnail(postbox: context.account.postbox, resource: file.resource, animated: true, nilIfEmpty: true))
self.emojiImageNode.frame = emojiFrame
let animatedStickerNode: AnimatedStickerNode
if let current = self.animatedStickerNode {
animatedStickerNode = current
} else {
animatedStickerNode = DefaultAnimatedStickerNodeImpl()
animatedStickerNode.started = { [weak self] in
self?.emojiImageNode.isHidden = true
}
self.animatedStickerNode = animatedStickerNode
self.emojiContainerNode.addSubnode(animatedStickerNode)
let pathPrefix = context.account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id)
animatedStickerNode.setup(source: AnimatedStickerResourceSource(account: context.account, resource: file.resource), width: 128, height: 128, playbackMode: .still(.start), mode: .direct(cachePathPrefix: pathPrefix))
animatedStickerNode.anchorPoint = CGPoint(x: 0.5, y: 1.0)
}
animatedStickerNode.autoplay = true
animatedStickerNode.visibility = true
self.stickerFetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: MediaResourceReference.media(media: .standalone(media: file), resource: file.resource)).start())
// let thumbnailDimensions = PixelDimensions(width: 512, height: 512)
// self.placeholderNode.update(backgroundColor: nil, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shimmeringColor: UIColor(rgb: 0xffffff, alpha: 0.3), data: file.immediateThumbnailData, size: emojiFrame.size, enableEffect: item.context.sharedContext.energyUsageSettings.fullTranslucency, imageSize: thumbnailDimensions.cgSize)
// self.placeholderNode.frame = emojiFrame
}
if let animatedStickerNode = self.animatedStickerNode {
animatedStickerNode.frame = emojiFrame
animatedStickerNode.updateLayout(size: emojiFrame.size)
}
}
@objc func buttonPressed() {
self.pressed?()
}
}