mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Storage improvements
This commit is contained in:
parent
dd06922e85
commit
808f5b80ff
@ -308,7 +308,7 @@ public final class CallListController: TelegramBaseController {
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = strongSelf.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
|
@ -950,7 +950,9 @@ final class ChatListFilterTabContainerNode: ASDisplayNode {
|
||||
}
|
||||
self.scrollNode.bounds = updatedBounds
|
||||
}
|
||||
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
|
||||
if abs(previousScrollBounds.minX - self.scrollNode.bounds.minX) > .ulpOfOne {
|
||||
transition.animateHorizontalOffsetAdditive(node: self.scrollNode, offset: previousScrollBounds.minX - self.scrollNode.bounds.minX)
|
||||
}
|
||||
|
||||
self.previousSelectedAbsFrame = selectedFrame.offsetBy(dx: -self.scrollNode.bounds.minX, dy: 0.0)
|
||||
self.previousSelectedFrame = selectedFrame
|
||||
|
@ -321,6 +321,19 @@ public class CheckLayer: CALayer {
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
public override init(layer: Any) {
|
||||
guard let layer = layer as? CheckLayer else {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
self.theme = layer.theme
|
||||
self.content = layer.content
|
||||
|
||||
super.init(layer: layer)
|
||||
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
public init(theme: CheckNodeTheme, content: CheckNodeContent = .check) {
|
||||
self.theme = theme
|
||||
self.content = content
|
||||
|
@ -765,4 +765,36 @@ public struct Transition {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setBackgroundColor(view: UIView, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||
self.setBackgroundColor(layer: view.layer, color: color, completion: completion)
|
||||
}
|
||||
|
||||
public func setBackgroundColor(layer: CALayer, color: UIColor, completion: ((Bool) -> Void)? = nil) {
|
||||
if let current = layer.backgroundColor, current == color.cgColor {
|
||||
completion?(true)
|
||||
return
|
||||
}
|
||||
|
||||
switch self.animation {
|
||||
case .none:
|
||||
layer.backgroundColor = color.cgColor
|
||||
completion?(true)
|
||||
case let .curve(duration, curve):
|
||||
let previousColor: CGColor = layer.backgroundColor ?? UIColor.clear.cgColor
|
||||
layer.backgroundColor = color.cgColor
|
||||
|
||||
layer.animate(
|
||||
from: previousColor,
|
||||
to: color.cgColor,
|
||||
keyPath: "backgroundColor",
|
||||
duration: duration,
|
||||
delay: 0.0,
|
||||
curve: curve,
|
||||
removeOnCompletion: true,
|
||||
additive: false,
|
||||
completion: completion
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -155,6 +155,15 @@ public final class ComponentView<EnvironmentType> {
|
||||
self.currentSize = size
|
||||
return size
|
||||
}
|
||||
|
||||
public func updateEnvironment(transition: Transition, @EnvironmentBuilder environment: () -> Environment<EnvironmentType>) -> CGSize? {
|
||||
guard let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize else {
|
||||
return nil
|
||||
}
|
||||
let size = self._update(transition: transition, component: currentComponent, maybeEnvironment: environment, updateEnvironment: true, forceUpdate: false, containerSize: currentContainerSize)
|
||||
self.currentSize = size
|
||||
return size
|
||||
}
|
||||
|
||||
private func _update(transition: Transition, component: AnyComponent<EnvironmentType>, maybeEnvironment: () -> Environment<EnvironmentType>, updateEnvironment: Bool, forceUpdate: Bool, containerSize: CGSize) -> CGSize {
|
||||
precondition(!self.isUpdating)
|
||||
|
@ -77,6 +77,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
case resetDatabaseAndCache(PresentationTheme)
|
||||
case resetHoles(PresentationTheme)
|
||||
case reindexUnread(PresentationTheme)
|
||||
case reindexCache
|
||||
case resetBiometricsData(PresentationTheme)
|
||||
case resetWebViewCache(PresentationTheme)
|
||||
case optimizeDatabase(PresentationTheme)
|
||||
@ -111,7 +112,7 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return DebugControllerSection.logging.rawValue
|
||||
case .enableRaiseToSpeak, .keepChatNavigationStack, .skipReadHistory, .crashOnSlowQueries:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases:
|
||||
case .clearTips, .crash, .resetData, .resetDatabase, .resetDatabaseAndCache, .resetHoles, .reindexUnread, .reindexCache, .resetBiometricsData, .resetWebViewCache, .optimizeDatabase, .photoPreview, .knockoutWallpaper, .playerEmbedding, .playlistPlayback, .voiceConference, .experimentalCompatibility, .enableDebugDataDisplay, .acceleratedStickers, .experimentalBackground, .inlineForums, .localTranscription, . enableReactionOverrides, .restorePurchases:
|
||||
return DebugControllerSection.experiments.rawValue
|
||||
case .preferredVideoCodec:
|
||||
return DebugControllerSection.videoExperiments.rawValue
|
||||
@ -170,40 +171,42 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
return 21
|
||||
case .reindexUnread:
|
||||
return 22
|
||||
case .resetBiometricsData:
|
||||
case .reindexCache:
|
||||
return 23
|
||||
case .resetWebViewCache:
|
||||
case .resetBiometricsData:
|
||||
return 24
|
||||
case .optimizeDatabase:
|
||||
case .resetWebViewCache:
|
||||
return 25
|
||||
case .photoPreview:
|
||||
case .optimizeDatabase:
|
||||
return 26
|
||||
case .knockoutWallpaper:
|
||||
case .photoPreview:
|
||||
return 27
|
||||
case .experimentalCompatibility:
|
||||
case .knockoutWallpaper:
|
||||
return 28
|
||||
case .enableDebugDataDisplay:
|
||||
case .experimentalCompatibility:
|
||||
return 29
|
||||
case .acceleratedStickers:
|
||||
case .enableDebugDataDisplay:
|
||||
return 30
|
||||
case .experimentalBackground:
|
||||
case .acceleratedStickers:
|
||||
return 31
|
||||
case .inlineForums:
|
||||
case .experimentalBackground:
|
||||
return 32
|
||||
case .localTranscription:
|
||||
case .inlineForums:
|
||||
return 33
|
||||
case .enableReactionOverrides:
|
||||
case .localTranscription:
|
||||
return 34
|
||||
case .restorePurchases:
|
||||
case .enableReactionOverrides:
|
||||
return 35
|
||||
case .playerEmbedding:
|
||||
case .restorePurchases:
|
||||
return 36
|
||||
case .playlistPlayback:
|
||||
case .playerEmbedding:
|
||||
return 37
|
||||
case .voiceConference:
|
||||
case .playlistPlayback:
|
||||
return 38
|
||||
case .voiceConference:
|
||||
return 39
|
||||
case let .preferredVideoCodec(index, _, _, _):
|
||||
return 39 + index
|
||||
return 40 + index
|
||||
case .disableVideoAspectScaling:
|
||||
return 100
|
||||
case .enableVoipTcp:
|
||||
@ -967,6 +970,46 @@ private enum DebugControllerEntry: ItemListNodeEntry {
|
||||
controller.dismiss()
|
||||
})
|
||||
})
|
||||
case .reindexCache:
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Reindex Cache", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
guard let context = arguments.context else {
|
||||
return
|
||||
}
|
||||
|
||||
var signal = context.engine.resources.reindexCacheInBackground(lowImpact: false)
|
||||
|
||||
var cancelImpl: (() -> Void)?
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let progressSignal = Signal<Never, NoError> { subscriber in
|
||||
let controller = OverlayStatusController(theme: presentationData.theme, type: .loading(cancelled: {
|
||||
cancelImpl?()
|
||||
}))
|
||||
arguments.presentController(controller, nil)
|
||||
return ActionDisposable { [weak controller] in
|
||||
Queue.mainQueue().async() {
|
||||
controller?.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|> runOn(Queue.mainQueue())
|
||||
|> delay(0.15, queue: Queue.mainQueue())
|
||||
let progressDisposable = progressSignal.start()
|
||||
|
||||
let reindexDisposable = MetaDisposable()
|
||||
|
||||
signal = signal
|
||||
|> afterDisposed {
|
||||
Queue.mainQueue().async {
|
||||
progressDisposable.dispose()
|
||||
}
|
||||
}
|
||||
cancelImpl = {
|
||||
reindexDisposable.set(nil)
|
||||
}
|
||||
reindexDisposable.set((signal
|
||||
|> deliverOnMainQueue).start(completed: {
|
||||
}))
|
||||
})
|
||||
case .resetBiometricsData:
|
||||
return ItemListActionItem(presentationData: presentationData, title: "Reset Biometrics Data", kind: .destructive, alignment: .natural, sectionId: self.section, style: .blocks, action: {
|
||||
let _ = updatePresentationPasscodeSettingsInteractively(accountManager: arguments.sharedContext.accountManager, { settings in
|
||||
@ -1210,6 +1253,7 @@ private func debugControllerEntries(sharedContext: SharedAccountContext, present
|
||||
entries.append(.resetHoles(presentationData.theme))
|
||||
if isMainApp {
|
||||
entries.append(.reindexUnread(presentationData.theme))
|
||||
entries.append(.reindexCache)
|
||||
entries.append(.resetWebViewCache(presentationData.theme))
|
||||
}
|
||||
entries.append(.optimizeDatabase(presentationData.theme))
|
||||
|
@ -34,6 +34,47 @@ private let completionKey = "CAAnimationUtils_completion"
|
||||
public let kCAMediaTimingFunctionSpring = "CAAnimationUtilsSpringCurve"
|
||||
public let kCAMediaTimingFunctionCustomSpringPrefix = "CAAnimationUtilsSpringCustomCurve"
|
||||
|
||||
private final class FrameRangeContext {
|
||||
private var animationCount: Int = 0
|
||||
private var displayLink: CADisplayLink?
|
||||
|
||||
init() {
|
||||
}
|
||||
|
||||
func add() {
|
||||
self.animationCount += 1
|
||||
self.update()
|
||||
}
|
||||
|
||||
func remove() {
|
||||
self.animationCount -= 1
|
||||
if self.animationCount < 0 {
|
||||
self.animationCount = 0
|
||||
assertionFailure()
|
||||
}
|
||||
self.update()
|
||||
}
|
||||
|
||||
@objc func displayEvent() {
|
||||
}
|
||||
|
||||
private func update() {
|
||||
if self.animationCount != 0 {
|
||||
if self.displayLink == nil {
|
||||
let displayLink = CADisplayLink(target: self, selector: #selector(self.displayEvent))
|
||||
self.displayLink = displayLink
|
||||
displayLink.add(to: .main, forMode: .common)
|
||||
displayLink.isPaused = false
|
||||
}
|
||||
} else if let displayLink = self.displayLink {
|
||||
self.displayLink = nil
|
||||
displayLink.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let frameRangeContext = FrameRangeContext()
|
||||
|
||||
public extension CAAnimation {
|
||||
var completion: ((Bool) -> Void)? {
|
||||
get {
|
||||
@ -56,7 +97,7 @@ private func adjustFrameRate(animation: CAAnimation) {
|
||||
if #available(iOS 15.0, *) {
|
||||
let maxFps = Float(UIScreen.main.maximumFramesPerSecond)
|
||||
if maxFps > 61.0 {
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: maxFps, maximum: maxFps, preferred: maxFps)
|
||||
animation.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: maxFps, preferred: maxFps)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -176,6 +217,8 @@ public extension CALayer {
|
||||
animationGroup.delegate = CALayerAnimationDelegate(animation: animationGroup, completion: completion)
|
||||
}
|
||||
|
||||
adjustFrameRate(animation: animationGroup)
|
||||
|
||||
self.add(animationGroup, forKey: key)
|
||||
}
|
||||
|
||||
|
@ -81,16 +81,12 @@ public final class ConstantDisplayLinkAnimator {
|
||||
guard let displayLink = self.displayLink else {
|
||||
return
|
||||
}
|
||||
if #available(iOS 10.0, *) {
|
||||
let preferredFramesPerSecond: Int
|
||||
if self.frameInterval == 1 {
|
||||
preferredFramesPerSecond = 60
|
||||
} else {
|
||||
preferredFramesPerSecond = 30
|
||||
if self.frameInterval == 1 {
|
||||
if #available(iOS 15.0, *) {
|
||||
self.displayLink?.preferredFrameRateRange = CAFrameRateRange(minimum: 60.0, maximum: 120.0, preferred: 120.0)
|
||||
}
|
||||
displayLink.preferredFramesPerSecond = preferredFramesPerSecond
|
||||
} else {
|
||||
displayLink.frameInterval = self.frameInterval
|
||||
displayLink.preferredFramesPerSecond = 30
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -55,7 +55,7 @@ public func representationFetchRangeForDisplayAtSize(representation: TelegramMed
|
||||
return nil
|
||||
}
|
||||
|
||||
public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false) -> Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError> {
|
||||
public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 1280.0, height: 1280.0), autoFetchFullSize: Bool = false, tryAdditionalRepresentations: Bool = false, synchronousLoad: Bool = false, useMiniThumbnailIfAvailable: Bool = false, forceThumbnail: Bool = false, automaticFetch: Bool = true) -> Signal<Tuple4<Data?, Data?, ChatMessagePhotoQuality, Bool>, NoError> {
|
||||
if !forceThumbnail, let progressiveRepresentation = progressiveImageRepresentation(photoReference.media.representations), progressiveRepresentation.progressiveSizes.count > 1 {
|
||||
enum SizeSource {
|
||||
case miniThumbnail(data: Data)
|
||||
@ -129,10 +129,12 @@ public func chatMessagePhotoDatas(postbox: Postbox, userLocation: MediaResourceU
|
||||
}
|
||||
})
|
||||
var fetchDisposable: Disposable?
|
||||
if autoFetchFullSize {
|
||||
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start()
|
||||
} else if useMiniThumbnailIfAvailable {
|
||||
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start()
|
||||
if automaticFetch {
|
||||
if autoFetchFullSize {
|
||||
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(largestByteSize), .default), statsCategory: .image).start()
|
||||
} else if useMiniThumbnailIfAvailable {
|
||||
fetchDisposable = fetchedMediaResource(mediaBox: postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: photoReference.resourceReference(progressiveRepresentation.resource), range: (0 ..< Int64(thumbnailByteSize), .default), statsCategory: .image).start()
|
||||
}
|
||||
}
|
||||
|
||||
return ActionDisposable {
|
||||
@ -1353,7 +1355,7 @@ public func avatarGalleryThumbnailPhoto(account: Account, representations: [Imag
|
||||
}
|
||||
}
|
||||
|
||||
public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceUserLocation, photoReference: ImageMediaReference, fullRepresentationSize: CGSize = CGSize(width: 127.0, height: 127.0), blurred: Bool = false, synchronousLoad: Bool = false, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let useMiniThumbnailIfAvailable: Bool = fullRepresentationSize.width < 40.0
|
||||
var updatedFullRepresentationSize = fullRepresentationSize
|
||||
if useMiniThumbnailIfAvailable, let largest = largestImageRepresentation(photoReference.media.representations) {
|
||||
@ -1361,7 +1363,7 @@ public func mediaGridMessagePhoto(account: Account, userLocation: MediaResourceU
|
||||
updatedFullRepresentationSize = largest.dimensions.cgSize
|
||||
}
|
||||
}
|
||||
let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred)
|
||||
let signal = chatMessagePhotoDatas(postbox: account.postbox, userLocation: userLocation, photoReference: photoReference, fullRepresentationSize: updatedFullRepresentationSize, autoFetchFullSize: true, tryAdditionalRepresentations: useMiniThumbnailIfAvailable, synchronousLoad: synchronousLoad, useMiniThumbnailIfAvailable: useMiniThumbnailIfAvailable, forceThumbnail: blurred, automaticFetch: automaticFetch)
|
||||
|
||||
return signal
|
||||
|> map { value in
|
||||
@ -1468,7 +1470,7 @@ public func gifPaneVideoThumbnail(account: Account, videoReference: FileMediaRef
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
})
|
||||
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .gif, reference: videoReference.resourceReference(thumbnailResource)).start()
|
||||
let fetched = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .other, reference: videoReference.resourceReference(thumbnailResource)).start()
|
||||
return ActionDisposable {
|
||||
data.dispose()
|
||||
fetched.dispose()
|
||||
@ -1847,10 +1849,10 @@ public func chatMessageWebFileCancelInteractiveFetch(account: Account, image: Te
|
||||
return account.postbox.mediaBox.cancelInteractiveResourceFetch(image.resource)
|
||||
}
|
||||
|
||||
public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource) -> Signal<Data?, NoError> {
|
||||
public func chatWebpageSnippetFileData(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, resource: MediaResource, automaticFetch: Bool = true) -> Signal<Data?, NoError> {
|
||||
let resourceData = account.postbox.mediaBox.resourceData(resource)
|
||||
|> map { next in
|
||||
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe)
|
||||
|> map { next in
|
||||
return next.size == 0 ? nil : try? Data(contentsOf: URL(fileURLWithPath: next.path), options: .mappedIfSafe)
|
||||
}
|
||||
|
||||
return Signal { subscriber in
|
||||
@ -1861,7 +1863,16 @@ public func chatWebpageSnippetFileData(account: Account, userLocation: MediaReso
|
||||
}, completed: {
|
||||
subscriber.putCompletion()
|
||||
}))
|
||||
disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: .image, reference: mediaReference.resourceReference(resource)).start())
|
||||
if automaticFetch {
|
||||
var userContentType: MediaResourceUserContentType = .other
|
||||
if let file = mediaReference.media as? TelegramMediaFile {
|
||||
userContentType = MediaResourceUserContentType(file: file)
|
||||
} else if let _ = mediaReference.media as? TelegramMediaImage {
|
||||
userContentType = .image
|
||||
}
|
||||
|
||||
disposable.add(fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: userLocation, userContentType: userContentType, reference: mediaReference.resourceReference(resource)).start())
|
||||
}
|
||||
return disposable
|
||||
}
|
||||
}
|
||||
@ -1889,8 +1900,8 @@ public func chatWebpageSnippetPhotoData(account: Account, userLocation: MediaRes
|
||||
}
|
||||
}
|
||||
|
||||
public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource)
|
||||
public func chatWebpageSnippetFile(account: Account, userLocation: MediaResourceUserLocation, mediaReference: AnyMediaReference, representation: TelegramMediaImageRepresentation, automaticFetch: Bool = true) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> {
|
||||
let signal = chatWebpageSnippetFileData(account: account, userLocation: userLocation, mediaReference: mediaReference, resource: representation.resource, automaticFetch: automaticFetch)
|
||||
|
||||
return signal |> map { fullSizeData in
|
||||
return { arguments in
|
||||
@ -1904,7 +1915,42 @@ public func chatWebpageSnippetFile(account: Account, userLocation: MediaResource
|
||||
}
|
||||
}
|
||||
|
||||
if let fullSizeImage = fullSizeImage {
|
||||
var blurredImage: UIImage?
|
||||
if fullSizeImage == nil {
|
||||
var immediateThumbnailData: Data?
|
||||
if let file = mediaReference.media as? TelegramMediaFile {
|
||||
immediateThumbnailData = file.immediateThumbnailData
|
||||
} else if let image = mediaReference.media as? TelegramMediaImage {
|
||||
immediateThumbnailData = image.immediateThumbnailData
|
||||
}
|
||||
|
||||
if let decodedThumbnailData = immediateThumbnailData.flatMap(decodeTinyThumbnail), let imageSource = CGImageSourceCreateWithData(decodedThumbnailData as CFData, nil), let image = CGImageSourceCreateImageAtIndex(imageSource, 0, nil) {
|
||||
let thumbnailSize = CGSize(width: image.width, height: image.height)
|
||||
let thumbnailContextSize = thumbnailSize.aspectFilled(CGSize(width: 20.0, height: 20.0))
|
||||
if let thumbnailContext = DrawingContext(size: thumbnailContextSize, scale: 1.0) {
|
||||
thumbnailContext.withFlippedContext { c in
|
||||
c.interpolationQuality = .none
|
||||
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContextSize))
|
||||
}
|
||||
imageFastBlur(Int32(thumbnailContextSize.width), Int32(thumbnailContextSize.height), Int32(thumbnailContext.bytesPerRow), thumbnailContext.bytes)
|
||||
|
||||
let thumbnailContext2Size = thumbnailSize.aspectFitted(CGSize(width: 100.0, height: 100.0))
|
||||
if let thumbnailContext2 = DrawingContext(size: thumbnailContext2Size, scale: 1.0) {
|
||||
thumbnailContext2.withFlippedContext { c in
|
||||
c.interpolationQuality = .none
|
||||
if let image = thumbnailContext.generateImage()?.cgImage {
|
||||
c.draw(image, in: CGRect(origin: CGPoint(), size: thumbnailContext2Size))
|
||||
}
|
||||
}
|
||||
imageFastBlur(Int32(thumbnailContext2Size.width), Int32(thumbnailContext2Size.height), Int32(thumbnailContext2.bytesPerRow), thumbnailContext2.bytes)
|
||||
|
||||
blurredImage = thumbnailContext2.generateImage()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let fullSizeImage = fullSizeImage ?? (blurredImage?.cgImage) {
|
||||
guard let context = DrawingContext(size: arguments.drawingSize, clear: true) else {
|
||||
return nil
|
||||
}
|
||||
@ -2455,9 +2501,9 @@ private func avatarGalleryPhotoDatas(account: Account, fileReference: FileMediaR
|
||||
if let _ = decodedThumbnailData {
|
||||
fetchedThumbnail = .complete()
|
||||
} else {
|
||||
fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[smallestIndex].reference)
|
||||
fetchedThumbnail = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[smallestIndex].reference)
|
||||
}
|
||||
let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: representations[largestIndex].reference)
|
||||
let fetchedFullSize = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: representations[largestIndex].reference)
|
||||
|
||||
let thumbnail = Signal<Data?, NoError> { subscriber in
|
||||
if let decodedThumbnailData = decodedThumbnailData {
|
||||
|
@ -113,7 +113,7 @@ private class GifVideoLayer: AVSampleBufferDisplayLayer {
|
||||
guard let file = self.file else {
|
||||
return
|
||||
}
|
||||
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .gif, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
|
||||
let frameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: self.userLocation, userContentType: .other, fileReference: .savedGif(media: file), layerHolder: nil, layer: self)
|
||||
self.frameManager = frameManager
|
||||
frameManager.started = { [weak self] in
|
||||
guard let strongSelf = self else {
|
||||
|
@ -464,7 +464,7 @@ public final class MultiplexedVideoNode: ASDisplayNode, UIScrollViewDelegate {
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
layerHolder.layer.frame = item.frame
|
||||
self.scrollNode.layer.addSublayer(layerHolder.layer)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .gif, fileReference: item.file.file, layerHolder: layerHolder)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: item.file.file, layerHolder: layerHolder)
|
||||
self.visibleLayers[item.id] = (manager, layerHolder)
|
||||
self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
|
||||
if let strongSelf = self {
|
||||
|
@ -31,6 +31,8 @@ swift_library(
|
||||
"//submodules/ContextUI",
|
||||
"//submodules/AnimatedAvatarSetNode",
|
||||
"//submodules/AvatarNode",
|
||||
"//submodules/PhotoResources",
|
||||
"//submodules/SemanticStatusNode",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
@ -13,15 +13,88 @@ import MultilineTextComponent
|
||||
import EmojiStatusComponent
|
||||
import Postbox
|
||||
|
||||
private func interpolateChartData(start: PieChartComponent.ChartData, end: PieChartComponent.ChartData, progress: CGFloat) -> PieChartComponent.ChartData {
|
||||
if start.items.count != end.items.count {
|
||||
return start
|
||||
}
|
||||
|
||||
var result = end
|
||||
for i in 0 ..< result.items.count {
|
||||
result.items[i].value = (1.0 - progress) * start.items[i].value + progress * end.items[i].value
|
||||
result.items[i].color = start.items[i].color.interpolateTo(end.items[i].color, fraction: progress) ?? end.items[i].color
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private func processChartData(data: PieChartComponent.ChartData) -> PieChartComponent.ChartData {
|
||||
var data = data
|
||||
|
||||
let minValue: Double = 0.01
|
||||
|
||||
var totalSum: CGFloat = 0.0
|
||||
for i in 0 ..< data.items.count {
|
||||
if data.items[i].value > 0.00001 {
|
||||
data.items[i].value = max(data.items[i].value, minValue)
|
||||
}
|
||||
totalSum += data.items[i].value
|
||||
}
|
||||
|
||||
var hasOneItem = false
|
||||
for i in 0 ..< data.items.count {
|
||||
if data.items[i].value != 0 && totalSum == data.items[i].value {
|
||||
data.items[i].value = 1.0
|
||||
hasOneItem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasOneItem {
|
||||
if abs(totalSum - 1.0) > 0.0001 {
|
||||
let deltaValue = totalSum - 1.0
|
||||
|
||||
var availableSum: Double = 0.0
|
||||
for i in 0 ..< data.items.count {
|
||||
let itemValue = data.items[i].value
|
||||
let availableItemValue = max(0.0, itemValue - minValue)
|
||||
if availableItemValue > 0.0 {
|
||||
availableSum += availableItemValue
|
||||
}
|
||||
}
|
||||
totalSum = 0.0
|
||||
let itemFraction = deltaValue / availableSum
|
||||
for i in 0 ..< data.items.count {
|
||||
let itemValue = data.items[i].value
|
||||
let availableItemValue = max(0.0, itemValue - minValue)
|
||||
if availableItemValue > 0.0 {
|
||||
let itemDelta = availableItemValue * itemFraction
|
||||
data.items[i].value -= itemDelta
|
||||
}
|
||||
totalSum += data.items[i].value
|
||||
}
|
||||
}
|
||||
|
||||
if totalSum > 0.0 && totalSum < 1.0 - 0.0001 {
|
||||
for i in 0 ..< data.items.count {
|
||||
data.items[i].value /= totalSum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
|
||||
final class PieChartComponent: Component {
|
||||
struct ChartData: Equatable {
|
||||
struct Item: Equatable {
|
||||
var id: AnyHashable
|
||||
var displayValue: Double
|
||||
var value: Double
|
||||
var color: UIColor
|
||||
|
||||
init(id: AnyHashable, value: Double, color: UIColor) {
|
||||
init(id: AnyHashable, displayValue: Double, value: Double, color: UIColor) {
|
||||
self.id = id
|
||||
self.displayValue = displayValue
|
||||
self.value = value
|
||||
self.color = color
|
||||
}
|
||||
@ -55,96 +128,98 @@ final class PieChartComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private var shapeLayers: [AnyHashable: SimpleShapeLayer] = [:]
|
||||
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
var selectedKey: AnyHashable?
|
||||
private final class ChartDataView: UIView {
|
||||
private(set) var theme: PresentationTheme?
|
||||
private(set) var data: ChartData?
|
||||
private(set) var selectedKey: AnyHashable?
|
||||
|
||||
private weak var state: EmptyComponentState?
|
||||
private var currentAnimation: (start: ChartData, end: ChartData, current: ChartData, progress: CGFloat)?
|
||||
private var animator: DisplayLinkAnimator?
|
||||
|
||||
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
self.backgroundColor = nil
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self)
|
||||
for (key, layer) in self.shapeLayers {
|
||||
if layer.frame.contains(point), let path = layer.path {
|
||||
if path.contains(self.layer.convert(point, to: layer)) {
|
||||
if self.selectedKey == key {
|
||||
self.selectedKey = nil
|
||||
} else {
|
||||
self.selectedKey = key
|
||||
}
|
||||
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
|
||||
break
|
||||
}
|
||||
deinit {
|
||||
self.animator?.invalidate()
|
||||
}
|
||||
|
||||
func setItems(theme: PresentationTheme, data: ChartData, selectedKey: AnyHashable?, animated: Bool) {
|
||||
let data = processChartData(data: data)
|
||||
|
||||
if self.theme !== theme || self.data != data || self.selectedKey != selectedKey {
|
||||
self.theme = theme
|
||||
self.selectedKey = selectedKey
|
||||
|
||||
if animated, let previous = self.data {
|
||||
var initialState = previous
|
||||
if let currentAnimation = self.currentAnimation {
|
||||
initialState = currentAnimation.current
|
||||
}
|
||||
self.currentAnimation = (initialState, data, initialState, 0.0)
|
||||
self.animator?.invalidate()
|
||||
self.animator = DisplayLinkAnimator(duration: 0.4, from: 0.0, to: 1.0, update: { [weak self] progress in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
let progress = listViewAnimationCurveSystem(progress)
|
||||
if let currentAnimationValue = self.currentAnimation {
|
||||
self.currentAnimation = (currentAnimationValue.start, currentAnimationValue.end, interpolateChartData(start: currentAnimationValue.start, end: currentAnimationValue.end, progress: progress), progress)
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}, completion: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.currentAnimation = nil
|
||||
self.setNeedsDisplay()
|
||||
})
|
||||
}
|
||||
|
||||
self.data = data
|
||||
|
||||
self.setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.state = state
|
||||
override func draw(_ rect: CGRect) {
|
||||
guard let context = UIGraphicsGetCurrentContext() else {
|
||||
return
|
||||
}
|
||||
guard let theme = self.theme, let data = self.currentAnimation?.current ?? self.data else {
|
||||
return
|
||||
}
|
||||
if data.items.isEmpty {
|
||||
return
|
||||
}
|
||||
|
||||
let innerDiameter: CGFloat = 100.0
|
||||
let spacing: CGFloat = 2.0
|
||||
let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
|
||||
let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5)
|
||||
//let minAngle: CGFloat = innerAngleSpacing * 2.0 + 2.0 / (innerDiameter * 0.5)
|
||||
|
||||
var valueSum: Double = 0.0
|
||||
for item in component.chartData.items {
|
||||
valueSum += item.value
|
||||
}
|
||||
var angles: [Double] = []
|
||||
var totalAngle: Double = 0.0
|
||||
for i in 0 ..< component.chartData.items.count {
|
||||
let item = component.chartData.items[i]
|
||||
var angle = item.value / valueSum * CGFloat.pi * 2.0
|
||||
if angle > .ulpOfOne {
|
||||
if angle < minAngle {
|
||||
angle = minAngle
|
||||
}
|
||||
totalAngle += angle
|
||||
}
|
||||
for i in 0 ..< data.items.count {
|
||||
let item = data.items[i]
|
||||
let angle = item.value * CGFloat.pi * 2.0
|
||||
angles.append(angle)
|
||||
}
|
||||
if totalAngle > CGFloat.pi * 2.0 {
|
||||
let deltaAngle = totalAngle - CGFloat.pi * 2.0
|
||||
|
||||
var availableAngleSum: Double = 0.0
|
||||
for i in 0 ..< angles.count {
|
||||
let itemAngle = angles[i]
|
||||
let availableItemAngle = max(0.0, itemAngle - minAngle)
|
||||
if availableItemAngle > 0.0 {
|
||||
availableAngleSum += availableItemAngle
|
||||
}
|
||||
}
|
||||
let itemFraction = deltaAngle / availableAngleSum
|
||||
for i in 0 ..< angles.count {
|
||||
let availableItemAngle = max(0.0, angles[i] - minAngle)
|
||||
if availableItemAngle > 0.0 {
|
||||
let itemDelta = availableItemAngle * itemFraction
|
||||
angles[i] -= itemDelta
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let diameter: CGFloat = 200.0
|
||||
let reducedDiameter: CGFloat = 170.0
|
||||
|
||||
var startAngle: CGFloat = 0.0
|
||||
for i in 0 ..< component.chartData.items.count {
|
||||
let item = component.chartData.items[i]
|
||||
for i in 0 ..< data.items.count {
|
||||
let item = data.items[i]
|
||||
|
||||
let itemOuterDiameter: CGFloat
|
||||
if let selectedKey = self.selectedKey {
|
||||
@ -157,21 +232,10 @@ final class PieChartComponent: Component {
|
||||
itemOuterDiameter = diameter
|
||||
}
|
||||
|
||||
let shapeLayerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.0), y: 0.0), size: CGSize(width: diameter, height: diameter))
|
||||
let shapeLayerFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: diameter, height: diameter))
|
||||
|
||||
let angleSpacing: CGFloat = spacing / (itemOuterDiameter * 0.5)
|
||||
|
||||
let shapeLayer: SimpleShapeLayer
|
||||
if let current = self.shapeLayers[item.id] {
|
||||
shapeLayer = current
|
||||
} else {
|
||||
shapeLayer = SimpleShapeLayer()
|
||||
self.shapeLayers[item.id] = shapeLayer
|
||||
self.layer.insertSublayer(shapeLayer, at: 0)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: shapeLayer, frame: shapeLayerFrame)
|
||||
|
||||
let angleValue: CGFloat = angles[i]
|
||||
|
||||
let innerStartAngle = startAngle + innerAngleSpacing * 0.5
|
||||
@ -187,14 +251,17 @@ final class PieChartComponent: Component {
|
||||
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: innerDiameter * 0.5, startAngle: innerEndAngle, endAngle: innerStartAngle, clockwise: true)
|
||||
path.addArc(center: CGPoint(x: diameter * 0.5, y: diameter * 0.5), radius: itemOuterDiameter * 0.5, startAngle: outerStartAngle, endAngle: outerEndAngle, clockwise: false)
|
||||
|
||||
transition.setShapeLayerPath(layer: shapeLayer, path: path)
|
||||
context.addPath(path)
|
||||
context.setFillColor(item.color.cgColor)
|
||||
context.fillPath()
|
||||
|
||||
startAngle += angleValue
|
||||
shapeLayer.fillColor = item.color.cgColor
|
||||
|
||||
let fractionValue: Double = floor(item.value * 100.0 * 10.0) / 10.0
|
||||
let fractionValue: Double = floor(item.displayValue * 100.0 * 10.0) / 10.0
|
||||
let fractionString: String
|
||||
if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
|
||||
if fractionValue < 0.1 {
|
||||
fractionString = "<0.1"
|
||||
} else if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
|
||||
fractionString = "\(Int(fractionValue))"
|
||||
} else {
|
||||
fractionString = "\(fractionValue)"
|
||||
@ -207,16 +274,125 @@ final class PieChartComponent: Component {
|
||||
label = ComponentView<Empty>()
|
||||
self.labels[item.id] = label
|
||||
}
|
||||
let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 16.0, design: .round, weight: .semibold), color: component.theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0))
|
||||
let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 16.0, design: .round, weight: .semibold), color: theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0))
|
||||
|
||||
var labelFrame: CGRect?
|
||||
|
||||
for step in 0 ... 6 {
|
||||
let stepFraction: CGFloat = CGFloat(step) / 6.0
|
||||
let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
|
||||
|
||||
if angleValue >= 0.001 {
|
||||
for step in 0 ... 20 {
|
||||
let stepFraction: CGFloat = CGFloat(step) / 20.0
|
||||
let centerOffset: CGFloat = 0.5 * (1.0 - stepFraction) + 0.65 * stepFraction
|
||||
|
||||
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
|
||||
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset)
|
||||
|
||||
let relLabelCenter = CGPoint(
|
||||
x: cos(midAngle) * centerDistance,
|
||||
y: sin(midAngle) * centerDistance
|
||||
)
|
||||
|
||||
let labelCenter = CGPoint(
|
||||
x: shapeLayerFrame.midX + relLabelCenter.x,
|
||||
y: shapeLayerFrame.midY + relLabelCenter.y
|
||||
)
|
||||
|
||||
func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat {
|
||||
let dx: CGFloat = p2.x - p1.x
|
||||
let dy: CGFloat = p2.y - p1.y
|
||||
let dr: CGFloat = sqrt(dx * dx + dy * dy)
|
||||
let D: CGFloat = p1.x * p2.y - p2.x * p1.y
|
||||
|
||||
var minDistance: CGFloat = 10000.0
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0)
|
||||
let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0
|
||||
let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
|
||||
let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
|
||||
let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0))
|
||||
minDistance = min(minDistance, distance)
|
||||
}
|
||||
|
||||
return minDistance
|
||||
}
|
||||
|
||||
func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat {
|
||||
let x1 = p1.x
|
||||
let y1 = p1.y
|
||||
let x2 = p2.x
|
||||
let y2 = p2.y
|
||||
let x3 = p3.x
|
||||
let y3 = p3.y
|
||||
let x4 = p4.x
|
||||
let y4 = p4.y
|
||||
|
||||
let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
|
||||
if abs(d) <= 0.00001 {
|
||||
return 10000.0
|
||||
}
|
||||
|
||||
let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d
|
||||
let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
|
||||
|
||||
let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0))
|
||||
return distance
|
||||
}
|
||||
|
||||
let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5)
|
||||
let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5)
|
||||
let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5)
|
||||
let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5)
|
||||
|
||||
let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
|
||||
let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
|
||||
let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
|
||||
let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
|
||||
|
||||
var distances: [CGFloat] = [
|
||||
intersectionOuterTopRight,
|
||||
intersectionInnerTopRight,
|
||||
intersectionOuterBottomRight,
|
||||
intersectionInnerBottomRight
|
||||
]
|
||||
|
||||
if angleValue < CGFloat.pi / 2.0 {
|
||||
distances.append(contentsOf: [
|
||||
intersectionLine1TopRight,
|
||||
intersectionLine1BottomRight,
|
||||
intersectionLine2TopRight,
|
||||
intersectionLine2BottomRight
|
||||
] as [CGFloat])
|
||||
}
|
||||
|
||||
var minDistance: CGFloat = 1000.0
|
||||
for distance in distances {
|
||||
minDistance = min(minDistance, distance + 1.0)
|
||||
}
|
||||
|
||||
let diagonalAngle = atan2(labelSize.height, labelSize.width)
|
||||
|
||||
let maxHalfWidth = cos(diagonalAngle) * minDistance
|
||||
let maxHalfHeight = sin(diagonalAngle) * minDistance
|
||||
|
||||
let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0)
|
||||
let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height))
|
||||
|
||||
let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize)
|
||||
|
||||
if finalSize.width >= labelSize.width {
|
||||
labelFrame = currentFrame
|
||||
break
|
||||
}
|
||||
if let labelFrame {
|
||||
if labelFrame.width > finalSize.width {
|
||||
continue
|
||||
}
|
||||
}
|
||||
labelFrame = currentFrame
|
||||
}
|
||||
} else {
|
||||
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
|
||||
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * centerOffset)
|
||||
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter * 0.5 - innerDiameter * 0.5) * 0.5)
|
||||
|
||||
let relLabelCenter = CGPoint(
|
||||
x: cos(midAngle) * centerDistance,
|
||||
@ -228,103 +404,14 @@ final class PieChartComponent: Component {
|
||||
y: shapeLayerFrame.midY + relLabelCenter.y
|
||||
)
|
||||
|
||||
func lineCircleIntersection(_ center: CGPoint, _ p1: CGPoint, _ p2: CGPoint, _ r: CGFloat) -> CGFloat {
|
||||
let dx: CGFloat = p2.x - p1.x
|
||||
let dy: CGFloat = p2.y - p1.y
|
||||
let dr: CGFloat = sqrt(dx * dx + dy * dy)
|
||||
let D: CGFloat = p1.x * p2.y - p2.x * p1.y
|
||||
|
||||
var minDistance: CGFloat = 10000.0
|
||||
|
||||
for i in 0 ..< 2 {
|
||||
let signFactor: CGFloat = i == 0 ? 1.0 : (-1.0)
|
||||
let dysign: CGFloat = dy < 0.0 ? -1.0 : 1.0
|
||||
let ix: CGFloat = (D * dy + signFactor * dysign * dx * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
|
||||
let iy: CGFloat = (-D * dx + signFactor * abs(dy) * sqrt(r * r * dr * dr - D * D)) / (dr * dr)
|
||||
let distance: CGFloat = sqrt(pow(ix - center.x, 2.0) + pow(iy - center.y, 2.0))
|
||||
minDistance = min(minDistance, distance)
|
||||
}
|
||||
|
||||
return minDistance
|
||||
}
|
||||
|
||||
func lineLineIntersection(_ p1: CGPoint, _ p2: CGPoint, _ p3: CGPoint, _ p4: CGPoint) -> CGFloat {
|
||||
let x1 = p1.x
|
||||
let y1 = p1.y
|
||||
let x2 = p2.x
|
||||
let y2 = p2.y
|
||||
let x3 = p3.x
|
||||
let y3 = p3.y
|
||||
let x4 = p4.x
|
||||
let y4 = p4.y
|
||||
|
||||
let d: CGFloat = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4)
|
||||
if abs(d) <= 0.00001 {
|
||||
return 10000.0
|
||||
}
|
||||
|
||||
let px: CGFloat = ((x1 * y2 - y1 * x2) * (x3 - x4) - (x1 - x2) * (x3 * y4 - y3 * x4)) / d
|
||||
let py: CGFloat = ((x1 * y2 - y1 * x2) * (y3 - y4) - (y1 - y2) * (x3 * y4 - y3 * x4)) / d
|
||||
|
||||
let distance: CGFloat = sqrt(pow(px - p1.x, 2.0) + pow(py - p1.y, 2.0))
|
||||
return distance
|
||||
}
|
||||
|
||||
let intersectionOuterTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), diameter * 0.5)
|
||||
let intersectionInnerTopRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), innerDiameter * 0.5)
|
||||
let intersectionOuterBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), diameter * 0.5)
|
||||
let intersectionInnerBottomRight = lineCircleIntersection(relLabelCenter, relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), innerDiameter * 0.5)
|
||||
|
||||
let intersectionLine1TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
|
||||
let intersectionLine1BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerStartAngle), y: sin(innerStartAngle)))
|
||||
let intersectionLine2TopRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y + labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
|
||||
let intersectionLine2BottomRight = lineLineIntersection(relLabelCenter, CGPoint(x: relLabelCenter.x + labelSize.width * 0.5, y: relLabelCenter.y - labelSize.height * 0.5), CGPoint(), CGPoint(x: cos(innerEndAngle), y: sin(innerEndAngle)))
|
||||
|
||||
var distances: [CGFloat] = [
|
||||
intersectionOuterTopRight,
|
||||
intersectionInnerTopRight,
|
||||
intersectionOuterBottomRight,
|
||||
intersectionInnerBottomRight
|
||||
]
|
||||
|
||||
if angleValue < CGFloat.pi / 2.0 {
|
||||
distances.append(contentsOf: [
|
||||
intersectionLine1TopRight,
|
||||
intersectionLine1BottomRight,
|
||||
intersectionLine2TopRight,
|
||||
intersectionLine2BottomRight
|
||||
] as [CGFloat])
|
||||
}
|
||||
|
||||
var minDistance: CGFloat = 1000.0
|
||||
for distance in distances {
|
||||
minDistance = min(minDistance, distance + 1.0)
|
||||
}
|
||||
|
||||
let diagonalAngle = atan2(labelSize.height, labelSize.width)
|
||||
|
||||
let maxHalfWidth = cos(diagonalAngle) * minDistance
|
||||
let maxHalfHeight = sin(diagonalAngle) * minDistance
|
||||
|
||||
let maxSize = CGSize(width: maxHalfWidth * 2.0, height: maxHalfHeight * 2.0)
|
||||
let finalSize = CGSize(width: min(labelSize.width, maxSize.width), height: min(labelSize.height, maxSize.height))
|
||||
|
||||
let currentFrame = CGRect(origin: CGPoint(x: labelCenter.x - finalSize.width * 0.5, y: labelCenter.y - finalSize.height * 0.5), size: finalSize)
|
||||
|
||||
if finalSize.width >= labelSize.width {
|
||||
labelFrame = currentFrame
|
||||
break
|
||||
}
|
||||
if let labelFrame {
|
||||
if labelFrame.width > finalSize.width {
|
||||
continue
|
||||
}
|
||||
}
|
||||
labelFrame = currentFrame
|
||||
let minSize = labelSize.aspectFitted(CGSize(width: 4.0, height: 4.0))
|
||||
labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - minSize.width * 0.5, y: labelCenter.y - minSize.height * 0.5), size: minSize)
|
||||
}
|
||||
|
||||
if let labelView = label.view, let labelFrame {
|
||||
var animateIn: Bool = false
|
||||
if labelView.superview == nil {
|
||||
animateIn = true
|
||||
self.addSubview(labelView)
|
||||
}
|
||||
|
||||
@ -338,11 +425,12 @@ final class PieChartComponent: Component {
|
||||
y: labelFrame.midY - shapeLayerFrame.midY
|
||||
)
|
||||
|
||||
let labelAlpha: CGFloat
|
||||
if let selectedKey = self.selectedKey {
|
||||
if selectedKey == item.id {
|
||||
transition.setAlpha(view: labelView, alpha: normalAlpha)
|
||||
labelAlpha = normalAlpha
|
||||
} else {
|
||||
transition.setAlpha(view: labelView, alpha: 0.0)
|
||||
labelAlpha = 0.0
|
||||
|
||||
let reducedFactor: CGFloat = (reducedDiameter - innerDiameter) / (diameter - innerDiameter)
|
||||
let reducedDiameterFactor: CGFloat = reducedDiameter / diameter
|
||||
@ -353,7 +441,16 @@ final class PieChartComponent: Component {
|
||||
relLabelCenter.y *= reducedDiameterFactor
|
||||
}
|
||||
} else {
|
||||
transition.setAlpha(view: labelView, alpha: normalAlpha)
|
||||
labelAlpha = normalAlpha
|
||||
}
|
||||
if labelView.alpha != labelAlpha {
|
||||
let transition: Transition
|
||||
if animateIn {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = Transition(animation: .curve(duration: 0.18, curve: .easeInOut))
|
||||
}
|
||||
transition.setAlpha(view: labelView, alpha: labelAlpha)
|
||||
}
|
||||
|
||||
let labelCenter = CGPoint(
|
||||
@ -361,10 +458,61 @@ final class PieChartComponent: Component {
|
||||
y: shapeLayerFrame.midY + relLabelCenter.y
|
||||
)
|
||||
|
||||
transition.setPosition(view: labelView, position: labelCenter)
|
||||
transition.setScale(view: labelView, scale: labelScale)
|
||||
labelView.center = labelCenter
|
||||
labelView.transform = CGAffineTransformMakeScale(labelScale, labelScale)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private let dataView: ChartDataView
|
||||
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
var selectedKey: AnyHashable?
|
||||
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.dataView = ChartDataView()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.dataView)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self)
|
||||
let _ = point
|
||||
/*for (key, layer) in self.shapeLayers {
|
||||
if layer.frame.contains(point), let path = layer.path {
|
||||
if path.contains(self.layer.convert(point, to: layer)) {
|
||||
if self.selectedKey == key {
|
||||
self.selectedKey = nil
|
||||
} else {
|
||||
self.selectedKey = key
|
||||
}
|
||||
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.3, curve: .spring)))
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: PieChartComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
self.state = state
|
||||
|
||||
transition.setFrame(view: self.dataView, frame: CGRect(origin: CGPoint(x: floor((availableSize.width - 200.0) / 2.0), y: 0.0), size: CGSize(width: 200.0, height: 200.0)))
|
||||
self.dataView.setItems(theme: component.theme, data: component.chartData, selectedKey: self.selectedKey, animated: !transition.animation.isImmediate)
|
||||
|
||||
return CGSize(width: availableSize.width, height: 200.0)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ import SolidRoundedButtonComponent
|
||||
|
||||
final class StorageCategoriesComponent: Component {
|
||||
struct CategoryData: Equatable {
|
||||
var key: AnyHashable
|
||||
var key: StorageUsageScreenComponent.Category
|
||||
var color: UIColor
|
||||
var title: String
|
||||
var size: Int64
|
||||
@ -25,7 +25,7 @@ final class StorageCategoriesComponent: Component {
|
||||
var isSelected: Bool
|
||||
var subcategories: [CategoryData]
|
||||
|
||||
init(key: AnyHashable, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) {
|
||||
init(key: StorageUsageScreenComponent.Category, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) {
|
||||
self.key = key
|
||||
self.title = title
|
||||
self.color = color
|
||||
@ -39,18 +39,27 @@ final class StorageCategoriesComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let categories: [CategoryData]
|
||||
let toggleCategorySelection: (AnyHashable) -> Void
|
||||
let isOtherExpanded: Bool
|
||||
let toggleCategorySelection: (StorageUsageScreenComponent.Category) -> Void
|
||||
let toggleOtherExpanded: () -> Void
|
||||
let clearAction: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
categories: [CategoryData],
|
||||
toggleCategorySelection: @escaping (AnyHashable) -> Void
|
||||
isOtherExpanded: Bool,
|
||||
toggleCategorySelection: @escaping (StorageUsageScreenComponent.Category) -> Void,
|
||||
toggleOtherExpanded: @escaping () -> Void,
|
||||
clearAction: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.categories = categories
|
||||
self.isOtherExpanded = isOtherExpanded
|
||||
self.toggleCategorySelection = toggleCategorySelection
|
||||
self.toggleOtherExpanded = toggleOtherExpanded
|
||||
self.clearAction = clearAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageCategoriesComponent, rhs: StorageCategoriesComponent) -> Bool {
|
||||
@ -63,14 +72,16 @@ final class StorageCategoriesComponent: Component {
|
||||
if lhs.categories != rhs.categories {
|
||||
return false
|
||||
}
|
||||
if lhs.isOtherExpanded != rhs.isOtherExpanded {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var itemViews: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
|
||||
private let button = ComponentView<Empty>()
|
||||
|
||||
private var expandedCategory: AnyHashable?
|
||||
private var component: StorageCategoriesComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
@ -89,6 +100,8 @@ final class StorageCategoriesComponent: Component {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let expandedCategory: StorageUsageScreenComponent.Category? = component.isOtherExpanded ? .other : nil
|
||||
|
||||
var totalSelectedSize: Int64 = 0
|
||||
var hasDeselected = false
|
||||
for category in component.categories {
|
||||
@ -111,7 +124,7 @@ final class StorageCategoriesComponent: Component {
|
||||
|
||||
var contentHeight: CGFloat = 0.0
|
||||
|
||||
var validKeys = Set<AnyHashable>()
|
||||
var validKeys = Set<StorageUsageScreenComponent.Category>()
|
||||
for i in 0 ..< component.categories.count {
|
||||
let category = component.categories[i]
|
||||
validKeys.insert(category.key)
|
||||
@ -134,7 +147,7 @@ final class StorageCategoriesComponent: Component {
|
||||
strings: component.strings,
|
||||
category: category,
|
||||
isExpandedLevel: false,
|
||||
isExpanded: self.expandedCategory == category.key,
|
||||
isExpanded: expandedCategory == category.key,
|
||||
hasNext: i != component.categories.count - 1,
|
||||
action: { [weak self] key, actionType in
|
||||
guard let self, let component = self.component else {
|
||||
@ -144,12 +157,7 @@ final class StorageCategoriesComponent: Component {
|
||||
switch actionType {
|
||||
case .generic:
|
||||
if let category = component.categories.first(where: { $0.key == key }), !category.subcategories.isEmpty {
|
||||
if self.expandedCategory == category.key {
|
||||
self.expandedCategory = nil
|
||||
} else {
|
||||
self.expandedCategory = category.key
|
||||
}
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.4, curve: .spring)))
|
||||
component.toggleOtherExpanded()
|
||||
} else {
|
||||
component.toggleCategorySelection(key)
|
||||
}
|
||||
@ -172,7 +180,7 @@ final class StorageCategoriesComponent: Component {
|
||||
contentHeight += itemSize.height
|
||||
}
|
||||
|
||||
var removeKeys: [AnyHashable] = []
|
||||
var removeKeys: [StorageUsageScreenComponent.Category] = []
|
||||
for (key, itemView) in self.itemViews {
|
||||
if !validKeys.contains(key) {
|
||||
if let itemComponentView = itemView.view {
|
||||
@ -221,7 +229,11 @@ final class StorageCategoriesComponent: Component {
|
||||
animationName: nil,
|
||||
iconPosition: .right,
|
||||
iconSpacing: 4.0,
|
||||
action: {
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
component.clearAction()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
|
@ -27,7 +27,7 @@ final class StorageCategoryItemComponent: Component {
|
||||
let isExpandedLevel: Bool
|
||||
let isExpanded: Bool
|
||||
let hasNext: Bool
|
||||
let action: (AnyHashable, ActionType) -> Void
|
||||
let action: (StorageUsageScreenComponent.Category, ActionType) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
@ -36,7 +36,7 @@ final class StorageCategoryItemComponent: Component {
|
||||
isExpandedLevel: Bool,
|
||||
isExpanded: Bool,
|
||||
hasNext: Bool,
|
||||
action: @escaping (AnyHashable, ActionType) -> Void
|
||||
action: @escaping (StorageUsageScreenComponent.Category, ActionType) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
@ -80,7 +80,7 @@ final class StorageCategoryItemComponent: Component {
|
||||
private let checkButtonArea: HighlightTrackingButton
|
||||
|
||||
private let subcategoryClippingContainer: UIView
|
||||
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||
private var itemViews: [StorageUsageScreenComponent.Category: ComponentView<Empty>] = [:]
|
||||
|
||||
private var component: StorageCategoryItemComponent?
|
||||
|
||||
@ -306,7 +306,7 @@ final class StorageCategoryItemComponent: Component {
|
||||
|
||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0)))
|
||||
|
||||
var validKeys = Set<AnyHashable>()
|
||||
var validKeys = Set<StorageUsageScreenComponent.Category>()
|
||||
if component.isExpanded {
|
||||
for i in 0 ..< component.category.subcategories.count {
|
||||
let category = component.category.subcategories[i]
|
||||
@ -358,7 +358,7 @@ final class StorageCategoryItemComponent: Component {
|
||||
}
|
||||
}
|
||||
|
||||
var removeKeys: [AnyHashable] = []
|
||||
var removeKeys: [StorageUsageScreenComponent.Category] = []
|
||||
for (key, itemView) in self.itemViews {
|
||||
if !validKeys.contains(key) {
|
||||
if let itemComponentView = itemView.view {
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -19,13 +19,20 @@ import AvatarNode
|
||||
private let avatarFont = avatarPlaceholderFont(size: 15.0)
|
||||
|
||||
private final class PeerListItemComponent: Component {
|
||||
enum SelectionState: Equatable {
|
||||
case none
|
||||
case editing(isSelected: Bool)
|
||||
}
|
||||
|
||||
let context: AccountContext
|
||||
let theme: PresentationTheme
|
||||
let sideInset: CGFloat
|
||||
let title: String
|
||||
let peer: EnginePeer?
|
||||
let label: String
|
||||
let selectionState: SelectionState
|
||||
let hasNext: Bool
|
||||
let action: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
@ -34,7 +41,9 @@ private final class PeerListItemComponent: Component {
|
||||
title: String,
|
||||
peer: EnginePeer?,
|
||||
label: String,
|
||||
hasNext: Bool
|
||||
selectionState: SelectionState,
|
||||
hasNext: Bool,
|
||||
action: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.theme = theme
|
||||
@ -42,7 +51,9 @@ private final class PeerListItemComponent: Component {
|
||||
self.title = title
|
||||
self.peer = peer
|
||||
self.label = label
|
||||
self.selectionState = selectionState
|
||||
self.hasNext = hasNext
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: PeerListItemComponent, rhs: PeerListItemComponent) -> Bool {
|
||||
@ -64,6 +75,9 @@ private final class PeerListItemComponent: Component {
|
||||
if lhs.label != rhs.label {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
if lhs.hasNext != rhs.hasNext {
|
||||
return false
|
||||
}
|
||||
@ -76,6 +90,11 @@ private final class PeerListItemComponent: Component {
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let avatarNode: AvatarNode
|
||||
|
||||
private var checkLayer: CheckLayer?
|
||||
|
||||
private var highlightBackgroundFrame: CGRect?
|
||||
private var highlightBackgroundLayer: SimpleLayer?
|
||||
|
||||
private var component: PeerListItemComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@ -87,23 +106,115 @@ private final class PeerListItemComponent: Component {
|
||||
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
self.layer.addSublayer(self.avatarNode.layer)
|
||||
|
||||
self.highligthedChanged = { [weak self] isHighlighted in
|
||||
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||
return
|
||||
}
|
||||
|
||||
if isHighlighted, case .none = component.selectionState {
|
||||
self.superview?.bringSubviewToFront(self)
|
||||
|
||||
let highlightBackgroundLayer: SimpleLayer
|
||||
if let current = self.highlightBackgroundLayer {
|
||||
highlightBackgroundLayer = current
|
||||
} else {
|
||||
highlightBackgroundLayer = SimpleLayer()
|
||||
self.highlightBackgroundLayer = highlightBackgroundLayer
|
||||
self.layer.insertSublayer(highlightBackgroundLayer, above: self.separatorLayer)
|
||||
highlightBackgroundLayer.backgroundColor = component.theme.list.itemHighlightedBackgroundColor.cgColor
|
||||
}
|
||||
highlightBackgroundLayer.frame = highlightBackgroundFrame
|
||||
highlightBackgroundLayer.opacity = 1.0
|
||||
} else {
|
||||
if let highlightBackgroundLayer = self.highlightBackgroundLayer {
|
||||
self.highlightBackgroundLayer = nil
|
||||
highlightBackgroundLayer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak highlightBackgroundLayer] _ in
|
||||
highlightBackgroundLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
self.addTarget(self, action: #selector(self.pressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func pressed() {
|
||||
guard let component = self.component, let peer = component.peer else {
|
||||
return
|
||||
}
|
||||
component.action(peer)
|
||||
}
|
||||
|
||||
func update(component: PeerListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
var hasSelectionUpdated = false
|
||||
if let previousComponent = self.component {
|
||||
switch previousComponent.selectionState {
|
||||
case .none:
|
||||
if case .none = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
case .editing:
|
||||
if case .editing = component.selectionState {
|
||||
} else {
|
||||
hasSelectionUpdated = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
|
||||
let height: CGFloat = 52.0
|
||||
let leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var leftInset: CGFloat = 62.0 + component.sideInset
|
||||
var avatarLeftInset: CGFloat = component.sideInset + 10.0
|
||||
|
||||
if case let .editing(isSelected) = component.selectionState {
|
||||
leftInset += 48.0
|
||||
avatarLeftInset += 48.0
|
||||
|
||||
let checkSize: CGFloat = 22.0
|
||||
|
||||
let checkLayer: CheckLayer
|
||||
if let current = self.checkLayer {
|
||||
checkLayer = current
|
||||
if themeUpdated {
|
||||
checkLayer.theme = CheckNodeTheme(theme: component.theme, style: .plain)
|
||||
}
|
||||
checkLayer.setSelected(isSelected, animated: !transition.animation.isImmediate)
|
||||
} else {
|
||||
checkLayer = CheckLayer(theme: CheckNodeTheme(theme: component.theme, style: .plain))
|
||||
self.checkLayer = checkLayer
|
||||
self.layer.addSublayer(checkLayer)
|
||||
checkLayer.frame = CGRect(origin: CGPoint(x: -checkSize, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize))
|
||||
checkLayer.setSelected(isSelected, animated: false)
|
||||
checkLayer.setNeedsDisplay()
|
||||
}
|
||||
transition.setFrame(layer: checkLayer, frame: CGRect(origin: CGPoint(x: 20.0, y: floor((height - checkSize) / 2.0)), size: CGSize(width: checkSize, height: checkSize)))
|
||||
} else {
|
||||
if let checkLayer = self.checkLayer {
|
||||
self.checkLayer = nil
|
||||
transition.setPosition(layer: checkLayer, position: CGPoint(x: -checkLayer.bounds.width * 0.5, y: checkLayer.position.y), completion: { [weak checkLayer] _ in
|
||||
checkLayer?.removeFromSuperlayer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
let rightInset: CGFloat = 16.0 + component.sideInset
|
||||
|
||||
let avatarSize: CGFloat = 40.0
|
||||
|
||||
self.avatarNode.frame = CGRect(origin: CGPoint(x: component.sideInset + 10.0, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
let avatarFrame = CGRect(origin: CGPoint(x: avatarLeftInset, y: floor((height - avatarSize) / 2.0)), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
if self.avatarNode.bounds.isEmpty {
|
||||
self.avatarNode.frame = avatarFrame
|
||||
} else {
|
||||
transition.setFrame(layer: self.avatarNode.layer, frame: avatarFrame)
|
||||
}
|
||||
if let peer = component.peer {
|
||||
self.avatarNode.setPeer(context: component.context, theme: component.theme, peer: peer, displayDimensions: CGSize(width: avatarSize, height: avatarSize))
|
||||
}
|
||||
@ -117,6 +228,12 @@ private final class PeerListItemComponent: Component {
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0)
|
||||
)
|
||||
|
||||
let previousTitleFrame = self.title.view?.frame
|
||||
var previousTitleContents: UIView?
|
||||
if hasSelectionUpdated {
|
||||
previousTitleContents = self.title.view?.snapshotView(afterScreenUpdates: false)
|
||||
}
|
||||
|
||||
let titleSize = self.title.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
@ -125,15 +242,31 @@ private final class PeerListItemComponent: Component {
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - leftInset - rightInset - labelSize.width - 4.0, height: 100.0)
|
||||
)
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||
if let titleView = self.title.view {
|
||||
if titleView.superview == nil {
|
||||
titleView.isUserInteractionEnabled = false
|
||||
self.addSubview(titleView)
|
||||
}
|
||||
transition.setFrame(view: titleView, frame: CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize))
|
||||
titleView.frame = titleFrame
|
||||
if let previousTitleFrame, previousTitleFrame.origin.x != titleFrame.origin.x {
|
||||
transition.animatePosition(view: titleView, from: CGPoint(x: previousTitleFrame.origin.x - titleFrame.origin.x, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
|
||||
if let previousTitleFrame, let previousTitleContents, previousTitleFrame.size != titleSize {
|
||||
previousTitleContents.frame = CGRect(origin: previousTitleFrame.origin, size: previousTitleFrame.size)
|
||||
self.addSubview(previousTitleContents)
|
||||
|
||||
transition.setFrame(view: previousTitleContents, frame: CGRect(origin: titleFrame.origin, size: previousTitleFrame.size))
|
||||
transition.setAlpha(view: previousTitleContents, alpha: 0.0, completion: { [weak previousTitleContents] _ in
|
||||
previousTitleContents?.removeFromSuperview()
|
||||
})
|
||||
transition.animateAlpha(view: titleView, from: 0.0, to: 1.0)
|
||||
}
|
||||
}
|
||||
if let labelView = self.label.view {
|
||||
if labelView.superview == nil {
|
||||
labelView.isUserInteractionEnabled = false
|
||||
self.addSubview(labelView)
|
||||
}
|
||||
transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize))
|
||||
@ -145,6 +278,8 @@ private final class PeerListItemComponent: Component {
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||
self.separatorLayer.isHidden = !component.hasNext
|
||||
|
||||
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.hasNext) ? UIScreenPixel : 0.0)))
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
@ -201,13 +336,19 @@ final class StoragePeerListPanelComponent: Component {
|
||||
|
||||
let context: AccountContext
|
||||
let items: Items?
|
||||
let selectionState: StorageUsageScreenComponent.SelectionState?
|
||||
let peerAction: (EnginePeer) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
items: Items?
|
||||
items: Items?,
|
||||
selectionState: StorageUsageScreenComponent.SelectionState?,
|
||||
peerAction: @escaping (EnginePeer) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.items = items
|
||||
self.selectionState = selectionState
|
||||
self.peerAction = peerAction
|
||||
}
|
||||
|
||||
static func ==(lhs: StoragePeerListPanelComponent, rhs: StoragePeerListPanelComponent) -> Bool {
|
||||
@ -217,6 +358,9 @@ final class StoragePeerListPanelComponent: Component {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.selectionState != rhs.selectionState {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -337,6 +481,13 @@ final class StoragePeerListPanelComponent: Component {
|
||||
self.visibleItems[id] = itemView
|
||||
}
|
||||
|
||||
let itemSelectionState: PeerListItemComponent.SelectionState
|
||||
if let selectionState = component.selectionState {
|
||||
itemSelectionState = .editing(isSelected: selectionState.selectedPeers.contains(id))
|
||||
} else {
|
||||
itemSelectionState = .none
|
||||
}
|
||||
|
||||
let _ = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(PeerListItemComponent(
|
||||
@ -346,7 +497,9 @@ final class StoragePeerListPanelComponent: Component {
|
||||
title: item.peer.displayTitle(strings: environment.strings, displayOrder: .firstLast),
|
||||
peer: item.peer,
|
||||
label: dataSizeString(item.size, formatting: dataSizeFormatting),
|
||||
hasNext: index != items.items.count - 1
|
||||
selectionState: itemSelectionState,
|
||||
hasNext: index != items.items.count - 1,
|
||||
action: component.peerAction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: itemLayout.containerWidth, height: itemLayout.itemHeight)
|
||||
@ -392,7 +545,10 @@ final class StoragePeerListPanelComponent: Component {
|
||||
title: "ABCDEF",
|
||||
peer: nil,
|
||||
label: "1000",
|
||||
hasNext: false
|
||||
selectionState: .none,
|
||||
hasNext: false,
|
||||
action: { _ in
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||
@ -408,7 +564,14 @@ final class StoragePeerListPanelComponent: Component {
|
||||
|
||||
self.ignoreScrolling = true
|
||||
let contentOffset = self.scrollView.bounds.minY
|
||||
transition.setFrame(view: self.scrollView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
transition.setPosition(view: self.scrollView, position: CGRect(origin: CGPoint(), size: availableSize).center)
|
||||
var scrollBounds = self.scrollView.bounds
|
||||
scrollBounds.size = availableSize
|
||||
if !environment.isScrollable {
|
||||
scrollBounds.origin = CGPoint()
|
||||
}
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollBounds)
|
||||
self.scrollView.isScrollEnabled = environment.isScrollable
|
||||
let contentSize = CGSize(width: availableSize.width, height: itemLayout.contentHeight)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
@ -419,7 +582,7 @@ final class StoragePeerListPanelComponent: Component {
|
||||
transition.animateBoundsOrigin(view: self.scrollView, from: CGPoint(x: 0.0, y: -deltaOffset), to: CGPoint(), additive: true)
|
||||
}
|
||||
self.ignoreScrolling = false
|
||||
self.updateScrolling(transition: .immediate)
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return availableSize
|
||||
}
|
||||
|
@ -5,19 +5,42 @@ import ComponentFlow
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
|
||||
final class StorageUsagePanelContainerEnvironment: Equatable {
|
||||
let isScrollable: Bool
|
||||
|
||||
init(
|
||||
isScrollable: Bool
|
||||
) {
|
||||
self.isScrollable = isScrollable
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsagePanelContainerEnvironment, rhs: StorageUsagePanelContainerEnvironment) -> Bool {
|
||||
if lhs.isScrollable != rhs.isScrollable {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
final class StorageUsagePanelEnvironment: Equatable {
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let containerInsets: UIEdgeInsets
|
||||
let isScrollable: Bool
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
containerInsets: UIEdgeInsets
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
containerInsets: UIEdgeInsets,
|
||||
isScrollable: Bool
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.containerInsets = containerInsets
|
||||
self.isScrollable = isScrollable
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsagePanelEnvironment, rhs: StorageUsagePanelEnvironment) -> Bool {
|
||||
@ -27,9 +50,15 @@ final class StorageUsagePanelEnvironment: Equatable {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.containerInsets != rhs.containerInsets {
|
||||
return false
|
||||
}
|
||||
if lhs.isScrollable != rhs.isScrollable {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@ -37,13 +66,16 @@ final class StorageUsagePanelEnvironment: Equatable {
|
||||
private final class StorageUsageHeaderItemComponent: CombinedComponent {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let activityFraction: CGFloat
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
title: String
|
||||
title: String,
|
||||
activityFraction: CGFloat
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.activityFraction = activityFraction
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsageHeaderItemComponent, rhs: StorageUsageHeaderItemComponent) -> Bool {
|
||||
@ -53,22 +85,38 @@ private final class StorageUsageHeaderItemComponent: CombinedComponent {
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.activityFraction != rhs.activityFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
static var body: Body {
|
||||
let text = Child(Text.self)
|
||||
let activeText = Child(Text.self)
|
||||
let inactiveText = Child(Text.self)
|
||||
|
||||
return { context in
|
||||
let text = text.update(
|
||||
component: Text(text: context.component.title, font: Font.semibold(15.0), color: context.component.theme.list.itemAccentColor),
|
||||
let activeText = activeText.update(
|
||||
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemAccentColor),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
let inactiveText = inactiveText.update(
|
||||
component: Text(text: context.component.title, font: Font.medium(14.0), color: context.component.theme.list.itemSecondaryTextColor),
|
||||
availableSize: context.availableSize,
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
context.add(text.position(CGPoint(x: text.size.width * 0.5, y: text.size.height * 0.5)))
|
||||
context.add(activeText
|
||||
.position(CGPoint(x: activeText.size.width * 0.5, y: activeText.size.height * 0.5))
|
||||
.opacity(context.component.activityFraction)
|
||||
)
|
||||
context.add(inactiveText
|
||||
.position(CGPoint(x: inactiveText.size.width * 0.5, y: inactiveText.size.height * 0.5))
|
||||
.opacity(1.0 - context.component.activityFraction)
|
||||
)
|
||||
|
||||
return text.size
|
||||
return activeText.size
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -89,13 +137,22 @@ private final class StorageUsageHeaderComponent: Component {
|
||||
|
||||
let theme: PresentationTheme
|
||||
let items: [Item]
|
||||
let activeIndex: Int
|
||||
let transitionFraction: CGFloat
|
||||
let switchToPanel: (AnyHashable) -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
items: [Item]
|
||||
items: [Item],
|
||||
activeIndex: Int,
|
||||
transitionFraction: CGFloat,
|
||||
switchToPanel: @escaping (AnyHashable) -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.items = items
|
||||
self.activeIndex = activeIndex
|
||||
self.transitionFraction = transitionFraction
|
||||
self.switchToPanel = switchToPanel
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsageHeaderComponent, rhs: StorageUsageHeaderComponent) -> Bool {
|
||||
@ -105,6 +162,12 @@ private final class StorageUsageHeaderComponent: Component {
|
||||
if lhs.items != rhs.items {
|
||||
return false
|
||||
}
|
||||
if lhs.activeIndex != rhs.activeIndex {
|
||||
return false
|
||||
}
|
||||
if lhs.transitionFraction != rhs.transitionFraction {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@ -122,19 +185,46 @@ private final class StorageUsageHeaderComponent: Component {
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.activeItemLayer)
|
||||
|
||||
self.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@objc private func tapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
let point = recognizer.location(in: self)
|
||||
var closestId: (CGFloat, AnyHashable)?
|
||||
if self.bounds.contains(point) {
|
||||
for (id, item) in self.visibleItems {
|
||||
if let itemView = item.view {
|
||||
let distance: CGFloat = min(abs(point.x - itemView.frame.minX), abs(point.x - itemView.frame.maxX))
|
||||
if let closestIdValue = closestId {
|
||||
if distance < closestIdValue.0 {
|
||||
closestId = (distance, id)
|
||||
}
|
||||
} else {
|
||||
closestId = (distance, id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let closestId = closestId, let component = self.component {
|
||||
component.switchToPanel(closestId.1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StorageUsageHeaderComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.component = component
|
||||
|
||||
var validIds = Set<AnyHashable>()
|
||||
for item in component.items {
|
||||
for i in 0 ..< component.items.count {
|
||||
let item = component.items[i]
|
||||
validIds.insert(item.id)
|
||||
|
||||
let itemView: ComponentView<Empty>
|
||||
@ -146,24 +236,59 @@ private final class StorageUsageHeaderComponent: Component {
|
||||
itemView = ComponentView()
|
||||
self.visibleItems[item.id] = itemView
|
||||
}
|
||||
|
||||
let activeIndex: CGFloat = CGFloat(component.activeIndex) - component.transitionFraction
|
||||
let activityDistance: CGFloat = abs(activeIndex - CGFloat(i))
|
||||
|
||||
let activityFraction: CGFloat
|
||||
if activityDistance < 1.0 {
|
||||
activityFraction = 1.0 - activityDistance
|
||||
} else {
|
||||
activityFraction = 0.0
|
||||
}
|
||||
|
||||
let itemSize = itemView.update(
|
||||
transition: itemTransition,
|
||||
component: AnyComponent(StorageUsageHeaderItemComponent(
|
||||
theme: component.theme,
|
||||
title: item.title
|
||||
title: item.title,
|
||||
activityFraction: activityFraction
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: availableSize
|
||||
)
|
||||
let itemFrame = CGRect(origin: CGPoint(x: 34.0, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
|
||||
|
||||
let itemHorizontalSpace = availableSize.width / CGFloat(component.items.count)
|
||||
let itemX: CGFloat
|
||||
if component.items.count == 1 {
|
||||
itemX = 37.0
|
||||
} else {
|
||||
itemX = itemHorizontalSpace * CGFloat(i) + floor((itemHorizontalSpace - itemSize.width) / 2.0)
|
||||
}
|
||||
|
||||
let itemFrame = CGRect(origin: CGPoint(x: itemX, y: floor((availableSize.height - itemSize.height) / 2.0)), size: itemSize)
|
||||
if let itemComponentView = itemView.view {
|
||||
if itemComponentView.superview == nil {
|
||||
self.addSubview(itemComponentView)
|
||||
itemComponentView.isUserInteractionEnabled = false
|
||||
}
|
||||
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: itemFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: itemFrame.width, height: 3.0)))
|
||||
}
|
||||
|
||||
if component.activeIndex < component.items.count {
|
||||
let activeView = self.visibleItems[component.items[component.activeIndex].id]?.view
|
||||
let nextIndex: Int
|
||||
if component.transitionFraction > 0.0 {
|
||||
nextIndex = max(0, component.activeIndex - 1)
|
||||
} else {
|
||||
nextIndex = min(component.items.count - 1, component.activeIndex + 1)
|
||||
}
|
||||
let nextView = self.visibleItems[component.items[nextIndex].id]?.view
|
||||
if let activeView = activeView, let nextView = nextView {
|
||||
let mergedFrame = activeView.frame.interpolate(to: nextView.frame, amount: abs(component.transitionFraction))
|
||||
transition.setFrame(layer: self.activeItemLayer, frame: CGRect(origin: CGPoint(x: mergedFrame.minX, y: availableSize.height - 3.0), size: CGSize(width: mergedFrame.width, height: 3.0)))
|
||||
}
|
||||
}
|
||||
|
||||
if themeUpdated {
|
||||
@ -197,6 +322,8 @@ private final class StorageUsageHeaderComponent: Component {
|
||||
}
|
||||
|
||||
final class StorageUsagePanelContainerComponent: Component {
|
||||
typealias EnvironmentType = StorageUsagePanelContainerEnvironment
|
||||
|
||||
struct Item: Equatable {
|
||||
let id: AnyHashable
|
||||
let title: String
|
||||
@ -215,17 +342,20 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
|
||||
let theme: PresentationTheme
|
||||
let strings: PresentationStrings
|
||||
let dateTimeFormat: PresentationDateTimeFormat
|
||||
let insets: UIEdgeInsets
|
||||
let items: [Item]
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
strings: PresentationStrings,
|
||||
dateTimeFormat: PresentationDateTimeFormat,
|
||||
insets: UIEdgeInsets,
|
||||
items: [Item]
|
||||
) {
|
||||
self.theme = theme
|
||||
self.strings = strings
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.insets = insets
|
||||
self.items = items
|
||||
}
|
||||
@ -237,6 +367,9 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.dateTimeFormat != rhs.dateTimeFormat {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
@ -246,47 +379,211 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private let topPanelBackgroundView: BlurredBackgroundView
|
||||
class View: UIView, UIGestureRecognizerDelegate {
|
||||
private let topPanelBackgroundView: UIView
|
||||
private let topPanelSeparatorLayer: SimpleLayer
|
||||
private let header = ComponentView<Empty>()
|
||||
|
||||
private var component: StorageUsagePanelContainerComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
private let panelsBackgroundLayer: SimpleLayer
|
||||
private var visiblePanels: [AnyHashable: ComponentView<StorageUsagePanelEnvironment>] = [:]
|
||||
private var actualVisibleIds = Set<AnyHashable>()
|
||||
private var currentId: AnyHashable?
|
||||
private var transitionFraction: CGFloat = 0.0
|
||||
private var animatingTransition: Bool = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.topPanelBackgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
self.topPanelBackgroundView = UIView()
|
||||
self.topPanelSeparatorLayer = SimpleLayer()
|
||||
|
||||
self.panelsBackgroundLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.layer.addSublayer(self.panelsBackgroundLayer)
|
||||
self.addSubview(self.topPanelBackgroundView)
|
||||
self.layer.addSublayer(self.topPanelSeparatorLayer)
|
||||
|
||||
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] point in
|
||||
guard let self, let component = self.component, let currentId = self.currentId else {
|
||||
return []
|
||||
}
|
||||
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
|
||||
return []
|
||||
}
|
||||
|
||||
/*if strongSelf.tabsContainerNode.bounds.contains(strongSelf.view.convert(point, to: strongSelf.tabsContainerNode.view)) {
|
||||
return []
|
||||
}*/
|
||||
|
||||
if index == 0 {
|
||||
return .left
|
||||
}
|
||||
return [.left, .right]
|
||||
})
|
||||
panRecognizer.delegate = self
|
||||
panRecognizer.delaysTouchesBegan = false
|
||||
panRecognizer.cancelsTouchesInView = true
|
||||
self.addGestureRecognizer(panRecognizer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let _ = otherGestureRecognizer as? InteractiveTransitionGestureRecognizer {
|
||||
return false
|
||||
}
|
||||
if let _ = otherGestureRecognizer as? UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func panGesture(_ recognizer: UIPanGestureRecognizer) {
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
func cancelContextGestures(view: UIView) {
|
||||
if let gestureRecognizers = view.gestureRecognizers {
|
||||
for gesture in gestureRecognizers {
|
||||
if let gesture = gesture as? ContextGesture {
|
||||
gesture.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
for subview in view.subviews {
|
||||
cancelContextGestures(view: subview)
|
||||
}
|
||||
}
|
||||
|
||||
cancelContextGestures(view: self)
|
||||
|
||||
//self.animatingTransition = true
|
||||
case .changed:
|
||||
guard let component = self.component, let currentId = self.currentId else {
|
||||
return
|
||||
}
|
||||
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = recognizer.translation(in: self)
|
||||
var transitionFraction = translation.x / self.bounds.width
|
||||
if index <= 0 {
|
||||
transitionFraction = min(0.0, transitionFraction)
|
||||
}
|
||||
if index >= component.items.count - 1 {
|
||||
transitionFraction = max(0.0, transitionFraction)
|
||||
}
|
||||
self.transitionFraction = transitionFraction
|
||||
self.state?.updated(transition: .immediate)
|
||||
|
||||
// let nextKey = availablePanes[updatedIndex]
|
||||
// print(transitionFraction)
|
||||
//self.paneTransitionPromise.set(transitionFraction)
|
||||
|
||||
//self.update(size: size, sideInset: sideInset, bottomInset: bottomInset, visibleHeight: visibleHeight, expansionFraction: expansionFraction, presentationData: presentationData, data: data, transition: .immediate)
|
||||
//self.currentPaneUpdated?(false)
|
||||
case .cancelled, .ended:
|
||||
guard let component = self.component, let currentId = self.currentId else {
|
||||
return
|
||||
}
|
||||
guard let index = component.items.firstIndex(where: { $0.id == currentId }) else {
|
||||
return
|
||||
}
|
||||
|
||||
let translation = recognizer.translation(in: self)
|
||||
let velocity = recognizer.velocity(in: self)
|
||||
var directionIsToRight: Bool?
|
||||
if abs(velocity.x) > 10.0 {
|
||||
directionIsToRight = velocity.x < 0.0
|
||||
} else {
|
||||
if abs(translation.x) > self.bounds.width / 2.0 {
|
||||
directionIsToRight = translation.x > self.bounds.width / 2.0
|
||||
}
|
||||
}
|
||||
if let directionIsToRight = directionIsToRight {
|
||||
var updatedIndex = index
|
||||
if directionIsToRight {
|
||||
updatedIndex = min(updatedIndex + 1, component.items.count - 1)
|
||||
} else {
|
||||
updatedIndex = max(updatedIndex - 1, 0)
|
||||
}
|
||||
self.currentId = component.items[updatedIndex].id
|
||||
}
|
||||
self.transitionFraction = 0.0
|
||||
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
|
||||
self.animatingTransition = false
|
||||
//self.currentPaneUpdated?(false)
|
||||
|
||||
//self.currentPaneStatusPromise.set(self.currentPane?.node.status ?? .single(nil))
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func update(component: StorageUsagePanelContainerComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: Transition) -> CGSize {
|
||||
let environment = environment[StorageUsagePanelContainerEnvironment.self].value
|
||||
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
||||
self.topPanelBackgroundView.updateColor(color: component.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.8), transition: .immediate)
|
||||
self.panelsBackgroundLayer.backgroundColor = component.theme.list.itemBlocksBackgroundColor.cgColor
|
||||
self.topPanelSeparatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
|
||||
}
|
||||
|
||||
let topPanelFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: 44.0))
|
||||
let topPanelCoverHeight: CGFloat = 10.0
|
||||
|
||||
let topPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: -topPanelCoverHeight), size: CGSize(width: availableSize.width, height: 44.0))
|
||||
transition.setFrame(view: self.topPanelBackgroundView, frame: topPanelFrame)
|
||||
self.topPanelBackgroundView.update(size: topPanelFrame.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
let lockScrollingTransition: Transition
|
||||
if themeUpdated {
|
||||
lockScrollingTransition = transition
|
||||
} else {
|
||||
lockScrollingTransition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut))
|
||||
}
|
||||
lockScrollingTransition.setBackgroundColor(view: self.topPanelBackgroundView, color: environment.isScrollable ? component.theme.rootController.navigationBar.blurredBackgroundColor : component.theme.list.itemBlocksBackgroundColor)
|
||||
|
||||
transition.setFrame(layer: self.panelsBackgroundLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY)))
|
||||
|
||||
transition.setFrame(layer: self.topPanelSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
|
||||
self.currentId = nil
|
||||
}
|
||||
if self.currentId == nil {
|
||||
self.currentId = component.items.first?.id
|
||||
}
|
||||
|
||||
var visibleIds = Set<AnyHashable>()
|
||||
var currentIndex: Int?
|
||||
if let currentId = self.currentId {
|
||||
visibleIds.insert(currentId)
|
||||
|
||||
if let index = component.items.firstIndex(where: { $0.id == currentId }) {
|
||||
currentIndex = index
|
||||
if index != 0 {
|
||||
visibleIds.insert(component.items[index - 1].id)
|
||||
}
|
||||
if index != component.items.count - 1 {
|
||||
visibleIds.insert(component.items[index + 1].id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = self.header.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(StorageUsageHeaderComponent(
|
||||
@ -296,6 +593,17 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
id: item.id,
|
||||
title: item.title
|
||||
)
|
||||
},
|
||||
activeIndex: currentIndex ?? 0,
|
||||
transitionFraction: self.transitionFraction,
|
||||
switchToPanel: { [weak self] id in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
if component.items.contains(where: { $0.id == id }) {
|
||||
self.currentId = id
|
||||
self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring)))
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
@ -308,45 +616,114 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
transition.setFrame(view: headerView, frame: topPanelFrame)
|
||||
}
|
||||
|
||||
if let currentIdValue = self.currentId, !component.items.contains(where: { $0.id == currentIdValue }) {
|
||||
self.currentId = nil
|
||||
}
|
||||
if self.currentId == nil {
|
||||
self.currentId = component.items.first?.id
|
||||
}
|
||||
|
||||
let childEnvironment = StorageUsagePanelEnvironment(
|
||||
theme: component.theme,
|
||||
strings: component.strings,
|
||||
containerInsets: UIEdgeInsets(top: topPanelFrame.height, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right)
|
||||
dateTimeFormat: component.dateTimeFormat,
|
||||
containerInsets: UIEdgeInsets(top: 0.0, left: component.insets.left, bottom: component.insets.bottom, right: component.insets.right),
|
||||
isScrollable: environment.isScrollable
|
||||
)
|
||||
|
||||
let centralPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: topPanelFrame.maxY), size: CGSize(width: availableSize.width, height: availableSize.height - topPanelFrame.maxY))
|
||||
|
||||
if self.animatingTransition {
|
||||
visibleIds = visibleIds.filter({ self.visiblePanels[$0] != nil })
|
||||
}
|
||||
|
||||
self.actualVisibleIds = visibleIds
|
||||
|
||||
for (id, _) in self.visiblePanels {
|
||||
visibleIds.insert(id)
|
||||
}
|
||||
|
||||
var validIds = Set<AnyHashable>()
|
||||
if let currentId = self.currentId, let panelItem = component.items.first(where: { $0.id == currentId }) {
|
||||
validIds.insert(panelItem.id)
|
||||
|
||||
let panel: ComponentView<StorageUsagePanelEnvironment>
|
||||
var panelTransition = transition
|
||||
if let current = self.visiblePanels[panelItem.id] {
|
||||
panel = current
|
||||
} else {
|
||||
panelTransition = .immediate
|
||||
panel = ComponentView()
|
||||
self.visiblePanels[panelItem.id] = panel
|
||||
}
|
||||
let _ = panel.update(
|
||||
transition: panelTransition,
|
||||
component: panelItem.panel,
|
||||
environment: {
|
||||
childEnvironment
|
||||
},
|
||||
containerSize: availableSize
|
||||
)
|
||||
if let panelView = panel.view {
|
||||
if panelView.superview == nil {
|
||||
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
|
||||
if let currentIndex {
|
||||
var anyAnchorOffset: CGFloat = 0.0
|
||||
for (id, panel) in self.visiblePanels {
|
||||
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }), let panelView = panel.view else {
|
||||
continue
|
||||
}
|
||||
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
|
||||
if itemIndex < currentIndex {
|
||||
itemFrame.origin.x -= itemFrame.width
|
||||
} else if itemIndex > currentIndex {
|
||||
itemFrame.origin.x += itemFrame.width
|
||||
}
|
||||
|
||||
anyAnchorOffset = itemFrame.minX - panelView.frame.minX
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
for id in visibleIds {
|
||||
guard let itemIndex = component.items.firstIndex(where: { $0.id == id }) else {
|
||||
continue
|
||||
}
|
||||
let panelItem = component.items[itemIndex]
|
||||
|
||||
var itemFrame = centralPanelFrame.offsetBy(dx: self.transitionFraction * availableSize.width, dy: 0.0)
|
||||
if itemIndex < currentIndex {
|
||||
itemFrame.origin.x -= itemFrame.width
|
||||
} else if itemIndex > currentIndex {
|
||||
itemFrame.origin.x += itemFrame.width
|
||||
}
|
||||
|
||||
validIds.insert(panelItem.id)
|
||||
|
||||
let panel: ComponentView<StorageUsagePanelEnvironment>
|
||||
var panelTransition = transition
|
||||
var animateInIfNeeded = false
|
||||
if let current = self.visiblePanels[panelItem.id] {
|
||||
panel = current
|
||||
|
||||
if let panelView = panel.view, !panelView.bounds.isEmpty {
|
||||
var wasHidden = false
|
||||
if abs(panelView.frame.minX - availableSize.width) < .ulpOfOne || abs(panelView.frame.maxX - 0.0) < .ulpOfOne {
|
||||
wasHidden = true
|
||||
}
|
||||
var isHidden = false
|
||||
if abs(itemFrame.minX - availableSize.width) < .ulpOfOne || abs(itemFrame.maxX - 0.0) < .ulpOfOne {
|
||||
isHidden = true
|
||||
}
|
||||
if wasHidden && isHidden {
|
||||
panelTransition = .immediate
|
||||
}
|
||||
}
|
||||
} else {
|
||||
panelTransition = .immediate
|
||||
animateInIfNeeded = true
|
||||
|
||||
panel = ComponentView()
|
||||
self.visiblePanels[panelItem.id] = panel
|
||||
}
|
||||
let _ = panel.update(
|
||||
transition: panelTransition,
|
||||
component: panelItem.panel,
|
||||
environment: {
|
||||
childEnvironment
|
||||
},
|
||||
containerSize: centralPanelFrame.size
|
||||
)
|
||||
if let panelView = panel.view {
|
||||
if panelView.superview == nil {
|
||||
self.insertSubview(panelView, belowSubview: self.topPanelBackgroundView)
|
||||
}
|
||||
|
||||
panelTransition.setFrame(view: panelView, frame: itemFrame, completion: { [weak self] _ in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
if !self.actualVisibleIds.contains(id) {
|
||||
if let panel = self.visiblePanels[id] {
|
||||
self.visiblePanels.removeValue(forKey: id)
|
||||
panel.view?.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
})
|
||||
if animateInIfNeeded && anyAnchorOffset != 0.0 {
|
||||
transition.animatePosition(view: panelView, from: CGPoint(x: -anyAnchorOffset, y: 0.0), to: CGPoint(), additive: true)
|
||||
}
|
||||
}
|
||||
panelTransition.setFrame(view: panelView, frame: CGRect(origin: CGPoint(), size: availableSize))
|
||||
}
|
||||
}
|
||||
|
||||
@ -371,7 +748,7 @@ final class StorageUsagePanelContainerComponent: Component {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<StorageUsagePanelContainerEnvironment>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,157 @@
|
||||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import ViewControllerComponent
|
||||
import ComponentDisplayAdapters
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import TelegramCore
|
||||
import MultilineTextComponent
|
||||
import EmojiStatusComponent
|
||||
import Postbox
|
||||
import TelegramStringFormatting
|
||||
import CheckNode
|
||||
import SolidRoundedButtonComponent
|
||||
|
||||
final class StorageUsageScreenSelectionPanelComponent: Component {
|
||||
let theme: PresentationTheme
|
||||
let title: String
|
||||
let label: String?
|
||||
let isEnabled: Bool
|
||||
let insets: UIEdgeInsets
|
||||
let action: () -> Void
|
||||
|
||||
init(
|
||||
theme: PresentationTheme,
|
||||
title: String,
|
||||
label: String?,
|
||||
isEnabled: Bool,
|
||||
insets: UIEdgeInsets,
|
||||
action: @escaping () -> Void
|
||||
) {
|
||||
self.theme = theme
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.isEnabled = isEnabled
|
||||
self.insets = insets
|
||||
self.action = action
|
||||
}
|
||||
|
||||
static func ==(lhs: StorageUsageScreenSelectionPanelComponent, rhs: StorageUsageScreenSelectionPanelComponent) -> Bool {
|
||||
if lhs.theme !== rhs.theme {
|
||||
return false
|
||||
}
|
||||
if lhs.title != rhs.title {
|
||||
return false
|
||||
}
|
||||
if lhs.label != rhs.label {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
return false
|
||||
}
|
||||
if lhs.insets != rhs.insets {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
class View: UIView {
|
||||
private let backgroundView: BlurredBackgroundView
|
||||
private let separatorLayer: SimpleLayer
|
||||
private let actionButton = ComponentView<Empty>()
|
||||
|
||||
private var component: StorageUsageScreenSelectionPanelComponent?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
self.backgroundView = BlurredBackgroundView(color: nil, enableBlur: true)
|
||||
self.separatorLayer = SimpleLayer()
|
||||
|
||||
super.init(frame: frame)
|
||||
|
||||
self.addSubview(self.backgroundView)
|
||||
self.layer.addSublayer(self.separatorLayer)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: StorageUsageScreenSelectionPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
let themeUpdated = self.component?.theme !== component.theme
|
||||
self.component = component
|
||||
|
||||
let topInset: CGFloat = 8.0
|
||||
|
||||
let bottomInset: CGFloat
|
||||
if component.insets.bottom == 0.0 {
|
||||
bottomInset = topInset
|
||||
} else {
|
||||
bottomInset = component.insets.bottom + 10.0
|
||||
}
|
||||
|
||||
let height: CGFloat = topInset + 50.0 + bottomInset
|
||||
|
||||
if themeUpdated {
|
||||
self.backgroundView.updateColor(color: component.theme.rootController.navigationBar.blurredBackgroundColor, transition: .immediate)
|
||||
self.separatorLayer.backgroundColor = component.theme.rootController.navigationBar.separatorColor.cgColor
|
||||
}
|
||||
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: availableSize.height, height: height))
|
||||
transition.setFrame(view: self.backgroundView, frame: backgroundFrame)
|
||||
self.backgroundView.update(size: backgroundFrame.size, transition: transition.containedViewLayoutTransition)
|
||||
|
||||
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel)))
|
||||
|
||||
let actionButtonSize = self.actionButton.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(SolidRoundedButtonComponent(
|
||||
title: component.title,
|
||||
label: component.label,
|
||||
theme: SolidRoundedButtonComponent.Theme(
|
||||
backgroundColor: component.theme.list.itemCheckColors.fillColor,
|
||||
backgroundColors: [],
|
||||
foregroundColor: component.theme.list.itemCheckColors.foregroundColor
|
||||
),
|
||||
font: .bold,
|
||||
fontSize: 17.0,
|
||||
height: 50.0,
|
||||
cornerRadius: 10.0,
|
||||
gloss: false,
|
||||
isEnabled: component.isEnabled,
|
||||
animationName: nil,
|
||||
iconPosition: .right,
|
||||
iconSpacing: 4.0,
|
||||
action: { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.component?.action()
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - component.insets.left - component.insets.right, height: 50.0)
|
||||
)
|
||||
if let actionButtonView = self.actionButton.view {
|
||||
if actionButtonView.superview == nil {
|
||||
self.addSubview(actionButtonView)
|
||||
}
|
||||
transition.setFrame(view: actionButtonView, frame: CGRect(origin: CGPoint(x: component.insets.left, y: topInset), size: actionButtonSize))
|
||||
}
|
||||
|
||||
return CGSize(width: availableSize.width, height: height)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
@ -1317,7 +1317,7 @@ private func extractAccountManagerState(records: AccountRecordsView<TelegramAcco
|
||||
var signals: Signal<Never, NoError> = .complete()
|
||||
|
||||
for (_, context, _) in activeAccounts.accounts {
|
||||
signals = signals |> then(context.account.cleanupTasks())
|
||||
signals = signals |> then(context.account.cleanupTasks(lowImpact: false))
|
||||
}
|
||||
|
||||
disposable.set(signals.start(completed: {
|
||||
|
@ -268,7 +268,7 @@ private final class ChatContextResultPeekNode: ASDisplayNode, PeekControllerCont
|
||||
let layerHolder = takeSampleBufferLayer()
|
||||
layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
|
||||
self.layer.addSublayer(layerHolder.layer)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .gif, fileReference: videoFileReference, layerHolder: layerHolder)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: videoFileReference, layerHolder: layerHolder)
|
||||
self.videoLayer = (thumbnailLayer, manager, layerHolder)
|
||||
thumbnailLayer.ready = { [weak self, weak thumbnailLayer, weak manager] in
|
||||
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
|
||||
|
@ -399,7 +399,7 @@ final class HorizontalListContextResultsChatInputPanelItemNode: ListViewItemNode
|
||||
layerHolder.layer.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
|
||||
strongSelf.layer.addSublayer(layerHolder.layer)
|
||||
|
||||
let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .gif, fileReference: .standalone(media: videoFile), layerHolder: layerHolder)
|
||||
let manager = SoftwareVideoLayerFrameManager(account: item.account, userLocation: .other, userContentType: .other, fileReference: .standalone(media: videoFile), layerHolder: layerHolder)
|
||||
strongSelf.videoLayer = (thumbnailLayer, manager, layerHolder)
|
||||
thumbnailLayer.ready = { [weak thumbnailLayer, weak manager] in
|
||||
if let strongSelf = self, let thumbnailLayer = thumbnailLayer, let manager = manager {
|
||||
|
@ -75,7 +75,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode {
|
||||
let source = UniversalSoftwareVideoSource(
|
||||
mediaBox: self.context.account.postbox.mediaBox,
|
||||
userLocation: userLocation,
|
||||
userContentType: .gif,
|
||||
userContentType: .other,
|
||||
fileReference: self.file,
|
||||
automaticallyFetchHeader: true
|
||||
)
|
||||
|
@ -65,7 +65,7 @@ private final class FrameSequenceThumbnailNode: ASDisplayNode {
|
||||
let source = UniversalSoftwareVideoSource(
|
||||
mediaBox: self.context.account.postbox.mediaBox,
|
||||
userLocation: userLocation,
|
||||
userContentType: .gif,
|
||||
userContentType: .other,
|
||||
fileReference: self.file,
|
||||
automaticallyFetchHeader: true
|
||||
)
|
||||
@ -301,7 +301,7 @@ private final class VisualMediaItemNode: ASDisplayNode {
|
||||
self.imageNode.layer.addSublayer(sampleBufferLayer.layer)
|
||||
}
|
||||
|
||||
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .gif, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer)
|
||||
self.videoLayerFrameManager = SoftwareVideoLayerFrameManager(account: self.context.account, userLocation: .peer(item.message.id.peerId), userContentType: .other, fileReference: FileMediaReference.message(message: MessageReference(item.message), media: file), layerHolder: sampleBufferLayer)
|
||||
self.videoLayerFrameManager?.start()
|
||||
}
|
||||
} else {
|
||||
|
Loading…
x
Reference in New Issue
Block a user