mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
Storage calculation
This commit is contained in:
parent
34909d0de9
commit
45ff6ba714
@ -65,11 +65,11 @@ public func peerAvatarImageData(account: Account, peerReference: PeerReference?,
|
|||||||
})
|
})
|
||||||
var fetchedDataDisposable: Disposable?
|
var fetchedDataDisposable: Disposable?
|
||||||
if let peerReference = peerReference {
|
if let peerReference = peerReference {
|
||||||
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start()
|
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .avatar(peer: peerReference, resource: smallProfileImage.resource), statsCategory: .generic).start()
|
||||||
} else if let authorOfMessage = authorOfMessage {
|
} else if let authorOfMessage = authorOfMessage {
|
||||||
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start()
|
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .messageAuthorAvatar(message: authorOfMessage, resource: smallProfileImage.resource), statsCategory: .generic).start()
|
||||||
} else {
|
} else {
|
||||||
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .image, reference: .standalone(resource: smallProfileImage.resource), statsCategory: .generic).start()
|
fetchedDataDisposable = fetchedMediaResource(mediaBox: account.postbox.mediaBox, userLocation: .other, userContentType: .avatar, reference: .standalone(resource: smallProfileImage.resource), statsCategory: .generic).start()
|
||||||
}
|
}
|
||||||
return ActionDisposable {
|
return ActionDisposable {
|
||||||
resourceDataDisposable.dispose()
|
resourceDataDisposable.dispose()
|
||||||
|
@ -605,7 +605,7 @@ public struct Transition {
|
|||||||
public func animateBounds(layer: CALayer, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
public func animateBounds(layer: CALayer, from fromValue: CGRect, to toValue: CGRect, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
break
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
layer.animate(
|
layer.animate(
|
||||||
from: NSValue(cgRect: fromValue),
|
from: NSValue(cgRect: fromValue),
|
||||||
@ -624,7 +624,7 @@ public struct Transition {
|
|||||||
public func animateBoundsOrigin(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
public func animateBoundsOrigin(layer: CALayer, from fromValue: CGPoint, to toValue: CGPoint, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
break
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
layer.animate(
|
layer.animate(
|
||||||
from: NSValue(cgPoint: fromValue),
|
from: NSValue(cgPoint: fromValue),
|
||||||
@ -643,7 +643,7 @@ public struct Transition {
|
|||||||
public func animateBoundsSize(layer: CALayer, from fromValue: CGSize, to toValue: CGSize, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
public func animateBoundsSize(layer: CALayer, from fromValue: CGSize, to toValue: CGSize, additive: Bool = false, completion: ((Bool) -> Void)? = nil) {
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
break
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
layer.animate(
|
layer.animate(
|
||||||
from: NSValue(cgSize: fromValue),
|
from: NSValue(cgSize: fromValue),
|
||||||
@ -661,6 +661,7 @@ public struct Transition {
|
|||||||
|
|
||||||
public func setCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
public func setCornerRadius(layer: CALayer, cornerRadius: CGFloat, completion: ((Bool) -> Void)? = nil) {
|
||||||
if layer.cornerRadius == cornerRadius {
|
if layer.cornerRadius == cornerRadius {
|
||||||
|
completion?(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
switch self.animation {
|
switch self.animation {
|
||||||
@ -693,8 +694,9 @@ public struct Transition {
|
|||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
layer.path = path
|
layer.path = path
|
||||||
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
if let previousPath = layer.path {
|
if let previousPath = layer.path, previousPath != path {
|
||||||
layer.animate(
|
layer.animate(
|
||||||
from: previousPath,
|
from: previousPath,
|
||||||
to: path,
|
to: path,
|
||||||
@ -709,6 +711,7 @@ public struct Transition {
|
|||||||
layer.path = path
|
layer.path = path
|
||||||
} else {
|
} else {
|
||||||
layer.path = path
|
layer.path = path
|
||||||
|
completion?(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -717,6 +720,7 @@ public struct Transition {
|
|||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
layer.lineWidth = lineWidth
|
layer.lineWidth = lineWidth
|
||||||
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
let previousLineWidth = layer.lineWidth
|
let previousLineWidth = layer.lineWidth
|
||||||
layer.lineWidth = lineWidth
|
layer.lineWidth = lineWidth
|
||||||
@ -739,6 +743,7 @@ public struct Transition {
|
|||||||
switch self.animation {
|
switch self.animation {
|
||||||
case .none:
|
case .none:
|
||||||
layer.lineDashPattern = pattern
|
layer.lineDashPattern = pattern
|
||||||
|
completion?(true)
|
||||||
case let .curve(duration, curve):
|
case let .curve(duration, curve):
|
||||||
if let previousLineDashPattern = layer.lineDashPattern {
|
if let previousLineDashPattern = layer.lineDashPattern {
|
||||||
layer.lineDashPattern = pattern
|
layer.lineDashPattern = pattern
|
||||||
@ -756,6 +761,7 @@ public struct Transition {
|
|||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
layer.lineDashPattern = pattern
|
layer.lineDashPattern = pattern
|
||||||
|
completion?(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -141,6 +141,7 @@ public final class ComponentView<EnvironmentType> {
|
|||||||
private var currentSize: CGSize?
|
private var currentSize: CGSize?
|
||||||
public private(set) var view: UIView?
|
public private(set) var view: UIView?
|
||||||
private(set) var isUpdating: Bool = false
|
private(set) var isUpdating: Bool = false
|
||||||
|
public weak var parentState: ComponentState?
|
||||||
|
|
||||||
public init() {
|
public init() {
|
||||||
}
|
}
|
||||||
@ -181,10 +182,15 @@ public final class ComponentView<EnvironmentType> {
|
|||||||
context.erasedEnvironment = environmentResult
|
context.erasedEnvironment = environmentResult
|
||||||
}
|
}
|
||||||
|
|
||||||
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()
|
var isStateUpdated = false
|
||||||
|
if componentState.isUpdated {
|
||||||
|
isStateUpdated = true
|
||||||
|
componentState.isUpdated = false
|
||||||
|
}
|
||||||
|
|
||||||
if !forceUpdate, !isEnvironmentUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
|
let isEnvironmentUpdated = context.erasedEnvironment.calculateIsUpdated()
|
||||||
|
|
||||||
|
if !forceUpdate, !isEnvironmentUpdated, !isStateUpdated, let currentComponent = self.currentComponent, let currentContainerSize = self.currentContainerSize, let currentSize = self.currentSize {
|
||||||
if currentContainerSize == containerSize && currentComponent == component {
|
if currentContainerSize == containerSize && currentComponent == component {
|
||||||
self.isUpdating = false
|
self.isUpdating = false
|
||||||
return currentSize
|
return currentSize
|
||||||
@ -197,9 +203,13 @@ public final class ComponentView<EnvironmentType> {
|
|||||||
guard let strongSelf = self else {
|
guard let strongSelf = self else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
|
if let parentState = strongSelf.parentState {
|
||||||
preconditionFailure()
|
parentState.updated(transition: transition)
|
||||||
} as () -> Environment<EnvironmentType>, updateEnvironment: false, forceUpdate: true, containerSize: containerSize)
|
} else {
|
||||||
|
let _ = strongSelf._update(transition: transition, component: component, maybeEnvironment: {
|
||||||
|
preconditionFailure()
|
||||||
|
} as () -> Environment<EnvironmentType>, updateEnvironment: false, forceUpdate: true, containerSize: containerSize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition)
|
let updatedSize = component._update(view: componentView, availableSize: containerSize, environment: context.erasedEnvironment, transition: transition)
|
||||||
@ -207,6 +217,9 @@ public final class ComponentView<EnvironmentType> {
|
|||||||
if isEnvironmentUpdated {
|
if isEnvironmentUpdated {
|
||||||
context.erasedEnvironment._isUpdated = false
|
context.erasedEnvironment._isUpdated = false
|
||||||
}
|
}
|
||||||
|
if isStateUpdated {
|
||||||
|
context.erasedState.isUpdated = false
|
||||||
|
}
|
||||||
|
|
||||||
self.isUpdating = false
|
self.isUpdating = false
|
||||||
|
|
||||||
|
@ -39,7 +39,7 @@ public func reactionStaticImage(context: AccountContext, animation: TelegramMedi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .emoji, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true, customColor: customColor)
|
let fetchFrame = animationCacheFetchFile(context: context, userLocation: .other, userContentType: .sticker, resource: MediaResourceReference.standalone(resource: animation.resource), type: type, keyframeOnly: true, customColor: customColor)
|
||||||
|
|
||||||
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
class AnimationCacheItemWriterImpl: AnimationCacheItemWriter {
|
||||||
let queue: Queue
|
let queue: Queue
|
||||||
|
@ -9,6 +9,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
public typealias Theme = SolidRoundedButtonTheme
|
public typealias Theme = SolidRoundedButtonTheme
|
||||||
|
|
||||||
public let title: String?
|
public let title: String?
|
||||||
|
public let label: String?
|
||||||
public let icon: UIImage?
|
public let icon: UIImage?
|
||||||
public let theme: SolidRoundedButtonTheme
|
public let theme: SolidRoundedButtonTheme
|
||||||
public let font: SolidRoundedButtonFont
|
public let font: SolidRoundedButtonFont
|
||||||
@ -16,6 +17,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
public let height: CGFloat
|
public let height: CGFloat
|
||||||
public let cornerRadius: CGFloat
|
public let cornerRadius: CGFloat
|
||||||
public let gloss: Bool
|
public let gloss: Bool
|
||||||
|
public let isEnabled: Bool
|
||||||
public let iconName: String?
|
public let iconName: String?
|
||||||
public let animationName: String?
|
public let animationName: String?
|
||||||
public let iconPosition: SolidRoundedButtonIconPosition
|
public let iconPosition: SolidRoundedButtonIconPosition
|
||||||
@ -25,6 +27,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
|
|
||||||
public init(
|
public init(
|
||||||
title: String? = nil,
|
title: String? = nil,
|
||||||
|
label: String? = nil,
|
||||||
icon: UIImage? = nil,
|
icon: UIImage? = nil,
|
||||||
theme: SolidRoundedButtonTheme,
|
theme: SolidRoundedButtonTheme,
|
||||||
font: SolidRoundedButtonFont = .bold,
|
font: SolidRoundedButtonFont = .bold,
|
||||||
@ -32,6 +35,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
height: CGFloat = 48.0,
|
height: CGFloat = 48.0,
|
||||||
cornerRadius: CGFloat = 24.0,
|
cornerRadius: CGFloat = 24.0,
|
||||||
gloss: Bool = false,
|
gloss: Bool = false,
|
||||||
|
isEnabled: Bool = true,
|
||||||
iconName: String? = nil,
|
iconName: String? = nil,
|
||||||
animationName: String? = nil,
|
animationName: String? = nil,
|
||||||
iconPosition: SolidRoundedButtonIconPosition = .left,
|
iconPosition: SolidRoundedButtonIconPosition = .left,
|
||||||
@ -40,6 +44,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
action: @escaping () -> Void
|
action: @escaping () -> Void
|
||||||
) {
|
) {
|
||||||
self.title = title
|
self.title = title
|
||||||
|
self.label = label
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.font = font
|
self.font = font
|
||||||
@ -47,6 +52,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
self.height = height
|
self.height = height
|
||||||
self.cornerRadius = cornerRadius
|
self.cornerRadius = cornerRadius
|
||||||
self.gloss = gloss
|
self.gloss = gloss
|
||||||
|
self.isEnabled = isEnabled
|
||||||
self.iconName = iconName
|
self.iconName = iconName
|
||||||
self.animationName = animationName
|
self.animationName = animationName
|
||||||
self.iconPosition = iconPosition
|
self.iconPosition = iconPosition
|
||||||
@ -59,6 +65,9 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
if lhs.title != rhs.title {
|
if lhs.title != rhs.title {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.label != rhs.label {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.icon !== rhs.icon {
|
if lhs.icon !== rhs.icon {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -80,6 +89,9 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
if lhs.gloss != rhs.gloss {
|
if lhs.gloss != rhs.gloss {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if lhs.isEnabled != rhs.isEnabled {
|
||||||
|
return false
|
||||||
|
}
|
||||||
if lhs.iconName != rhs.iconName {
|
if lhs.iconName != rhs.iconName {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -108,6 +120,7 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
if self.button == nil {
|
if self.button == nil {
|
||||||
let button = SolidRoundedButtonView(
|
let button = SolidRoundedButtonView(
|
||||||
title: component.title,
|
title: component.title,
|
||||||
|
label: component.label,
|
||||||
icon: component.icon,
|
icon: component.icon,
|
||||||
theme: component.theme,
|
theme: component.theme,
|
||||||
font: component.font,
|
font: component.font,
|
||||||
@ -127,12 +140,15 @@ public final class SolidRoundedButtonComponent: Component {
|
|||||||
|
|
||||||
if let button = self.button {
|
if let button = self.button {
|
||||||
button.title = component.title
|
button.title = component.title
|
||||||
|
button.label = component.label
|
||||||
button.iconPosition = component.iconPosition
|
button.iconPosition = component.iconPosition
|
||||||
button.iconSpacing = component.iconSpacing
|
button.iconSpacing = component.iconSpacing
|
||||||
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
|
button.icon = component.iconName.flatMap { UIImage(bundleImageName: $0) }
|
||||||
button.animation = component.animationName
|
button.animation = component.animationName
|
||||||
button.gloss = component.gloss
|
button.gloss = component.gloss
|
||||||
|
|
||||||
|
button.isEnabled = component.isEnabled
|
||||||
|
|
||||||
button.updateTheme(component.theme)
|
button.updateTheme(component.theme)
|
||||||
let height = button.updateLayout(width: availableSize.width, transition: .immediate)
|
let height = button.updateLayout(width: availableSize.width, transition: .immediate)
|
||||||
transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil)
|
transition.setFrame(view: button, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)), completion: nil)
|
||||||
|
@ -250,6 +250,10 @@ private final class FetchManagerCategoryContext {
|
|||||||
userContentType = .image
|
userContentType = .image
|
||||||
case .video:
|
case .video:
|
||||||
userContentType = .video
|
userContentType = .video
|
||||||
|
case .audio:
|
||||||
|
userContentType = .audio
|
||||||
|
case .file:
|
||||||
|
userContentType = .file
|
||||||
default:
|
default:
|
||||||
userContentType = .other
|
userContentType = .other
|
||||||
}
|
}
|
||||||
@ -376,6 +380,10 @@ private final class FetchManagerCategoryContext {
|
|||||||
userContentType = .image
|
userContentType = .image
|
||||||
case .video:
|
case .video:
|
||||||
userContentType = .video
|
userContentType = .video
|
||||||
|
case .audio:
|
||||||
|
userContentType = .audio
|
||||||
|
case .file:
|
||||||
|
userContentType = .file
|
||||||
default:
|
default:
|
||||||
userContentType = .other
|
userContentType = .other
|
||||||
}
|
}
|
||||||
@ -407,6 +415,10 @@ private final class FetchManagerCategoryContext {
|
|||||||
userContentType = .image
|
userContentType = .image
|
||||||
case .video:
|
case .video:
|
||||||
userContentType = .video
|
userContentType = .video
|
||||||
|
case .audio:
|
||||||
|
userContentType = .audio
|
||||||
|
case .file:
|
||||||
|
userContentType = .file
|
||||||
default:
|
default:
|
||||||
userContentType = .other
|
userContentType = .other
|
||||||
}
|
}
|
||||||
|
@ -77,3 +77,11 @@
|
|||||||
+ (NSData *)_manuallyEncryptedMessage:(NSData *)preparedData messageId:(int64_t)messageId authKey:(MTDatacenterAuthKey *)authKey;
|
+ (NSData *)_manuallyEncryptedMessage:(NSData *)preparedData messageId:(int64_t)messageId authKey:(MTDatacenterAuthKey *)authKey;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
//#define DIRSTAT_FAST_ONLY 0x1
|
||||||
|
struct darwin_dirstat {
|
||||||
|
off_t total_size;
|
||||||
|
uint64_t descendants;
|
||||||
|
};
|
||||||
|
|
||||||
|
int dirstat_np(const char *path, int flags, struct darwin_dirstat *ds, size_t dirstat_size);
|
||||||
|
@ -561,7 +561,7 @@ public final class MediaBox {
|
|||||||
paths.partial,
|
paths.partial,
|
||||||
paths.partial + ".meta"
|
paths.partial + ".meta"
|
||||||
])
|
])
|
||||||
if let fileContext = MediaBoxFileContext(queue: self.dataQueue, manager: self.dataFileManager, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") {
|
if let fileContext = MediaBoxFileContext(queue: self.dataQueue, manager: self.dataFileManager, storageBox: self.storageBox, resourceId: id.stringRepresentation.data(using: .utf8)!, path: paths.complete, partialPath: paths.partial, metaPath: paths.partial + ".meta") {
|
||||||
context = fileContext
|
context = fileContext
|
||||||
self.fileContexts[resourceId] = fileContext
|
self.fileContexts[resourceId] = fileContext
|
||||||
} else {
|
} else {
|
||||||
@ -598,7 +598,7 @@ public final class MediaBox {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let location = parameters?.location {
|
if let parameters = parameters, let location = parameters.location {
|
||||||
var messageNamespace: Int32 = 0
|
var messageNamespace: Int32 = 0
|
||||||
var messageIdValue: Int32 = 0
|
var messageIdValue: Int32 = 0
|
||||||
if let messageId = location.messageId {
|
if let messageId = location.messageId {
|
||||||
@ -606,7 +606,9 @@ public final class MediaBox {
|
|||||||
messageIdValue = messageId.id
|
messageIdValue = messageId.id
|
||||||
}
|
}
|
||||||
|
|
||||||
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: messageNamespace), messageId: messageIdValue), to: resource.id.stringRepresentation.data(using: .utf8)!)
|
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: messageNamespace), messageId: messageIdValue), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters.contentType.rawValue)
|
||||||
|
} else {
|
||||||
|
self.storageBox.add(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters?.contentType.rawValue ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else {
|
guard let (fileContext, releaseContext) = self.fileContext(for: resource.id) else {
|
||||||
@ -771,7 +773,7 @@ public final class MediaBox {
|
|||||||
self.dataQueue.async {
|
self.dataQueue.async {
|
||||||
let paths = self.storePathsForId(resource.id)
|
let paths = self.storePathsForId(resource.id)
|
||||||
|
|
||||||
if let location = parameters?.location {
|
if let parameters = parameters, let location = parameters.location {
|
||||||
var messageNamespace: Int32 = 0
|
var messageNamespace: Int32 = 0
|
||||||
var messageIdValue: Int32 = 0
|
var messageIdValue: Int32 = 0
|
||||||
if let messageId = location.messageId {
|
if let messageId = location.messageId {
|
||||||
@ -779,7 +781,9 @@ public final class MediaBox {
|
|||||||
messageIdValue = messageId.id
|
messageIdValue = messageId.id
|
||||||
}
|
}
|
||||||
|
|
||||||
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: messageNamespace), messageId: messageIdValue), to: resource.id.stringRepresentation.data(using: .utf8)!)
|
self.storageBox.add(reference: StorageBox.Reference(peerId: location.peerId.toInt64(), messageNamespace: UInt8(clamping: messageNamespace), messageId: messageIdValue), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters.contentType.rawValue)
|
||||||
|
} else {
|
||||||
|
self.storageBox.add(reference: StorageBox.Reference(peerId: 0, messageNamespace: 0, messageId: 0), to: resource.id.stringRepresentation.data(using: .utf8)!, contentType: parameters?.contentType.rawValue ?? 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let _ = fileSize(paths.complete) {
|
if let _ = fileSize(paths.complete) {
|
||||||
@ -1289,9 +1293,17 @@ public final class MediaBox {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
storageBox.addEmptyReferencesIfNotReferenced(ids: results.map { name -> Data in
|
storageBox.addEmptyReferencesIfNotReferenced(ids: results.map { name -> (id: Data, size: Int64) in
|
||||||
return MediaBox.idForFileName(name: name).data(using: .utf8)!
|
let resourceId = MediaBox.idForFileName(name: name)
|
||||||
}, completion: { addedCount in
|
let paths = self.storePathsForId(MediaResourceId(resourceId))
|
||||||
|
var size: Int64 = 0
|
||||||
|
if let value = fileSize(paths.complete) {
|
||||||
|
size = value
|
||||||
|
} else if let value = fileSize(paths.partial) {
|
||||||
|
size = value
|
||||||
|
}
|
||||||
|
return (resourceId.data(using: .utf8)!, size)
|
||||||
|
}, contentType: MediaResourceUserContentType.other.rawValue, completion: { addedCount in
|
||||||
if addedCount != 0 {
|
if addedCount != 0 {
|
||||||
postboxLog("UpdateResourceIndex: added \(addedCount) unreferenced ids")
|
postboxLog("UpdateResourceIndex: added \(addedCount) unreferenced ids")
|
||||||
}
|
}
|
||||||
|
@ -460,6 +460,8 @@ private class MediaBoxPartialFileDataRequest {
|
|||||||
final class MediaBoxPartialFile {
|
final class MediaBoxPartialFile {
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
private let manager: MediaBoxFileManager
|
private let manager: MediaBoxFileManager
|
||||||
|
private let storageBox: StorageBox
|
||||||
|
private let resourceId: Data
|
||||||
private let path: String
|
private let path: String
|
||||||
private let metaPath: String
|
private let metaPath: String
|
||||||
private let completePath: String
|
private let completePath: String
|
||||||
@ -476,9 +478,12 @@ final class MediaBoxPartialFile {
|
|||||||
private var currentFetch: (Promise<[(Range<Int64>, MediaBoxFetchPriority)]>, Disposable)?
|
private var currentFetch: (Promise<[(Range<Int64>, MediaBoxFetchPriority)]>, Disposable)?
|
||||||
private var processedAtLeastOneFetch: Bool = false
|
private var processedAtLeastOneFetch: Bool = false
|
||||||
|
|
||||||
init?(queue: Queue, manager: MediaBoxFileManager, path: String, metaPath: String, completePath: String, completed: @escaping (Int64) -> Void) {
|
init?(queue: Queue, manager: MediaBoxFileManager, storageBox: StorageBox, resourceId: Data, path: String, metaPath: String, completePath: String, completed: @escaping (Int64) -> Void) {
|
||||||
assert(queue.isCurrent())
|
assert(queue.isCurrent())
|
||||||
self.manager = manager
|
self.manager = manager
|
||||||
|
self.storageBox = storageBox
|
||||||
|
self.resourceId = resourceId
|
||||||
|
|
||||||
if let fd = manager.open(path: path, mode: .readwrite) {
|
if let fd = manager.open(path: path, mode: .readwrite) {
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
self.path = path
|
self.path = path
|
||||||
@ -504,6 +509,7 @@ final class MediaBoxPartialFile {
|
|||||||
} else {
|
} else {
|
||||||
self.fileMap = MediaBoxFileMap()
|
self.fileMap = MediaBoxFileMap()
|
||||||
}
|
}
|
||||||
|
self.storageBox.update(id: self.resourceId, size: self.fileMap.sum)
|
||||||
self.missingRanges = MediaBoxFileMissingRanges()
|
self.missingRanges = MediaBoxFileMissingRanges()
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
@ -586,6 +592,8 @@ final class MediaBoxPartialFile {
|
|||||||
}
|
}
|
||||||
self.statusRequests.removeAll()
|
self.statusRequests.removeAll()
|
||||||
|
|
||||||
|
self.storageBox.update(id: self.resourceId, size: self.fileMap.sum)
|
||||||
|
|
||||||
self.completed(self.fileMap.sum)
|
self.completed(self.fileMap.sum)
|
||||||
} else {
|
} else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
@ -629,6 +637,8 @@ final class MediaBoxPartialFile {
|
|||||||
}
|
}
|
||||||
self.statusRequests.removeAll()
|
self.statusRequests.removeAll()
|
||||||
|
|
||||||
|
self.storageBox.update(id: self.resourceId, size: size)
|
||||||
|
|
||||||
self.completed(size)
|
self.completed(size)
|
||||||
} else {
|
} else {
|
||||||
assertionFailure()
|
assertionFailure()
|
||||||
@ -675,6 +685,8 @@ final class MediaBoxPartialFile {
|
|||||||
self.fileMap.fill(range)
|
self.fileMap.fill(range)
|
||||||
self.fileMap.serialize(manager: self.manager, to: self.metaPath)
|
self.fileMap.serialize(manager: self.manager, to: self.metaPath)
|
||||||
|
|
||||||
|
self.storageBox.update(id: self.resourceId, size: self.fileMap.sum)
|
||||||
|
|
||||||
self.checkDataRequestsAfterFill(range: range)
|
self.checkDataRequestsAfterFill(range: range)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1184,7 +1196,7 @@ final class MediaBoxFileContext {
|
|||||||
return self.references.isEmpty
|
return self.references.isEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
init?(queue: Queue, manager: MediaBoxFileManager, path: String, partialPath: String, metaPath: String) {
|
init?(queue: Queue, manager: MediaBoxFileManager, storageBox: StorageBox, resourceId: Data, path: String, partialPath: String, metaPath: String) {
|
||||||
assert(queue.isCurrent())
|
assert(queue.isCurrent())
|
||||||
|
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
@ -1195,7 +1207,7 @@ final class MediaBoxFileContext {
|
|||||||
var completeImpl: ((Int64) -> Void)?
|
var completeImpl: ((Int64) -> Void)?
|
||||||
if let size = fileSize(path) {
|
if let size = fileSize(path) {
|
||||||
self.content = .complete(path, size)
|
self.content = .complete(path, size)
|
||||||
} else if let file = MediaBoxPartialFile(queue: queue, manager: manager, path: partialPath, metaPath: metaPath, completePath: path, completed: { size in
|
} else if let file = MediaBoxPartialFile(queue: queue, manager: manager, storageBox: storageBox, resourceId: resourceId, path: partialPath, metaPath: metaPath, completePath: path, completed: { size in
|
||||||
completeImpl?(size)
|
completeImpl?(size)
|
||||||
}) {
|
}) {
|
||||||
self.content = .partial(file)
|
self.content = .partial(file)
|
||||||
|
@ -50,14 +50,14 @@ public final class MediaResourceStorageLocation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public enum MediaResourceUserContentType: UInt8, Equatable {
|
public enum MediaResourceUserContentType: UInt8, Equatable {
|
||||||
case image = 0
|
case other = 0
|
||||||
case video = 1
|
case image = 1
|
||||||
case audio = 2
|
case video = 2
|
||||||
case file = 3
|
case audio = 3
|
||||||
case gif = 4
|
case file = 4
|
||||||
case emoji = 5
|
case gif = 5
|
||||||
case sticker = 6
|
case sticker = 6
|
||||||
case other = 7
|
case avatar = 7
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct MediaResourceFetchParameters {
|
public struct MediaResourceFetchParameters {
|
||||||
|
@ -19,6 +19,14 @@ private func md5Hash(_ data: Data) -> HashId {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public final class StorageBox {
|
public final class StorageBox {
|
||||||
|
public struct Stats {
|
||||||
|
public var contentTypes: [UInt8: Int64]
|
||||||
|
|
||||||
|
public init(contentTypes: [UInt8: Int64]) {
|
||||||
|
self.contentTypes = contentTypes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public struct Reference {
|
public struct Reference {
|
||||||
public var peerId: Int64
|
public var peerId: Int64
|
||||||
public var messageNamespace: UInt8
|
public var messageNamespace: UInt8
|
||||||
@ -53,15 +61,78 @@ public final class StorageBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private struct ItemInfo {
|
||||||
|
var id: Data
|
||||||
|
var contentType: UInt8
|
||||||
|
var size: Int64
|
||||||
|
|
||||||
|
init(id: Data, contentType: UInt8, size: Int64) {
|
||||||
|
self.id = id
|
||||||
|
self.contentType = contentType
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
init(buffer: MemoryBuffer) {
|
||||||
|
var id = Data()
|
||||||
|
var contentType: UInt8 = 0
|
||||||
|
var size: Int64 = 0
|
||||||
|
|
||||||
|
withExtendedLifetime(buffer, {
|
||||||
|
let readBuffer = ReadBuffer(memoryBufferNoCopy: buffer)
|
||||||
|
var version: UInt8 = 0
|
||||||
|
readBuffer.read(&version, offset: 0, length: 1)
|
||||||
|
let _ = version
|
||||||
|
|
||||||
|
var idLength: UInt16 = 0
|
||||||
|
readBuffer.read(&idLength, offset: 0, length: 2)
|
||||||
|
id.count = Int(idLength)
|
||||||
|
id.withUnsafeMutableBytes { buffer -> Void in
|
||||||
|
readBuffer.read(buffer.baseAddress!, offset: 0, length: buffer.count)
|
||||||
|
}
|
||||||
|
|
||||||
|
readBuffer.read(&contentType, offset: 0, length: 1)
|
||||||
|
|
||||||
|
readBuffer.read(&size, offset: 0, length: 8)
|
||||||
|
})
|
||||||
|
|
||||||
|
self.id = id
|
||||||
|
self.contentType = contentType
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
|
||||||
|
func serialize() -> MemoryBuffer {
|
||||||
|
let writeBuffer = WriteBuffer()
|
||||||
|
|
||||||
|
var version: UInt8 = 0
|
||||||
|
writeBuffer.write(&version, length: 1)
|
||||||
|
|
||||||
|
var idLength = UInt16(clamping: self.id.count)
|
||||||
|
writeBuffer.write(&idLength, length: 2)
|
||||||
|
self.id.withUnsafeBytes { buffer in
|
||||||
|
writeBuffer.write(buffer.baseAddress!, length: Int(idLength))
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentType = self.contentType
|
||||||
|
writeBuffer.write(&contentType, length: 1)
|
||||||
|
|
||||||
|
var size = self.size
|
||||||
|
writeBuffer.write(&size, length: 8)
|
||||||
|
|
||||||
|
return writeBuffer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private final class Impl {
|
private final class Impl {
|
||||||
let queue: Queue
|
let queue: Queue
|
||||||
let logger: StorageBox.Logger
|
let logger: StorageBox.Logger
|
||||||
let basePath: String
|
let basePath: String
|
||||||
let valueBox: SqliteValueBox
|
let valueBox: SqliteValueBox
|
||||||
let hashIdToIdTable: ValueBoxTable
|
let hashIdToInfoTable: ValueBoxTable
|
||||||
let idToReferenceTable: ValueBoxTable
|
let idToReferenceTable: ValueBoxTable
|
||||||
let peerIdToIdTable: ValueBoxTable
|
let peerIdToIdTable: ValueBoxTable
|
||||||
let peerIdTable: ValueBoxTable
|
let peerIdTable: ValueBoxTable
|
||||||
|
let peerContentTypeStatsTable: ValueBoxTable
|
||||||
|
let contentTypeStatsTable: ValueBoxTable
|
||||||
|
|
||||||
init(queue: Queue, logger: StorageBox.Logger, basePath: String) {
|
init(queue: Queue, logger: StorageBox.Logger, basePath: String) {
|
||||||
self.queue = queue
|
self.queue = queue
|
||||||
@ -80,20 +151,81 @@ public final class StorageBox {
|
|||||||
}
|
}
|
||||||
self.valueBox = valueBox
|
self.valueBox = valueBox
|
||||||
|
|
||||||
self.hashIdToIdTable = ValueBoxTable(id: 5, keyType: .binary, compactValuesOnCreation: true)
|
self.hashIdToInfoTable = ValueBoxTable(id: 15, keyType: .binary, compactValuesOnCreation: true)
|
||||||
self.idToReferenceTable = ValueBoxTable(id: 6, keyType: .binary, compactValuesOnCreation: true)
|
self.idToReferenceTable = ValueBoxTable(id: 16, keyType: .binary, compactValuesOnCreation: true)
|
||||||
self.peerIdToIdTable = ValueBoxTable(id: 7, keyType: .binary, compactValuesOnCreation: true)
|
self.peerIdToIdTable = ValueBoxTable(id: 17, keyType: .binary, compactValuesOnCreation: true)
|
||||||
self.peerIdTable = ValueBoxTable(id: 8, keyType: .binary, compactValuesOnCreation: true)
|
self.peerIdTable = ValueBoxTable(id: 18, keyType: .binary, compactValuesOnCreation: true)
|
||||||
|
self.peerContentTypeStatsTable = ValueBoxTable(id: 19, keyType: .binary, compactValuesOnCreation: true)
|
||||||
|
self.contentTypeStatsTable = ValueBoxTable(id: 20, keyType: .binary, compactValuesOnCreation: true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func add(reference: Reference, to id: Data) {
|
private func internalAddSize(contentType: UInt8, delta: Int64) {
|
||||||
|
let key = ValueBoxKey(length: 1)
|
||||||
|
key.setUInt8(0, value: contentType)
|
||||||
|
|
||||||
|
var currentSize: Int64 = 0
|
||||||
|
if let value = self.valueBox.get(self.contentTypeStatsTable, key: key) {
|
||||||
|
value.read(¤tSize, offset: 0, length: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSize += delta
|
||||||
|
|
||||||
|
if currentSize < 0 {
|
||||||
|
assertionFailure()
|
||||||
|
currentSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
private func internalAddSize(peerId: Int64, contentType: UInt8, delta: Int64) {
|
||||||
|
let key = ValueBoxKey(length: 8 + 1)
|
||||||
|
key.setInt64(0, value: peerId)
|
||||||
|
key.setUInt8(8, value: contentType)
|
||||||
|
|
||||||
|
var currentSize: Int64 = 0
|
||||||
|
if let value = self.valueBox.get(self.contentTypeStatsTable, key: key) {
|
||||||
|
value.read(¤tSize, offset: 0, length: 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
currentSize += delta
|
||||||
|
|
||||||
|
if currentSize < 0 {
|
||||||
|
assertionFailure()
|
||||||
|
currentSize = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valueBox.set(self.contentTypeStatsTable, key: key, value: MemoryBuffer(memory: ¤tSize, capacity: 8, length: 8, freeWhenDone: false))
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(reference: Reference, to id: Data, contentType: UInt8) {
|
||||||
self.valueBox.begin()
|
self.valueBox.begin()
|
||||||
|
|
||||||
let hashId = md5Hash(id)
|
let hashId = md5Hash(id)
|
||||||
|
|
||||||
let mainKey = ValueBoxKey(length: 16)
|
let mainKey = ValueBoxKey(length: 16)
|
||||||
mainKey.setData(0, value: hashId.data)
|
mainKey.setData(0, value: hashId.data)
|
||||||
self.valueBox.setOrIgnore(self.hashIdToIdTable, key: mainKey, value: MemoryBuffer(data: id))
|
|
||||||
|
var previousContentType: UInt8?
|
||||||
|
var size: Int64 = 0
|
||||||
|
if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) {
|
||||||
|
var info = ItemInfo(buffer: currentInfoValue)
|
||||||
|
if info.contentType != contentType {
|
||||||
|
previousContentType = info.contentType
|
||||||
|
}
|
||||||
|
size = info.size
|
||||||
|
info.contentType = contentType
|
||||||
|
self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: info.serialize())
|
||||||
|
} else {
|
||||||
|
self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: ItemInfo(id: id, contentType: contentType, size: 0).serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
if let previousContentType = previousContentType, previousContentType != contentType {
|
||||||
|
if size != 0 {
|
||||||
|
self.internalAddSize(contentType: previousContentType, delta: -size)
|
||||||
|
self.internalAddSize(contentType: contentType, delta: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
|
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
|
||||||
idKey.setData(0, value: hashId.data)
|
idKey.setData(0, value: hashId.data)
|
||||||
@ -145,25 +277,62 @@ public final class StorageBox {
|
|||||||
self.valueBox.commit()
|
self.valueBox.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addEmptyReferencesIfNotReferenced(ids: [Data]) -> Int {
|
func update(id: Data, size: Int64) {
|
||||||
|
self.valueBox.begin()
|
||||||
|
|
||||||
|
let hashId = md5Hash(id)
|
||||||
|
|
||||||
|
let mainKey = ValueBoxKey(length: 16)
|
||||||
|
mainKey.setData(0, value: hashId.data)
|
||||||
|
|
||||||
|
if let currentInfoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) {
|
||||||
|
var info = ItemInfo(buffer: currentInfoValue)
|
||||||
|
|
||||||
|
var sizeDelta: Int64 = 0
|
||||||
|
if info.size != size {
|
||||||
|
sizeDelta = size - info.size
|
||||||
|
info.size = size
|
||||||
|
|
||||||
|
self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: info.serialize())
|
||||||
|
}
|
||||||
|
|
||||||
|
if sizeDelta != 0 {
|
||||||
|
self.internalAddSize(contentType: info.contentType, delta: sizeDelta)
|
||||||
|
}
|
||||||
|
|
||||||
|
var peerIds: [Int64] = []
|
||||||
|
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
|
||||||
|
peerIds.append(key.getInt64(0))
|
||||||
|
return true
|
||||||
|
}, limit: 0)
|
||||||
|
|
||||||
|
for peerId in peerIds {
|
||||||
|
let _ = peerId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.valueBox.commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func addEmptyReferencesIfNotReferenced(ids: [(id: Data, size: Int64)], contentType: UInt8) -> Int {
|
||||||
self.valueBox.begin()
|
self.valueBox.begin()
|
||||||
|
|
||||||
var addedCount = 0
|
var addedCount = 0
|
||||||
|
|
||||||
for id in ids {
|
for (id, size) in ids {
|
||||||
let reference = Reference(peerId: 0, messageNamespace: 0, messageId: 0)
|
let reference = Reference(peerId: 0, messageNamespace: 0, messageId: 0)
|
||||||
|
|
||||||
let hashId = md5Hash(id)
|
let hashId = md5Hash(id)
|
||||||
|
|
||||||
let mainKey = ValueBoxKey(length: 16)
|
let mainKey = ValueBoxKey(length: 16)
|
||||||
mainKey.setData(0, value: hashId.data)
|
mainKey.setData(0, value: hashId.data)
|
||||||
if self.valueBox.exists(self.hashIdToIdTable, key: mainKey) {
|
if self.valueBox.exists(self.hashIdToInfoTable, key: mainKey) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
addedCount += 1
|
addedCount += 1
|
||||||
|
|
||||||
self.valueBox.setOrIgnore(self.hashIdToIdTable, key: mainKey, value: MemoryBuffer(data: id))
|
self.valueBox.set(self.hashIdToInfoTable, key: mainKey, value: ItemInfo(id: id, contentType: contentType, size: size).serialize())
|
||||||
|
|
||||||
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
|
let idKey = ValueBoxKey(length: hashId.data.count + 8 + 1 + 4)
|
||||||
idKey.setData(0, value: hashId.data)
|
idKey.setData(0, value: hashId.data)
|
||||||
@ -229,7 +398,15 @@ public final class StorageBox {
|
|||||||
let hashId = md5Hash(id)
|
let hashId = md5Hash(id)
|
||||||
mainKey.setData(0, value: hashId.data)
|
mainKey.setData(0, value: hashId.data)
|
||||||
|
|
||||||
self.valueBox.remove(self.hashIdToIdTable, key: mainKey, secure: false)
|
guard let infoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let info = ItemInfo(buffer: infoValue)
|
||||||
|
self.valueBox.remove(self.hashIdToInfoTable, key: mainKey, secure: false)
|
||||||
|
|
||||||
|
if info.size != 0 {
|
||||||
|
self.internalAddSize(contentType: info.contentType, delta: -info.size)
|
||||||
|
}
|
||||||
|
|
||||||
var referenceKeys: [ValueBoxKey] = []
|
var referenceKeys: [ValueBoxKey] = []
|
||||||
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
|
self.valueBox.range(self.idToReferenceTable, start: mainKey, end: mainKey.successor, keys: { key in
|
||||||
@ -302,8 +479,9 @@ public final class StorageBox {
|
|||||||
let mainKey = ValueBoxKey(length: 16)
|
let mainKey = ValueBoxKey(length: 16)
|
||||||
for hashId in hashIds {
|
for hashId in hashIds {
|
||||||
mainKey.setData(0, value: hashId)
|
mainKey.setData(0, value: hashId)
|
||||||
if let value = self.valueBox.get(self.hashIdToIdTable, key: mainKey) {
|
if let infoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) {
|
||||||
result.append(value.makeData())
|
let info = ItemInfo(buffer: infoValue)
|
||||||
|
result.append(info.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -336,8 +514,9 @@ public final class StorageBox {
|
|||||||
} else {
|
} else {
|
||||||
if let currentId = currentId, !currentReferences.isEmpty {
|
if let currentId = currentId, !currentReferences.isEmpty {
|
||||||
mainKey.setData(0, value: currentId)
|
mainKey.setData(0, value: currentId)
|
||||||
if let value = self.valueBox.get(self.hashIdToIdTable, key: mainKey) {
|
if let infoValue = self.valueBox.get(self.hashIdToInfoTable, key: mainKey) {
|
||||||
result.append(StorageBox.Entry(id: value.makeData(), references: currentReferences))
|
let info = ItemInfo(buffer: infoValue)
|
||||||
|
result.append(StorageBox.Entry(id: info.id, references: currentReferences))
|
||||||
}
|
}
|
||||||
currentReferences.removeAll(keepingCapacity: true)
|
currentReferences.removeAll(keepingCapacity: true)
|
||||||
}
|
}
|
||||||
@ -384,6 +563,20 @@ public final class StorageBox {
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getStats() -> Stats {
|
||||||
|
var contentTypes: [UInt8: Int64] = [:]
|
||||||
|
|
||||||
|
self.valueBox.scan(self.contentTypeStatsTable, values: { key, value in
|
||||||
|
var size: Int64 = 0
|
||||||
|
value.read(&size, offset: 0, length: 8)
|
||||||
|
contentTypes[key.getUInt8(0)] = size
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
return Stats(contentTypes: contentTypes)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private let queue: Queue
|
private let queue: Queue
|
||||||
@ -396,15 +589,21 @@ public final class StorageBox {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
public func add(reference: Reference, to id: Data) {
|
public func add(reference: Reference, to id: Data, contentType: UInt8) {
|
||||||
self.impl.with { impl in
|
self.impl.with { impl in
|
||||||
impl.add(reference: reference, to: id)
|
impl.add(reference: reference, to: id, contentType: contentType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addEmptyReferencesIfNotReferenced(ids: [Data], completion: @escaping (Int) -> Void) {
|
public func update(id: Data, size: Int64) {
|
||||||
self.impl.with { impl in
|
self.impl.with { impl in
|
||||||
let addedCount = impl.addEmptyReferencesIfNotReferenced(ids: ids)
|
impl.update(id: id, size: size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func addEmptyReferencesIfNotReferenced(ids: [(id: Data, size: Int64)], contentType: UInt8, completion: @escaping (Int) -> Void) {
|
||||||
|
self.impl.with { impl in
|
||||||
|
let addedCount = impl.addEmptyReferencesIfNotReferenced(ids: ids, contentType: contentType)
|
||||||
|
|
||||||
completion(addedCount)
|
completion(addedCount)
|
||||||
}
|
}
|
||||||
@ -451,4 +650,13 @@ public final class StorageBox {
|
|||||||
return EmptyDisposable
|
return EmptyDisposable
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func getStats() -> Signal<Stats, NoError> {
|
||||||
|
return self.impl.signalWith { impl, subscriber in
|
||||||
|
subscriber.putNext(impl.getStats())
|
||||||
|
subscriber.putCompletion()
|
||||||
|
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -109,6 +109,7 @@ swift_library(
|
|||||||
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
"//submodules/TelegramUI/Components/NotificationPeerExceptionController",
|
||||||
"//submodules/TelegramUI/Components/ChatTimerScreen",
|
"//submodules/TelegramUI/Components/ChatTimerScreen",
|
||||||
"//submodules/AnimatedAvatarSetNode",
|
"//submodules/AnimatedAvatarSetNode",
|
||||||
|
"//submodules/TelegramUI/Components/StorageUsageScreen",
|
||||||
],
|
],
|
||||||
visibility = [
|
visibility = [
|
||||||
"//visibility:public",
|
"//visibility:public",
|
||||||
|
@ -12,6 +12,7 @@ import PresentationDataUtils
|
|||||||
import AccountContext
|
import AccountContext
|
||||||
import OpenInExternalAppUI
|
import OpenInExternalAppUI
|
||||||
import ItemListPeerActionItem
|
import ItemListPeerActionItem
|
||||||
|
import StorageUsageScreen
|
||||||
|
|
||||||
private final class DataAndStorageControllerArguments {
|
private final class DataAndStorageControllerArguments {
|
||||||
let openStorageUsage: () -> Void
|
let openStorageUsage: () -> Void
|
||||||
@ -594,7 +595,10 @@ public func dataAndStorageController(context: AccountContext, focusOnItemTag: Da
|
|||||||
})
|
})
|
||||||
|
|
||||||
let arguments = DataAndStorageControllerArguments(openStorageUsage: {
|
let arguments = DataAndStorageControllerArguments(openStorageUsage: {
|
||||||
pushControllerImpl?(storageUsageController(context: context, cacheUsagePromise: cacheUsagePromise))
|
pushControllerImpl?(StorageUsageScreen(context: context, makeStorageUsageExceptionsScreen: { category in
|
||||||
|
return storageUsageExceptionsScreen(context: context, category: category)
|
||||||
|
}))
|
||||||
|
//pushControllerImpl?(storageUsageController(context: context, cacheUsagePromise: cacheUsagePromise))
|
||||||
}, openNetworkUsage: {
|
}, openNetworkUsage: {
|
||||||
pushControllerImpl?(networkUsageStatsController(context: context))
|
pushControllerImpl?(networkUsageStatsController(context: context))
|
||||||
}, openProxy: {
|
}, openProxy: {
|
||||||
|
@ -765,6 +765,14 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var label: String? {
|
||||||
|
didSet {
|
||||||
|
if let width = self.validLayout {
|
||||||
|
_ = self.updateLayout(width: width, transition: .immediate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public var subtitle: String? {
|
public var subtitle: String? {
|
||||||
didSet {
|
didSet {
|
||||||
if let width = self.validLayout {
|
if let width = self.validLayout {
|
||||||
@ -779,6 +787,14 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var isEnabled: Bool = true {
|
||||||
|
didSet {
|
||||||
|
if self.isEnabled != oldValue {
|
||||||
|
self.titleNode.alpha = self.isEnabled ? 1.0 : 0.6
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private var animationTimer: SwiftSignalKit.Timer?
|
private var animationTimer: SwiftSignalKit.Timer?
|
||||||
public var animation: String? {
|
public var animation: String? {
|
||||||
didSet {
|
didSet {
|
||||||
@ -854,13 +870,14 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
|
|
||||||
public var progressType: SolidRoundedButtonProgressType = .fullSize
|
public var progressType: SolidRoundedButtonProgressType = .fullSize
|
||||||
|
|
||||||
public init(title: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
public init(title: String? = nil, label: String? = nil, icon: UIImage? = nil, theme: SolidRoundedButtonTheme, font: SolidRoundedButtonFont = .bold, fontSize: CGFloat = 17.0, height: CGFloat = 48.0, cornerRadius: CGFloat = 24.0, gloss: Bool = false) {
|
||||||
self.theme = theme
|
self.theme = theme
|
||||||
self.font = font
|
self.font = font
|
||||||
self.fontSize = fontSize
|
self.fontSize = fontSize
|
||||||
self.buttonHeight = height
|
self.buttonHeight = height
|
||||||
self.buttonCornerRadius = cornerRadius
|
self.buttonCornerRadius = cornerRadius
|
||||||
self.title = title
|
self.title = title
|
||||||
|
self.label = label
|
||||||
self.gloss = gloss
|
self.gloss = gloss
|
||||||
|
|
||||||
self.buttonBackgroundNode = UIImageView()
|
self.buttonBackgroundNode = UIImageView()
|
||||||
@ -1174,7 +1191,13 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
self.buttonBackgroundAnimationView?.image = nil
|
self.buttonBackgroundAnimationView?.image = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor)
|
let titleText = NSMutableAttributedString()
|
||||||
|
titleText.append(NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor))
|
||||||
|
if let label = self.label {
|
||||||
|
titleText.append(NSAttributedString(string: " " + label, font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor.withAlphaComponent(0.6)))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.titleNode.attributedText = titleText
|
||||||
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
|
self.subtitleNode.attributedText = NSAttributedString(string: self.subtitle ?? "", font: Font.regular(14.0), textColor: theme.foregroundColor)
|
||||||
|
|
||||||
self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor)
|
self.iconNode.image = generateTintedImage(image: self.iconNode.image, color: theme.foregroundColor)
|
||||||
@ -1219,10 +1242,14 @@ public final class SolidRoundedButtonView: UIView {
|
|||||||
|
|
||||||
transition.updateFrame(view: self.buttonNode, frame: buttonFrame)
|
transition.updateFrame(view: self.buttonNode, frame: buttonFrame)
|
||||||
|
|
||||||
if self.title != self.titleNode.attributedText?.string {
|
let titleText = NSMutableAttributedString()
|
||||||
self.titleNode.attributedText = NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: self.theme.foregroundColor)
|
titleText.append(NSAttributedString(string: self.title ?? "", font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor))
|
||||||
|
if let label = self.label {
|
||||||
|
titleText.append(NSAttributedString(string: " " + label, font: self.font == .bold ? Font.semibold(self.fontSize) : Font.regular(self.fontSize), textColor: theme.foregroundColor.withAlphaComponent(0.6)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
self.titleNode.attributedText = titleText
|
||||||
|
|
||||||
let iconSize: CGSize
|
let iconSize: CGSize
|
||||||
if let _ = self.animationNode {
|
if let _ = self.animationNode {
|
||||||
iconSize = CGSize(width: 30.0, height: 30.0)
|
iconSize = CGSize(width: 30.0, height: 30.0)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Postbox
|
import Postbox
|
||||||
import SwiftSignalKit
|
import SwiftSignalKit
|
||||||
|
import MtProtoKit
|
||||||
|
|
||||||
public enum PeerCacheUsageCategory: Int32 {
|
public enum PeerCacheUsageCategory: Int32 {
|
||||||
case image = 0
|
case image = 0
|
||||||
@ -52,6 +52,130 @@ private final class CacheUsageStatsState {
|
|||||||
var upperBound: MessageIndex?
|
var upperBound: MessageIndex?
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public struct StorageUsageStats: Equatable {
|
||||||
|
public enum CategoryKey: Hashable {
|
||||||
|
case photos
|
||||||
|
case videos
|
||||||
|
case files
|
||||||
|
case music
|
||||||
|
case stickers
|
||||||
|
case avatars
|
||||||
|
case misc
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct CategoryData: Equatable {
|
||||||
|
public var size: Int64
|
||||||
|
|
||||||
|
public init(size: Int64) {
|
||||||
|
self.size = size
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var categories: [CategoryKey: CategoryData]
|
||||||
|
|
||||||
|
public init(categories: [CategoryKey: CategoryData]) {
|
||||||
|
self.categories = categories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct AllStorageUsageStats: Equatable {
|
||||||
|
public struct PeerStats: Equatable {
|
||||||
|
public var peer: EnginePeer
|
||||||
|
public var stats: StorageUsageStats
|
||||||
|
|
||||||
|
public init(peer: EnginePeer, stats: StorageUsageStats) {
|
||||||
|
self.peer = peer
|
||||||
|
self.stats = stats
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var totalStats: StorageUsageStats
|
||||||
|
public var peers: [EnginePeer.Id: PeerStats]
|
||||||
|
|
||||||
|
public init(totalStats: StorageUsageStats, peers: [EnginePeer.Id: PeerStats]) {
|
||||||
|
self.totalStats = totalStats
|
||||||
|
self.peers = peers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func _internal_collectStorageUsageStats(account: Account) -> Signal<AllStorageUsageStats, NoError> {
|
||||||
|
let additionalStats = Signal<Int64, NoError> { subscriber in
|
||||||
|
DispatchQueue.global().async {
|
||||||
|
var totalSize: Int64 = 0
|
||||||
|
|
||||||
|
let additionalPaths: [String] = [
|
||||||
|
"cache",
|
||||||
|
"animation-cache",
|
||||||
|
"short-cache",
|
||||||
|
]
|
||||||
|
|
||||||
|
for path in additionalPaths {
|
||||||
|
let fullPath: String
|
||||||
|
if path.isEmpty {
|
||||||
|
fullPath = account.postbox.mediaBox.basePath
|
||||||
|
} else {
|
||||||
|
fullPath = account.postbox.mediaBox.basePath + "/\(path)"
|
||||||
|
}
|
||||||
|
|
||||||
|
var s = darwin_dirstat()
|
||||||
|
var result = dirstat_np(fullPath, 1, &s, MemoryLayout<darwin_dirstat>.size)
|
||||||
|
if result != -1 {
|
||||||
|
totalSize += Int64(s.total_size)
|
||||||
|
} else {
|
||||||
|
result = dirstat_np(fullPath, 0, &s, MemoryLayout<darwin_dirstat>.size)
|
||||||
|
if result != -1 {
|
||||||
|
totalSize += Int64(s.total_size)
|
||||||
|
print(s.descendants)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriber.putNext(totalSize)
|
||||||
|
subscriber.putCompletion()
|
||||||
|
}
|
||||||
|
|
||||||
|
return EmptyDisposable
|
||||||
|
}
|
||||||
|
|
||||||
|
return combineLatest(
|
||||||
|
additionalStats,
|
||||||
|
account.postbox.mediaBox.storageBox.getStats()
|
||||||
|
)
|
||||||
|
|> deliverOnMainQueue
|
||||||
|
|> mapToSignal { additionalStats, allStats -> Signal<AllStorageUsageStats, NoError> in
|
||||||
|
var mappedCategories: [StorageUsageStats.CategoryKey: StorageUsageStats.CategoryData] = [:]
|
||||||
|
for (key, value) in allStats.contentTypes {
|
||||||
|
let mappedCategory: StorageUsageStats.CategoryKey
|
||||||
|
switch key {
|
||||||
|
case MediaResourceUserContentType.image.rawValue:
|
||||||
|
mappedCategory = .photos
|
||||||
|
case MediaResourceUserContentType.video.rawValue:
|
||||||
|
mappedCategory = .videos
|
||||||
|
case MediaResourceUserContentType.file.rawValue:
|
||||||
|
mappedCategory = .files
|
||||||
|
case MediaResourceUserContentType.audio.rawValue:
|
||||||
|
mappedCategory = .music
|
||||||
|
case MediaResourceUserContentType.avatar.rawValue:
|
||||||
|
mappedCategory = .avatars
|
||||||
|
case MediaResourceUserContentType.sticker.rawValue:
|
||||||
|
mappedCategory = .stickers
|
||||||
|
default:
|
||||||
|
mappedCategory = .misc
|
||||||
|
}
|
||||||
|
mappedCategories[mappedCategory] = StorageUsageStats.CategoryData(size: value)
|
||||||
|
}
|
||||||
|
|
||||||
|
if additionalStats != 0 {
|
||||||
|
mappedCategories[.misc, default: StorageUsageStats.CategoryData(size: 0)].size += additionalStats
|
||||||
|
}
|
||||||
|
|
||||||
|
return .single(AllStorageUsageStats(
|
||||||
|
totalStats: StorageUsageStats(categories: mappedCategories),
|
||||||
|
peers: [:]
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal<CacheUsageStatsResult, NoError> {
|
func _internal_collectCacheUsageStats(account: Account, peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal<CacheUsageStatsResult, NoError> {
|
||||||
return account.postbox.mediaBox.storageBox.all()
|
return account.postbox.mediaBox.storageBox.all()
|
||||||
|> mapToSignal { entries -> Signal<CacheUsageStatsResult, NoError> in
|
|> mapToSignal { entries -> Signal<CacheUsageStatsResult, NoError> in
|
||||||
|
@ -8,10 +8,12 @@ public typealias EngineTempBoxFile = TempBoxFile
|
|||||||
|
|
||||||
public extension MediaResourceUserContentType {
|
public extension MediaResourceUserContentType {
|
||||||
init(file: TelegramMediaFile) {
|
init(file: TelegramMediaFile) {
|
||||||
if file.isSticker || file.isAnimatedSticker {
|
if file.isMusic || file.isVoice {
|
||||||
|
self = .audio
|
||||||
|
} else if file.isSticker || file.isAnimatedSticker {
|
||||||
self = .sticker
|
self = .sticker
|
||||||
} else if file.isCustomEmoji {
|
} else if file.isCustomEmoji {
|
||||||
self = .emoji
|
self = .sticker
|
||||||
} else if file.isVideo {
|
} else if file.isVideo {
|
||||||
if file.isAnimated {
|
if file.isAnimated {
|
||||||
self = .gif
|
self = .gif
|
||||||
@ -220,6 +222,10 @@ public extension TelegramEngine {
|
|||||||
public func collectCacheUsageStats(peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal<CacheUsageStatsResult, NoError> {
|
public func collectCacheUsageStats(peerId: PeerId? = nil, additionalCachePaths: [String] = [], logFilesPath: String? = nil) -> Signal<CacheUsageStatsResult, NoError> {
|
||||||
return _internal_collectCacheUsageStats(account: self.account, peerId: peerId, additionalCachePaths: additionalCachePaths, logFilesPath: logFilesPath)
|
return _internal_collectCacheUsageStats(account: self.account, peerId: peerId, additionalCachePaths: additionalCachePaths, logFilesPath: logFilesPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public func collectStorageUsageStats() -> Signal<AllStorageUsageStats, NoError> {
|
||||||
|
return _internal_collectStorageUsageStats(account: self.account)
|
||||||
|
}
|
||||||
|
|
||||||
public func clearCachedMediaResources(mediaResourceIds: Set<MediaResourceId>) -> Signal<Float, NoError> {
|
public func clearCachedMediaResources(mediaResourceIds: Set<MediaResourceId>) -> Signal<Float, NoError> {
|
||||||
return _internal_clearCachedMediaResources(account: self.account, mediaResourceIds: mediaResourceIds)
|
return _internal_clearCachedMediaResources(account: self.account, mediaResourceIds: mediaResourceIds)
|
||||||
|
37
submodules/TelegramUI/Components/StorageUsageScreen/BUILD
Normal file
37
submodules/TelegramUI/Components/StorageUsageScreen/BUILD
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
|
||||||
|
|
||||||
|
swift_library(
|
||||||
|
name = "StorageUsageScreen",
|
||||||
|
module_name = "StorageUsageScreen",
|
||||||
|
srcs = glob([
|
||||||
|
"Sources/**/*.swift",
|
||||||
|
]),
|
||||||
|
copts = [
|
||||||
|
"-warnings-as-errors",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||||
|
"//submodules/Display:Display",
|
||||||
|
"//submodules/Postbox:Postbox",
|
||||||
|
"//submodules/TelegramCore:TelegramCore",
|
||||||
|
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||||
|
"//submodules/ComponentFlow:ComponentFlow",
|
||||||
|
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||||
|
"//submodules/Components/ComponentDisplayAdapters:ComponentDisplayAdapters",
|
||||||
|
"//submodules/Components/MultilineTextComponent:MultilineTextComponent",
|
||||||
|
"//submodules/Components/SolidRoundedButtonComponent",
|
||||||
|
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||||
|
"//submodules/AccountContext:AccountContext",
|
||||||
|
"//submodules/AppBundle:AppBundle",
|
||||||
|
"//submodules/TelegramStringFormatting:TelegramStringFormatting",
|
||||||
|
"//submodules/PresentationDataUtils:PresentationDataUtils",
|
||||||
|
"//submodules/TelegramUI/Components/EmojiStatusComponent",
|
||||||
|
"//submodules/CheckNode",
|
||||||
|
"//submodules/Markdown",
|
||||||
|
"//submodules/ContextUI",
|
||||||
|
"//submodules/AnimatedAvatarSetNode",
|
||||||
|
],
|
||||||
|
visibility = [
|
||||||
|
"//visibility:public",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,226 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
final class PieChartComponent: Component {
|
||||||
|
struct ChartData: Equatable {
|
||||||
|
struct Item: Equatable {
|
||||||
|
var id: AnyHashable
|
||||||
|
var value: Double
|
||||||
|
var color: UIColor
|
||||||
|
|
||||||
|
init(id: AnyHashable, value: Double, color: UIColor) {
|
||||||
|
self.id = id
|
||||||
|
self.value = value
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var items: [Item]
|
||||||
|
|
||||||
|
init(items: [Item]) {
|
||||||
|
self.items = items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let chartData: ChartData
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
chartData: ChartData
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.chartData = chartData
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: PieChartComponent, rhs: PieChartComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.chartData != rhs.chartData {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class View: UIView {
|
||||||
|
private var shapeLayers: [AnyHashable: SimpleShapeLayer] = [:]
|
||||||
|
private var labels: [AnyHashable: ComponentView<Empty>] = [:]
|
||||||
|
var selectedKey: AnyHashable?
|
||||||
|
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
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)
|
||||||
|
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
|
||||||
|
|
||||||
|
let innerDiameter: CGFloat = 100.0
|
||||||
|
let spacing: CGFloat = 2.0
|
||||||
|
let innerAngleSpacing: CGFloat = spacing / (innerDiameter * 0.5)
|
||||||
|
|
||||||
|
var valueSum: Double = 0.0
|
||||||
|
for item in component.chartData.items {
|
||||||
|
valueSum += item.value
|
||||||
|
}
|
||||||
|
|
||||||
|
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]
|
||||||
|
|
||||||
|
let itemOuterDiameter: CGFloat
|
||||||
|
if let selectedKey = self.selectedKey {
|
||||||
|
if selectedKey == item.id {
|
||||||
|
itemOuterDiameter = diameter
|
||||||
|
} else {
|
||||||
|
itemOuterDiameter = reducedDiameter
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
itemOuterDiameter = diameter
|
||||||
|
}
|
||||||
|
|
||||||
|
let shapeLayerFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - diameter) / 2.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 = item.value / valueSum * CGFloat.pi * 2.0
|
||||||
|
|
||||||
|
let innerStartAngle = startAngle + innerAngleSpacing * 0.5
|
||||||
|
var innerEndAngle = startAngle + angleValue - innerAngleSpacing * 0.5
|
||||||
|
innerEndAngle = max(innerEndAngle, innerStartAngle)
|
||||||
|
|
||||||
|
let outerStartAngle = startAngle + angleSpacing * 0.5
|
||||||
|
var outerEndAngle = startAngle + angleValue - angleSpacing * 0.5
|
||||||
|
outerEndAngle = max(outerEndAngle, outerStartAngle)
|
||||||
|
|
||||||
|
let path = CGMutablePath()
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
startAngle += angleValue
|
||||||
|
shapeLayer.fillColor = item.color.cgColor
|
||||||
|
|
||||||
|
let fractionValue: Double = floor(item.value * 100.0 * 10.0) / 10.0
|
||||||
|
let fractionString: String
|
||||||
|
if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
|
||||||
|
fractionString = "\(Int(fractionValue))"
|
||||||
|
} else {
|
||||||
|
fractionString = "\(fractionValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
let label: ComponentView<Empty>
|
||||||
|
if let current = self.labels[item.id] {
|
||||||
|
label = current
|
||||||
|
} else {
|
||||||
|
label = ComponentView<Empty>()
|
||||||
|
self.labels[item.id] = label
|
||||||
|
}
|
||||||
|
let labelSize = label.update(transition: .immediate, component: AnyComponent(Text(text: "\(fractionString)%", font: Font.with(size: 15.0, design: .round, weight: .medium), color: component.theme.list.itemCheckColors.foregroundColor)), environment: {}, containerSize: CGSize(width: 100.0, height: 100.0))
|
||||||
|
|
||||||
|
let midAngle: CGFloat = (innerStartAngle + innerEndAngle) * 0.5
|
||||||
|
let centerDistance: CGFloat = (innerDiameter * 0.5 + (diameter - innerDiameter) * 0.25)
|
||||||
|
let labelCenter = CGPoint(
|
||||||
|
x: shapeLayerFrame.midX + cos(midAngle) * centerDistance,
|
||||||
|
y: shapeLayerFrame.midY + sin(midAngle) * centerDistance
|
||||||
|
)
|
||||||
|
let labelFrame = CGRect(origin: CGPoint(x: labelCenter.x - labelSize.width * 0.5, y: labelCenter.y - labelSize.height * 0.5), size: labelSize)
|
||||||
|
|
||||||
|
//x2 + y2 = r2
|
||||||
|
//x = sqrt(r2 - y2)
|
||||||
|
//y = sqrt(r2 - x2)
|
||||||
|
|
||||||
|
/*let localLabelRect = labelFrame.offsetBy(dx: -shapeLayerFrame.midX, dy: -shapeLayerFrame.midY)
|
||||||
|
let outerIntersectionX1 = sqrt(pow(diameter * 0.5, 2.0) - pow(localLabelRect.minY, 2.0))
|
||||||
|
let outerIntersectionX2 = sqrt(pow(diameter * 0.5, 2.0) - pow(localLabelRect.maxY, 2.0))
|
||||||
|
let outerIntersectionY1 = sqrt(pow(diameter * 0.5, 2.0) - pow(localLabelRect.minX, 2.0))
|
||||||
|
let outerIntersectionY2 = sqrt(pow(diameter * 0.5, 2.0) - pow(localLabelRect.maxX, 2.0))*/
|
||||||
|
|
||||||
|
if let labelView = label.view {
|
||||||
|
if labelView.superview == nil {
|
||||||
|
self.addSubview(labelView)
|
||||||
|
}
|
||||||
|
labelView.bounds = CGRect(origin: CGPoint(), size: labelFrame.size)
|
||||||
|
transition.setPosition(view: labelView, position: labelFrame.center)
|
||||||
|
|
||||||
|
if let selectedKey = self.selectedKey {
|
||||||
|
if selectedKey == item.id {
|
||||||
|
transition.setAlpha(view: labelView, alpha: 1.0)
|
||||||
|
} else {
|
||||||
|
transition.setAlpha(view: labelView, alpha: 0.0)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
transition.setAlpha(view: labelView, alpha: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: 200.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,254 @@
|
|||||||
|
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 CheckNode
|
||||||
|
import SolidRoundedButtonComponent
|
||||||
|
|
||||||
|
final class StorageCategoriesComponent: Component {
|
||||||
|
struct CategoryData: Equatable {
|
||||||
|
var key: AnyHashable
|
||||||
|
var color: UIColor
|
||||||
|
var title: String
|
||||||
|
var size: Int64
|
||||||
|
var sizeFraction: Double
|
||||||
|
var isSelected: Bool
|
||||||
|
var subcategories: [CategoryData]
|
||||||
|
|
||||||
|
init(key: AnyHashable, color: UIColor, title: String, size: Int64, sizeFraction: Double, isSelected: Bool, subcategories: [CategoryData]) {
|
||||||
|
self.key = key
|
||||||
|
self.title = title
|
||||||
|
self.color = color
|
||||||
|
self.size = size
|
||||||
|
self.sizeFraction = sizeFraction
|
||||||
|
self.isSelected = isSelected
|
||||||
|
self.subcategories = subcategories
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let categories: [CategoryData]
|
||||||
|
let toggleCategorySelection: (AnyHashable) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
categories: [CategoryData],
|
||||||
|
toggleCategorySelection: @escaping (AnyHashable) -> Void
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.categories = categories
|
||||||
|
self.toggleCategorySelection = toggleCategorySelection
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StorageCategoriesComponent, rhs: StorageCategoriesComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.categories != rhs.categories {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class View: UIView {
|
||||||
|
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||||
|
private let button = ComponentView<Empty>()
|
||||||
|
|
||||||
|
private var expandedCategory: AnyHashable?
|
||||||
|
private var component: StorageCategoriesComponent?
|
||||||
|
private weak var state: EmptyComponentState?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.clipsToBounds = true
|
||||||
|
self.layer.cornerRadius = 10.0
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: StorageCategoriesComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
self.component = component
|
||||||
|
self.state = state
|
||||||
|
|
||||||
|
var totalSelectedSize: Int64 = 0
|
||||||
|
var hasDeselected = false
|
||||||
|
for category in component.categories {
|
||||||
|
if !category.subcategories.isEmpty {
|
||||||
|
for subcategory in category.subcategories {
|
||||||
|
if subcategory.isSelected {
|
||||||
|
totalSelectedSize += subcategory.size
|
||||||
|
} else {
|
||||||
|
hasDeselected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if category.isSelected {
|
||||||
|
totalSelectedSize += category.size
|
||||||
|
} else {
|
||||||
|
hasDeselected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var contentHeight: CGFloat = 0.0
|
||||||
|
|
||||||
|
var validKeys = Set<AnyHashable>()
|
||||||
|
for i in 0 ..< component.categories.count {
|
||||||
|
let category = component.categories[i]
|
||||||
|
validKeys.insert(category.key)
|
||||||
|
|
||||||
|
var itemTransition = transition
|
||||||
|
let itemView: ComponentView<Empty>
|
||||||
|
if let current = self.itemViews[category.key] {
|
||||||
|
itemView = current
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
itemView = ComponentView()
|
||||||
|
itemView.parentState = state
|
||||||
|
self.itemViews[category.key] = itemView
|
||||||
|
}
|
||||||
|
|
||||||
|
let itemSize = itemView.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(StorageCategoryItemComponent(
|
||||||
|
theme: component.theme,
|
||||||
|
strings: component.strings,
|
||||||
|
category: category,
|
||||||
|
isExpandedLevel: false,
|
||||||
|
isExpanded: self.expandedCategory == category.key,
|
||||||
|
hasNext: i != component.categories.count - 1,
|
||||||
|
action: { [weak self] key, actionType in
|
||||||
|
guard let self, let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
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)))
|
||||||
|
} else {
|
||||||
|
component.toggleCategorySelection(key)
|
||||||
|
}
|
||||||
|
case .toggle:
|
||||||
|
component.toggleCategorySelection(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: contentHeight), size: itemSize)
|
||||||
|
if let itemComponentView = itemView.view {
|
||||||
|
if itemComponentView.superview == nil {
|
||||||
|
self.addSubview(itemComponentView)
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += itemSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeKeys: [AnyHashable] = []
|
||||||
|
for (key, itemView) in self.itemViews {
|
||||||
|
if !validKeys.contains(key) {
|
||||||
|
if let itemComponentView = itemView.view {
|
||||||
|
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
|
||||||
|
itemComponentView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
removeKeys.append(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key in removeKeys {
|
||||||
|
self.itemViews.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
//TODO:localize
|
||||||
|
let clearTitle: String
|
||||||
|
let label: String?
|
||||||
|
if totalSelectedSize == 0 {
|
||||||
|
clearTitle = "Clear"
|
||||||
|
label = nil
|
||||||
|
} else if hasDeselected {
|
||||||
|
clearTitle = "Clear Selected"
|
||||||
|
label = dataSizeString(totalSelectedSize, formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: "."))
|
||||||
|
} else {
|
||||||
|
clearTitle = "Clear All Cache"
|
||||||
|
label = dataSizeString(totalSelectedSize, formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: "."))
|
||||||
|
}
|
||||||
|
|
||||||
|
contentHeight += 8.0
|
||||||
|
let buttonSize = self.button.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(SolidRoundedButtonComponent(
|
||||||
|
title: clearTitle,
|
||||||
|
label: 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: totalSelectedSize != 0,
|
||||||
|
animationName: nil,
|
||||||
|
iconPosition: .right,
|
||||||
|
iconSpacing: 4.0,
|
||||||
|
action: {
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width - 16.0 * 2.0, height: 50.0)
|
||||||
|
)
|
||||||
|
let buttonFrame = CGRect(origin: CGPoint(x: 16.0, y: contentHeight), size: buttonSize)
|
||||||
|
if let buttonView = button.view {
|
||||||
|
if buttonView.superview == nil {
|
||||||
|
self.addSubview(buttonView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: buttonView, frame: buttonFrame)
|
||||||
|
}
|
||||||
|
contentHeight += buttonSize.height
|
||||||
|
|
||||||
|
contentHeight += 16.0
|
||||||
|
|
||||||
|
self.backgroundColor = component.theme.list.itemBlocksBackgroundColor
|
||||||
|
|
||||||
|
return CGSize(width: availableSize.width, height: contentHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,387 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
final class StorageCategoryItemComponent: Component {
|
||||||
|
enum ActionType {
|
||||||
|
case toggle
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let strings: PresentationStrings
|
||||||
|
let category: StorageCategoriesComponent.CategoryData
|
||||||
|
let isExpandedLevel: Bool
|
||||||
|
let isExpanded: Bool
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: (AnyHashable, ActionType) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
strings: PresentationStrings,
|
||||||
|
category: StorageCategoriesComponent.CategoryData,
|
||||||
|
isExpandedLevel: Bool,
|
||||||
|
isExpanded: Bool,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping (AnyHashable, ActionType) -> Void
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.strings = strings
|
||||||
|
self.category = category
|
||||||
|
self.isExpandedLevel = isExpandedLevel
|
||||||
|
self.isExpanded = isExpanded
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StorageCategoryItemComponent, rhs: StorageCategoryItemComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.strings !== rhs.strings {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.category != rhs.category {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.isExpandedLevel != rhs.isExpandedLevel {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.isExpanded != rhs.isExpanded {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class View: HighlightTrackingButton {
|
||||||
|
private let checkLayer: CheckLayer
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let titleValue = ComponentView<Empty>()
|
||||||
|
private let label = ComponentView<Empty>()
|
||||||
|
private var iconView: UIImageView?
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
|
||||||
|
private let checkButtonArea: HighlightTrackingButton
|
||||||
|
|
||||||
|
private let subcategoryClippingContainer: UIView
|
||||||
|
private var itemViews: [AnyHashable: ComponentView<Empty>] = [:]
|
||||||
|
|
||||||
|
private var component: StorageCategoryItemComponent?
|
||||||
|
|
||||||
|
private var highlightBackgroundFrame: CGRect?
|
||||||
|
private var highlightBackgroundLayer: SimpleLayer?
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.checkLayer = CheckLayer()
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
|
||||||
|
self.checkButtonArea = HighlightTrackingButton()
|
||||||
|
|
||||||
|
self.subcategoryClippingContainer = UIView()
|
||||||
|
self.subcategoryClippingContainer.clipsToBounds = true
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.addSubview(self.subcategoryClippingContainer)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
self.layer.addSublayer(self.checkLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.checkButtonArea)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] isHighlighted in
|
||||||
|
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHighlighted {
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.checkButtonArea.addTarget(self, action: #selector(self.checkPressed), for: .touchUpInside)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func pressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(component.category.key, .generic)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func checkPressed() {
|
||||||
|
guard let component = self.component else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(component.category.key, .toggle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
|
||||||
|
guard let result = super.hitTest(point, with: event) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if result === self.subcategoryClippingContainer {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: StorageCategoryItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
var leftInset: CGFloat = 62.0
|
||||||
|
var additionalLeftInset: CGFloat = 0.0
|
||||||
|
|
||||||
|
if component.isExpandedLevel {
|
||||||
|
additionalLeftInset += 45.0
|
||||||
|
}
|
||||||
|
leftInset += additionalLeftInset
|
||||||
|
|
||||||
|
let rightInset: CGFloat = 16.0
|
||||||
|
|
||||||
|
var availableWidth: CGFloat = availableSize.width - leftInset
|
||||||
|
|
||||||
|
if !component.category.subcategories.isEmpty {
|
||||||
|
let iconView: UIImageView
|
||||||
|
if let current = self.iconView {
|
||||||
|
iconView = current
|
||||||
|
if themeUpdated {
|
||||||
|
iconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
iconView = UIImageView()
|
||||||
|
iconView.image = PresentationResourcesItemList.disclosureArrowImage(component.theme)
|
||||||
|
self.iconView = iconView
|
||||||
|
self.addSubview(iconView)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = iconView.image {
|
||||||
|
availableWidth -= image.size.width + 6.0
|
||||||
|
transition.setBounds(view: iconView, bounds: CGRect(origin: CGPoint(), size: image.size))
|
||||||
|
}
|
||||||
|
} else if let iconView = self.iconView {
|
||||||
|
self.iconView = nil
|
||||||
|
iconView.removeFromSuperview()
|
||||||
|
}
|
||||||
|
|
||||||
|
let fractionValue: Double = floor(component.category.sizeFraction * 100.0 * 10.0) / 10.0
|
||||||
|
let fractionString: String
|
||||||
|
if abs(Double(Int(fractionValue)) - fractionValue) < 0.001 {
|
||||||
|
fractionString = "\(Int(fractionValue))"
|
||||||
|
} else {
|
||||||
|
fractionString = "\(fractionValue)"
|
||||||
|
}
|
||||||
|
|
||||||
|
let labelSize = self.label.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Text(text: dataSizeString(Int(component.category.size), formatting: DataSizeStringFormatting(strings: component.strings, decimalSeparator: ".")), font: Font.regular(17.0), color: component.theme.list.itemSecondaryTextColor)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||||
|
)
|
||||||
|
availableWidth = max(1.0, availableWidth - labelSize.width - 4.0)
|
||||||
|
|
||||||
|
let titleValueSize = self.titleValue.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Text(text: "\(fractionString)%", font: Font.regular(17.0), color: component.theme.list.itemSecondaryTextColor)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||||
|
)
|
||||||
|
availableWidth = max(1.0, availableWidth - titleValueSize.width - 1.0)
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Text(text: component.category.title, font: Font.regular(17.0), color: component.theme.list.itemPrimaryTextColor)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
var height: CGFloat = 44.0
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
let titleValueFrame = CGRect(origin: CGPoint(x: titleFrame.maxX + 4.0, y: floor((height - titleValueSize.height) / 2.0)), size: titleValueSize)
|
||||||
|
|
||||||
|
var labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
|
||||||
|
|
||||||
|
if let iconView = self.iconView, let image = iconView.image {
|
||||||
|
labelFrame.origin.x -= image.size.width - 6.0
|
||||||
|
|
||||||
|
transition.setPosition(view: iconView, position: CGPoint(x: availableSize.width - rightInset + 6.0 - floor(image.size.width * 0.5), y: floor(height * 0.5)))
|
||||||
|
let angle: CGFloat = component.isExpanded ? CGFloat.pi : 0.0
|
||||||
|
transition.setTransform(view: iconView, transform: CATransform3DMakeRotation(CGFloat.pi * 0.5 - angle, 0.0, 0.0, 1.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: titleView, frame: titleFrame)
|
||||||
|
}
|
||||||
|
if let titleValueView = self.titleValue.view {
|
||||||
|
if titleValueView.superview == nil {
|
||||||
|
titleValueView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(titleValueView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: titleValueView, frame: titleValueFrame)
|
||||||
|
}
|
||||||
|
if let labelView = self.label.view {
|
||||||
|
if labelView.superview == nil {
|
||||||
|
labelView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(labelView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: labelView, frame: labelFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.checkLayer.theme = CheckNodeTheme(
|
||||||
|
backgroundColor: component.category.color,
|
||||||
|
strokeColor: component.theme.list.itemCheckColors.foregroundColor,
|
||||||
|
borderColor: component.theme.list.itemCheckColors.strokeColor,
|
||||||
|
overlayBorder: false,
|
||||||
|
hasInset: false,
|
||||||
|
hasShadow: false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let checkDiameter: CGFloat = 22.0
|
||||||
|
let checkFrame = CGRect(origin: CGPoint(x: titleFrame.minX - 20.0 - checkDiameter, y: floor((height - checkDiameter) / 2.0)), size: CGSize(width: checkDiameter, height: checkDiameter))
|
||||||
|
self.checkLayer.frame = checkFrame
|
||||||
|
|
||||||
|
transition.setFrame(view: self.checkButtonArea, frame: CGRect(origin: CGPoint(x: additionalLeftInset, y: 0.0), size: CGSize(width: leftInset - additionalLeftInset, height: height)))
|
||||||
|
|
||||||
|
if self.checkLayer.selected != component.category.isSelected {
|
||||||
|
self.checkLayer.setSelected(component.category.isSelected, animated: !transition.animation.isImmediate)
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
|
||||||
|
}
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
transition.setAlpha(layer: self.separatorLayer, alpha: (component.isExpanded || component.hasNext) ? 1.0 : 0.0)
|
||||||
|
|
||||||
|
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + ((component.isExpanded || component.hasNext) ? UIScreenPixel : 0.0)))
|
||||||
|
|
||||||
|
var validKeys = Set<AnyHashable>()
|
||||||
|
if component.isExpanded {
|
||||||
|
for i in 0 ..< component.category.subcategories.count {
|
||||||
|
let category = component.category.subcategories[i]
|
||||||
|
validKeys.insert(category.key)
|
||||||
|
|
||||||
|
var itemTransition = transition
|
||||||
|
let itemView: ComponentView<Empty>
|
||||||
|
if let current = self.itemViews[category.key] {
|
||||||
|
itemView = current
|
||||||
|
} else {
|
||||||
|
itemTransition = .immediate
|
||||||
|
itemView = ComponentView()
|
||||||
|
self.itemViews[category.key] = itemView
|
||||||
|
}
|
||||||
|
|
||||||
|
itemView.parentState = state
|
||||||
|
let itemSize = itemView.update(
|
||||||
|
transition: itemTransition,
|
||||||
|
component: AnyComponent(StorageCategoryItemComponent(
|
||||||
|
theme: component.theme,
|
||||||
|
strings: component.strings,
|
||||||
|
category: category,
|
||||||
|
isExpandedLevel: true,
|
||||||
|
isExpanded: false,
|
||||||
|
hasNext: i != component.category.subcategories.count - 1,
|
||||||
|
action: { [weak self] key, _ in
|
||||||
|
guard let self else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.component?.action(key, .toggle)
|
||||||
|
}
|
||||||
|
)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableSize.width, height: 1000.0)
|
||||||
|
)
|
||||||
|
let itemFrame = CGRect(origin: CGPoint(x: 0.0, y: height), size: itemSize)
|
||||||
|
if let itemComponentView = itemView.view {
|
||||||
|
if itemComponentView.superview == nil {
|
||||||
|
self.subcategoryClippingContainer.addSubview(itemComponentView)
|
||||||
|
if !transition.animation.isImmediate {
|
||||||
|
itemComponentView.alpha = 0.0
|
||||||
|
transition.setAlpha(view: itemComponentView, alpha: 1.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
itemTransition.setFrame(view: itemComponentView, frame: itemFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
height += itemSize.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var removeKeys: [AnyHashable] = []
|
||||||
|
for (key, itemView) in self.itemViews {
|
||||||
|
if !validKeys.contains(key) {
|
||||||
|
if let itemComponentView = itemView.view {
|
||||||
|
transition.setAlpha(view: itemComponentView, alpha: 0.0, completion: { [weak itemComponentView] _ in
|
||||||
|
itemComponentView?.removeFromSuperview()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
removeKeys.append(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for key in removeKeys {
|
||||||
|
self.itemViews.removeValue(forKey: key)
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(view: self.subcategoryClippingContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height)))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,224 @@
|
|||||||
|
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
|
||||||
|
|
||||||
|
final class StoragePeerTypeItemComponent: Component {
|
||||||
|
enum ActionType {
|
||||||
|
case toggle
|
||||||
|
case generic
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme: PresentationTheme
|
||||||
|
let iconName: String
|
||||||
|
let title: String
|
||||||
|
let value: String
|
||||||
|
let hasNext: Bool
|
||||||
|
let action: (View) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
theme: PresentationTheme,
|
||||||
|
iconName: String,
|
||||||
|
title: String,
|
||||||
|
value: String,
|
||||||
|
hasNext: Bool,
|
||||||
|
action: @escaping (View) -> Void
|
||||||
|
) {
|
||||||
|
self.theme = theme
|
||||||
|
self.iconName = iconName
|
||||||
|
self.title = title
|
||||||
|
self.value = value
|
||||||
|
self.hasNext = hasNext
|
||||||
|
self.action = action
|
||||||
|
}
|
||||||
|
|
||||||
|
static func ==(lhs: StoragePeerTypeItemComponent, rhs: StoragePeerTypeItemComponent) -> Bool {
|
||||||
|
if lhs.theme !== rhs.theme {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.iconName != rhs.iconName {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.title != rhs.title {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.value != rhs.value {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if lhs.hasNext != rhs.hasNext {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
class View: HighlightTrackingButton {
|
||||||
|
private let iconView: UIImageView
|
||||||
|
private let title = ComponentView<Empty>()
|
||||||
|
private let label = ComponentView<Empty>()
|
||||||
|
private let separatorLayer: SimpleLayer
|
||||||
|
private let arrowIconView: UIImageView
|
||||||
|
|
||||||
|
private var component: StoragePeerTypeItemComponent?
|
||||||
|
|
||||||
|
private var highlightBackgroundFrame: CGRect?
|
||||||
|
private var highlightBackgroundLayer: SimpleLayer?
|
||||||
|
|
||||||
|
var labelView: UIView? {
|
||||||
|
return self.label.view
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
self.separatorLayer = SimpleLayer()
|
||||||
|
self.iconView = UIImageView()
|
||||||
|
self.arrowIconView = UIImageView()
|
||||||
|
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
self.layer.addSublayer(self.separatorLayer)
|
||||||
|
|
||||||
|
self.addSubview(self.iconView)
|
||||||
|
self.addSubview(self.arrowIconView)
|
||||||
|
|
||||||
|
self.highligthedChanged = { [weak self] isHighlighted in
|
||||||
|
guard let self, let component = self.component, let highlightBackgroundFrame = self.highlightBackgroundFrame else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if isHighlighted {
|
||||||
|
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 else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
component.action(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setHasAssociatedMenu(_ hasAssociatedMenu: Bool) {
|
||||||
|
let transition: Transition
|
||||||
|
if hasAssociatedMenu {
|
||||||
|
transition = .immediate
|
||||||
|
} else {
|
||||||
|
transition = .easeInOut(duration: 0.25)
|
||||||
|
}
|
||||||
|
if let view = self.label.view {
|
||||||
|
transition.setAlpha(view: view, alpha: hasAssociatedMenu ? 0.5 : 1.0)
|
||||||
|
}
|
||||||
|
transition.setAlpha(view: self.arrowIconView, alpha: hasAssociatedMenu ? 0.5 : 1.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func update(component: StoragePeerTypeItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: Transition) -> CGSize {
|
||||||
|
let themeUpdated = self.component?.theme !== component.theme
|
||||||
|
|
||||||
|
self.component = component
|
||||||
|
|
||||||
|
let leftInset: CGFloat = 62.0
|
||||||
|
let rightInset: CGFloat = 32.0
|
||||||
|
|
||||||
|
var availableWidth: CGFloat = availableSize.width - leftInset - rightInset
|
||||||
|
|
||||||
|
let labelSize = self.label.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Text(text: component.value, font: Font.regular(17.0), color: component.theme.list.itemSecondaryTextColor)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||||
|
)
|
||||||
|
availableWidth = max(1.0, availableWidth - labelSize.width - 4.0)
|
||||||
|
|
||||||
|
let titleSize = self.title.update(
|
||||||
|
transition: transition,
|
||||||
|
component: AnyComponent(Text(text: component.title, font: Font.regular(17.0), color: component.theme.list.itemPrimaryTextColor)),
|
||||||
|
environment: {},
|
||||||
|
containerSize: CGSize(width: availableWidth, height: 100.0)
|
||||||
|
)
|
||||||
|
|
||||||
|
let height: CGFloat = 44.0
|
||||||
|
|
||||||
|
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - titleSize.height) / 2.0)), size: titleSize)
|
||||||
|
let labelFrame = CGRect(origin: CGPoint(x: availableSize.width - rightInset - labelSize.width, y: floor((height - labelSize.height) / 2.0)), size: labelSize)
|
||||||
|
|
||||||
|
if let titleView = self.title.view {
|
||||||
|
if titleView.superview == nil {
|
||||||
|
titleView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(titleView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: titleView, frame: titleFrame)
|
||||||
|
}
|
||||||
|
if let labelView = self.label.view {
|
||||||
|
if labelView.superview == nil {
|
||||||
|
labelView.isUserInteractionEnabled = false
|
||||||
|
self.addSubview(labelView)
|
||||||
|
}
|
||||||
|
transition.setFrame(view: labelView, frame: labelFrame)
|
||||||
|
}
|
||||||
|
|
||||||
|
if themeUpdated {
|
||||||
|
self.separatorLayer.backgroundColor = component.theme.list.itemBlocksSeparatorColor.cgColor
|
||||||
|
self.iconView.image = UIImage(bundleImageName: component.iconName)
|
||||||
|
self.arrowIconView.image = PresentationResourcesItemList.disclosureOptionArrowsImage(component.theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let image = self.iconView.image {
|
||||||
|
transition.setFrame(view: self.iconView, frame: CGRect(origin: CGPoint(x: floor((leftInset - image.size.width) / 2.0), y: floor((height - image.size.height) / 2.0)), size: image.size))
|
||||||
|
}
|
||||||
|
if let image = self.arrowIconView.image {
|
||||||
|
transition.setFrame(view: self.arrowIconView, frame: CGRect(origin: CGPoint(x: availableSize.width - rightInset + 5.0, y: floor((height - image.size.height) / 2.0)), size: image.size))
|
||||||
|
}
|
||||||
|
|
||||||
|
transition.setFrame(layer: self.separatorLayer, frame: CGRect(origin: CGPoint(x: leftInset, y: height), size: CGSize(width: availableSize.width - leftInset, height: UIScreenPixel)))
|
||||||
|
|
||||||
|
transition.setAlpha(layer: self.separatorLayer, alpha: component.hasNext ? 1.0 : 0.0)
|
||||||
|
|
||||||
|
self.highlightBackgroundFrame = CGRect(origin: CGPoint(), size: CGSize(width: availableSize.width, height: height + (component.hasNext ? UIScreenPixel : 0.0)))
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -84,6 +84,7 @@ import NotificationPeerExceptionController
|
|||||||
import StickerPackPreviewUI
|
import StickerPackPreviewUI
|
||||||
import ChatListHeaderComponent
|
import ChatListHeaderComponent
|
||||||
import ChatControllerInteraction
|
import ChatControllerInteraction
|
||||||
|
import StorageUsageScreen
|
||||||
|
|
||||||
enum PeerInfoAvatarEditingMode {
|
enum PeerInfoAvatarEditingMode {
|
||||||
case generic
|
case generic
|
||||||
|
Loading…
x
Reference in New Issue
Block a user