Various improvements
This commit is contained in:
parent
5fbe230308
commit
3e6363abf9
351 changed files with 11565 additions and 12031 deletions
|
|
@ -1570,6 +1570,8 @@ plist_fragment(
|
|||
<string>here-location</string>
|
||||
<string>yandexmaps</string>
|
||||
<string>yandexnavi</string>
|
||||
<string>yandextaxi</string>
|
||||
<string>yangoride</string>
|
||||
<string>comgooglemaps</string>
|
||||
<string>youtube</string>
|
||||
<string>twitter</string>
|
||||
|
|
@ -1598,6 +1600,7 @@ plist_fragment(
|
|||
<string>dolphin</string>
|
||||
<string>instagram-stories</string>
|
||||
<string>yangomaps</string>
|
||||
<string>vivaldi</string>
|
||||
</array>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
|
|
|
|||
|
|
@ -16339,3 +16339,6 @@ Error: %8$@";
|
|||
"RecentSessions.ConnectedBot.TerminateCheckbox" = "Also terminate %@";
|
||||
|
||||
"Chat.GuestChatMessageTooltip" = "This bot can't read the chat – only the messages where it was mentioned.";
|
||||
|
||||
"MediaPicker.SetNewGroupPhoto" = "Set new group photo";
|
||||
"MediaPicker.SetNewChannelPhoto" = "Set new channel photo";
|
||||
|
|
|
|||
|
|
@ -185,6 +185,22 @@ public enum WallpaperUrlParameter {
|
|||
case gradient([UInt32], Int32?)
|
||||
}
|
||||
|
||||
public enum PeerType: Equatable {
|
||||
case user(isBot: Bool)
|
||||
case group
|
||||
case channel
|
||||
|
||||
public static func getType(for peer: EnginePeer) -> PeerType {
|
||||
if case let .user(user) = peer {
|
||||
return .user(isBot: user.botInfo != nil)
|
||||
} else if case let .channel(channel) = peer, case .broadcast = channel.info {
|
||||
return .channel
|
||||
} else {
|
||||
return .group
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct ResolvedBotChoosePeerTypes: OptionSet {
|
||||
public var rawValue: UInt32
|
||||
|
||||
|
|
@ -1484,7 +1500,7 @@ public protocol SharedAccountContext: AnyObject {
|
|||
func makeBotPreviewEditorScreen(context: AccountContext, source: Any?, target: Stories.PendingTarget, transitionArguments: (UIView, CGRect, UIImage?)?, transitionOut: @escaping () -> BotPreviewEditorTransitionOut?, externalState: MediaEditorTransitionOutExternalState, completion: @escaping (MediaEditorScreenResult, @escaping (@escaping () -> Void) -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
|
||||
func makeStickerEditorScreen(context: AccountContext, source: Any?, mode: StickerEditorMode, transitionArguments: (UIView, CGRect, UIImage?)?, completion: @escaping (TelegramMediaFile, [String], @escaping () -> Void) -> Void, cancelled: @escaping () -> Void) -> ViewController
|
||||
func makeStickerMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController
|
||||
func makeAvatarMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?)
|
||||
func makeAvatarMediaPickerScreen(context: AccountContext, peerType: PeerType, getSourceRect: @escaping () -> CGRect?, canDelete: Bool, performDelete: @escaping () -> Void, completion: @escaping (Any?, UIView?, CGRect, UIImage?, Bool, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> (ViewController?, Any?)
|
||||
func makeStoryMediaPickerScreen(context: AccountContext, isDark: Bool, forCollage: Bool, selectionLimit: Int?, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping (Bool?) -> (UIView, CGRect)?, @escaping () -> Void) -> Void, multipleCompletion: @escaping ([Any], Bool) -> Void, dismissed: @escaping () -> Void, groupsPresented: @escaping () -> Void) -> ViewController
|
||||
func makeStickerPickerScreen(context: AccountContext, inputData: Promise<StickerPickerInput>, completion: @escaping (FileMediaReference) -> Void) -> ViewController
|
||||
func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController
|
||||
|
|
|
|||
|
|
@ -264,6 +264,23 @@ public extension ChatMessageItemAssociatedData {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func isPollVotingRestricted(poll: TelegramMediaPoll, accountTestingEnvironment: Bool, currentTimestamp: Int32) -> Bool {
|
||||
if !poll.countries.isEmpty, let accountCountry = self.accountCountry, !poll.countries.contains(accountCountry) {
|
||||
return true
|
||||
}
|
||||
|
||||
if poll.restrictToSubscribers {
|
||||
let period: Int32 = accountTestingEnvironment ? 5 * 60 : 24 * 60 * 60
|
||||
if !self.isParticipant {
|
||||
return true
|
||||
} else if let invitedOn = self.invitedOn, invitedOn + period > currentTimestamp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
public enum ChatControllerInteractionLongTapAction {
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ public final class AdInfoScreen: ViewController {
|
|||
self.scrollNode = ASScrollNode()
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = true
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = true
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.canCancelContentTouches = true
|
||||
if #available(iOS 11.0, *) {
|
||||
|
|
|
|||
|
|
@ -341,6 +341,7 @@
|
|||
textView.opaque = NO;
|
||||
}
|
||||
textView.textContainerInset = self.textContainerInset;
|
||||
textView.scrollsToTop = false;
|
||||
|
||||
// Configure textView with UITextInputTraits
|
||||
{
|
||||
|
|
@ -366,6 +367,7 @@
|
|||
_placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer];
|
||||
_placeholderTextKitComponents.textView.userInteractionEnabled = NO;
|
||||
_placeholderTextKitComponents.textView.accessibilityElementsHidden = YES;
|
||||
_placeholderTextKitComponents.textView.scrollsToTop = false;
|
||||
configureTextView(_placeholderTextKitComponents.textView);
|
||||
|
||||
// Create and configure our text view.
|
||||
|
|
|
|||
|
|
@ -1563,12 +1563,22 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
|
|||
if let data = view.cachedData as? CachedUserData {
|
||||
return data.sendPaidMessageStars
|
||||
} else if let channel = peerViewMainPeer(view) as? TelegramChannel {
|
||||
if channel.isMonoForum, let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
|
||||
return nil
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData, let sendPaidMessageStarsValue = cachedData.sendPaidMessageStars, sendPaidMessageStarsValue == .zero {
|
||||
return nil
|
||||
if channel.isMonoForum {
|
||||
if let linkedMonoforumId = channel.linkedMonoforumId, let mainChannel = view.peers[linkedMonoforumId] as? TelegramChannel, mainChannel.hasPermission(.manageDirect) {
|
||||
return nil
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars, value == .zero {
|
||||
return nil
|
||||
} else {
|
||||
return channel.sendPaidMessageStars
|
||||
}
|
||||
} else {
|
||||
return channel.sendPaidMessageStars
|
||||
if channel.flags.contains(.isCreator) || channel.adminRights != nil {
|
||||
return nil
|
||||
} else if let cachedData = view.cachedData as? CachedChannelData, let value = cachedData.sendPaidMessageStars {
|
||||
return value == .zero ? nil : value
|
||||
} else {
|
||||
return channel.sendPaidMessageStars
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return nil
|
||||
|
|
@ -1600,6 +1610,7 @@ final class AttachmentPanel: ASDisplayNode, ASScrollViewDelegate, ASGestureRecog
|
|||
self.containerNode.layer.cornerCurve = .continuous
|
||||
}
|
||||
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
|
|
|
|||
|
|
@ -11,10 +11,21 @@ swift_library(
|
|||
],
|
||||
deps = [
|
||||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/ComponentFlow:ComponentFlow",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/LocalAuth:LocalAuth",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/Components/ViewControllerComponent:ViewControllerComponent",
|
||||
"//submodules/Components/ResizableSheetComponent",
|
||||
"//submodules/Components/MultilineTextComponent",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
"//submodules/TelegramUI/Components/ListSectionComponent",
|
||||
"//submodules/TelegramUI/Components/ListActionItemComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent",
|
||||
"//submodules/TelegramUI/Components/AlertComponent/AlertInputFieldComponent",
|
||||
"//submodules/ItemListUI:ItemListUI",
|
||||
"//submodules/PasswordSetupUI:PasswordSetupUI",
|
||||
"//submodules/PhotoResources:PhotoResources",
|
||||
|
|
|
|||
|
|
@ -117,6 +117,7 @@ final class BotCheckoutActionButton: HighlightTrackingButtonNode {
|
|||
} else {
|
||||
applePayButton = PKPaymentButton(paymentButtonType: .buy, paymentButtonStyle: .black)
|
||||
}
|
||||
applePayButton.cornerRadius = BotCheckoutActionButton.height * 0.5
|
||||
applePayButton.addTarget(self, action: #selector(self.applePayButtonPressed), for: .touchUpInside)
|
||||
self.view.addSubview(applePayButton)
|
||||
self.applePayButton = applePayButton
|
||||
|
|
|
|||
|
|
@ -141,9 +141,10 @@ public final class BotCheckoutController: ViewController {
|
|||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
|
||||
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
self._hasGlassStyle = true
|
||||
|
||||
var title = self.presentationData.strings.Checkout_Title
|
||||
if invoice.flags.contains(.isTest) {
|
||||
|
|
@ -151,7 +152,7 @@ public final class BotCheckoutController: ViewController {
|
|||
}
|
||||
self.title = title
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
}
|
||||
|
||||
required public init(coder aDecoder: NSCoder) {
|
||||
|
|
|
|||
|
|
@ -1077,13 +1077,13 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
|||
if methods.isEmpty {
|
||||
openNewCard(nil, nil)
|
||||
} else {
|
||||
strongSelf.present(BotCheckoutPaymentMethodSheetController(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
|
||||
strongSelf.controller?.push(BotCheckoutPaymentMethodScreen(context: strongSelf.context, currentMethod: strongSelf.currentPaymentMethod, methods: methods, applyValue: { method in
|
||||
applyPaymentMethod(method)
|
||||
}, newCard: {
|
||||
openNewCard(nil, nil)
|
||||
}, otherMethod: { url, title in
|
||||
openNewCard(url, title)
|
||||
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1091,14 +1091,14 @@ final class BotCheckoutControllerNode: ItemListControllerNode, PKPaymentAuthoriz
|
|||
openShippingMethodImpl = { [weak self] in
|
||||
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let shippingOptions = strongSelf.currentValidatedFormInfo?.shippingOptions, !shippingOptions.isEmpty {
|
||||
strongSelf.controller?.view.endEditing(true)
|
||||
strongSelf.present(BotCheckoutPaymentShippingOptionSheetController(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in
|
||||
strongSelf.controller?.push(BotCheckoutShippingOptionScreen(context: strongSelf.context, currency: paymentFormValue.invoice.currency, options: shippingOptions, currentId: strongSelf.currentShippingOptionId, applyValue: { id in
|
||||
if let strongSelf = self, let paymentFormValue = strongSelf.paymentFormValue, let currentFormInfo = strongSelf.currentFormInfo {
|
||||
strongSelf.currentShippingOptionId = id
|
||||
strongSelf.paymentFormAndInfo.set(.single((paymentFormValue, currentFormInfo, strongSelf.currentValidatedFormInfo, strongSelf.currentShippingOptionId, strongSelf.currentPaymentMethod, strongSelf.currentTipAmount)))
|
||||
|
||||
strongSelf.updateActionButton()
|
||||
}
|
||||
}), ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ class BotCheckoutHeaderItemNode: ListViewItemNode {
|
|||
}
|
||||
|
||||
let contentSize = CGSize(width: params.width, height: contentHeight)
|
||||
let insets = itemListNeighborsPlainInsets(neighbors)
|
||||
let insets = itemListNeighborsGroupedInsets(neighbors, params)
|
||||
|
||||
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
|
||||
|
||||
|
|
|
|||
|
|
@ -60,14 +60,16 @@ final class BotCheckoutInfoController: ViewController {
|
|||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
|
||||
|
||||
self._hasGlassStyle = true
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
|
||||
|
||||
self.title = self.presentationData.strings.CheckoutInfo_Title
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.rightBarButtonItem = self.doneItem
|
||||
self.doneItem?.isEnabled = false
|
||||
}
|
||||
|
|
@ -84,13 +86,13 @@ final class BotCheckoutInfoController: ViewController {
|
|||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}, openCountrySelection: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false)
|
||||
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true)
|
||||
controller.completeWithCountryCode = { _, id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updateCountry(id)
|
||||
}
|
||||
}
|
||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
strongSelf.push(controller)
|
||||
}
|
||||
}, updateStatus: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
|
|
|
|||
|
|
@ -455,7 +455,7 @@ final class BotCheckoutInfoControllerNode: ViewControllerTracingNode, ASScrollVi
|
|||
|
||||
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
var sideInset: CGFloat = 0.0
|
||||
if layout.size.width >= 375.0 {
|
||||
if layout.size.width >= 320.0 {
|
||||
sideInset = inset
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -55,14 +55,16 @@ final class BotCheckoutNativeCardEntryController: ViewController {
|
|||
|
||||
self.presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData))
|
||||
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: self.presentationData, style: .glass))
|
||||
|
||||
self._hasGlassStyle = true
|
||||
self.statusBar.statusBarStyle = self.presentationData.theme.rootController.statusBarStyle.style
|
||||
|
||||
self.doneItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
self.doneItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
|
||||
|
||||
self.title = self.presentationData.strings.Checkout_NewCard_Title
|
||||
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(self.cancelPressed))
|
||||
self.navigationItem.rightBarButtonItem = self.doneItem
|
||||
self.doneItem?.isEnabled = false
|
||||
}
|
||||
|
|
@ -78,13 +80,13 @@ final class BotCheckoutNativeCardEntryController: ViewController {
|
|||
self?.presentingViewController?.dismiss(animated: false, completion: nil)
|
||||
}, openCountrySelection: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false)
|
||||
let controller = AuthorizationSequenceCountrySelectionController(strings: strongSelf.presentationData.strings, theme: strongSelf.presentationData.theme, displayCodes: false, glass: true)
|
||||
controller.completeWithCountryCode = { _, id in
|
||||
if let strongSelf = self {
|
||||
strongSelf.controllerNode.updateCountry(id)
|
||||
}
|
||||
}
|
||||
strongSelf.present(controller, in: .window(.root), with: ViewControllerPresentationArguments(presentationAnimation: .modalSheet))
|
||||
strongSelf.push(controller)
|
||||
}
|
||||
}, updateStatus: { [weak self] status in
|
||||
if let strongSelf = self {
|
||||
|
|
|
|||
|
|
@ -468,7 +468,7 @@ final class BotCheckoutNativeCardEntryControllerNode: ViewControllerTracingNode,
|
|||
|
||||
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
var sideInset: CGFloat = 0.0
|
||||
if layout.size.width >= 375.0 {
|
||||
if layout.size.width >= 320.0 {
|
||||
sideInset = inset
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,374 +1,96 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import AsyncDisplayKit
|
||||
import Display
|
||||
import TelegramCore
|
||||
import SwiftSignalKit
|
||||
import TelegramPresentationData
|
||||
import AccountContext
|
||||
import ComponentFlow
|
||||
import AlertComponent
|
||||
import AlertInputFieldComponent
|
||||
|
||||
private final class BotCheckoutPassworInputFieldNode: ASDisplayNode, UITextFieldDelegate {
|
||||
private var theme: PresentationTheme
|
||||
private let backgroundNode: ASImageNode
|
||||
private let textInputNode: TextFieldNode
|
||||
private let placeholderNode: ASTextNode
|
||||
|
||||
var updateHeight: (() -> Void)?
|
||||
var complete: (() -> Void)?
|
||||
var textChanged: ((String) -> Void)?
|
||||
|
||||
private let backgroundInsets = UIEdgeInsets(top: 8.0, left: 16.0, bottom: 15.0, right: 16.0)
|
||||
private let inputInsets = UIEdgeInsets(top: 5.0, left: 12.0, bottom: 5.0, right: 12.0)
|
||||
|
||||
var text: String {
|
||||
get {
|
||||
return self.textInputNode.textField.text ?? ""
|
||||
}
|
||||
set {
|
||||
self.textInputNode.textField.text = newValue
|
||||
self.placeholderNode.isHidden = !newValue.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
var placeholder: String = "" {
|
||||
didSet {
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
}
|
||||
}
|
||||
|
||||
init(theme: PresentationTheme, placeholder: String) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode = ASImageNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.backgroundNode.displaysAsynchronously = false
|
||||
self.backgroundNode.displayWithoutProcessing = true
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: theme.actionSheet.inputHollowBackgroundColor, strokeColor: theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
|
||||
self.textInputNode = TextFieldNode()
|
||||
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.textField.clipsToBounds = true
|
||||
self.textInputNode.hitTestSlop = UIEdgeInsets(top: -5.0, left: -5.0, bottom: -5.0, right: -5.0)
|
||||
self.textInputNode.textField.keyboardAppearance = theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.textInputNode.textField.returnKeyType = .done
|
||||
self.textInputNode.textField.isSecureTextEntry = true
|
||||
self.textInputNode.textField.tintColor = theme.actionSheet.controlAccentColor
|
||||
self.textInputNode.textField.textColor = theme.actionSheet.inputTextColor
|
||||
|
||||
self.placeholderNode = ASTextNode()
|
||||
self.placeholderNode.isUserInteractionEnabled = false
|
||||
self.placeholderNode.displaysAsynchronously = false
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: placeholder, font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
|
||||
super.init()
|
||||
|
||||
self.textInputNode.textField.delegate = self
|
||||
self.textInputNode.textField.addTarget(self, action: #selector(self.textDidChange), for: .editingChanged)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.textInputNode)
|
||||
self.addSubnode(self.placeholderNode)
|
||||
}
|
||||
|
||||
func updateTheme(_ theme: PresentationTheme) {
|
||||
self.theme = theme
|
||||
|
||||
self.backgroundNode.image = generateStretchableFilledCircleImage(diameter: 12.0, color: self.theme.actionSheet.inputHollowBackgroundColor, strokeColor: self.theme.actionSheet.inputBorderColor, strokeWidth: 1.0)
|
||||
self.textInputNode.textField.keyboardAppearance = self.theme.rootController.keyboardColor.keyboardAppearance
|
||||
self.placeholderNode.attributedText = NSAttributedString(string: self.placeholderNode.attributedText?.string ?? "", font: Font.regular(17.0), textColor: self.theme.actionSheet.inputPlaceholderColor)
|
||||
self.textInputNode.textField.tintColor = self.theme.actionSheet.controlAccentColor
|
||||
self.textInputNode.textField.typingAttributes = [NSAttributedString.Key.font: Font.regular(17.0), NSAttributedString.Key.foregroundColor: theme.actionSheet.inputTextColor]
|
||||
self.textInputNode.textField.textColor = self.theme.actionSheet.inputTextColor
|
||||
}
|
||||
|
||||
func updateLayout(width: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: width)
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
|
||||
let backgroundFrame = CGRect(origin: CGPoint(x: backgroundInsets.left, y: backgroundInsets.top), size: CGSize(width: width - backgroundInsets.left - backgroundInsets.right, height: panelHeight - backgroundInsets.top - backgroundInsets.bottom))
|
||||
transition.updateFrame(node: self.backgroundNode, frame: backgroundFrame)
|
||||
|
||||
let placeholderSize = self.placeholderNode.measure(backgroundFrame.size)
|
||||
transition.updateFrame(node: self.placeholderNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY + floor((backgroundFrame.size.height - placeholderSize.height) / 2.0)), size: placeholderSize))
|
||||
|
||||
transition.updateFrame(node: self.textInputNode, frame: CGRect(origin: CGPoint(x: backgroundFrame.minX + inputInsets.left, y: backgroundFrame.minY), size: CGSize(width: backgroundFrame.size.width - inputInsets.left - inputInsets.right, height: backgroundFrame.size.height)))
|
||||
|
||||
return panelHeight
|
||||
}
|
||||
|
||||
func activateInput() {
|
||||
self.textInputNode.becomeFirstResponder()
|
||||
}
|
||||
|
||||
func deactivateInput() {
|
||||
self.textInputNode.resignFirstResponder()
|
||||
}
|
||||
|
||||
func shake() {
|
||||
self.layer.addShakeAnimation()
|
||||
func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> ViewController {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
|
||||
let inputState = AlertInputFieldComponent.ExternalState()
|
||||
let doneInProgressPromise = ValuePromise<Bool>(false)
|
||||
let doneIsEnabled: Signal<Bool, NoError> = combineLatest(inputState.valueSignal, doneInProgressPromise.get())
|
||||
|> map { value, isInProgress in
|
||||
return !value.isEmpty && !isInProgress
|
||||
}
|
||||
|
||||
@objc func textDidChange() {
|
||||
self.updateTextNodeText(animated: true)
|
||||
self.textChanged?(self.textInputNode.textField.text ?? "")
|
||||
self.placeholderNode.isHidden = !(self.textInputNode.textField.text ?? "").isEmpty
|
||||
}
|
||||
|
||||
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
|
||||
if text == "\n" {
|
||||
self.complete?()
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private func calculateTextFieldMetrics(width: CGFloat) -> CGFloat {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
let inputInsets = self.inputInsets
|
||||
|
||||
let unboundTextFieldHeight = max(33.0, ceil(self.textInputNode.measure(CGSize(width: width - backgroundInsets.left - backgroundInsets.right - inputInsets.left - inputInsets.right, height: CGFloat.greatestFiniteMagnitude)).height))
|
||||
|
||||
return min(61.0, max(33.0, unboundTextFieldHeight))
|
||||
}
|
||||
|
||||
private func updateTextNodeText(animated: Bool) {
|
||||
let backgroundInsets = self.backgroundInsets
|
||||
|
||||
let textFieldHeight = self.calculateTextFieldMetrics(width: self.bounds.size.width)
|
||||
|
||||
let panelHeight = textFieldHeight + backgroundInsets.top + backgroundInsets.bottom
|
||||
if !self.bounds.size.height.isEqual(to: panelHeight) {
|
||||
self.updateHeight?()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func clearPressed() {
|
||||
self.textInputNode.textField.text = nil
|
||||
self.deactivateInput()
|
||||
}
|
||||
}
|
||||
var content: [AnyComponentWithIdentity<AlertComponentEnvironment>] = []
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "title",
|
||||
component: AnyComponent(
|
||||
AlertTitleComponent(title: strings.Checkout_PasswordEntry_Title)
|
||||
)
|
||||
))
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "text",
|
||||
component: AnyComponent(
|
||||
AlertTextComponent(content: .plain(strings.Checkout_PasswordEntry_Text(cartTitle).string))
|
||||
)
|
||||
))
|
||||
|
||||
private final class BotCheckoutPasswordAlertContentNode: AlertContentNode {
|
||||
private let context: AccountContext
|
||||
private let period: Int32
|
||||
private let requiresBiometrics: Bool
|
||||
private let completion: (TemporaryTwoStepPasswordToken) -> Void
|
||||
|
||||
private let titleNode: ASTextNode
|
||||
private let textNode: ASTextNode
|
||||
|
||||
private let actionNodesSeparator: ASDisplayNode
|
||||
private let actionNodes: [TextAlertContentActionNode]
|
||||
private let actionVerticalSeparators: [ASDisplayNode]
|
||||
|
||||
private let cancelActionNode: TextAlertContentActionNode
|
||||
private let doneActionNode: TextAlertContentActionNode
|
||||
|
||||
let inputFieldNode: BotCheckoutPassworInputFieldNode
|
||||
|
||||
private var validLayout: CGSize?
|
||||
private var isVerifying = false
|
||||
private let disposable = MetaDisposable()
|
||||
|
||||
private let hapticFeedback = HapticFeedback()
|
||||
|
||||
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, passwordTip: String?, cardTitle: String, period: Int32, requiresBiometrics: Bool, cancel: @escaping () -> Void, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) {
|
||||
self.context = context
|
||||
self.period = period
|
||||
self.requiresBiometrics = requiresBiometrics
|
||||
self.completion = completion
|
||||
|
||||
let alertTheme = AlertControllerTheme(presentationTheme: theme, fontSize: .regular)
|
||||
|
||||
let titleNode = ASTextNode()
|
||||
titleNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Title, font: Font.semibold(17.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
|
||||
titleNode.displaysAsynchronously = false
|
||||
titleNode.isUserInteractionEnabled = false
|
||||
titleNode.maximumNumberOfLines = 1
|
||||
titleNode.truncationMode = .byTruncatingTail
|
||||
self.titleNode = titleNode
|
||||
|
||||
self.textNode = ASTextNode()
|
||||
self.textNode.attributedText = NSAttributedString(string: strings.Checkout_PasswordEntry_Text(cardTitle).string, font: Font.regular(13.0), textColor: theme.actionSheet.primaryTextColor, paragraphAlignment: .center)
|
||||
self.textNode.displaysAsynchronously = false
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
|
||||
self.inputFieldNode = BotCheckoutPassworInputFieldNode(theme: theme, placeholder: passwordTip ?? "")
|
||||
|
||||
self.actionNodesSeparator = ASDisplayNode()
|
||||
self.actionNodesSeparator.isLayerBacked = true
|
||||
self.actionNodesSeparator.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
|
||||
|
||||
self.cancelActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .genericAction, title: strings.Common_Cancel, action: {
|
||||
cancel()
|
||||
}))
|
||||
|
||||
var doneImpl: (() -> Void)?
|
||||
self.doneActionNode = TextAlertContentActionNode(theme: alertTheme, action: TextAlertAction(type: .defaultAction, title: strings.Checkout_PasswordEntry_Pay, action: {
|
||||
doneImpl?()
|
||||
}))
|
||||
|
||||
self.actionNodes = [self.cancelActionNode, self.doneActionNode]
|
||||
|
||||
var actionVerticalSeparators: [ASDisplayNode] = []
|
||||
if self.actionNodes.count > 1 {
|
||||
for _ in 0 ..< self.actionNodes.count - 1 {
|
||||
let separatorNode = ASDisplayNode()
|
||||
separatorNode.isLayerBacked = true
|
||||
separatorNode.backgroundColor = theme.actionSheet.opaqueItemSeparatorColor
|
||||
actionVerticalSeparators.append(separatorNode)
|
||||
}
|
||||
}
|
||||
self.actionVerticalSeparators = actionVerticalSeparators
|
||||
|
||||
super.init()
|
||||
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.textNode)
|
||||
|
||||
self.addSubnode(self.actionNodesSeparator)
|
||||
|
||||
for actionNode in self.actionNodes {
|
||||
self.addSubnode(actionNode)
|
||||
}
|
||||
|
||||
for separatorNode in self.actionVerticalSeparators {
|
||||
self.addSubnode(separatorNode)
|
||||
}
|
||||
|
||||
self.addSubnode(self.inputFieldNode)
|
||||
|
||||
self.inputFieldNode.textChanged = { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.updateState()
|
||||
}
|
||||
}
|
||||
|
||||
self.updateState()
|
||||
|
||||
doneImpl = { [weak self] in
|
||||
self?.verify()
|
||||
}
|
||||
var applyImpl: (() -> Void)?
|
||||
content.append(AnyComponentWithIdentity(
|
||||
id: "input",
|
||||
component: AnyComponent(
|
||||
AlertInputFieldComponent(
|
||||
context: context,
|
||||
placeholder: passwordTip ?? "",
|
||||
isSecureTextEntry: true,
|
||||
autocapitalizationType: .none,
|
||||
autocorrectionType: .no,
|
||||
isInitiallyFocused: true,
|
||||
externalState: inputState,
|
||||
returnKeyAction: {
|
||||
applyImpl?()
|
||||
}
|
||||
)
|
||||
)
|
||||
))
|
||||
|
||||
var isVerifying = false
|
||||
let disposable = MetaDisposable()
|
||||
var dismissImpl: (() -> Void)?
|
||||
let alertController = AlertScreen(
|
||||
configuration: AlertScreen.Configuration(allowInputInset: true),
|
||||
content: content,
|
||||
actions: [
|
||||
.init(title: strings.Common_Cancel),
|
||||
.init(title: strings.Checkout_PasswordEntry_Pay, type: .default, action: {
|
||||
applyImpl?()
|
||||
}, autoDismiss: false, isEnabled: doneIsEnabled, progress: doneInProgressPromise.get())
|
||||
],
|
||||
updatedPresentationData: (initial: presentationData, signal: context.sharedContext.presentationData)
|
||||
)
|
||||
alertController.dismissed = { _ in
|
||||
disposable.dispose()
|
||||
}
|
||||
|
||||
override func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let previousLayout = self.validLayout
|
||||
self.validLayout = size
|
||||
|
||||
let insets = UIEdgeInsets(top: 18.0, left: 18.0, bottom: 18.0, right: 18.0)
|
||||
|
||||
let titleSize = titleNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
let textSize = self.textNode.measure(CGSize(width: size.width - insets.left - insets.right, height: CGFloat.greatestFiniteMagnitude))
|
||||
|
||||
let actionsHeight: CGFloat = 44.0
|
||||
|
||||
var minActionsWidth: CGFloat = 0.0
|
||||
let maxActionWidth: CGFloat = floor(size.width / CGFloat(self.actionNodes.count))
|
||||
let actionTitleInsets: CGFloat = 8.0
|
||||
for actionNode in self.actionNodes {
|
||||
let actionTitleSize = actionNode.titleNode.updateLayout(CGSize(width: maxActionWidth, height: actionsHeight))
|
||||
minActionsWidth += actionTitleSize.width + actionTitleInsets
|
||||
}
|
||||
|
||||
let contentWidth = max(max(titleSize.width, textSize.width), minActionsWidth)
|
||||
|
||||
let spacing: CGFloat = 6.0
|
||||
let titleFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - titleSize.width) / 2.0), y: insets.top), size: titleSize)
|
||||
transition.updateFrame(node: titleNode, frame: titleFrame)
|
||||
|
||||
let textFrame = CGRect(origin: CGPoint(x: insets.left + floor((contentWidth - textSize.width) / 2.0), y: titleFrame.maxY + spacing), size: textSize)
|
||||
transition.updateFrame(node: self.textNode, frame: textFrame)
|
||||
|
||||
let resultSize = CGSize(width: contentWidth + insets.left + insets.right, height: titleSize.height + spacing + textSize.height + actionsHeight + insets.top + insets.bottom + 46.0)
|
||||
|
||||
let inputFieldWidth = resultSize.width
|
||||
let inputFieldHeight = self.inputFieldNode.updateLayout(width: inputFieldWidth, transition: transition)
|
||||
transition.updateFrame(node: self.inputFieldNode, frame: CGRect(x: 0.0, y: resultSize.height - 36.0 - actionsHeight - insets.bottom, width: resultSize.width, height: inputFieldHeight))
|
||||
|
||||
self.actionNodesSeparator.frame = CGRect(origin: CGPoint(x: 0.0, y: resultSize.height - actionsHeight - UIScreenPixel), size: CGSize(width: resultSize.width, height: UIScreenPixel))
|
||||
|
||||
var actionOffset: CGFloat = 0.0
|
||||
let actionWidth: CGFloat = floor(resultSize.width / CGFloat(self.actionNodes.count))
|
||||
var separatorIndex = -1
|
||||
var nodeIndex = 0
|
||||
for actionNode in self.actionNodes {
|
||||
if separatorIndex >= 0 {
|
||||
let separatorNode = self.actionVerticalSeparators[separatorIndex]
|
||||
transition.updateFrame(node: separatorNode, frame: CGRect(origin: CGPoint(x: actionOffset - UIScreenPixel, y: resultSize.height - actionsHeight), size: CGSize(width: UIScreenPixel, height: actionsHeight - UIScreenPixel)))
|
||||
}
|
||||
separatorIndex += 1
|
||||
|
||||
let currentActionWidth: CGFloat
|
||||
if nodeIndex == self.actionNodes.count - 1 {
|
||||
currentActionWidth = resultSize.width - actionOffset
|
||||
} else {
|
||||
currentActionWidth = actionWidth
|
||||
}
|
||||
|
||||
let actionNodeFrame = CGRect(origin: CGPoint(x: actionOffset, y: resultSize.height - actionsHeight), size: CGSize(width: currentActionWidth, height: actionsHeight))
|
||||
|
||||
actionOffset += currentActionWidth
|
||||
transition.updateFrame(node: actionNode, frame: actionNodeFrame)
|
||||
|
||||
nodeIndex += 1
|
||||
}
|
||||
|
||||
if previousLayout == nil {
|
||||
self.inputFieldNode.activateInput()
|
||||
}
|
||||
|
||||
return resultSize
|
||||
}
|
||||
|
||||
@objc func textFieldChanged(_ textField: UITextField) {
|
||||
self.updateState()
|
||||
}
|
||||
|
||||
private func updateState() {
|
||||
var enabled = true
|
||||
if self.isVerifying || self.inputFieldNode.text.isEmpty {
|
||||
enabled = false
|
||||
}
|
||||
self.doneActionNode.actionEnabled = enabled
|
||||
}
|
||||
|
||||
private func verify() {
|
||||
let text = self.inputFieldNode.text
|
||||
guard !text.isEmpty else {
|
||||
|
||||
applyImpl = {
|
||||
let password = inputState.value
|
||||
guard !isVerifying, !password.isEmpty else {
|
||||
return
|
||||
}
|
||||
|
||||
self.isVerifying = true
|
||||
self.disposable.set((self.context.engine.auth.requestTemporaryTwoStepPasswordToken(password: text, period: self.period, requiresBiometrics: self.requiresBiometrics) |> deliverOnMainQueue).start(next: { [weak self] token in
|
||||
if let strongSelf = self {
|
||||
strongSelf.completion(token)
|
||||
}
|
||||
}, error: { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
strongSelf.inputFieldNode.shake()
|
||||
strongSelf.hapticFeedback.error()
|
||||
strongSelf.isVerifying = false
|
||||
strongSelf.updateState()
|
||||
}
|
||||
}))
|
||||
self.updateState()
|
||||
}
|
||||
}
|
||||
|
||||
func botCheckoutPasswordEntryController(context: AccountContext, strings: PresentationStrings, passwordTip: String?, cartTitle: String, period: Int32, requiresBiometrics: Bool, completion: @escaping (TemporaryTwoStepPasswordToken) -> Void) -> AlertController {
|
||||
var dismissImpl: (() -> Void)?
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let controller = AlertController(theme: AlertControllerTheme(presentationData: presentationData), contentNode: BotCheckoutPasswordAlertContentNode(context: context, theme: presentationData.theme, strings: strings, passwordTip: passwordTip, cardTitle: cartTitle, period: period, requiresBiometrics: requiresBiometrics, cancel: {
|
||||
dismissImpl?()
|
||||
}, completion: { token in
|
||||
completion(token)
|
||||
dismissImpl?()
|
||||
}))
|
||||
dismissImpl = { [weak controller] in
|
||||
controller?.dismissAnimated()
|
||||
isVerifying = true
|
||||
doneInProgressPromise.set(true)
|
||||
|
||||
disposable.set((context.engine.auth.requestTemporaryTwoStepPasswordToken(password: password, period: period, requiresBiometrics: requiresBiometrics)
|
||||
|> deliverOnMainQueue).start(next: { token in
|
||||
completion(token)
|
||||
dismissImpl?()
|
||||
}, error: { _ in
|
||||
inputState.animateError()
|
||||
isVerifying = false
|
||||
doneInProgressPromise.set(false)
|
||||
}))
|
||||
}
|
||||
return controller
|
||||
dismissImpl = { [weak alertController] in
|
||||
alertController?.dismiss(completion: nil)
|
||||
}
|
||||
return alertController
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,536 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import ViewControllerComponent
|
||||
import ResizableSheetComponent
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import MultilineTextComponent
|
||||
import ButtonComponent
|
||||
import ListSectionComponent
|
||||
import ListActionItemComponent
|
||||
import GlassBarButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
struct BotCheckoutPaymentWebToken: Equatable {
|
||||
let title: String
|
||||
let data: String
|
||||
var saveOnServer: Bool
|
||||
}
|
||||
|
||||
enum BotCheckoutPaymentMethod: Equatable {
|
||||
case savedCredentials(BotPaymentSavedCredentials)
|
||||
case webToken(BotCheckoutPaymentWebToken)
|
||||
case applePay
|
||||
case other(BotPaymentMethod)
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case let .savedCredentials(credentials):
|
||||
switch credentials {
|
||||
case let .card(_, title):
|
||||
return title
|
||||
}
|
||||
case let .webToken(token):
|
||||
return token.title
|
||||
case .applePay:
|
||||
return "Apple Pay"
|
||||
case let .other(method):
|
||||
return method.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func splitSavedCardTitle(_ title: String) -> (String, String?) {
|
||||
guard let separatorIndex = title.lastIndex(of: "*") else {
|
||||
return (title, nil)
|
||||
}
|
||||
|
||||
let name = String(title[..<separatorIndex]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let suffix = String(title[title.index(after: separatorIndex)...]).trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !name.isEmpty, !suffix.isEmpty else {
|
||||
return (title, nil)
|
||||
}
|
||||
|
||||
return (name, "•••• \(suffix)")
|
||||
}
|
||||
|
||||
private final class BotCheckoutPaymentMethodContentComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let methods: [BotCheckoutPaymentMethod]
|
||||
let selectedMethod: BotCheckoutPaymentMethod?
|
||||
let selectMethod: (BotCheckoutPaymentMethod) -> Void
|
||||
let addCard: () -> Void
|
||||
|
||||
init(
|
||||
methods: [BotCheckoutPaymentMethod],
|
||||
selectedMethod: BotCheckoutPaymentMethod?,
|
||||
selectMethod: @escaping (BotCheckoutPaymentMethod) -> Void,
|
||||
addCard: @escaping () -> Void
|
||||
) {
|
||||
self.methods = methods
|
||||
self.selectedMethod = selectedMethod
|
||||
self.selectMethod = selectMethod
|
||||
self.addCard = addCard
|
||||
}
|
||||
|
||||
static func ==(lhs: BotCheckoutPaymentMethodContentComponent, rhs: BotCheckoutPaymentMethodContentComponent) -> Bool {
|
||||
if lhs.methods != rhs.methods {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedMethod != rhs.selectedMethod {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let section = ComponentView<Empty>()
|
||||
|
||||
private var component: BotCheckoutPaymentMethodContentComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BotCheckoutPaymentMethodContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let theme = environment.theme.withModalBlocksBackground()
|
||||
let itemFontSize: CGFloat = 17.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var contentHeight: CGFloat = 76.0 + 9.0
|
||||
|
||||
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||
for i in 0 ..< component.methods.count {
|
||||
let method = component.methods[i]
|
||||
let isSelected = method == component.selectedMethod
|
||||
|
||||
var title = method.title
|
||||
var icon: ListActionItemComponent.Icon?
|
||||
var accessory: ListActionItemComponent.Accessory?
|
||||
|
||||
switch method {
|
||||
case let .savedCredentials(credentials):
|
||||
switch credentials {
|
||||
case let .card(_, cardTitle):
|
||||
let (cardName, cardSuffix) = splitSavedCardTitle(cardTitle)
|
||||
title = cardName
|
||||
if let cardSuffix {
|
||||
accessory = .custom(ListActionItemComponent.CustomAccessory(
|
||||
component: AnyComponentWithIdentity(
|
||||
id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"),
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: cardSuffix,
|
||||
font: Font.regular(itemFontSize),
|
||||
textColor: theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))
|
||||
),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
|
||||
))
|
||||
}
|
||||
}
|
||||
case .applePay:
|
||||
title = "Apple Pay"
|
||||
icon = ListActionItemComponent.Icon(
|
||||
component: AnyComponentWithIdentity(
|
||||
id: AnyHashable("apple-pay"),
|
||||
component: AnyComponent(BundleIconComponent(
|
||||
name: "Bot Payments/ApplePayLogo",
|
||||
tintColor: nil
|
||||
))
|
||||
)
|
||||
)
|
||||
case let .webToken(token):
|
||||
let (cardName, cardSuffix) = splitSavedCardTitle(token.title)
|
||||
title = cardName
|
||||
if let cardSuffix {
|
||||
accessory = .custom(ListActionItemComponent.CustomAccessory(
|
||||
component: AnyComponentWithIdentity(
|
||||
id: AnyHashable("card-suffix-\(i)-\(cardSuffix)"),
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: cardSuffix,
|
||||
font: Font.regular(itemFontSize),
|
||||
textColor: theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))
|
||||
),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
|
||||
))
|
||||
}
|
||||
case let .other(method):
|
||||
title = method.title
|
||||
}
|
||||
|
||||
items.append(AnyComponentWithIdentity(id: AnyHashable("method-\(i)"), component: AnyComponent(ListActionItemComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
title: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: title,
|
||||
font: Font.regular(itemFontSize),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
|
||||
isSelected: isSelected,
|
||||
toggle: {
|
||||
component.selectMethod(method)
|
||||
}
|
||||
)),
|
||||
icon: icon,
|
||||
accessory: accessory,
|
||||
action: { _ in
|
||||
component.selectMethod(method)
|
||||
}
|
||||
))))
|
||||
}
|
||||
|
||||
items.append(AnyComponentWithIdentity(id: AnyHashable("add-card"), component: AnyComponent(ListActionItemComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
title: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environment.strings.Checkout_PaymentMethod_New,
|
||||
font: Font.regular(itemFontSize),
|
||||
textColor: theme.list.itemAccentColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
contentInsets: UIEdgeInsets(top: 12.0, left: 45.0, bottom: 12.0, right: 0.0),
|
||||
leftIcon: nil,
|
||||
icon: nil,
|
||||
accessory: nil,
|
||||
action: { _ in
|
||||
component.addCard()
|
||||
}
|
||||
))))
|
||||
|
||||
self.section.parentState = state
|
||||
let sectionSize = self.section.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: items
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize)
|
||||
if let sectionView = self.section.view {
|
||||
if sectionView.superview == nil {
|
||||
self.addSubview(sectionView)
|
||||
}
|
||||
transition.setFrame(view: sectionView, frame: sectionFrame)
|
||||
}
|
||||
contentHeight += sectionSize.height
|
||||
contentHeight += 112.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class BotCheckoutPaymentMethodScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let currentMethod: BotCheckoutPaymentMethod?
|
||||
let methods: [BotCheckoutPaymentMethod]
|
||||
let applyValue: (BotCheckoutPaymentMethod) -> Void
|
||||
let newCard: () -> Void
|
||||
let otherMethod: (String, String) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
currentMethod: BotCheckoutPaymentMethod?,
|
||||
methods: [BotCheckoutPaymentMethod],
|
||||
applyValue: @escaping (BotCheckoutPaymentMethod) -> Void,
|
||||
newCard: @escaping () -> Void,
|
||||
otherMethod: @escaping (String, String) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.currentMethod = currentMethod
|
||||
self.methods = methods
|
||||
self.applyValue = applyValue
|
||||
self.newCard = newCard
|
||||
self.otherMethod = otherMethod
|
||||
}
|
||||
|
||||
static func ==(lhs: BotCheckoutPaymentMethodScreenComponent, rhs: BotCheckoutPaymentMethodScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.currentMethod != rhs.currentMethod {
|
||||
return false
|
||||
}
|
||||
if lhs.methods != rhs.methods {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
|
||||
private let animateOut = ActionSlot<Action<Void>>()
|
||||
|
||||
private var component: BotCheckoutPaymentMethodScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var selectedMethod: BotCheckoutPaymentMethod?
|
||||
private var isDismissing = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BotCheckoutPaymentMethodScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
if self.component == nil {
|
||||
if let currentMethod = component.currentMethod, component.methods.contains(currentMethod) {
|
||||
self.selectedMethod = currentMethod
|
||||
} else {
|
||||
self.selectedMethod = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let controller = environmentValue.controller
|
||||
let theme = environmentValue.theme.withModalBlocksBackground()
|
||||
|
||||
let dismiss: (Bool, (() -> Void)?) -> Void = { [weak self] animated, completion in
|
||||
guard let self, !self.isDismissing else {
|
||||
return
|
||||
}
|
||||
self.isDismissing = true
|
||||
|
||||
let performDismiss: () -> Void = {
|
||||
if let controller = controller() as? BotCheckoutPaymentMethodScreen {
|
||||
controller.completePendingDismiss()
|
||||
controller.dismiss(animated: false)
|
||||
}
|
||||
completion?()
|
||||
}
|
||||
|
||||
if animated {
|
||||
self.animateOut.invoke(Action { _ in
|
||||
performDismiss()
|
||||
})
|
||||
} else {
|
||||
performDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
let sheetSize = self.sheet.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
|
||||
content: AnyComponent<ViewControllerComponentContainer.Environment>(BotCheckoutPaymentMethodContentComponent(
|
||||
methods: component.methods,
|
||||
selectedMethod: self.selectedMethod,
|
||||
selectMethod: { [weak self] method in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.selectedMethod = method
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
},
|
||||
addCard: { [weak self] in
|
||||
guard let self, let component = self.component else {
|
||||
return
|
||||
}
|
||||
dismiss(true, {
|
||||
component.newCard()
|
||||
})
|
||||
}
|
||||
)),
|
||||
titleItem: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environmentValue.strings.Checkout_PaymentMethod,
|
||||
font: Font.semibold(17.0),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
leftItem: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
dismiss(true, nil)
|
||||
}
|
||||
)
|
||||
),
|
||||
//TODO:localize
|
||||
bottomItem: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("proceed"),
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: "Proceed",
|
||||
badge: 0,
|
||||
textColor: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: theme.list.itemCheckColors.fillColor
|
||||
))
|
||||
),
|
||||
isEnabled: self.selectedMethod != nil,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let selectedMethod = self.selectedMethod else {
|
||||
return
|
||||
}
|
||||
dismiss(true, {
|
||||
switch selectedMethod {
|
||||
case let .other(method):
|
||||
component.otherMethod(method.url, method.title)
|
||||
default:
|
||||
component.applyValue(selectedMethod)
|
||||
}
|
||||
})
|
||||
}
|
||||
)),
|
||||
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
|
||||
animateOut: self.animateOut
|
||||
)),
|
||||
environment: {
|
||||
environmentValue
|
||||
ResizableSheetComponentEnvironment(
|
||||
theme: theme,
|
||||
statusBarHeight: environmentValue.statusBarHeight,
|
||||
safeInsets: environmentValue.safeInsets,
|
||||
inputHeight: 0.0,
|
||||
metrics: environmentValue.metrics,
|
||||
deviceMetrics: environmentValue.deviceMetrics,
|
||||
isDisplaying: environmentValue.isVisible,
|
||||
isCentered: environmentValue.metrics.widthClass == .regular,
|
||||
screenSize: availableSize,
|
||||
regularMetricsSize: nil,
|
||||
dismiss: { animated in
|
||||
dismiss(animated, nil)
|
||||
}
|
||||
)
|
||||
},
|
||||
forceUpdate: true,
|
||||
containerSize: availableSize
|
||||
)
|
||||
self.sheet.parentState = state
|
||||
if let sheetView = self.sheet.view {
|
||||
if sheetView.superview == nil {
|
||||
self.addSubview(sheetView)
|
||||
}
|
||||
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class BotCheckoutPaymentMethodScreen: ViewControllerComponentContainer {
|
||||
private var isDismissed = false
|
||||
private var dismissCompletion: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
|
||||
super.init(
|
||||
context: context,
|
||||
component: BotCheckoutPaymentMethodScreenComponent(
|
||||
context: context,
|
||||
currentMethod: currentMethod,
|
||||
methods: methods,
|
||||
applyValue: applyValue,
|
||||
newCard: newCard,
|
||||
otherMethod: otherMethod
|
||||
),
|
||||
navigationBarAppearance: .none
|
||||
)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate func completePendingDismiss() {
|
||||
let dismissCompletion = self.dismissCompletion
|
||||
self.dismissCompletion = nil
|
||||
dismissCompletion?()
|
||||
}
|
||||
|
||||
func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
|
||||
override func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
self.dismissCompletion = completion
|
||||
|
||||
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
} else {
|
||||
self.completePendingDismiss()
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import AppBundle
|
||||
|
||||
struct BotCheckoutPaymentWebToken: Equatable {
|
||||
let title: String
|
||||
let data: String
|
||||
var saveOnServer: Bool
|
||||
}
|
||||
|
||||
enum BotCheckoutPaymentMethod: Equatable {
|
||||
case savedCredentials(BotPaymentSavedCredentials)
|
||||
case webToken(BotCheckoutPaymentWebToken)
|
||||
case applePay
|
||||
case other(BotPaymentMethod)
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case let .savedCredentials(credentials):
|
||||
switch credentials {
|
||||
case let .card(_, title):
|
||||
return title
|
||||
}
|
||||
case let .webToken(token):
|
||||
return token.title
|
||||
case .applePay:
|
||||
return "Apple Pay"
|
||||
case let .other(method):
|
||||
return method.title
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class BotCheckoutPaymentMethodSheetController: ActionSheetController {
|
||||
private var presentationDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, currentMethod: BotCheckoutPaymentMethod?, methods: [BotCheckoutPaymentMethod], applyValue: @escaping (BotCheckoutPaymentMethod) -> Void, newCard: @escaping () -> Void, otherMethod: @escaping (String, String) -> Void) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
|
||||
|
||||
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
|
||||
}
|
||||
}).strict()
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetTextItem(title: strings.Checkout_PaymentMethod))
|
||||
|
||||
for method in methods {
|
||||
let title: String
|
||||
let icon: UIImage?
|
||||
switch method {
|
||||
case let .savedCredentials(credentials):
|
||||
switch credentials {
|
||||
case let .card(_, cardTitle):
|
||||
title = cardTitle
|
||||
icon = nil
|
||||
}
|
||||
case let .webToken(token):
|
||||
title = token.title
|
||||
icon = nil
|
||||
case .applePay:
|
||||
title = "Apple Pay"
|
||||
icon = UIImage(bundleImageName: "Bot Payments/ApplePayLogo")?.precomposed()
|
||||
case let .other(method):
|
||||
title = method.title
|
||||
icon = nil
|
||||
}
|
||||
let value: Bool?
|
||||
if let currentMethod = currentMethod {
|
||||
value = method == currentMethod
|
||||
} else {
|
||||
value = nil
|
||||
}
|
||||
items.append(BotCheckoutPaymentMethodItem(title: title, icon: icon, value: value, action: { [weak self] _ in
|
||||
if case let .other(method) = method {
|
||||
otherMethod(method.url, method.title)
|
||||
} else {
|
||||
applyValue(method)
|
||||
}
|
||||
self?.dismissAnimated()
|
||||
}))
|
||||
}
|
||||
|
||||
items.append(ActionSheetButtonItem(title: strings.Checkout_PaymentMethod_New, action: { [weak self] in
|
||||
self?.dismissAnimated()
|
||||
newCard()
|
||||
}))
|
||||
|
||||
self.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
|
||||
self?.dismissAnimated()
|
||||
}),
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
public class BotCheckoutPaymentMethodItem: ActionSheetItem {
|
||||
public let title: String
|
||||
public let icon: UIImage?
|
||||
public let value: Bool?
|
||||
public let action: (Bool) -> Void
|
||||
|
||||
public init(title: String, icon: UIImage?, value: Bool?, action: @escaping (Bool) -> Void) {
|
||||
self.title = title
|
||||
self.icon = icon
|
||||
self.value = value
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
let node = BotCheckoutPaymentMethodItemNode(theme: theme)
|
||||
node.setItem(self)
|
||||
return node
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ActionSheetItemNode) {
|
||||
guard let node = node as? BotCheckoutPaymentMethodItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
node.setItem(self)
|
||||
node.requestLayoutUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
public class BotCheckoutPaymentMethodItemNode: ActionSheetItemNode {
|
||||
private let defaultFont: UIFont
|
||||
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
private var item: BotCheckoutPaymentMethodItem?
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
private let titleNode: ASTextNode
|
||||
private let iconNode: ASImageNode
|
||||
private let checkNode: ASImageNode
|
||||
|
||||
public override init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
|
||||
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
self.iconNode.displaysAsynchronously = false
|
||||
self.iconNode.displayWithoutProcessing = true
|
||||
|
||||
self.checkNode = ASImageNode()
|
||||
self.checkNode.isUserInteractionEnabled = false
|
||||
self.checkNode.displayWithoutProcessing = true
|
||||
self.checkNode.displaysAsynchronously = false
|
||||
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(theme.controlAccentColor.cgColor)
|
||||
context.setLineWidth(2.0)
|
||||
context.move(to: CGPoint(x: 12.0, y: 1.0))
|
||||
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
|
||||
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
|
||||
context.strokePath()
|
||||
})
|
||||
|
||||
super.init(theme: theme)
|
||||
|
||||
self.view.addSubview(self.button)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.iconNode)
|
||||
self.addSubnode(self.checkNode)
|
||||
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backgroundNode.backgroundColor = theme.itemHighlightedBackgroundColor
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
strongSelf.backgroundNode.backgroundColor = theme.itemBackgroundColor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setItem(_ item: BotCheckoutPaymentMethodItem) {
|
||||
self.item = item
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor)
|
||||
self.iconNode.image = item.icon
|
||||
if let value = item.value {
|
||||
self.checkNode.isHidden = !value
|
||||
} else {
|
||||
self.checkNode.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let size = CGSize(width: constrainedSize.width, height: 57.0)
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
var checkInset: CGFloat = 15.0
|
||||
if let _ = self.item?.value {
|
||||
checkInset = 44.0
|
||||
}
|
||||
|
||||
let iconSize: CGSize
|
||||
if let image = self.iconNode.image {
|
||||
iconSize = image.size
|
||||
} else {
|
||||
iconSize = CGSize()
|
||||
}
|
||||
let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - iconSize.width - 15.0 - 8.0, height: size.height))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.iconNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - iconSize.width, y: floorToScreenPixels((size.height - iconSize.height) / 2.0)), size: iconSize)
|
||||
|
||||
if let image = self.checkNode.image {
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
self.updateInternalLayout(size, constrainedSize: constrainedSize)
|
||||
return size
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
if let item = self.item {
|
||||
let updatedValue: Bool
|
||||
if let value = item.value {
|
||||
updatedValue = !value
|
||||
} else {
|
||||
updatedValue = true
|
||||
}
|
||||
item.action(updatedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,224 +0,0 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import AsyncDisplayKit
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
|
||||
final class BotCheckoutPaymentShippingOptionSheetController: ActionSheetController {
|
||||
private var presentationDisposable: Disposable?
|
||||
|
||||
init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) {
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let strings = presentationData.strings
|
||||
|
||||
super.init(theme: ActionSheetControllerTheme(presentationData: presentationData))
|
||||
|
||||
self.presentationDisposable = context.sharedContext.presentationData.start(next: { [weak self] presentationData in
|
||||
if let strongSelf = self {
|
||||
strongSelf.theme = ActionSheetControllerTheme(presentationData: presentationData)
|
||||
}
|
||||
}).strict()
|
||||
|
||||
var items: [ActionSheetItem] = []
|
||||
|
||||
items.append(ActionSheetTextItem(title: strings.Checkout_ShippingMethod))
|
||||
|
||||
let dismissAction: () -> Void = { [weak self] in
|
||||
self?.dismissAnimated()
|
||||
}
|
||||
|
||||
let toggleCheck: (String, Int) -> Void = { [weak self] id, itemIndex in
|
||||
for i in 0 ..< options.count {
|
||||
self?.updateItem(groupIndex: 0, itemIndex: i + 1, { item in
|
||||
if let item = item as? BotCheckoutPaymentShippingOptionItem, let value = item.value {
|
||||
return BotCheckoutPaymentShippingOptionItem(title: item.title, label: item.label, value: i == itemIndex ? !value : false, action: item.action)
|
||||
}
|
||||
return item
|
||||
})
|
||||
}
|
||||
applyValue(id)
|
||||
dismissAction()
|
||||
}
|
||||
|
||||
var itemIndex = 0
|
||||
for option in options {
|
||||
let index = itemIndex
|
||||
var totalPrice: Int64 = 0
|
||||
for price in option.prices {
|
||||
totalPrice += price.amount
|
||||
}
|
||||
let value: Bool?
|
||||
if let currentId = currentId {
|
||||
value = option.id == currentId
|
||||
} else {
|
||||
value = nil
|
||||
}
|
||||
items.append(BotCheckoutPaymentShippingOptionItem(title: option.title, label: formatCurrencyAmount(totalPrice, currency: currency), value: value, action: { value in
|
||||
toggleCheck(option.id, index)
|
||||
}))
|
||||
itemIndex += 1
|
||||
}
|
||||
|
||||
self.setItemGroups([
|
||||
ActionSheetItemGroup(items: items),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strings.Common_Cancel, action: { [weak self] in
|
||||
self?.dismissAnimated()
|
||||
}),
|
||||
])
|
||||
])
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.presentationDisposable?.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
public class BotCheckoutPaymentShippingOptionItem: ActionSheetItem {
|
||||
public let title: String
|
||||
public let label: String
|
||||
public let value: Bool?
|
||||
public let action: (Bool) -> Void
|
||||
|
||||
public init(title: String, label: String, value: Bool?, action: @escaping (Bool) -> Void) {
|
||||
self.title = title
|
||||
self.label = label
|
||||
self.value = value
|
||||
self.action = action
|
||||
}
|
||||
|
||||
public func node(theme: ActionSheetControllerTheme) -> ActionSheetItemNode {
|
||||
let node = BotCheckoutPaymentShippingOptionItemNode(theme: theme)
|
||||
node.setItem(self)
|
||||
return node
|
||||
}
|
||||
|
||||
public func updateNode(_ node: ActionSheetItemNode) {
|
||||
guard let node = node as? BotCheckoutPaymentShippingOptionItemNode else {
|
||||
assertionFailure()
|
||||
return
|
||||
}
|
||||
|
||||
node.setItem(self)
|
||||
node.requestLayoutUpdate()
|
||||
}
|
||||
}
|
||||
|
||||
public class BotCheckoutPaymentShippingOptionItemNode: ActionSheetItemNode {
|
||||
private let defaultFont: UIFont
|
||||
|
||||
private let theme: ActionSheetControllerTheme
|
||||
|
||||
private var item: BotCheckoutPaymentShippingOptionItem?
|
||||
|
||||
private let button: HighlightTrackingButton
|
||||
private let titleNode: ASTextNode
|
||||
private let labelNode: ASTextNode
|
||||
private let checkNode: ASImageNode
|
||||
|
||||
public override init(theme: ActionSheetControllerTheme) {
|
||||
self.theme = theme
|
||||
self.defaultFont = Font.regular(floor(theme.baseFontSize * 20.0 / 17.0))
|
||||
|
||||
self.button = HighlightTrackingButton()
|
||||
|
||||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.labelNode = ASTextNode()
|
||||
self.labelNode.maximumNumberOfLines = 1
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
|
||||
self.checkNode = ASImageNode()
|
||||
self.checkNode.isUserInteractionEnabled = false
|
||||
self.checkNode.displayWithoutProcessing = true
|
||||
self.checkNode.displaysAsynchronously = false
|
||||
self.checkNode.image = generateImage(CGSize(width: 14.0, height: 11.0), rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: CGPoint(), size: size))
|
||||
context.setStrokeColor(theme.controlAccentColor.cgColor)
|
||||
context.setLineWidth(2.0)
|
||||
context.move(to: CGPoint(x: 12.0, y: 1.0))
|
||||
context.addLine(to: CGPoint(x: 4.16482734, y: 9.0))
|
||||
context.addLine(to: CGPoint(x: 1.0, y: 5.81145833))
|
||||
context.strokePath()
|
||||
})
|
||||
|
||||
super.init(theme: theme)
|
||||
|
||||
self.view.addSubview(self.button)
|
||||
self.addSubnode(self.titleNode)
|
||||
self.addSubnode(self.labelNode)
|
||||
self.addSubnode(self.checkNode)
|
||||
|
||||
self.button.highligthedChanged = { [weak self] highlighted in
|
||||
if let strongSelf = self {
|
||||
if highlighted {
|
||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemHighlightedBackgroundColor
|
||||
} else {
|
||||
UIView.animate(withDuration: 0.3, animations: {
|
||||
strongSelf.backgroundNode.backgroundColor = strongSelf.theme.itemBackgroundColor
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.button.addTarget(self, action: #selector(self.buttonPressed), for: .touchUpInside)
|
||||
}
|
||||
|
||||
func setItem(_ item: BotCheckoutPaymentShippingOptionItem) {
|
||||
self.item = item
|
||||
|
||||
self.titleNode.attributedText = NSAttributedString(string: item.title, font: self.defaultFont, textColor: self.theme.primaryTextColor)
|
||||
self.labelNode.attributedText = NSAttributedString(string: item.label, font: self.defaultFont, textColor: self.theme.primaryTextColor)
|
||||
if let value = item.value {
|
||||
self.checkNode.isHidden = !value
|
||||
} else {
|
||||
self.checkNode.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
public override func updateLayout(constrainedSize: CGSize, transition: ContainedViewLayoutTransition) -> CGSize {
|
||||
let size = CGSize(width: constrainedSize.width, height: 57.0)
|
||||
|
||||
self.button.frame = CGRect(origin: CGPoint(), size: size)
|
||||
|
||||
var checkInset: CGFloat = 15.0
|
||||
if let _ = self.item?.value {
|
||||
checkInset = 44.0
|
||||
}
|
||||
|
||||
let labelSize = self.labelNode.measure(CGSize(width: size.width - 44.0 - 15.0 - 8.0, height: size.height))
|
||||
let titleSize = self.titleNode.measure(CGSize(width: size.width - 44.0 - labelSize.width - 15.0 - 8.0, height: size.height))
|
||||
self.titleNode.frame = CGRect(origin: CGPoint(x: checkInset, y: floorToScreenPixels((size.height - titleSize.height) / 2.0)), size: titleSize)
|
||||
self.labelNode.frame = CGRect(origin: CGPoint(x: size.width - 15.0 - labelSize.width, y: floorToScreenPixels((size.height - labelSize.height) / 2.0)), size: labelSize)
|
||||
|
||||
if let image = self.checkNode.image {
|
||||
self.checkNode.frame = CGRect(origin: CGPoint(x: floor((44.0 - image.size.width) / 2.0), y: floor((size.height - image.size.height) / 2.0)), size: image.size)
|
||||
}
|
||||
|
||||
self.updateInternalLayout(size, constrainedSize: constrainedSize)
|
||||
return size
|
||||
}
|
||||
|
||||
@objc func buttonPressed() {
|
||||
if let item = self.item {
|
||||
let updatedValue: Bool
|
||||
if let value = item.value {
|
||||
updatedValue = !value
|
||||
} else {
|
||||
updatedValue = true
|
||||
}
|
||||
item.action(updatedValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,418 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import SwiftSignalKit
|
||||
import TelegramCore
|
||||
import AccountContext
|
||||
import TelegramStringFormatting
|
||||
import ViewControllerComponent
|
||||
import ResizableSheetComponent
|
||||
import TelegramPresentationData
|
||||
import PresentationDataUtils
|
||||
import MultilineTextComponent
|
||||
import ButtonComponent
|
||||
import ListSectionComponent
|
||||
import ListActionItemComponent
|
||||
import GlassBarButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
private final class BotCheckoutShippingOptionContentComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let currency: String
|
||||
let options: [BotPaymentShippingOption]
|
||||
let selectedId: String?
|
||||
let selectOption: (String) -> Void
|
||||
|
||||
init(
|
||||
currency: String,
|
||||
options: [BotPaymentShippingOption],
|
||||
selectedId: String?,
|
||||
selectOption: @escaping (String) -> Void
|
||||
) {
|
||||
self.currency = currency
|
||||
self.options = options
|
||||
self.selectedId = selectedId
|
||||
self.selectOption = selectOption
|
||||
}
|
||||
|
||||
static func ==(lhs: BotCheckoutShippingOptionContentComponent, rhs: BotCheckoutShippingOptionContentComponent) -> Bool {
|
||||
if lhs.currency != rhs.currency {
|
||||
return false
|
||||
}
|
||||
if lhs.options != rhs.options {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedId != rhs.selectedId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let section = ComponentView<Empty>()
|
||||
|
||||
private var component: BotCheckoutShippingOptionContentComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BotCheckoutShippingOptionContentComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environment = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let theme = environment.theme.withModalBlocksBackground()
|
||||
let itemFontSize: CGFloat = 17.0
|
||||
let sideInset: CGFloat = 16.0
|
||||
|
||||
var contentHeight: CGFloat = 76.0 + 9.0
|
||||
|
||||
var items: [AnyComponentWithIdentity<Empty>] = []
|
||||
for option in component.options {
|
||||
let totalPrice = option.prices.reduce(Int64(0)) { current, price in
|
||||
return current + price.amount
|
||||
}
|
||||
let priceText = formatCurrencyAmount(totalPrice, currency: component.currency)
|
||||
let isSelected = option.id == component.selectedId
|
||||
|
||||
items.append(AnyComponentWithIdentity(id: option.id, component: AnyComponent(ListActionItemComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
title: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: option.title,
|
||||
font: Font.regular(itemFontSize),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
leftIcon: .check(ListActionItemComponent.LeftIcon.Check(
|
||||
isSelected: isSelected,
|
||||
toggle: {
|
||||
component.selectOption(option.id)
|
||||
}
|
||||
)),
|
||||
icon: nil,
|
||||
accessory: .custom(ListActionItemComponent.CustomAccessory(
|
||||
component: AnyComponentWithIdentity(
|
||||
id: AnyHashable("price-\(option.id)"),
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: priceText,
|
||||
font: Font.regular(itemFontSize),
|
||||
textColor: theme.list.itemSecondaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
))
|
||||
),
|
||||
insets: UIEdgeInsets(top: 0.0, left: 8.0, bottom: 0.0, right: 16.0)
|
||||
)),
|
||||
action: { _ in
|
||||
component.selectOption(option.id)
|
||||
}
|
||||
))))
|
||||
}
|
||||
|
||||
self.section.parentState = state
|
||||
let sectionSize = self.section.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ListSectionComponent(
|
||||
theme: theme,
|
||||
style: .glass,
|
||||
header: nil,
|
||||
footer: nil,
|
||||
items: items
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width - sideInset * 2.0, height: 10000.0)
|
||||
)
|
||||
let sectionFrame = CGRect(origin: CGPoint(x: sideInset, y: contentHeight), size: sectionSize)
|
||||
if let sectionView = self.section.view {
|
||||
if sectionView.superview == nil {
|
||||
self.addSubview(sectionView)
|
||||
}
|
||||
transition.setFrame(view: sectionView, frame: sectionFrame)
|
||||
}
|
||||
contentHeight += sectionSize.height
|
||||
contentHeight += 112.0
|
||||
|
||||
return CGSize(width: availableSize.width, height: contentHeight)
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
private final class BotCheckoutShippingOptionScreenComponent: Component {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
let context: AccountContext
|
||||
let currency: String
|
||||
let options: [BotPaymentShippingOption]
|
||||
let currentId: String?
|
||||
let applyValue: (String) -> Void
|
||||
|
||||
init(
|
||||
context: AccountContext,
|
||||
currency: String,
|
||||
options: [BotPaymentShippingOption],
|
||||
currentId: String?,
|
||||
applyValue: @escaping (String) -> Void
|
||||
) {
|
||||
self.context = context
|
||||
self.currency = currency
|
||||
self.options = options
|
||||
self.currentId = currentId
|
||||
self.applyValue = applyValue
|
||||
}
|
||||
|
||||
static func ==(lhs: BotCheckoutShippingOptionScreenComponent, rhs: BotCheckoutShippingOptionScreenComponent) -> Bool {
|
||||
if lhs.context !== rhs.context {
|
||||
return false
|
||||
}
|
||||
if lhs.currency != rhs.currency {
|
||||
return false
|
||||
}
|
||||
if lhs.options != rhs.options {
|
||||
return false
|
||||
}
|
||||
if lhs.currentId != rhs.currentId {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView {
|
||||
private let sheet = ComponentView<(ViewControllerComponentContainer.Environment, ResizableSheetComponentEnvironment)>()
|
||||
private let animateOut = ActionSlot<Action<Void>>()
|
||||
|
||||
private var component: BotCheckoutShippingOptionScreenComponent?
|
||||
private weak var state: EmptyComponentState?
|
||||
private var selectedId: String?
|
||||
private var isDismissing = false
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func update(component: BotCheckoutShippingOptionScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
if self.component == nil {
|
||||
if let currentId = component.currentId, component.options.contains(where: { $0.id == currentId }) {
|
||||
self.selectedId = currentId
|
||||
} else {
|
||||
self.selectedId = nil
|
||||
}
|
||||
}
|
||||
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let environmentValue = environment[ViewControllerComponentContainer.Environment.self].value
|
||||
let controller = environmentValue.controller
|
||||
let theme = environmentValue.theme.withModalBlocksBackground()
|
||||
|
||||
let dismiss: (Bool) -> Void = { [weak self] animated in
|
||||
guard let self, !self.isDismissing else {
|
||||
return
|
||||
}
|
||||
self.isDismissing = true
|
||||
|
||||
let performDismiss: () -> Void = {
|
||||
if let controller = controller() as? BotCheckoutShippingOptionScreen {
|
||||
controller.completePendingDismiss()
|
||||
controller.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
if animated {
|
||||
self.animateOut.invoke(Action { _ in
|
||||
performDismiss()
|
||||
})
|
||||
} else {
|
||||
performDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
let sheetSize = self.sheet.update(
|
||||
transition: transition,
|
||||
component: AnyComponent(ResizableSheetComponent<ViewControllerComponentContainer.Environment>(
|
||||
content: AnyComponent<ViewControllerComponentContainer.Environment>(BotCheckoutShippingOptionContentComponent(
|
||||
currency: component.currency,
|
||||
options: component.options,
|
||||
selectedId: self.selectedId,
|
||||
selectOption: { [weak self] id in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.selectedId = id
|
||||
self.state?.updated(transition: .spring(duration: 0.35))
|
||||
}
|
||||
)),
|
||||
titleItem: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(
|
||||
string: environmentValue.strings.Checkout_ShippingMethod,
|
||||
font: Font.semibold(17.0),
|
||||
textColor: theme.list.itemPrimaryTextColor
|
||||
)),
|
||||
maximumNumberOfLines: 1
|
||||
)),
|
||||
leftItem: AnyComponent(
|
||||
GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: theme.overallDarkAppearance,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(id: "close", component: AnyComponent(
|
||||
BundleIconComponent(
|
||||
name: "Navigation/Close",
|
||||
tintColor: theme.chat.inputPanel.panelControlColor
|
||||
)
|
||||
)),
|
||||
action: { _ in
|
||||
dismiss(true)
|
||||
}
|
||||
)
|
||||
),
|
||||
//TODO:localize
|
||||
bottomItem: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: theme.list.itemCheckColors.fillColor,
|
||||
foreground: theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("proceed"),
|
||||
component: AnyComponent(ButtonTextContentComponent(
|
||||
text: "Proceed",
|
||||
badge: 0,
|
||||
textColor: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeBackground: theme.list.itemCheckColors.foregroundColor,
|
||||
badgeForeground: theme.list.itemCheckColors.fillColor
|
||||
))
|
||||
),
|
||||
isEnabled: self.selectedId != nil,
|
||||
displaysProgress: false,
|
||||
action: { [weak self] in
|
||||
guard let self, let component = self.component, let selectedId = self.selectedId else {
|
||||
return
|
||||
}
|
||||
component.applyValue(selectedId)
|
||||
dismiss(true)
|
||||
}
|
||||
)),
|
||||
backgroundColor: .color(theme.list.modalBlocksBackgroundColor),
|
||||
animateOut: self.animateOut
|
||||
)),
|
||||
environment: {
|
||||
environmentValue
|
||||
ResizableSheetComponentEnvironment(
|
||||
theme: theme,
|
||||
statusBarHeight: environmentValue.statusBarHeight,
|
||||
safeInsets: environmentValue.safeInsets,
|
||||
inputHeight: 0.0,
|
||||
metrics: environmentValue.metrics,
|
||||
deviceMetrics: environmentValue.deviceMetrics,
|
||||
isDisplaying: environmentValue.isVisible,
|
||||
isCentered: environmentValue.metrics.widthClass == .regular,
|
||||
screenSize: availableSize,
|
||||
regularMetricsSize: nil,
|
||||
dismiss: { animated in
|
||||
dismiss(animated)
|
||||
}
|
||||
)
|
||||
},
|
||||
forceUpdate: true,
|
||||
containerSize: availableSize
|
||||
)
|
||||
self.sheet.parentState = state
|
||||
if let sheetView = self.sheet.view {
|
||||
if sheetView.superview == nil {
|
||||
self.addSubview(sheetView)
|
||||
}
|
||||
transition.setFrame(view: sheetView, frame: CGRect(origin: .zero, size: sheetSize))
|
||||
}
|
||||
|
||||
return availableSize
|
||||
}
|
||||
}
|
||||
|
||||
func makeView() -> View {
|
||||
return View(frame: CGRect())
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<ViewControllerComponentContainer.Environment>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
final class BotCheckoutShippingOptionScreen: ViewControllerComponentContainer {
|
||||
private var isDismissed = false
|
||||
private var dismissCompletion: (() -> Void)?
|
||||
|
||||
init(context: AccountContext, currency: String, options: [BotPaymentShippingOption], currentId: String?, applyValue: @escaping (String) -> Void) {
|
||||
super.init(
|
||||
context: context,
|
||||
component: BotCheckoutShippingOptionScreenComponent(
|
||||
context: context,
|
||||
currency: currency,
|
||||
options: options,
|
||||
currentId: currentId,
|
||||
applyValue: applyValue
|
||||
),
|
||||
navigationBarAppearance: .none
|
||||
)
|
||||
|
||||
self.statusBar.statusBarStyle = .Ignore
|
||||
self.navigationPresentation = .flatModal
|
||||
self.blocksBackgroundWhenInOverlay = true
|
||||
}
|
||||
|
||||
required init(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
fileprivate func completePendingDismiss() {
|
||||
let dismissCompletion = self.dismissCompletion
|
||||
self.dismissCompletion = nil
|
||||
dismissCompletion?()
|
||||
}
|
||||
|
||||
func dismissAnimated() {
|
||||
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
}
|
||||
}
|
||||
|
||||
override func dismiss(completion: (() -> Void)? = nil) {
|
||||
if !self.isDismissed {
|
||||
self.isDismissed = true
|
||||
self.dismissCompletion = completion
|
||||
|
||||
if let view = self.node.hostView.findTaggedView(tag: ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View.Tag()) as? ResizableSheetComponent<ViewControllerComponentContainer.Environment>.View {
|
||||
view.dismissAnimated()
|
||||
} else {
|
||||
self.completePendingDismiss()
|
||||
self.dismiss(animated: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -65,7 +65,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
}, tapMessage: nil, clickThroughMessage: { _, _ in
|
||||
}, toggleMessagesSelection: { _, _ in
|
||||
}, sendCurrentMessage: { _, _ in
|
||||
}, sendMessage: { _ in
|
||||
}, sendMessage: { _, _ in
|
||||
}, sendSticker: { _, _, _, _, _, _, _, _, _ in
|
||||
return false
|
||||
}, sendEmoji: { _, _, _ in
|
||||
|
|
@ -83,8 +83,8 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
controller.dismiss()
|
||||
}
|
||||
}, openExternalInstantPage: { _ in
|
||||
}, shareCurrentLocation: {
|
||||
}, shareAccountContact: {
|
||||
}, shareCurrentLocation: { _ in
|
||||
}, shareAccountContact: { _ in
|
||||
}, sendBotCommand: { _, _ in
|
||||
}, openInstantPage: { message, _ in
|
||||
if let openMessageImpl = openMessageImpl {
|
||||
|
|
@ -134,7 +134,7 @@ public final class BrowserBookmarksScreen: ViewController {
|
|||
}, displaySwipeToReplyHint: {
|
||||
}, dismissReplyMarkupMessage: { _ in
|
||||
}, openMessagePollResults: { _, _ in
|
||||
}, openPollCreation: { _ in
|
||||
}, openPollCreation: { _, _ in
|
||||
}, openPollMedia: { _, _ in
|
||||
}, displayPollSolution: { _, _ in
|
||||
}, displayPsa: { _, _ in
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.delegate = self
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
if #available(iOSApplicationExtension 11.0, iOS 11.0, *) {
|
||||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
|
|
@ -1532,12 +1533,12 @@ final class BrowserInstantPageContent: UIView, BrowserContent, UIScrollViewDeleg
|
|||
}
|
||||
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in
|
||||
let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: baseUrl), openUrl: { [weak self] url in
|
||||
if let self {
|
||||
self.context.sharedContext.openExternalUrl(context: self.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
}
|
||||
})
|
||||
self.present(actionSheet, nil)
|
||||
self.push(actionSheet)
|
||||
}
|
||||
|
||||
private func openMedia(_ media: InstantPageMedia) {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,40 @@ public class Readability: NSObject, WKNavigationDelegate {
|
|||
|
||||
if let (html, subresources) = extractHtmlString(from: archiveData) {
|
||||
self.subresources = subresources
|
||||
self.webView.loadHTMLString(html, baseURL: url.baseURL)
|
||||
self.sanitizeHtmlString(html) { [weak self] html in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.webView.loadHTMLString(html, baseURL: url.baseURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func sanitizeHtmlString(_ html: String, completion: @escaping (String) -> Void) {
|
||||
guard let readerModeJS = loadFile(name: "ReaderMode", type: "js") else {
|
||||
completion(htmlByRemovingScriptTags(html))
|
||||
return
|
||||
}
|
||||
|
||||
let domPurifyJS = extractDOMPurifyScript(from: readerModeJS) ?? readerModeJS
|
||||
self.webView.evaluateJavaScript(domPurifyJS) { [weak self] _, error in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
|
||||
guard error == nil, let htmlLiteral = javascriptStringLiteral(html) else {
|
||||
completion(htmlByRemovingScriptTags(html))
|
||||
return
|
||||
}
|
||||
|
||||
let sanitizeJS = """
|
||||
(function(html) {
|
||||
return DOMPurify.sanitize(html, {WHOLE_DOCUMENT: true, ADD_TAGS: ["iframe"]});
|
||||
})(\(htmlLiteral));
|
||||
"""
|
||||
self.webView.evaluateJavaScript(sanitizeJS) { result, _ in
|
||||
completion((result as? String) ?? htmlByRemovingScriptTags(html))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -49,6 +82,7 @@ public class Readability: NSObject, WKNavigationDelegate {
|
|||
return
|
||||
}
|
||||
guard let page = parseJson(result, url: self.url.absoluteString) else {
|
||||
completion(nil, error)
|
||||
return
|
||||
}
|
||||
completion(page, nil)
|
||||
|
|
@ -95,6 +129,32 @@ func loadFile(name: String, type: String) -> String? {
|
|||
return userScript
|
||||
}
|
||||
|
||||
private func extractDOMPurifyScript(from readerModeJS: String) -> String? {
|
||||
guard let range = readerModeJS.range(of: "\n\n(function () {") else {
|
||||
return nil
|
||||
}
|
||||
return String(readerModeJS[..<range.lowerBound])
|
||||
}
|
||||
|
||||
private func javascriptStringLiteral(_ input: String) -> String? {
|
||||
guard let data = try? JSONSerialization.data(withJSONObject: [input], options: []),
|
||||
var arrayString = String(data: data, encoding: .utf8),
|
||||
arrayString.count >= 2 else {
|
||||
return nil
|
||||
}
|
||||
arrayString.removeFirst()
|
||||
arrayString.removeLast()
|
||||
return arrayString
|
||||
}
|
||||
|
||||
private func htmlByRemovingScriptTags(_ input: String) -> String {
|
||||
guard let regex = try? NSRegularExpression(pattern: "<script\\b[^>]*>[\\s\\S]*?</script\\s*>", options: [.caseInsensitive]) else {
|
||||
return input
|
||||
}
|
||||
let range = NSRange(input.startIndex ..< input.endIndex, in: input)
|
||||
return regex.stringByReplacingMatches(in: input, options: [], range: range, withTemplate: "")
|
||||
}
|
||||
|
||||
private func extractHtmlString(from webArchiveData: Data) -> (String, [Any]?)? {
|
||||
if let webArchiveDict = try? PropertyListSerialization.propertyList(from: webArchiveData, format: nil) as? [String: Any],
|
||||
let mainResource = webArchiveDict["WebMainResource"] as? [String: Any],
|
||||
|
|
@ -708,30 +768,38 @@ private func parseVideo(_ input: [String: Any], _ media: inout [EngineMedia.Id:
|
|||
)
|
||||
}
|
||||
|
||||
private func firstElement(withTag tag: String, in input: [Any], skippingSubtreesWithTag skippedTag: String? = nil) -> [String: Any]? {
|
||||
for item in input {
|
||||
guard let item = item as? [String: Any] else {
|
||||
continue
|
||||
}
|
||||
let itemTag = item["tag"] as? String
|
||||
if itemTag == tag {
|
||||
return item
|
||||
}
|
||||
if itemTag == skippedTag {
|
||||
continue
|
||||
}
|
||||
if let content = item["content"] as? [Any], let result = firstElement(withTag: tag, in: content, skippingSubtreesWithTag: skippedTag) {
|
||||
return result
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func parseFigure(_ input: [String: Any], _ media: inout [EngineMedia.Id: EngineRawMedia]) -> InstantPageBlock? {
|
||||
guard let content = input["content"] as? [Any] else {
|
||||
return nil
|
||||
}
|
||||
var block: InstantPageBlock?
|
||||
var caption: RichText?
|
||||
for item in content {
|
||||
if let item = item as? [String: Any], let tag = item["tag"] as? String {
|
||||
if tag == "p", let content = item["content"] as? [Any] {
|
||||
for item in content {
|
||||
if let item = item as? [String: Any], let tag = item["tag"] as? String {
|
||||
if tag == "iframe" {
|
||||
block = parseVideo(item, &media)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if tag == "iframe" {
|
||||
block = parseVideo(item, &media)
|
||||
} else if tag == "img" {
|
||||
block = parseImage(item, &media)
|
||||
} else if tag == "figcaption" {
|
||||
caption = trim(parseRichText(item, &media))
|
||||
}
|
||||
}
|
||||
if let iframe = firstElement(withTag: "iframe", in: content, skippingSubtreesWithTag: "figcaption") {
|
||||
block = parseVideo(iframe, &media)
|
||||
} else if let image = firstElement(withTag: "img", in: content, skippingSubtreesWithTag: "figcaption") {
|
||||
block = parseImage(image, &media)
|
||||
}
|
||||
if let figcaption = firstElement(withTag: "figcaption", in: content) {
|
||||
caption = trim(parseRichText(figcaption, &media))
|
||||
}
|
||||
guard var block else {
|
||||
return nil
|
||||
|
|
|
|||
|
|
@ -1868,7 +1868,7 @@ public final class CalendarMessageScreen: ViewController {
|
|||
self._hasGlassStyle = true
|
||||
self.navigationPresentation = .modal
|
||||
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___close", style: .plain, target: self, action: #selector(dismissPressed)), animated: false)
|
||||
self.navigationItem.setTitle(self.presentationData.strings.MessageCalendar_Title, animated: false)
|
||||
|
||||
if self.enableMessageRangeDeletion {
|
||||
|
|
@ -1894,7 +1894,7 @@ public final class CalendarMessageScreen: ViewController {
|
|||
self.node.toggleSelectionMode()
|
||||
|
||||
if self.node.selectionState != nil {
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
|
||||
} else {
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Select, style: .plain, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
|
||||
}
|
||||
|
|
@ -1904,7 +1904,7 @@ public final class CalendarMessageScreen: ViewController {
|
|||
self.node.selectDay(timestamp: timestamp)
|
||||
|
||||
if self.node.selectionState != nil {
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.toggleSelectPressed)), animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ public final class CallListController: TelegramBaseController {
|
|||
if let isEmpty = self.isEmpty, isEmpty {
|
||||
} else {
|
||||
if self.editingMode {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
|
||||
} else {
|
||||
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
|
||||
}
|
||||
|
|
@ -222,7 +222,7 @@ public final class CallListController: TelegramBaseController {
|
|||
//self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: PresentationResourcesRootController.navigationCallIcon(self.presentationData.theme), style: .plain, target: self, action: #selector(self.callPressed))
|
||||
case .navigation:
|
||||
if self.editingMode {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed))
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed))
|
||||
} else {
|
||||
self.navigationItem.rightBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Edit, style: .plain, target: self, action: #selector(self.editPressed))
|
||||
}
|
||||
|
|
@ -679,7 +679,7 @@ public final class CallListController: TelegramBaseController {
|
|||
|
||||
switch self.mode {
|
||||
case .tab:
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
|
||||
self.navigationItem.setLeftBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true)
|
||||
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(customDisplayNode: buttonNode), animated: true)
|
||||
self.navigationItem.rightBarButtonItem?.setCustomAction({
|
||||
|
|
@ -691,7 +691,7 @@ public final class CallListController: TelegramBaseController {
|
|||
pressedImpl?()
|
||||
})
|
||||
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: self.presentationData.strings.Common_Done, style: .done, target: self, action: #selector(self.donePressed)), animated: true)
|
||||
self.navigationItem.setRightBarButton(UIBarButtonItem(title: "___done", style: .done, target: self, action: #selector(self.donePressed)), animated: true)
|
||||
}
|
||||
|
||||
self.controllerNode.updateState { state in
|
||||
|
|
|
|||
|
|
@ -940,7 +940,7 @@ final class CallListControllerNode: ASDisplayNode {
|
|||
insets.top += max(navigationBarHeight, layout.insets(options: [.statusBar]).top)
|
||||
|
||||
let inset: CGFloat
|
||||
if layout.size.width >= 375.0 {
|
||||
if layout.size.width >= 320.0 {
|
||||
inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
} else {
|
||||
inset = 0.0
|
||||
|
|
|
|||
|
|
@ -129,6 +129,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/AlertComponent/AlertTransferHeaderComponent",
|
||||
"//submodules/TelegramUI/Components/AvatarComponent",
|
||||
"//submodules/TelegramUI/Components/PeerManagement/OwnershipTransferController",
|
||||
"//submodules/TelegramUI/Components/GlassControls",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -6244,7 +6244,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController
|
|||
if case .chatList(.root) = self.chatListDisplayNode.mainContainerNode.location {
|
||||
super.setToolbar(toolbar, transition: transition)
|
||||
} else {
|
||||
self.chatListDisplayNode.toolbar = toolbar
|
||||
self.chatListDisplayNode.toolbarData = toolbar
|
||||
self.requestLayout(transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import MediaPlaybackHeaderPanelComponent
|
|||
import LiveLocationHeaderPanelComponent
|
||||
import ChatListHeaderNoticeComponent
|
||||
import ChatListFilterTabContainerNode
|
||||
import GlassControls
|
||||
|
||||
public enum ChatListContainerNodeFilter: Equatable {
|
||||
case all
|
||||
|
|
@ -1134,8 +1135,8 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
let navigationBarView = ComponentView<Empty>()
|
||||
weak var controller: ChatListControllerImpl?
|
||||
|
||||
var toolbar: Toolbar?
|
||||
private var toolbarNode: ToolbarNode?
|
||||
private var toolbar: ComponentView<Empty>?
|
||||
var toolbarData: Toolbar?
|
||||
var toolbarActionSelected: ((ToolbarActionOption) -> Void)?
|
||||
|
||||
private var isSearchDisplayControllerActive: ChatListNavigationBar.ActiveSearch?
|
||||
|
|
@ -1395,10 +1396,6 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
self.mainContainerNode.updatePresentationData(presentationData)
|
||||
self.inlineStackContainerNode?.updatePresentationData(presentationData)
|
||||
self.searchDisplayController?.updatePresentationData(presentationData)
|
||||
|
||||
if let toolbarNode = self.toolbarNode {
|
||||
toolbarNode.updateTheme(ToolbarTheme(rootControllerTheme: self.presentationData.theme))
|
||||
}
|
||||
}
|
||||
|
||||
private func updateNavigationBar(layout: ContainerViewLayout, deferScrollApplication: Bool, transition: ComponentTransition) -> (navigationHeight: CGFloat, storiesInset: CGFloat) {
|
||||
|
|
@ -1842,53 +1839,101 @@ final class ChatListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
insets.left += layout.safeInsets.left
|
||||
insets.right += layout.safeInsets.right
|
||||
|
||||
if let toolbar = self.toolbar {
|
||||
var tabBarHeight: CGFloat
|
||||
var options: ContainerViewLayoutInsetOptions = []
|
||||
if layout.metrics.widthClass == .regular {
|
||||
options.insert(.input)
|
||||
if let toolbarData = self.toolbarData {
|
||||
var panelsBottomInset: CGFloat = layout.insets(options: []).bottom
|
||||
if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 {
|
||||
panelsBottomInset = inputHeight + 8.0
|
||||
}
|
||||
|
||||
var heightInset: CGFloat = 0.0
|
||||
if case .forum = self.location {
|
||||
heightInset = 4.0
|
||||
}
|
||||
|
||||
let bottomInset: CGFloat = layout.insets(options: options).bottom
|
||||
if !layout.safeInsets.left.isZero {
|
||||
tabBarHeight = 34.0 + bottomInset
|
||||
insets.bottom += 34.0
|
||||
if panelsBottomInset == 0.0 {
|
||||
panelsBottomInset = 8.0
|
||||
} else {
|
||||
tabBarHeight = 49.0 - heightInset + bottomInset
|
||||
insets.bottom += 49.0 - heightInset
|
||||
panelsBottomInset = max(panelsBottomInset, 8.0)
|
||||
}
|
||||
|
||||
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
|
||||
let sideInset: CGFloat = 20.0
|
||||
let toolbarHeight = 44.0
|
||||
let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight))
|
||||
|
||||
if let toolbarNode = self.toolbarNode {
|
||||
transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
|
||||
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: transition)
|
||||
let toolbar: ComponentView<Empty>
|
||||
var toolbarTransition = ComponentTransition(transition)
|
||||
if let current = self.toolbar {
|
||||
toolbar = current
|
||||
} else {
|
||||
let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: self.presentationData.theme), displaySeparator: true, left: { [weak self] in
|
||||
self?.toolbarActionSelected?(.left)
|
||||
}, right: { [weak self] in
|
||||
self?.toolbarActionSelected?(.right)
|
||||
}, middle: { [weak self] in
|
||||
self?.toolbarActionSelected?(.middle)
|
||||
})
|
||||
toolbarNode.frame = toolbarFrame
|
||||
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: bottomInset, toolbar: toolbar, transition: .immediate)
|
||||
self.addSubnode(toolbarNode)
|
||||
self.toolbarNode = toolbarNode
|
||||
if transition.isAnimated {
|
||||
toolbarNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
toolbar = ComponentView()
|
||||
self.toolbar = toolbar
|
||||
toolbarTransition = .immediate
|
||||
}
|
||||
|
||||
let _ = toolbar.update(
|
||||
transition: toolbarTransition,
|
||||
component: AnyComponent(GlassControlPanelComponent(
|
||||
theme: self.presentationData.theme,
|
||||
leftItem: toolbarData.leftAction.flatMap { value in
|
||||
return GlassControlPanelComponent.Item(
|
||||
items: [GlassControlGroupComponent.Item(
|
||||
id: "left_" + value.title,
|
||||
content: .text(value.title),
|
||||
action: value.isEnabled ? { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.toolbarActionSelected?(.left)
|
||||
} : nil
|
||||
)],
|
||||
background: .panel
|
||||
)
|
||||
},
|
||||
centralItem: toolbarData.middleAction.flatMap { value in
|
||||
return GlassControlPanelComponent.Item(
|
||||
items: [GlassControlGroupComponent.Item(
|
||||
id: "right_" + value.title,
|
||||
content: .text(value.title),
|
||||
action: value.isEnabled ? { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.toolbarActionSelected?(.middle)
|
||||
} : nil
|
||||
)],
|
||||
background: .panel
|
||||
)
|
||||
},
|
||||
rightItem: toolbarData.rightAction.flatMap { value in
|
||||
return GlassControlPanelComponent.Item(
|
||||
items: [GlassControlGroupComponent.Item(
|
||||
id: "right_" + value.title,
|
||||
content: .text(value.title),
|
||||
action: value.isEnabled ? { [weak self] in
|
||||
guard let self else {
|
||||
return
|
||||
}
|
||||
self.toolbarActionSelected?(.right)
|
||||
} : nil
|
||||
)],
|
||||
background: .panel
|
||||
)
|
||||
},
|
||||
centerAlignmentIfPossible: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: toolbarFrame.size
|
||||
)
|
||||
|
||||
if let toolbarView = toolbar.view {
|
||||
if toolbarView.superview == nil {
|
||||
self.view.addSubview(toolbarView)
|
||||
toolbarView.alpha = 0.0
|
||||
}
|
||||
toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame)
|
||||
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0)
|
||||
}
|
||||
} else if let toolbar = self.toolbar {
|
||||
self.toolbar = nil
|
||||
if let toolbarView = toolbar.view {
|
||||
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in
|
||||
toolbarView?.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
} else if let toolbarNode = self.toolbarNode {
|
||||
self.toolbarNode = nil
|
||||
transition.updateAlpha(node: toolbarNode, alpha: 0.0, completion: { [weak toolbarNode] _ in
|
||||
toolbarNode?.removeFromSupernode()
|
||||
})
|
||||
}
|
||||
|
||||
var childrenLayout = layout
|
||||
|
|
|
|||
|
|
@ -2071,6 +2071,11 @@ public func chatListFilterPresetController(context: AccountContext, currentPrese
|
|||
)
|
||||
|> deliverOnMainQueue
|
||||
|> map { presentationData, stateWithPeers, peerView, premiumLimits, sharedLinks, currentPreset -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
var presentationData = presentationData
|
||||
|
||||
let updatedTheme = presentationData.theme.withModalBlocksBackground()
|
||||
presentationData = presentationData.withUpdated(theme: updatedTheme)
|
||||
|
||||
let (state, includePeers, excludePeers) = stateWithPeers
|
||||
|
||||
let isPremium = peerView.peers[peerView.peerId]?.isPremium ?? false
|
||||
|
|
|
|||
|
|
@ -613,6 +613,11 @@ public func chatListFilterPresetListController(context: AccountContext, mode: Ch
|
|||
)
|
||||
)
|
||||
|> map { presentationData, state, filtersWithCountsValue, preferences, updatedFilterOrderValue, suggestedFilters, peer, allLimits, displayTags -> (ItemListControllerState, (ItemListNodeState, Any)) in
|
||||
var presentationData = presentationData
|
||||
|
||||
let updatedTheme = presentationData.theme.withModalBlocksBackground()
|
||||
presentationData = presentationData.withUpdated(theme: updatedTheme)
|
||||
|
||||
let isPremium = peer?.isPremium ?? false
|
||||
let limits = allLimits.0
|
||||
let premiumLimits = allLimits.1
|
||||
|
|
|
|||
|
|
@ -425,9 +425,9 @@ final class ChatListFilterPresetListItemNode: ItemListRevealOptionsItemNode {
|
|||
transition.updateFrame(node: strongSelf.topStripeNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: layoutSize.width, height: separatorHeight)))
|
||||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height + bottomStripeOffset), size: CGSize(width: layoutSize.width - bottomStripeInset - params.rightInset - separatorRightInset, height: separatorHeight)))
|
||||
|
||||
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset), size: titleLayout.size))
|
||||
transition.updateFrame(node: strongSelf.titleNode.textNode, frame: CGRect(origin: CGPoint(x: leftInset + revealOffset + editingOffset, y: verticalInset + 1.0), size: titleLayout.size))
|
||||
|
||||
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset), size: labelLayout.size)
|
||||
let labelFrame = CGRect(origin: CGPoint(x: params.width - rightArrowInset - labelLayout.size.width + revealOffset, y: verticalInset + 1.0), size: labelLayout.size)
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
transition.updateAlpha(node: strongSelf.labelNode, alpha: reorderControlSizeAndApply != nil ? 0.0 : 1.0)
|
||||
|
|
|
|||
|
|
@ -215,14 +215,8 @@ public class ChatListFilterPresetListSuggestedItemNode: ListViewItemNode, ItemLi
|
|||
|
||||
let (labelLayout, labelApply) = makeLabelLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.label, font: labelFont, textColor:labelBadgeColor), backgroundColor: nil, maximumNumberOfLines: multilineLabel ? 0 : 1, truncationType: .end, constrainedSize: CGSize(width: labelConstrain, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let verticalInset: CGFloat
|
||||
switch item.systemStyle {
|
||||
case .glass:
|
||||
verticalInset = 15.0
|
||||
case .legacy:
|
||||
verticalInset = 11.0
|
||||
}
|
||||
let titleSpacing: CGFloat = 3.0
|
||||
let verticalInset: CGFloat = 11.0
|
||||
let titleSpacing: CGFloat = 2.0
|
||||
|
||||
let height: CGFloat
|
||||
height = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + labelLayout.size.height
|
||||
|
|
|
|||
|
|
@ -6542,10 +6542,10 @@ private final class EmptyResultsButton: Component {
|
|||
transition: transition,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: component.theme.list.itemCheckColors.fillColor,
|
||||
foreground: component.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9),
|
||||
cornerRadius: 10.0
|
||||
pressedColor: component.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
),
|
||||
content: buttonContent,
|
||||
isEnabled: isEnabled,
|
||||
|
|
@ -6557,7 +6557,7 @@ private final class EmptyResultsButton: Component {
|
|||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: availableSize.width, height: 50.0)
|
||||
containerSize: CGSize(width: availableSize.width, height: 52.0)
|
||||
)
|
||||
if let buttonView = self.button.view {
|
||||
if buttonView.superview == nil {
|
||||
|
|
|
|||
|
|
@ -185,6 +185,7 @@ class ChatListArchiveInfoItemNode: ListViewItemNode, ASScrollViewDelegate {
|
|||
|
||||
self.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.isPagingEnabled = true
|
||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
||||
|
|
|
|||
|
|
@ -640,9 +640,9 @@ private let ungroupIcon = ItemListRevealOptionIcon.animation(animation: "anim_un
|
|||
private let readIcon = ItemListRevealOptionIcon.animation(animation: "anim_read", scale: 1.0, offset: 0.0, replaceColors: nil, flip: false)
|
||||
private let unreadIcon = ItemListRevealOptionIcon.animation(animation: "anim_unread", scale: 1.0, offset: 0.0, replaceColors: [0x2194fa], flip: false)
|
||||
private let archiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_archive", scale: 1.0, offset: 2.0, replaceColors: [0xa9a9ad], flip: false)
|
||||
private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.642, offset: -9.0, replaceColors: [0xa9a9ad], flip: false)
|
||||
private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: 2.0, replaceColors: [0xbdbdc2], flip: false)
|
||||
private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -20.0, replaceColors: [0xbdbdc2], flip: true)
|
||||
private let unarchiveIcon = ItemListRevealOptionIcon.animation(animation: "anim_unarchive", scale: 0.52, offset: -6.0, replaceColors: [0xa9a9ad], flip: false)
|
||||
private let hideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.1, offset: 2.0, replaceColors: [0xbdbdc2], flip: false)
|
||||
private let unhideIcon = ItemListRevealOptionIcon.animation(animation: "anim_hide", scale: 1.0, offset: -15.0, replaceColors: [0xbdbdc2], flip: true)
|
||||
private let startIcon = ItemListRevealOptionIcon.animation(animation: "anim_play", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false)
|
||||
private let closeIcon = ItemListRevealOptionIcon.animation(animation: "anim_pause", scale: 1.0, offset: 0.0, replaceColors: [0xbdbdc2], flip: false)
|
||||
|
||||
|
|
@ -1393,8 +1393,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
private var isHighlighted: Bool = false
|
||||
private var isRevealHighlighted: Bool = false
|
||||
private var isSeparatorHiddenByLowerReveal: Bool = false
|
||||
private weak var revealHighlightedUpperNeighbor: ChatListItemNode?
|
||||
private var keepRevealHighlightUntilClosed: Bool = false
|
||||
private var nextHasActiveRevealControls: Bool = false
|
||||
private var skipFadeout: Bool = false
|
||||
private var customAnimationInProgress: Bool = false
|
||||
|
|
@ -1744,7 +1743,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
deinit {
|
||||
self.revealHighlightedUpperNeighbor?.updateSeparatorHiddenByLowerReveal(false, transition: .immediate)
|
||||
self.cachedDataDisposable.dispose()
|
||||
}
|
||||
|
||||
|
|
@ -2147,42 +2145,13 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
private func updateSeparatorAlpha(transition: ContainedViewLayoutTransition, inlineNavigationProgress: CGFloat? = nil) {
|
||||
let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls || self.isSeparatorHiddenByLowerReveal) ? 0.0 : 1.0
|
||||
let revealSeparatorAlpha: CGFloat = (self.isRevealHighlighted || self.nextHasActiveRevealControls) ? 0.0 : 1.0
|
||||
if let inlineNavigationProgress = inlineNavigationProgress ?? self.item?.interaction.inlineNavigationLocation?.progress {
|
||||
transition.updateAlpha(node: self.separatorNode, alpha: (1.0 - inlineNavigationProgress) * revealSeparatorAlpha)
|
||||
} else {
|
||||
transition.updateAlpha(node: self.separatorNode, alpha: revealSeparatorAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateSeparatorHiddenByLowerReveal(_ value: Bool, transition: ContainedViewLayoutTransition) {
|
||||
self.isSeparatorHiddenByLowerReveal = value
|
||||
self.updateSeparatorAlpha(transition: transition)
|
||||
}
|
||||
|
||||
private func updateUpperNeighborSeparatorForReveal(_ value: Bool, transition: ContainedViewLayoutTransition) {
|
||||
if value {
|
||||
var upperNeighbor: ChatListItemNode?
|
||||
if let supernode = self.supernode, let subnodes = supernode.subnodes {
|
||||
for case let node as ChatListItemNode in subnodes {
|
||||
if node !== self && abs(node.frame.maxY - self.frame.minY) <= 2.0 {
|
||||
if upperNeighbor == nil || node.frame.maxY > upperNeighbor!.frame.maxY {
|
||||
upperNeighbor = node
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.revealHighlightedUpperNeighbor !== upperNeighbor {
|
||||
self.revealHighlightedUpperNeighbor?.updateSeparatorHiddenByLowerReveal(false, transition: transition)
|
||||
self.revealHighlightedUpperNeighbor = upperNeighbor
|
||||
}
|
||||
upperNeighbor?.updateSeparatorHiddenByLowerReveal(true, transition: transition)
|
||||
} else if let upperNeighbor = self.revealHighlightedUpperNeighbor {
|
||||
upperNeighbor.updateSeparatorHiddenByLowerReveal(false, transition: transition)
|
||||
self.revealHighlightedUpperNeighbor = nil
|
||||
}
|
||||
}
|
||||
|
||||
override public func tapped() {
|
||||
guard let item = self.item, item.editing else {
|
||||
|
|
@ -3559,7 +3528,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
let (mentionBadgeLayout, mentionBadgeApply) = mentionBadgeLayout(CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), badgeDiameter, badgeFont, currentMentionBadgeImage, mentionBadgeContent)
|
||||
|
||||
var actionButtonTitleNodeLayoutAndApply: (TextNodeLayout, () -> TextNode)?
|
||||
if case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||
if !item.editing, case .none = badgeContent, case .none = mentionBadgeContent, case let .chat(itemPeer) = contentPeer, case let .user(user) = itemPeer.chatMainPeer, let botInfo = user.botInfo, botInfo.flags.contains(.hasWebApp) {
|
||||
actionButtonTitleNodeLayoutAndApply = makeActionButtonTitleNodeLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.presentationData.strings.ChatList_InlineButtonOpenApp, font: Font.semibold(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0)), textColor: theme.unreadBadgeActiveTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: rawContentWidth, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
}
|
||||
|
||||
|
|
@ -4413,16 +4382,17 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.statusNode.fontSize = item.presentationData.fontSize.itemListBaseFontSize
|
||||
let _ = strongSelf.statusNode.transitionToState(statusState, animated: animateContent)
|
||||
|
||||
let rightAccessoryVerticalOffset: CGFloat = -2.0
|
||||
var nextBadgeX: CGFloat = contentRect.maxX
|
||||
if let _ = currentBadgeBackgroundImage {
|
||||
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0, width: badgeLayout.width, height: badgeLayout.height)
|
||||
let badgeFrame = CGRect(x: nextBadgeX - badgeLayout.width, y: contentRect.maxY - badgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: badgeLayout.width, height: badgeLayout.height)
|
||||
|
||||
transition.updateFrame(node: strongSelf.badgeNode, frame: badgeFrame)
|
||||
nextBadgeX -= badgeLayout.width + 6.0
|
||||
}
|
||||
|
||||
if currentMentionBadgeImage != nil || currentBadgeBackgroundImage != nil {
|
||||
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
|
||||
let badgeFrame = CGRect(x: nextBadgeX - mentionBadgeLayout.width, y: contentRect.maxY - mentionBadgeLayout.height - 2.0 + rightAccessoryVerticalOffset, width: mentionBadgeLayout.width, height: mentionBadgeLayout.height)
|
||||
|
||||
transition.updateFrame(node: strongSelf.mentionBadgeNode, frame: badgeFrame)
|
||||
nextBadgeX -= mentionBadgeLayout.width + 6.0
|
||||
|
|
@ -4433,7 +4403,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
strongSelf.pinnedIconNode.isHidden = false
|
||||
|
||||
let pinnedIconSize = currentPinnedIconImage.size
|
||||
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0, width: pinnedIconSize.width, height: pinnedIconSize.height)
|
||||
let pinnedIconFrame = CGRect(x: nextBadgeX - pinnedIconSize.width, y: contentRect.maxY - pinnedIconSize.height - 2.0 + rightAccessoryVerticalOffset, width: pinnedIconSize.width, height: pinnedIconSize.height)
|
||||
|
||||
strongSelf.pinnedIconNode.frame = pinnedIconFrame
|
||||
nextBadgeX -= pinnedIconSize.width + 6.0
|
||||
|
|
@ -4450,11 +4420,14 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
let actionButtonSize = CGSize(width: actionButtonTitleNodeLayout.size.width + actionButtonSideInset * 2.0, height: actionButtonTitleNodeLayout.size.height + actionButtonTopInset + actionButtonBottomInset)
|
||||
var actionButtonFrame = CGRect(x: nextBadgeX - actionButtonSize.width, y: contentRect.minY + floor((contentRect.height - actionButtonSize.height) * 0.5), width: actionButtonSize.width, height: actionButtonSize.height)
|
||||
actionButtonFrame.origin.y = max(actionButtonFrame.origin.y, dateFrame.maxY + floor(item.presentationData.fontSize.itemListBaseFontSize * 4.0 / 17.0))
|
||||
actionButtonFrame.origin.y += 4.0
|
||||
|
||||
let actionButtonNode: HighlightableButtonNode
|
||||
var animateActionButtonIn = false
|
||||
if let current = strongSelf.actionButtonNode {
|
||||
actionButtonNode = current
|
||||
} else {
|
||||
animateActionButtonIn = true
|
||||
actionButtonNode = HighlightableButtonNode()
|
||||
strongSelf.actionButtonNode = actionButtonNode
|
||||
strongSelf.mainContentContainerNode.addSubnode(actionButtonNode)
|
||||
|
|
@ -4483,23 +4456,40 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
actionButtonNode.addSubnode(actionButtonTitleNode)
|
||||
}
|
||||
|
||||
actionButtonNode.isUserInteractionEnabled = true
|
||||
actionButtonNode.frame = actionButtonFrame
|
||||
actionButtonBackgroundView.frame = CGRect(origin: CGPoint(), size: actionButtonFrame.size)
|
||||
actionButtonTitleNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels((actionButtonFrame.width - actionButtonTitleNodeLayout.size.width) * 0.5), y: actionButtonTopInset), size: actionButtonTitleNodeLayout.size)
|
||||
if animateActionButtonIn {
|
||||
actionButtonNode.alpha = 0.0
|
||||
}
|
||||
transition.updateAlpha(node: actionButtonNode, alpha: 1.0)
|
||||
|
||||
nextBadgeX -= actionButtonSize.width + 6.0
|
||||
} else {
|
||||
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
|
||||
actionButtonTitleNode.removeFromSupernode()
|
||||
strongSelf.actionButtonTitleNode = nil
|
||||
}
|
||||
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
|
||||
actionButtonBackgroundView.removeFromSuperview()
|
||||
strongSelf.actionButtonBackgroundView = nil
|
||||
}
|
||||
if let actionButtonNode = strongSelf.actionButtonNode {
|
||||
actionButtonNode.removeFromSupernode()
|
||||
let actionButtonTitleNode = strongSelf.actionButtonTitleNode
|
||||
let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView
|
||||
actionButtonNode.isUserInteractionEnabled = false
|
||||
|
||||
strongSelf.actionButtonTitleNode = nil
|
||||
strongSelf.actionButtonBackgroundView = nil
|
||||
strongSelf.actionButtonNode = nil
|
||||
|
||||
transition.updateAlpha(node: actionButtonNode, alpha: 0.0, completion: { [weak actionButtonNode, weak actionButtonTitleNode, weak actionButtonBackgroundView] _ in
|
||||
actionButtonTitleNode?.removeFromSupernode()
|
||||
actionButtonBackgroundView?.removeFromSuperview()
|
||||
actionButtonNode?.removeFromSupernode()
|
||||
})
|
||||
} else {
|
||||
if let actionButtonTitleNode = strongSelf.actionButtonTitleNode {
|
||||
actionButtonTitleNode.removeFromSupernode()
|
||||
strongSelf.actionButtonTitleNode = nil
|
||||
}
|
||||
if let actionButtonBackgroundView = strongSelf.actionButtonBackgroundView {
|
||||
actionButtonBackgroundView.removeFromSuperview()
|
||||
strongSelf.actionButtonBackgroundView = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -5140,7 +5130,6 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
} else {
|
||||
strongSelf.updateSeparatorAlpha(transition: transition)
|
||||
}
|
||||
strongSelf.updateUpperNeighborSeparatorForReveal(strongSelf.isRevealHighlighted, transition: transition)
|
||||
|
||||
if case let .peer(peerData) = item.content, let customMessageListData = peerData.customMessageListData {
|
||||
if customMessageListData.hideSeparator {
|
||||
|
|
@ -5336,12 +5325,22 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
let highlightedBackgroundFrame = self.highlightedBackgroundNode.frame
|
||||
transition.updateFrame(node: self.highlightedBackgroundNode, frame: CGRect(origin: CGPoint(x: offset, y: highlightedBackgroundFrame.minY), size: highlightedBackgroundFrame.size))
|
||||
|
||||
let isRevealHighlighted = !offset.isZero
|
||||
if !offset.isZero {
|
||||
self.keepRevealHighlightUntilClosed = true
|
||||
}
|
||||
let isRevealHighlighted = !offset.isZero || self.keepRevealHighlightUntilClosed
|
||||
if self.isRevealHighlighted != isRevealHighlighted {
|
||||
self.isRevealHighlighted = isRevealHighlighted
|
||||
self.updateIsHighlighted(transition: transition)
|
||||
}
|
||||
self.updateUpperNeighborSeparatorForReveal(isRevealHighlighted, transition: transition)
|
||||
}
|
||||
|
||||
private func clearRevealHighlightIfNeeded(transition: ContainedViewLayoutTransition) {
|
||||
self.keepRevealHighlightUntilClosed = false
|
||||
if self.revealOffset.isZero && self.isRevealHighlighted {
|
||||
self.isRevealHighlighted = false
|
||||
self.updateIsHighlighted(transition: transition)
|
||||
}
|
||||
}
|
||||
|
||||
override public func touchesToOtherItemsPrevented() {
|
||||
|
|
@ -5349,9 +5348,11 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
if let item = self.item {
|
||||
item.interaction.setPeerIdWithRevealedOptions(nil, nil)
|
||||
}
|
||||
self.clearRevealHighlightIfNeeded(transition: .immediate)
|
||||
}
|
||||
|
||||
override public func revealOptionsInteractivelyOpened() {
|
||||
self.keepRevealHighlightUntilClosed = true
|
||||
if let item = self.item {
|
||||
switch item.index {
|
||||
case let .chatList(index):
|
||||
|
|
@ -5363,6 +5364,7 @@ public class ChatListItemNode: ItemListRevealOptionsItemNode {
|
|||
}
|
||||
|
||||
override public func revealOptionsInteractivelyClosed() {
|
||||
self.clearRevealHighlightIfNeeded(transition: .animated(duration: 0.2, curve: .easeInOut))
|
||||
if let item = self.item {
|
||||
switch item.index {
|
||||
case let .chatList(index):
|
||||
|
|
|
|||
|
|
@ -7,19 +7,22 @@ public final class Image: Component {
|
|||
public let size: CGSize?
|
||||
public let contentMode: UIImageView.ContentMode
|
||||
public let cornerRadius: CGFloat
|
||||
public let flipHorizontally: Bool
|
||||
|
||||
public init(
|
||||
image: UIImage?,
|
||||
tintColor: UIColor? = nil,
|
||||
size: CGSize? = nil,
|
||||
contentMode: UIImageView.ContentMode = .scaleToFill,
|
||||
cornerRadius: CGFloat = 0.0
|
||||
cornerRadius: CGFloat = 0.0,
|
||||
flipHorizontally: Bool = false
|
||||
) {
|
||||
self.image = image
|
||||
self.tintColor = tintColor
|
||||
self.size = size
|
||||
self.contentMode = contentMode
|
||||
self.cornerRadius = cornerRadius
|
||||
self.flipHorizontally = flipHorizontally
|
||||
}
|
||||
|
||||
public static func ==(lhs: Image, rhs: Image) -> Bool {
|
||||
|
|
@ -38,6 +41,9 @@ public final class Image: Component {
|
|||
if lhs.cornerRadius != rhs.cornerRadius {
|
||||
return false
|
||||
}
|
||||
if lhs.flipHorizontally != rhs.flipHorizontally {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
@ -51,7 +57,11 @@ public final class Image: Component {
|
|||
}
|
||||
|
||||
func update(component: Image, availableSize: CGSize, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
self.image = component.image
|
||||
if component.flipHorizontally, let cgImage = component.image?.cgImage {
|
||||
self.image = UIImage(cgImage: cgImage, scale: component.image?.scale ?? 0.0, orientation: .upMirrored)
|
||||
} else {
|
||||
self.image = component.image
|
||||
}
|
||||
self.contentMode = component.contentMode
|
||||
self.clipsToBounds = component.cornerRadius > 0.0
|
||||
|
||||
|
|
|
|||
|
|
@ -11,15 +11,17 @@ public final class BundleIconComponent: Component {
|
|||
public let scaleFactor: CGFloat
|
||||
public let shadowColor: UIColor?
|
||||
public let shadowBlur: CGFloat
|
||||
public let flipHorizontally: Bool
|
||||
public let flipVertically: Bool
|
||||
|
||||
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipVertically: Bool = false) {
|
||||
public init(name: String, tintColor: UIColor?, maxSize: CGSize? = nil, scaleFactor: CGFloat = 1.0, shadowColor: UIColor? = nil, shadowBlur: CGFloat = 0.0, flipHorizontally: Bool = false, flipVertically: Bool = false) {
|
||||
self.name = name
|
||||
self.tintColor = tintColor
|
||||
self.maxSize = maxSize
|
||||
self.scaleFactor = scaleFactor
|
||||
self.shadowColor = shadowColor
|
||||
self.shadowBlur = shadowBlur
|
||||
self.flipHorizontally = flipHorizontally
|
||||
self.flipVertically = flipVertically
|
||||
}
|
||||
|
||||
|
|
@ -42,6 +44,9 @@ public final class BundleIconComponent: Component {
|
|||
if lhs.shadowBlur != rhs.shadowBlur {
|
||||
return false
|
||||
}
|
||||
if lhs.flipHorizontally != rhs.flipHorizontally {
|
||||
return false
|
||||
}
|
||||
if lhs.flipVertically != rhs.flipVertically {
|
||||
return false
|
||||
}
|
||||
|
|
@ -77,7 +82,9 @@ public final class BundleIconComponent: Component {
|
|||
}
|
||||
})
|
||||
}
|
||||
if component.flipVertically, let cgImage = image?.cgImage {
|
||||
if component.flipHorizontally, let cgImage = image?.cgImage {
|
||||
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .upMirrored)
|
||||
} else if component.flipVertically, let cgImage = image?.cgImage {
|
||||
self.image = UIImage(cgImage: cgImage, scale: image?.scale ?? 0.0, orientation: .down)
|
||||
} else {
|
||||
self.image = image
|
||||
|
|
|
|||
|
|
@ -291,6 +291,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollNode.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
self.itemNodes = reactions.map { reaction, count in
|
||||
return ItemNode(context: context, availableReactions: availableReactions, reaction: reaction, animationCache: animationCache, animationRenderer: animationRenderer, count: count)
|
||||
|
|
@ -897,6 +898,7 @@ public final class ReactionListContextMenuContent: ContextControllerItemsContent
|
|||
self.scrollNode.view.contentInsetAdjustmentBehavior = .never
|
||||
}
|
||||
self.scrollNode.clipsToBounds = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
|
|||
|
|
@ -104,19 +104,19 @@ public final class DeviceLocationManager: NSObject {
|
|||
self.currentTopMode = topMode
|
||||
if let topMode = topMode {
|
||||
self.log?("setting mode \(topMode)")
|
||||
switch topMode {
|
||||
case .preciseForeground:
|
||||
self.manager.allowsBackgroundLocationUpdates = false
|
||||
case .preciseAlways:
|
||||
self.manager.allowsBackgroundLocationUpdates = true
|
||||
}
|
||||
|
||||
if previousTopMode == nil {
|
||||
if !self.requestedAuthorization {
|
||||
self.requestedAuthorization = true
|
||||
self.manager.requestAlwaysAuthorization()
|
||||
}
|
||||
|
||||
switch topMode {
|
||||
case .preciseForeground:
|
||||
self.manager.allowsBackgroundLocationUpdates = false
|
||||
case .preciseAlways:
|
||||
self.manager.allowsBackgroundLocationUpdates = true
|
||||
}
|
||||
|
||||
|
||||
self.manager.startUpdatingLocation()
|
||||
self.manager.startUpdatingHeading()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -88,6 +88,12 @@ final class ActionSheetControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
}
|
||||
}
|
||||
|
||||
override func didLoad() {
|
||||
super.didLoad()
|
||||
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
}
|
||||
|
||||
func performHighlightedAction() {
|
||||
self.itemGroupsContainerNode.performHighlightedAction()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ final class ActionSheetItemGroupNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.scrollNode.view.canCancelContentTouches = true
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
super.init()
|
||||
|
||||
|
|
|
|||
|
|
@ -72,8 +72,21 @@ public extension CALayer {
|
|||
func makeAnimation(from: Any?, to: Any, keyPath: String, timingFunction: String, duration: Double, delay: Double = 0.0, mediaTimingFunction: CAMediaTimingFunction? = nil, removeOnCompletion: Bool = true, additive: Bool = false, completion: ((Bool) -> Void)? = nil) -> CAAnimation {
|
||||
if timingFunction.hasPrefix(kCAMediaTimingFunctionCustomSpringPrefix) {
|
||||
let components = timingFunction.components(separatedBy: "_")
|
||||
let damping = Float(components[1]) ?? 100.0
|
||||
let initialVelocity = Float(components[2]) ?? 0.0
|
||||
let mass: Float
|
||||
let stiffness: Float
|
||||
let damping: Float
|
||||
let initialVelocity: Float
|
||||
if components.count >= 5 {
|
||||
mass = Float(components[1]) ?? 5.0
|
||||
stiffness = Float(components[2]) ?? 900.0
|
||||
damping = Float(components[3]) ?? 100.0
|
||||
initialVelocity = Float(components[4]) ?? 0.0
|
||||
} else {
|
||||
mass = 5.0
|
||||
stiffness = 900.0
|
||||
damping = components.count > 1 ? (Float(components[1]) ?? 100.0) : 100.0
|
||||
initialVelocity = components.count > 2 ? (Float(components[2]) ?? 0.0) : 0.0
|
||||
}
|
||||
|
||||
let animation = CASpringAnimation(keyPath: keyPath)
|
||||
animation.fromValue = from
|
||||
|
|
@ -83,10 +96,10 @@ public extension CALayer {
|
|||
if let completion = completion {
|
||||
animation.delegate = CALayerAnimationDelegate(animation: animation, completion: completion)
|
||||
}
|
||||
animation.mass = CGFloat(mass)
|
||||
animation.stiffness = CGFloat(stiffness)
|
||||
animation.damping = CGFloat(damping)
|
||||
animation.initialVelocity = CGFloat(initialVelocity)
|
||||
animation.mass = 5.0
|
||||
animation.stiffness = 900.0
|
||||
animation.duration = animation.settlingDuration
|
||||
animation.timingFunction = CAMediaTimingFunction.init(name: .linear)
|
||||
let k = Float(UIView.animationDurationFactor())
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ public enum ContainedViewLayoutTransitionCurve: Equatable, Hashable {
|
|||
case easeInOut
|
||||
case easeIn
|
||||
case spring
|
||||
case customSpring(damping: CGFloat, initialVelocity: CGFloat)
|
||||
case customSpring(mass: CGFloat = 5.0, stiffness: CGFloat = 900.0, damping: CGFloat, initialVelocity: CGFloat)
|
||||
case custom(Float, Float, Float, Float)
|
||||
|
||||
public static var slide: ContainedViewLayoutTransitionCurve {
|
||||
|
|
@ -52,8 +52,8 @@ public extension ContainedViewLayoutTransitionCurve {
|
|||
return CAMediaTimingFunctionName.easeIn.rawValue
|
||||
case .spring:
|
||||
return kCAMediaTimingFunctionSpring
|
||||
case let .customSpring(damping, initialVelocity):
|
||||
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(damping)_\(initialVelocity)"
|
||||
case let .customSpring(mass, stiffness, damping, initialVelocity):
|
||||
return "\(kCAMediaTimingFunctionCustomSpringPrefix)_\(mass)_\(stiffness)_\(damping)_\(initialVelocity)"
|
||||
case .custom:
|
||||
return CAMediaTimingFunctionName.easeInEaseOut.rawValue
|
||||
}
|
||||
|
|
@ -124,8 +124,8 @@ private extension CALayer {
|
|||
let timingFunction: String
|
||||
let mediaTimingFunction: CAMediaTimingFunction?
|
||||
switch curve {
|
||||
case .spring:
|
||||
timingFunction = kCAMediaTimingFunctionSpring
|
||||
case .spring, .customSpring:
|
||||
timingFunction = curve.timingFunction
|
||||
mediaTimingFunction = nil
|
||||
default:
|
||||
timingFunction = CAMediaTimingFunctionName.easeInEaseOut.rawValue
|
||||
|
|
|
|||
|
|
@ -509,6 +509,7 @@ open class ListViewImpl: ASDisplayNode, ListView, ASScrollViewDelegate, ASGestur
|
|||
self.scroller.contentSize = CGSize(width: 0.0, height: infiniteScrollSize * 2.0)
|
||||
self.scroller.isHidden = true
|
||||
self.scroller.delegate = self.wrappedScrollViewDelegate
|
||||
self.scroller.scrollsToTop = false
|
||||
self.view.addSubview(self.scroller)
|
||||
self.scroller.panGestureRecognizer.cancelsTouchesInView = true
|
||||
self.view.addGestureRecognizer(self.scroller.panGestureRecognizer)
|
||||
|
|
|
|||
|
|
@ -172,6 +172,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
private var inCallStatusBar: StatusBar?
|
||||
private var updateInCallStatusBarState: CallStatusBarNode?
|
||||
private var globalScrollToTopNode: ScrollToTopNode?
|
||||
private var windowScrollToTopProxyViews: WindowScrollToTopProxyViews?
|
||||
private var rootContainer: RootContainer?
|
||||
private var rootModalFrame: NavigationModalFrame?
|
||||
private var modalContainers: [NavigationModalContainer] = []
|
||||
|
|
@ -331,6 +332,54 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
}
|
||||
|
||||
deinit {
|
||||
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
|
||||
}
|
||||
|
||||
private func getWindowScrollToTopProxyViews() -> WindowScrollToTopProxyViews {
|
||||
if let windowScrollToTopProxyViews = self.windowScrollToTopProxyViews {
|
||||
return windowScrollToTopProxyViews
|
||||
}
|
||||
|
||||
let windowScrollToTopProxyViews = WindowScrollToTopProxyViews(scrollToTop: { [weak self] subject in
|
||||
self?.scrollToTop(subject)
|
||||
})
|
||||
self.windowScrollToTopProxyViews = windowScrollToTopProxyViews
|
||||
return windowScrollToTopProxyViews
|
||||
}
|
||||
|
||||
private func windowScrollToTopReferenceView(window: UIWindow) -> UIView? {
|
||||
var view: UIView? = self.view
|
||||
while let currentView = view {
|
||||
if currentView.superview === window {
|
||||
return currentView
|
||||
}
|
||||
view = currentView.superview
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func updateWindowScrollToTopProxyViews(layout: ContainerViewLayout) {
|
||||
guard let window = self.view.window, let rootContainer = self.rootContainer else {
|
||||
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = true
|
||||
self.windowScrollToTopProxyViews?.update(window: nil, mode: .disabled, referenceView: nil)
|
||||
return
|
||||
}
|
||||
|
||||
(self.globalScrollToTopNode?.view as? ScrollToTopView)?.scrollsToTop = false
|
||||
let referenceView = self.windowScrollToTopReferenceView(window: window)
|
||||
let proxyViews = self.getWindowScrollToTopProxyViews()
|
||||
switch rootContainer {
|
||||
case .flat:
|
||||
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
|
||||
let frame = self.view.convert(CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: scrollToTopHeight)), to: window)
|
||||
proxyViews.update(window: window, mode: .flat(frame: frame), referenceView: referenceView)
|
||||
case let .split(container):
|
||||
let frames = container.scrollToTopProxyFrames(layout: layout)
|
||||
proxyViews.update(window: window, mode: .split(
|
||||
masterFrame: container.view.convert(frames.master, to: window),
|
||||
detailFrame: container.view.convert(frames.detail, to: window)
|
||||
), referenceView: referenceView)
|
||||
}
|
||||
}
|
||||
|
||||
public func combinedSupportedOrientations(currentOrientationToLock: UIInterfaceOrientationMask) -> ViewControllerSupportedOrientations {
|
||||
|
|
@ -498,7 +547,7 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
}
|
||||
|
||||
if let globalScrollToTopNode = self.globalScrollToTopNode {
|
||||
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1))
|
||||
globalScrollToTopNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: layout.size.width, height: 1.0))
|
||||
}
|
||||
|
||||
var overlayContainerLayout = layout
|
||||
|
|
@ -1018,8 +1067,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
case let .flat(flatContainer):
|
||||
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
}, scrollToTop: { [weak self] subject in
|
||||
self?.scrollToTop(subject)
|
||||
})
|
||||
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
|
||||
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
|
||||
|
|
@ -1053,8 +1100,6 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
} else {
|
||||
let splitContainer = NavigationSplitContainer(theme: self.theme, controllerRemoved: { [weak self] controller in
|
||||
self?.controllerRemoved(controller)
|
||||
}, scrollToTop: { [weak self] subject in
|
||||
self?.scrollToTop(subject)
|
||||
})
|
||||
if let detailsPlaceholderNode = self.detailsPlaceholderNode {
|
||||
self.displayNode.insertSubnode(splitContainer, aboveSubnode: detailsPlaceholderNode)
|
||||
|
|
@ -1385,6 +1430,8 @@ open class NavigationController: UINavigationController, ContainableController,
|
|||
}
|
||||
}
|
||||
|
||||
self.updateWindowScrollToTopProxyViews(layout: layout)
|
||||
|
||||
self.isUpdatingContainers = false
|
||||
|
||||
if notifyGlobalOverlayControllersUpdated {
|
||||
|
|
|
|||
|
|
@ -91,6 +91,7 @@ final class NavigationModalContainer: ASDisplayNode, ASScrollViewDelegate, ASGes
|
|||
self.scrollNode.view.clipsToBounds = false
|
||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
||||
self.scrollNode.view.tag = 0x5C4011
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
let panRecognizer = InteractiveTransitionGestureRecognizer(target: self, action: #selector(self.panGesture(_:)), allowedDirections: { [weak self] _ in
|
||||
guard let strongSelf = self, !strongSelf.isDismissed else {
|
||||
|
|
|
|||
|
|
@ -11,8 +11,6 @@ enum NavigationSplitContainerScrollToTop {
|
|||
final class NavigationSplitContainer: ASDisplayNode {
|
||||
private var theme: NavigationControllerTheme
|
||||
|
||||
private let masterScrollToTopView: ScrollToTopView
|
||||
private let detailScrollToTopView: ScrollToTopView
|
||||
private let masterContainer: NavigationContainer
|
||||
private let detailContainer: NavigationContainer
|
||||
private let separator: ASDisplayNode
|
||||
|
|
@ -39,18 +37,9 @@ final class NavigationSplitContainer: ASDisplayNode {
|
|||
self.detailContainer.isInFocus = isInFocus
|
||||
}
|
||||
|
||||
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void, scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
|
||||
init(theme: NavigationControllerTheme, controllerRemoved: @escaping (ViewController) -> Void) {
|
||||
self.theme = theme
|
||||
|
||||
self.masterScrollToTopView = ScrollToTopView(frame: CGRect())
|
||||
self.masterScrollToTopView.action = {
|
||||
scrollToTop(.master)
|
||||
}
|
||||
self.detailScrollToTopView = ScrollToTopView(frame: CGRect())
|
||||
self.detailScrollToTopView.action = {
|
||||
scrollToTop(.detail)
|
||||
}
|
||||
|
||||
self.masterContainer = NavigationContainer(isFlat: false, controllerRemoved: controllerRemoved)
|
||||
self.masterContainer.clipsToBounds = true
|
||||
|
||||
|
|
@ -65,8 +54,6 @@ final class NavigationSplitContainer: ASDisplayNode {
|
|||
self.addSubnode(self.masterContainer)
|
||||
self.addSubnode(self.detailContainer)
|
||||
self.addSubnode(self.separator)
|
||||
self.view.addSubview(self.masterScrollToTopView)
|
||||
self.view.addSubview(self.detailScrollToTopView)
|
||||
}
|
||||
|
||||
func hasNonReadyControllers() -> Bool {
|
||||
|
|
@ -83,13 +70,21 @@ final class NavigationSplitContainer: ASDisplayNode {
|
|||
self.separator.backgroundColor = theme.navigationBar.separatorColor
|
||||
}
|
||||
|
||||
func scrollToTopProxyFrames(layout: ContainerViewLayout) -> (master: CGRect, detail: CGRect) {
|
||||
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
|
||||
let detailWidth = layout.size.width - masterWidth
|
||||
let scrollToTopHeight = max(layout.statusBarHeight ?? layout.safeInsets.top, 1.0)
|
||||
|
||||
return (
|
||||
master: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: scrollToTopHeight)),
|
||||
detail: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: scrollToTopHeight))
|
||||
)
|
||||
}
|
||||
|
||||
func update(layout: ContainerViewLayout, masterControllers: [ViewController], detailControllers: [ViewController], detailsPlaceholderNode: NavigationDetailsPlaceholderNode?, transition: ContainedViewLayoutTransition) {
|
||||
let masterWidth: CGFloat = min(max(320.0, floor(layout.size.width / 3.0)), floor(layout.size.width / 2.0))
|
||||
let detailWidth = layout.size.width - masterWidth
|
||||
|
||||
self.masterScrollToTopView.frame = CGRect(origin: CGPoint(x: 0.0, y: -1.0), size: CGSize(width: masterWidth, height: 10.0))
|
||||
self.detailScrollToTopView.frame = CGRect(origin: CGPoint(x: masterWidth, y: -1.0), size: CGSize(width: detailWidth, height: 1.0))
|
||||
|
||||
transition.updateFrame(node: self.masterContainer, frame: CGRect(origin: CGPoint(), size: CGSize(width: masterWidth, height: layout.size.height)))
|
||||
transition.updateFrame(node: self.detailContainer, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: detailWidth, height: layout.size.height)))
|
||||
transition.updateFrame(node: self.separator, frame: CGRect(origin: CGPoint(x: masterWidth, y: 0.0), size: CGSize(width: UIScreenPixel, height: layout.size.height)))
|
||||
|
|
|
|||
|
|
@ -35,10 +35,6 @@ class ScrollToTopView: UIScrollView, UIScrollViewDelegate {
|
|||
|
||||
return false
|
||||
}
|
||||
|
||||
func scrollViewDidScrollToTop(_ scrollView: UIScrollView) {
|
||||
print("scrollViewDidScrollToTop")
|
||||
}
|
||||
}
|
||||
|
||||
class ScrollToTopNode: ASDisplayNode {
|
||||
|
|
@ -52,3 +48,83 @@ class ScrollToTopNode: ASDisplayNode {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
final class WindowScrollToTopProxyViews {
|
||||
enum Mode {
|
||||
case disabled
|
||||
case flat(frame: CGRect)
|
||||
case split(masterFrame: CGRect, detailFrame: CGRect)
|
||||
}
|
||||
|
||||
private let flatView: ScrollToTopView
|
||||
private let masterView: ScrollToTopView
|
||||
private let detailView: ScrollToTopView
|
||||
|
||||
init(scrollToTop: @escaping (NavigationSplitContainerScrollToTop) -> Void) {
|
||||
self.flatView = ScrollToTopView(frame: CGRect())
|
||||
self.masterView = ScrollToTopView(frame: CGRect())
|
||||
self.detailView = ScrollToTopView(frame: CGRect())
|
||||
|
||||
self.flatView.action = {
|
||||
scrollToTop(.master)
|
||||
}
|
||||
self.masterView.action = {
|
||||
scrollToTop(.master)
|
||||
}
|
||||
self.detailView.action = {
|
||||
scrollToTop(.detail)
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.disable(self.flatView)
|
||||
self.disable(self.masterView)
|
||||
self.disable(self.detailView)
|
||||
}
|
||||
|
||||
func update(window: UIWindow?, mode: Mode, referenceView: UIView?) {
|
||||
guard let window else {
|
||||
self.disable(self.flatView)
|
||||
self.disable(self.masterView)
|
||||
self.disable(self.detailView)
|
||||
return
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case .disabled:
|
||||
self.disable(self.flatView)
|
||||
self.disable(self.masterView)
|
||||
self.disable(self.detailView)
|
||||
case let .flat(frame):
|
||||
self.activate(self.flatView, in: window, frame: frame, referenceView: referenceView)
|
||||
self.disable(self.masterView)
|
||||
self.disable(self.detailView)
|
||||
case let .split(masterFrame, detailFrame):
|
||||
self.disable(self.flatView)
|
||||
self.activate(self.masterView, in: window, frame: masterFrame, referenceView: referenceView)
|
||||
self.activate(self.detailView, in: window, frame: detailFrame, referenceView: referenceView)
|
||||
}
|
||||
}
|
||||
|
||||
private func activate(_ view: ScrollToTopView, in window: UIWindow, frame: CGRect, referenceView: UIView?) {
|
||||
if let referenceView, referenceView.superview === window {
|
||||
if view.superview !== window {
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
window.insertSubview(view, aboveSubview: referenceView)
|
||||
} else if view.superview !== window {
|
||||
view.removeFromSuperview()
|
||||
window.addSubview(view)
|
||||
}
|
||||
|
||||
view.isHidden = false
|
||||
view.scrollsToTop = true
|
||||
view.frame = frame
|
||||
}
|
||||
|
||||
private func disable(_ view: ScrollToTopView) {
|
||||
view.scrollsToTop = false
|
||||
view.isHidden = true
|
||||
view.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -108,6 +108,10 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/StickerPickerScreen",
|
||||
"//submodules/TelegramUI/Components/MediaEditor/ImageObjectSeparation",
|
||||
"//submodules/TelegramUI/Components/SegmentControlComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
"//submodules/TelegramUI/Components/LiquidLens",
|
||||
"//submodules/TelegramUI/Components/TabSelectionRecognizer",
|
||||
"//submodules/TelegramUI/Components/GlassControls",
|
||||
],
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
|
|
|
|||
|
|
@ -14,6 +14,8 @@ import SegmentControlComponent
|
|||
import MultilineTextComponent
|
||||
import HexColor
|
||||
import MediaEditor
|
||||
import GlassBarButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
private let palleteColors: [UInt32] = [
|
||||
0xffffff, 0xebebeb, 0xd6d6d6, 0xc2c2c2, 0xadadad, 0x999999, 0x858585, 0x707070, 0x5c5c5c, 0x474747, 0x333333, 0x000000,
|
||||
|
|
@ -1819,30 +1821,6 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
}
|
||||
|
||||
final class State: ComponentState {
|
||||
var cachedEyedropperImage: UIImage?
|
||||
var eyedropperImage: UIImage {
|
||||
let eyedropperImage: UIImage
|
||||
if let image = self.cachedEyedropperImage {
|
||||
eyedropperImage = image
|
||||
} else {
|
||||
eyedropperImage = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Eyedropper"), color: .white)!
|
||||
self.cachedEyedropperImage = eyedropperImage
|
||||
}
|
||||
return eyedropperImage
|
||||
}
|
||||
|
||||
var cachedCloseImage: UIImage?
|
||||
var closeImage: UIImage {
|
||||
let closeImage: UIImage
|
||||
if let image = self.cachedCloseImage {
|
||||
closeImage = image
|
||||
} else {
|
||||
closeImage = generateCloseButtonImage(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff))!
|
||||
self.cachedCloseImage = closeImage
|
||||
}
|
||||
return closeImage
|
||||
}
|
||||
|
||||
var selectedMode: Int = 0
|
||||
var selectedColor: DrawingColor
|
||||
|
||||
|
|
@ -1884,8 +1862,8 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
}
|
||||
|
||||
static var body: Body {
|
||||
let eyedropperButton = Child(Button.self)
|
||||
let closeButton = Child(Button.self)
|
||||
let eyedropperButton = Child(GlassBarButtonComponent.self)
|
||||
let closeButton = Child(GlassBarButtonComponent.self)
|
||||
let title = Child(MultilineTextComponent.self)
|
||||
let modeControl = Child(SegmentControlComponent.self)
|
||||
|
||||
|
|
@ -1918,50 +1896,45 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
let sideInset: CGFloat = 16.0
|
||||
|
||||
let eyedropperButton = eyedropperButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.eyedropperImage)
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: true,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(BundleIconComponent(name: "Media Editor/Eyedropper", tintColor: .white))
|
||||
),
|
||||
action: { [weak component] in
|
||||
component?.eyedropper()
|
||||
action: { _ in
|
||||
component.eyedropper()
|
||||
}
|
||||
).minSize(CGSize(width: 30.0, height: 30.0)),
|
||||
availableSize: CGSize(width: 19.0, height: 19.0),
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(eyedropperButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + eyedropperButton.size.width + 1.0, y: 29.0))
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - eyedropperButton.size.width / 2.0 - 16.0, y: 16.0 + eyedropperButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
let closeButton = closeButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(ZStack([
|
||||
AnyComponentWithIdentity(
|
||||
id: "background",
|
||||
component: AnyComponent(
|
||||
BlurredBackgroundComponent(
|
||||
color: UIColor(rgb: 0x888888, alpha: 0.1)
|
||||
)
|
||||
)
|
||||
),
|
||||
AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(
|
||||
Image(image: state.closeImage)
|
||||
)
|
||||
),
|
||||
])),
|
||||
action: { [weak component] in
|
||||
component?.dismiss()
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: true,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(BundleIconComponent(name: "Navigation/Close", tintColor: .white))
|
||||
),
|
||||
action: { _ in
|
||||
component.dismiss()
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(closeButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - closeButton.size.width - 1.0, y: 29.0))
|
||||
.clipsToBounds(true)
|
||||
.cornerRadius(15.0)
|
||||
.position(CGPoint(x: 16.0 + closeButton.size.width / 2.0, y: 16.0 + closeButton.size.height / 2.0))
|
||||
)
|
||||
|
||||
let title = title.update(
|
||||
|
|
@ -1979,12 +1952,11 @@ private final class ColorPickerContent: CombinedComponent {
|
|||
transition: .immediate
|
||||
)
|
||||
context.add(title
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: 29.0))
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: 16.0 + 22.0))
|
||||
)
|
||||
|
||||
var contentHeight: CGFloat = 58.0
|
||||
var contentHeight: CGFloat = 76.0
|
||||
|
||||
//backgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)
|
||||
let modeControl = modeControl.update(
|
||||
component: SegmentControlComponent(
|
||||
theme: SegmentControlComponent.Theme(backgroundColor: UIColor(rgb: 0xffffff, alpha: 0.07), legacyBackgroundColor: .clear, foregroundColor: UIColor(rgb: 0x6f7075, alpha: 0.6), textColor: .white, dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)),
|
||||
|
|
|
|||
|
|
@ -25,6 +25,9 @@ import FastBlur
|
|||
import MediaEditor
|
||||
import StickerPickerScreen
|
||||
import ImageObjectSeparation
|
||||
import GlassBarButtonComponent
|
||||
import GlassControls
|
||||
import BundleIconComponent
|
||||
|
||||
public struct DrawingResultData {
|
||||
public let data: Data?
|
||||
|
|
@ -363,15 +366,17 @@ private final class ReferenceContentSource: ContextReferenceContentSource {
|
|||
private let sourceView: UIView
|
||||
private let contentArea: CGRect
|
||||
private let customPosition: CGPoint
|
||||
private let actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition
|
||||
|
||||
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint) {
|
||||
init(sourceView: UIView, contentArea: CGRect, customPosition: CGPoint, actionsPosition: ContextControllerReferenceViewInfo.ActionsPosition = .top) {
|
||||
self.sourceView = sourceView
|
||||
self.contentArea = contentArea
|
||||
self.customPosition = customPosition
|
||||
self.actionsPosition = actionsPosition
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: .top)
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: self.contentArea, customPosition: self.customPosition, actionsPosition: self.actionsPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -467,13 +472,11 @@ private let bottomGradientTag = GenericComponentViewTag()
|
|||
private let undoButtonTag = GenericComponentViewTag()
|
||||
private let redoButtonTag = GenericComponentViewTag()
|
||||
private let clearAllButtonTag = GenericComponentViewTag()
|
||||
private let topButtonsTag = GenericComponentViewTag()
|
||||
private let colorButtonTag = GenericComponentViewTag()
|
||||
private let addButtonTag = GenericComponentViewTag()
|
||||
private let toolsTag = GenericComponentViewTag()
|
||||
private let modeTag = GenericComponentViewTag()
|
||||
private let flipButtonTag = GenericComponentViewTag()
|
||||
private let fillButtonTag = GenericComponentViewTag()
|
||||
private let zoomOutButtonTag = GenericComponentViewTag()
|
||||
private let textSettingsTag = GenericComponentViewTag()
|
||||
private let sizeSliderTag = GenericComponentViewTag()
|
||||
private let fontTag = GenericComponentViewTag()
|
||||
|
|
@ -489,6 +492,12 @@ private let colorTags = [color1Tag, color2Tag, color3Tag, color4Tag, color5Tag,
|
|||
private let cancelButtonTag = GenericComponentViewTag()
|
||||
private let doneButtonTag = GenericComponentViewTag()
|
||||
|
||||
enum DrawingMode: Int {
|
||||
case drawing
|
||||
case sticker
|
||||
case text
|
||||
}
|
||||
|
||||
private final class DrawingScreenComponent: CombinedComponent {
|
||||
typealias EnvironmentType = ViewControllerComponentContainer.Environment
|
||||
|
||||
|
|
@ -616,7 +625,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
final class State: ComponentState {
|
||||
enum ImageKey: Hashable {
|
||||
case undo
|
||||
case redo
|
||||
case done
|
||||
case add
|
||||
case fill
|
||||
|
|
@ -633,8 +641,6 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
switch key {
|
||||
case .undo:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Undo"), color: .white)!
|
||||
case .redo:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Redo"), color: .white)!
|
||||
case .done:
|
||||
image = generateTintedImage(image: UIImage(bundleImageName: "Media Editor/Done"), color: .white)!
|
||||
case .add:
|
||||
|
|
@ -652,13 +658,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
return image
|
||||
}
|
||||
}
|
||||
|
||||
enum Mode {
|
||||
case drawing
|
||||
case sticker
|
||||
case text
|
||||
}
|
||||
|
||||
|
||||
private let context: AccountContext
|
||||
private let updateToolState: ActionSlot<DrawingToolState>
|
||||
private let insertEntity: ActionSlot<DrawingEntity>
|
||||
|
|
@ -675,7 +675,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
private let entityViewForEntity: (DrawingEntity) -> DrawingEntityView?
|
||||
private let present: (ViewController) -> Void
|
||||
|
||||
var currentMode: Mode
|
||||
var currentMode: DrawingMode
|
||||
var drawingState: DrawingState
|
||||
var drawingViewState: DrawingView.NavigationState
|
||||
var currentColor: DrawingColor
|
||||
|
|
@ -992,7 +992,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
self.present(contextController)
|
||||
}
|
||||
|
||||
func updateCurrentMode(_ mode: Mode, update: Bool = true) {
|
||||
func updateCurrentMode(_ mode: DrawingMode, update: Bool = true) {
|
||||
self.currentMode = mode
|
||||
if let selectedEntity = self.selectedEntity {
|
||||
if selectedEntity is DrawingStickerEntity || selectedEntity is DrawingTextEntity {
|
||||
|
|
@ -1058,15 +1058,13 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
let topGradient = Child(BlurredGradientComponent.self)
|
||||
let bottomGradient = Child(BlurredGradientComponent.self)
|
||||
|
||||
let undoButton = Child(Button.self)
|
||||
let undoButton = Child(GlassBarButtonComponent.self)
|
||||
let redoButton = Child(GlassBarButtonComponent.self)
|
||||
let clearAllButton = Child(GlassBarButtonComponent.self)
|
||||
let topButtons = Child(GlassControlPanelComponent.self)
|
||||
|
||||
let redoButton = Child(Button.self)
|
||||
let clearAllButton = Child(Button.self)
|
||||
|
||||
let zoomOutButton = Child(Button.self)
|
||||
|
||||
let tools = Child(ToolsComponent.self)
|
||||
let modeAndSize = Child(ModeAndSizeComponent.self)
|
||||
let mode = Child(ModeComponent.self)
|
||||
|
||||
let colorButton = Child(ColorSwatchComponent.self)
|
||||
|
||||
|
|
@ -1082,16 +1080,11 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
let swatch8Button = Child(ColorSwatchComponent.self)
|
||||
|
||||
let addButton = Child(Button.self)
|
||||
|
||||
let flipButton = Child(Button.self)
|
||||
let fillButton = Child(Button.self)
|
||||
|
||||
let backButton = Child(Button.self)
|
||||
let doneButton = Child(Button.self)
|
||||
|
||||
let backButton = Child(GlassBarButtonComponent.self)
|
||||
let doneButton = Child(GlassBarButtonComponent.self)
|
||||
|
||||
let textSize = Child(TextSizeSliderComponent.self)
|
||||
let textCancelButton = Child(Button.self)
|
||||
let textDoneButton = Child(Button.self)
|
||||
|
||||
let presetColors: [DrawingColor] = [
|
||||
DrawingColor(rgb: 0xff453a),
|
||||
|
|
@ -1220,6 +1213,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
var additionalBottomInset: CGFloat = 0.0
|
||||
if component.sourceHint == .storyEditor {
|
||||
additionalBottomInset = max(0.0, previewBottomInset - environment.safeInsets.bottom - 49.0)
|
||||
} else {
|
||||
additionalBottomInset = 8.0
|
||||
}
|
||||
|
||||
if let textEntity = state.selectedEntity as? DrawingTextEntity {
|
||||
|
|
@ -1318,8 +1313,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
)
|
||||
}
|
||||
|
||||
let rightButtonPosition = rightEdge - 24.0
|
||||
var offsetX: CGFloat = leftEdge + 24.0
|
||||
let rightButtonPosition = rightEdge - 28.0
|
||||
var offsetX: CGFloat = leftEdge + 28.0
|
||||
let delta: CGFloat = (rightButtonPosition - offsetX) / 7.0
|
||||
|
||||
let applySwatchColor: (DrawingColor) -> Void = { [weak state] color in
|
||||
|
|
@ -1608,6 +1603,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
}
|
||||
|
||||
var hasTopButtons = false
|
||||
var centerItems: [GlassControlGroupComponent.Item] = []
|
||||
if let entity = state.selectedEntity {
|
||||
var isFilled: Bool?
|
||||
if let entity = entity as? DrawingSimpleShapeEntity {
|
||||
|
|
@ -1625,12 +1621,13 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
|
||||
hasTopButtons = isFilled != nil || hasFlip
|
||||
|
||||
if let isFilled = isFilled {
|
||||
let fillButton = fillButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(isFilled ? .fill : .stroke))
|
||||
),
|
||||
if controlsAreVisible {
|
||||
if let isFilled = isFilled {
|
||||
centerItems.append(GlassControlGroupComponent.Item(
|
||||
id: AnyHashable("fill"),
|
||||
content: .customIcon(id: AnyHashable("fill"), component: AnyComponent(
|
||||
Image(image: state.image(isFilled ? .fill : .stroke), size: CGSize(width: 30.0, height: 30.0))
|
||||
), insets: .zero),
|
||||
action: { [weak state] in
|
||||
guard let state = state else {
|
||||
return
|
||||
|
|
@ -1661,25 +1658,15 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
}
|
||||
state.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(fillButtonTag),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(fillButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.opacity(!controlsAreVisible ? 0.0 : 1.0)
|
||||
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||
)
|
||||
}
|
||||
|
||||
if hasFlip {
|
||||
let flipButton = flipButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.flip))
|
||||
),
|
||||
))
|
||||
}
|
||||
|
||||
if hasFlip {
|
||||
centerItems.append(GlassControlGroupComponent.Item(
|
||||
id: AnyHashable("flip"),
|
||||
content: .customIcon(id: AnyHashable("flip"), component: AnyComponent(
|
||||
Image(image: state.image(.flip), size: CGSize(width: 30.0, height: 30.0))
|
||||
), insets: .zero),
|
||||
action: { [weak state] in
|
||||
guard let state = state else {
|
||||
return
|
||||
|
|
@ -1695,17 +1682,8 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
}
|
||||
state.updated(transition: .easeInOut(duration: 0.2))
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(flipButtonTag),
|
||||
availableSize: CGSize(width: 30.0, height: 30.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(flipButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.opacity(!controlsAreVisible ? 0.0 : 1.0)
|
||||
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||
)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1730,28 +1708,24 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
sizeValue = entity.lineWidth
|
||||
}
|
||||
}
|
||||
if state.drawingViewState.canZoomOut && !hasTopButtons {
|
||||
let zoomOutButton = zoomOutButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
ZoomOutButtonContent(
|
||||
title: strings.Paint_ZoomOut,
|
||||
image: state.image(.zoomOut)
|
||||
)
|
||||
),
|
||||
action: {
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.zoomOut)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(zoomOutButtonTag),
|
||||
availableSize: CGSize(width: 120.0, height: 33.0),
|
||||
transition: .immediate
|
||||
)
|
||||
context.add(zoomOutButton
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: environment.safeInsets.top + 32.0 - UIScreenPixel))
|
||||
.appear(.default(scale: true, alpha: true))
|
||||
.disappear(.default(scale: true, alpha: true))
|
||||
)
|
||||
if state.drawingViewState.canZoomOut && !hasTopButtons && controlsAreVisible {
|
||||
centerItems.append(GlassControlGroupComponent.Item(
|
||||
id: AnyHashable("zoomOut"),
|
||||
content: .customIcon(id: AnyHashable("zoomOut"), component: AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(
|
||||
Image(image: state.image(.zoomOut), size: CGSize(width: 24.0, height: 24.0))
|
||||
)),
|
||||
AnyComponentWithIdentity(id: "label", component: AnyComponent(
|
||||
Text(text: environment.strings.Paint_ZoomOut, font: Font.regular(17.0), color: .white)
|
||||
)),
|
||||
], spacing: 2.0)
|
||||
), insets: UIEdgeInsets(top: 0.0, left: 10.0, bottom: 0.0, right: 12.0)),
|
||||
action: {
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.zoomOut)
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
if let sizeValue {
|
||||
|
|
@ -1784,111 +1758,170 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
.position(CGPoint(x: textSize.size.width / 2.0, y: topInset + (context.availableSize.height - topInset - bottomInset) / 2.0))
|
||||
.opacity(sizeSliderVisible && controlsAreVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let undoButton = undoButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.undo))
|
||||
),
|
||||
isEnabled: state.drawingViewState.canUndo,
|
||||
action: {
|
||||
|
||||
var leftItems: [GlassControlGroupComponent.Item] = []
|
||||
var rightItems: [GlassControlGroupComponent.Item] = []
|
||||
|
||||
if !isEditingText && controlsAreVisible {
|
||||
leftItems.append(GlassControlGroupComponent.Item(
|
||||
id: "undo",
|
||||
content: .customIcon(id: "undo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canUndo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0))), insets: .zero),
|
||||
action: state.drawingViewState.canUndo ? {
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.undo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(undoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
} : nil
|
||||
))
|
||||
|
||||
if state.drawingViewState.canRedo {
|
||||
leftItems.append(GlassControlGroupComponent.Item(
|
||||
id: "redo",
|
||||
content: .customIcon(id: "redo", component: AnyComponent(Image(image: state.image(.undo), tintColor: state.drawingViewState.canRedo ? .white : .white.withAlphaComponent(0.4), size: CGSize(width: 30.0, height: 30.0), flipHorizontally: true)), insets: .zero),
|
||||
action: state.drawingViewState.canRedo ? {
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.redo)
|
||||
} : nil
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
let undoButton = undoButton.update(
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: true,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(Image(image: state.image(.undo)))
|
||||
),
|
||||
action: { _ in
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.undo)
|
||||
},
|
||||
tag: undoButtonTag
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(undoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 2.0, y: topInset))
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width / 2.0 + 16.0, y: topInset))
|
||||
.scale(isEditingText ? 0.01 : 1.0)
|
||||
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
|
||||
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||
.opacity(0.0)
|
||||
//.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
|
||||
)
|
||||
|
||||
|
||||
let redoButton = redoButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.redo))
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: true,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(Image(image: state.image(.undo), flipHorizontally: true))
|
||||
),
|
||||
action: {
|
||||
action: { _ in
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.redo)
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(redoButtonTag),
|
||||
availableSize: CGSize(width: 24.0, height: 24.0),
|
||||
},
|
||||
tag: redoButtonTag
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(redoButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + undoButton.size.width + 2.0 + redoButton.size.width / 2.0, y: topInset))
|
||||
.scale(state.drawingViewState.canRedo && !isEditingText ? 1.0 : 0.01)
|
||||
.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
|
||||
.shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil)
|
||||
.opacity(0.0)
|
||||
//.opacity(state.drawingViewState.canRedo && !isEditingText && controlsAreVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let clearAllButton = clearAllButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white)),
|
||||
textShadowColor: component.sourceHint == .storyEditor ? UIColor(rgb: 0x000000, alpha: 0.35) : nil,
|
||||
textShadowBlur: 2.0
|
||||
)
|
||||
component: GlassBarButtonComponent(
|
||||
size: nil,
|
||||
backgroundColor: nil,
|
||||
isDark: true,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "label",
|
||||
component: AnyComponent(MultilineTextComponent(
|
||||
text: .plain(NSAttributedString(string: strings.Paint_Clear, font: Font.regular(17.0), textColor: .white))
|
||||
))
|
||||
),
|
||||
isEnabled: state.drawingViewState.canClear,
|
||||
action: {
|
||||
action: { _ in
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.clear)
|
||||
}
|
||||
).tagged(clearAllButtonTag),
|
||||
},
|
||||
tag: clearAllButtonTag
|
||||
),
|
||||
availableSize: CGSize(width: 180.0, height: 30.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(clearAllButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - clearAllButton.size.width / 2.0 - 13.0, y: topInset))
|
||||
.scale(isEditingText ? 0.01 : 1.0)
|
||||
.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
|
||||
.opacity(0.0)
|
||||
//.opacity(isEditingText || !controlsAreVisible ? 0.0 : 1.0)
|
||||
)
|
||||
|
||||
let textCancelButton = textCancelButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Text(text: environment.strings.Common_Cancel, font: Font.regular(17.0), color: .white)
|
||||
),
|
||||
if !isEditingText && controlsAreVisible {
|
||||
rightItems.append(GlassControlGroupComponent.Item(
|
||||
id: "clearAll",
|
||||
content: .text(strings.Paint_Clear),
|
||||
action: state.drawingViewState.canClear ? {
|
||||
dismissEyedropper.invoke(Void())
|
||||
performAction.invoke(.clear)
|
||||
} : nil
|
||||
))
|
||||
}
|
||||
|
||||
if isEditingText {
|
||||
leftItems.append(GlassControlGroupComponent.Item(
|
||||
id: "cancel",
|
||||
content: .text(strings.Common_Cancel),
|
||||
action: { [weak state] in
|
||||
if let entity = state?.selectedEntity as? DrawingTextEntity {
|
||||
endEditingTextEntityView.invoke((entity.uuid, true))
|
||||
}
|
||||
}
|
||||
),
|
||||
availableSize: CGSize(width: 100.0, height: 30.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(textCancelButton
|
||||
.position(CGPoint(x: environment.safeInsets.left + textCancelButton.size.width / 2.0 + 13.0, y: topInset))
|
||||
.scale(isEditingText ? 1.0 : 0.01)
|
||||
.opacity(isEditingText ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let textDoneButton = textDoneButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Text(text: environment.strings.Common_Done, font: Font.semibold(17.0), color: .white)
|
||||
),
|
||||
))
|
||||
|
||||
rightItems.append(GlassControlGroupComponent.Item(
|
||||
id: "done",
|
||||
content: .text(strings.Common_Done),
|
||||
action: { [weak state] in
|
||||
if let entity = state?.selectedEntity as? DrawingTextEntity {
|
||||
endEditingTextEntityView.invoke((entity.uuid, false))
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
let topButtons = topButtons.update(
|
||||
component: GlassControlPanelComponent(
|
||||
theme: defaultDarkPresentationTheme,
|
||||
leftItem: leftItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
||||
items: leftItems,
|
||||
background: .panel
|
||||
),
|
||||
centralItem: centerItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
||||
items: centerItems,
|
||||
background: .panel
|
||||
),
|
||||
rightItem: rightItems.isEmpty ? nil : GlassControlPanelComponent.Item(
|
||||
items: rightItems,
|
||||
background: .panel
|
||||
),
|
||||
centerAlignmentIfPossible: true,
|
||||
isDark: true,
|
||||
tag: topButtonsTag
|
||||
),
|
||||
availableSize: CGSize(width: 100.0, height: 30.0),
|
||||
availableSize: CGSize(width: context.availableSize.width - 32.0, height: 44.0),
|
||||
transition: context.transition
|
||||
)
|
||||
context.add(textDoneButton
|
||||
.position(CGPoint(x: context.availableSize.width - environment.safeInsets.right - textDoneButton.size.width / 2.0 - 13.0, y: topInset))
|
||||
.scale(isEditingText ? 1.0 : 0.01)
|
||||
.opacity(isEditingText ? 1.0 : 0.0)
|
||||
context.add(topButtons
|
||||
.position(CGPoint(x: context.availableSize.width / 2.0, y: topInset))
|
||||
//.opacity(isEditingText ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
var color: DrawingColor?
|
||||
|
|
@ -1930,7 +1963,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
transition: context.transition
|
||||
)
|
||||
context.add(colorButton
|
||||
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 2.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
|
||||
.position(CGPoint(x: leftEdge + colorButton.size.width / 2.0 + 6.0, y: context.availableSize.height - environment.safeInsets.bottom - colorButton.size.height / 2.0 - 89.0 - additionalBottomInset))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.opacity(controlsAreVisible ? 1.0 : 0.0)
|
||||
|
|
@ -1979,7 +2012,7 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
transition: .immediate
|
||||
)
|
||||
context.add(addButton
|
||||
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 2.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
|
||||
.position(CGPoint(x: rightEdge - addButton.size.width / 2.0 - 6.0, y: context.availableSize.height - environment.safeInsets.bottom - addButton.size.height / 2.0 - 89.0 - additionalBottomInset))
|
||||
.appear(.default(scale: true))
|
||||
.disappear(.default(scale: true))
|
||||
.cornerRadius(12.0)
|
||||
|
|
@ -1987,27 +2020,38 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
)
|
||||
|
||||
let doneButton = doneButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
Image(image: state.image(.done))
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: UIColor(rgb: 0x0088ff),
|
||||
isDark: true,
|
||||
state: .tintedGlass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: "Navigation/Done", tintColor: .white)
|
||||
)
|
||||
),
|
||||
action: { [weak state] in
|
||||
action: { [weak state] _ in
|
||||
dismissEyedropper.invoke(Void())
|
||||
state?.saveToolState()
|
||||
apply.invoke(Void())
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(doneButtonTag),
|
||||
availableSize: CGSize(width: 33.0, height: 33.0),
|
||||
},
|
||||
tag: doneButtonTag
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: .immediate
|
||||
)
|
||||
|
||||
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 14.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
doneButtonPosition.x = doneButtonPosition.x - 2.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
doneButtonPosition.x -= 20.0
|
||||
}
|
||||
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset
|
||||
doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) + controlsBottomInset + 5.0
|
||||
} else {
|
||||
doneButtonPosition.x = doneButtonPosition.x - 12.0
|
||||
doneButtonPosition.y -= 3.0 - UIScreenPixel
|
||||
}
|
||||
context.add(doneButton
|
||||
.position(doneButtonPosition)
|
||||
|
|
@ -2026,117 +2070,82 @@ private final class DrawingScreenComponent: CombinedComponent {
|
|||
})
|
||||
.opacity(controlsAreVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
let selectedIndex: Int
|
||||
switch state.currentMode {
|
||||
case .drawing:
|
||||
selectedIndex = 0
|
||||
case .sticker:
|
||||
selectedIndex = 1
|
||||
case .text:
|
||||
selectedIndex = 2
|
||||
}
|
||||
|
||||
var selectedSize: CGFloat = 0.0
|
||||
if let entity = state.selectedEntity {
|
||||
selectedSize = entity.lineWidth
|
||||
} else {
|
||||
selectedSize = state.drawingState.toolState(for: state.drawingState.selectedTool).size ?? 0.0
|
||||
}
|
||||
|
||||
let modeAndSize = modeAndSize.update(
|
||||
component: ModeAndSizeComponent(
|
||||
values: [ strings.Paint_Draw, strings.Paint_Sticker, strings.Paint_Text],
|
||||
sizeValue: selectedSize,
|
||||
isEditing: false,
|
||||
isEnabled: true,
|
||||
rightInset: modeRightInset - 57.0,
|
||||
tag: modeTag,
|
||||
selectedIndex: selectedIndex,
|
||||
selectionChanged: { [weak state] index in
|
||||
dismissEyedropper.invoke(Void())
|
||||
guard let state = state else {
|
||||
return
|
||||
}
|
||||
switch index {
|
||||
case 1:
|
||||
state.presentStickerPicker()
|
||||
case 2:
|
||||
state.addTextEntity()
|
||||
default:
|
||||
state.updateCurrentMode(.drawing)
|
||||
}
|
||||
},
|
||||
sizeUpdated: { [weak state] size in
|
||||
if let state = state {
|
||||
|
||||
let mode = mode.update(
|
||||
component: ModeComponent(
|
||||
isTablet: false,
|
||||
strings: environment.strings,
|
||||
tintColor: .white,
|
||||
availableModes: [.drawing, .sticker, .text],
|
||||
currentMode: state.currentMode,
|
||||
updatedMode: { [weak state] mode in
|
||||
if let state {
|
||||
dismissEyedropper.invoke(Void())
|
||||
state.updateBrushSize(size)
|
||||
if state.selectedEntity == nil {
|
||||
previewBrushSize.invoke(size)
|
||||
switch mode {
|
||||
case .drawing:
|
||||
state.updateCurrentMode(.drawing)
|
||||
case .sticker:
|
||||
state.presentStickerPicker()
|
||||
case .text:
|
||||
state.addTextEntity()
|
||||
}
|
||||
}
|
||||
},
|
||||
sizeReleased: {
|
||||
previewBrushSize.invoke(nil)
|
||||
}
|
||||
tag: modeTag
|
||||
),
|
||||
availableSize: CGSize(width: availableWidth - 57.0 - modeRightInset, height: context.availableSize.height),
|
||||
availableSize: CGSize(width: context.availableSize.width - 66.0 * 2.0, height: 44.0),
|
||||
transition: context.transition
|
||||
)
|
||||
var modeAndSizePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - modeAndSize.size.height / 2.0 - 9.0)
|
||||
var modePosition = CGPoint(x: context.availableSize.width / 2.0 - (modeRightInset - 57.0) / 2.0, y: context.availableSize.height - environment.safeInsets.bottom - mode.size.height / 2.0 - 9.0)
|
||||
if component.sourceHint == .storyEditor {
|
||||
modeAndSizePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + modeAndSize.size.height / 2.0) + controlsBottomInset
|
||||
modePosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 8.0 + mode.size.height / 2.0) + controlsBottomInset
|
||||
} else {
|
||||
modePosition.y += 4.0
|
||||
}
|
||||
context.add(modeAndSize
|
||||
.position(modeAndSizePosition)
|
||||
context.add(mode
|
||||
.position(modePosition)
|
||||
.opacity(controlsAreVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
var animatingOut = false
|
||||
if let appearanceTransition = context.transition.userData(DrawingScreenTransition.self), case .animateOut = appearanceTransition {
|
||||
animatingOut = true
|
||||
}
|
||||
|
||||
if animatingOut && component.sourceHint == .storyEditor {
|
||||
|
||||
} else {
|
||||
let backButton = backButton.update(
|
||||
component: Button(
|
||||
content: AnyComponent(
|
||||
LottieAnimationComponent(
|
||||
animation: LottieAnimationComponent.AnimationItem(
|
||||
name: "media_backToCancel",
|
||||
mode: .animating(loop: false),
|
||||
range: animatingOut || component.isAvatar ? (0.5, 1.0) : (0.0, 0.5)
|
||||
),
|
||||
colors: ["__allcolors__": .white],
|
||||
size: CGSize(width: 33.0, height: 33.0)
|
||||
)
|
||||
),
|
||||
action: { [weak state] in
|
||||
if let state = state {
|
||||
dismissEyedropper.invoke(Void())
|
||||
state.saveToolState()
|
||||
dismiss.invoke(Void())
|
||||
}
|
||||
let backButton = backButton.update(
|
||||
component: GlassBarButtonComponent(
|
||||
size: CGSize(width: 44.0, height: 44.0),
|
||||
backgroundColor: nil,
|
||||
isDark: true,
|
||||
state: .glass,
|
||||
component: AnyComponentWithIdentity(
|
||||
id: "icon",
|
||||
component: AnyComponent(
|
||||
BundleIconComponent(name: "Navigation/Close", tintColor: .white)
|
||||
)
|
||||
),
|
||||
action: { [weak state] _ in
|
||||
if let state {
|
||||
dismissEyedropper.invoke(Void())
|
||||
state.saveToolState()
|
||||
dismiss.invoke(Void())
|
||||
}
|
||||
).minSize(CGSize(width: 44.0, height: 44.0)).tagged(cancelButtonTag),
|
||||
availableSize: CGSize(width: 33.0, height: 33.0),
|
||||
transition: .immediate
|
||||
)
|
||||
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
backButtonPosition.x = backButtonPosition.x + 2.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
backButtonPosition.x += 20.0
|
||||
}
|
||||
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset
|
||||
},
|
||||
tag: cancelButtonTag
|
||||
),
|
||||
availableSize: CGSize(width: 44.0, height: 44.0),
|
||||
transition: .immediate
|
||||
)
|
||||
var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 14.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel)
|
||||
if component.sourceHint == .storyEditor {
|
||||
backButtonPosition.x = backButtonPosition.x + 2.0
|
||||
if case .regular = environment.metrics.widthClass {
|
||||
backButtonPosition.x += 20.0
|
||||
}
|
||||
context.add(backButton
|
||||
.position(backButtonPosition)
|
||||
.opacity(controlsAreVisible ? 1.0 : 0.0)
|
||||
)
|
||||
backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) + controlsBottomInset + 5.0
|
||||
} else {
|
||||
backButtonPosition.x = backButtonPosition.x + 12.0
|
||||
backButtonPosition.y -= 3.0 - UIScreenPixel
|
||||
}
|
||||
context.add(backButton
|
||||
.position(backButtonPosition)
|
||||
.opacity(controlsAreVisible ? 1.0 : 0.0)
|
||||
)
|
||||
|
||||
return context.availableSize
|
||||
}
|
||||
|
|
@ -2224,22 +2233,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||
self.performAction.connect { [weak self] action in
|
||||
if let self {
|
||||
if case .clear = action {
|
||||
let actionSheet = ActionSheetController(presentationData: self.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Paint_ClearConfirm, color: .destructive, action: { [weak actionSheet, weak self] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
self?._drawingView?.performAction(action)
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: self.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
self.controller?.present(actionSheet, in: .window(.root))
|
||||
let sourceView: UIView
|
||||
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View, let rightItemView = topButtonsView.rightItemView, let clearAllView = rightItemView.itemView(id: AnyHashable("clearAll")) {
|
||||
sourceView = clearAllView
|
||||
} else {
|
||||
sourceView = self.view
|
||||
}
|
||||
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(ContextMenuActionItem(text: self.presentationData.strings.Paint_ClearConfirm, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
self?._drawingView?.performAction(.clear)
|
||||
}))
|
||||
]
|
||||
let presentationData = self.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint(), actionsPosition: .bottom)), items: .single(ContextController.Items(content: .list(items))))
|
||||
self.controller?.present(contextController, in: .window(.root))
|
||||
} else {
|
||||
self._drawingView?.performAction(action)
|
||||
}
|
||||
|
|
@ -2403,22 +2414,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||
self.dismiss.connect { [weak self] _ in
|
||||
if let strongSelf = self {
|
||||
if strongSelf.drawingView.canUndo || strongSelf.entitiesView.hasChanges {
|
||||
let actionSheet = ActionSheetController(presentationData: strongSelf.presentationData.withUpdated(theme: defaultDarkColorPresentationTheme))
|
||||
actionSheet.setItemGroups([
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, color: .accent, action: { [weak actionSheet, weak self] in
|
||||
actionSheet?.dismissAnimated()
|
||||
|
||||
self?.controller?.requestDismiss()
|
||||
})
|
||||
]),
|
||||
ActionSheetItemGroup(items: [
|
||||
ActionSheetButtonItem(title: strongSelf.presentationData.strings.Common_Cancel, color: .accent, font: .bold, action: { [weak actionSheet] in
|
||||
actionSheet?.dismissAnimated()
|
||||
})
|
||||
])
|
||||
])
|
||||
strongSelf.controller?.present(actionSheet, in: .window(.root))
|
||||
let sourceView = strongSelf.componentHost.findTaggedView(tag: cancelButtonTag) ?? strongSelf.view
|
||||
let items: [ContextMenuItem] = [
|
||||
.action(ContextMenuActionItem(text: strongSelf.presentationData.strings.PhotoEditor_DiscardChanges, textColor: .destructive, icon: { _ in
|
||||
return nil
|
||||
}, action: { [weak self] f in
|
||||
f.dismissWithResult(.default)
|
||||
self?.controller?.requestDismiss()
|
||||
}))
|
||||
]
|
||||
let presentationData = strongSelf.presentationData.withUpdated(theme: defaultDarkPresentationTheme)
|
||||
let contextController = makeContextController(presentationData: presentationData, source: .reference(ReferenceContentSource(sourceView: sourceView, contentArea: UIScreen.main.bounds, customPosition: CGPoint())), items: .single(ContextController.Items(content: .list(items))))
|
||||
strongSelf.controller?.present(contextController, in: .window(.root))
|
||||
} else {
|
||||
strongSelf.controller?.requestDismiss()
|
||||
}
|
||||
|
|
@ -2497,17 +2504,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||
if let view = self.componentHost.findTaggedView(tag: bottomGradientTag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
|
||||
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View {
|
||||
topButtonsView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
|
||||
if let leftItemView = topButtonsView.leftItemView {
|
||||
leftItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
leftItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
}
|
||||
if let rightItemView = topButtonsView.rightItemView {
|
||||
rightItemView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
rightItemView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
|
||||
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
if let view = self.componentHost.findTaggedView(tag: modeTag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) {
|
||||
buttonView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
if let view = self.componentHost.findTaggedView(tag: addButtonTag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
}
|
||||
var delay: Double = 0.0
|
||||
for tag in colorTags {
|
||||
|
|
@ -2520,6 +2534,15 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
||||
view.layer.animatePosition(from: CGPoint(x: -33.0, y: 0.0), to: CGPoint(), duration: 0.3, additive: true)
|
||||
}
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: doneButtonTag) {
|
||||
view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
view.layer.animateScale(from: 0.01, to: 1.0, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
func animateOut(completion: @escaping () -> Void) {
|
||||
|
|
@ -2536,43 +2559,24 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||
}
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: undoButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: redoButtonTag), buttonView.alpha > 0.0 {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: clearAllButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
if let topButtonsView = self.componentHost.findTaggedView(tag: topButtonsTag) as? GlassControlPanelComponent.View {
|
||||
topButtonsView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
|
||||
if let view = topButtonsView.leftItemView {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
if let view = topButtonsView.rightItemView {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: colorButtonTag) as? ColorSwatchComponent.View {
|
||||
view.animateOut()
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: addButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: flipButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: fillButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: zoomOutButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
buttonView.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
if let view = self.componentHost.findTaggedView(tag: addButtonTag) {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
if let view = self.componentHost.findTaggedView(tag: sizeSliderTag) {
|
||||
view.layer.animatePosition(from: CGPoint(), to: CGPoint(x: -33.0, y: 0.0), duration: 0.3, removeOnCompletion: false, additive: true)
|
||||
|
|
@ -2596,12 +2600,18 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U
|
|||
})
|
||||
}
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: modeTag) as? ModeAndSizeComponent.View {
|
||||
view.animateOut()
|
||||
if let view = self.componentHost.findTaggedView(tag: modeTag) {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
if let buttonView = self.componentHost.findTaggedView(tag: doneButtonTag) {
|
||||
buttonView.alpha = 0.0
|
||||
buttonView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3, removeOnCompletion: false)
|
||||
}
|
||||
|
||||
if let view = self.componentHost.findTaggedView(tag: doneButtonTag) {
|
||||
view.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false)
|
||||
view.layer.animateScale(from: 1.0, to: 0.01, duration: 0.3)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,95 +2,123 @@ import Foundation
|
|||
import UIKit
|
||||
import Display
|
||||
import ComponentFlow
|
||||
import LegacyComponents
|
||||
import TelegramCore
|
||||
import SegmentedControlNode
|
||||
import MultilineTextComponent
|
||||
import TelegramPresentationData
|
||||
import GlassBackgroundComponent
|
||||
import LiquidLens
|
||||
import TabSelectionRecognizer
|
||||
|
||||
private func generateMaskPath(size: CGSize, leftRadius: CGFloat, rightRadius: CGFloat) -> UIBezierPath {
|
||||
let path = UIBezierPath()
|
||||
path.addArc(withCenter: CGPoint(x: leftRadius, y: size.height / 2.0), radius: leftRadius, startAngle: .pi * 0.5, endAngle: -.pi * 0.5, clockwise: true)
|
||||
path.addArc(withCenter: CGPoint(x: size.width - rightRadius, y: size.height / 2.0), radius: rightRadius, startAngle: -.pi * 0.5, endAngle: .pi * 0.5, clockwise: true)
|
||||
path.close()
|
||||
return path
|
||||
private let buttonSize = CGSize(width: 55.0, height: 44.0)
|
||||
private let tabletButtonSize = CGSize(width: 55.0, height: 44.0)
|
||||
|
||||
extension DrawingMode {
|
||||
func title(strings: PresentationStrings) -> String {
|
||||
switch self {
|
||||
case .drawing:
|
||||
return strings.Paint_Draw
|
||||
case .sticker:
|
||||
return strings.Paint_Sticker
|
||||
case .text:
|
||||
return strings.Paint_Text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func generateKnobImage() -> UIImage? {
|
||||
let side: CGFloat = 28.0
|
||||
let margin: CGFloat = 10.0
|
||||
|
||||
let image = generateImage(CGSize(width: side + margin * 2.0, height: side + margin * 2.0), opaque: false, rotatedContext: { size, context in
|
||||
context.clear(CGRect(origin: .zero, size: size))
|
||||
|
||||
context.setShadow(offset: CGSize(width: 0.0, height: 0.0), blur: 9.0, color: UIColor(rgb: 0x000000, alpha: 0.3).cgColor)
|
||||
context.setFillColor(UIColor.white.cgColor)
|
||||
context.fillEllipse(in: CGRect(origin: CGPoint(x: margin, y: margin), size: CGSize(width: side, height: side)))
|
||||
})
|
||||
return image?.stretchableImage(withLeftCapWidth: Int(margin + side * 0.5), topCapHeight: Int(margin + side * 0.5))
|
||||
}
|
||||
|
||||
final class ModeAndSizeComponent: Component {
|
||||
let values: [String]
|
||||
let sizeValue: CGFloat
|
||||
let isEditing: Bool
|
||||
let isEnabled: Bool
|
||||
let rightInset: CGFloat
|
||||
final class ModeComponent: Component {
|
||||
let isTablet: Bool
|
||||
let strings: PresentationStrings
|
||||
let tintColor: UIColor
|
||||
let availableModes: [DrawingMode]
|
||||
let currentMode: DrawingMode
|
||||
let updatedMode: (DrawingMode) -> Void
|
||||
let tag: AnyObject?
|
||||
let selectedIndex: Int
|
||||
let selectionChanged: (Int) -> Void
|
||||
let sizeUpdated: (CGFloat) -> Void
|
||||
let sizeReleased: () -> Void
|
||||
|
||||
init(values: [String], sizeValue: CGFloat, isEditing: Bool, isEnabled: Bool, rightInset: CGFloat, tag: AnyObject?, selectedIndex: Int, selectionChanged: @escaping (Int) -> Void, sizeUpdated: @escaping (CGFloat) -> Void, sizeReleased: @escaping () -> Void) {
|
||||
self.values = values
|
||||
self.sizeValue = sizeValue
|
||||
self.isEditing = isEditing
|
||||
self.isEnabled = isEnabled
|
||||
self.rightInset = rightInset
|
||||
init(
|
||||
isTablet: Bool,
|
||||
strings: PresentationStrings,
|
||||
tintColor: UIColor,
|
||||
availableModes: [DrawingMode],
|
||||
currentMode: DrawingMode,
|
||||
updatedMode: @escaping (DrawingMode) -> Void,
|
||||
tag: AnyObject?
|
||||
) {
|
||||
self.isTablet = isTablet
|
||||
self.strings = strings
|
||||
self.tintColor = tintColor
|
||||
self.availableModes = availableModes
|
||||
self.currentMode = currentMode
|
||||
self.updatedMode = updatedMode
|
||||
self.tag = tag
|
||||
self.selectedIndex = selectedIndex
|
||||
self.selectionChanged = selectionChanged
|
||||
self.sizeUpdated = sizeUpdated
|
||||
self.sizeReleased = sizeReleased
|
||||
}
|
||||
|
||||
static func ==(lhs: ModeAndSizeComponent, rhs: ModeAndSizeComponent) -> Bool {
|
||||
if lhs.values != rhs.values {
|
||||
static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool {
|
||||
if lhs.isTablet != rhs.isTablet {
|
||||
return false
|
||||
}
|
||||
if lhs.sizeValue != rhs.sizeValue {
|
||||
if lhs.strings !== rhs.strings {
|
||||
return false
|
||||
}
|
||||
if lhs.isEditing != rhs.isEditing {
|
||||
if lhs.tintColor != rhs.tintColor {
|
||||
return false
|
||||
}
|
||||
if lhs.isEnabled != rhs.isEnabled {
|
||||
if lhs.availableModes != rhs.availableModes {
|
||||
return false
|
||||
}
|
||||
if lhs.rightInset != rhs.rightInset {
|
||||
return false
|
||||
}
|
||||
if lhs.selectedIndex != rhs.selectedIndex {
|
||||
if lhs.currentMode != rhs.currentMode {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
final class View: UIView, UIGestureRecognizerDelegate, ComponentTaggedView {
|
||||
private let backgroundNode: NavigationBackgroundNode
|
||||
private let node: SegmentedControlNode
|
||||
|
||||
final class View: UIView, ComponentTaggedView, UIScrollViewDelegate, UIGestureRecognizerDelegate {
|
||||
private final class ScrollView: UIScrollView {
|
||||
override func touchesShouldCancel(in view: UIView) -> Bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private var knob: UIImageView
|
||||
private struct LayoutData {
|
||||
var containerSize: CGSize
|
||||
var selectedFrame: CGRect
|
||||
var cornerRadius: CGFloat?
|
||||
var isTablet: Bool
|
||||
}
|
||||
|
||||
private let maskLayer = SimpleShapeLayer()
|
||||
private var component: ModeComponent?
|
||||
private var state: EmptyComponentState?
|
||||
|
||||
private var isEditing: Bool?
|
||||
private var isControlEnabled: Bool?
|
||||
private var sliderWidth: CGFloat = 0.0
|
||||
final class ItemView: HighlightTrackingButton {
|
||||
init() {
|
||||
super.init(frame: .zero)
|
||||
}
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
func update(isTablet: Bool, value: String, selected: Bool, tintColor: UIColor) -> CGSize {
|
||||
let title = NSMutableAttributedString(string: value, font: Font.with(size: 15.0, design: .regular, weight: .medium), textColor: UIColor(rgb: 0xffffff), paragraphAlignment: .center)
|
||||
self.setAttributedTitle(title, for: .normal)
|
||||
self.sizeToFit()
|
||||
return CGSize(width: self.titleLabel?.bounds.size.width ?? 0.0, height: buttonSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate var updated: (CGFloat) -> Void = { _ in }
|
||||
fileprivate var released: () -> Void = { }
|
||||
private var backgroundView = UIView()
|
||||
private var backgroundContainer = GlassBackgroundContainerView()
|
||||
|
||||
private var liquidLensView: LiquidLensView?
|
||||
private let scrollView = ScrollView()
|
||||
private let selectedScrollView = UIView()
|
||||
private var ignoreScrolling = false
|
||||
private var layoutData: LayoutData?
|
||||
|
||||
private var itemViews: [AnyHashable: ItemView] = [:]
|
||||
private var selectedItemViews: [AnyHashable: ItemView] = [:]
|
||||
|
||||
private var tabSelectionRecognizer: TabSelectionRecognizer?
|
||||
private var selectionGestureState: (startX: CGFloat, currentX: CGFloat, itemId: AnyHashable)?
|
||||
|
||||
private var component: ModeAndSizeComponent?
|
||||
public func matches(tag: Any) -> Bool {
|
||||
if let component = self.component, let componentTag = component.tag {
|
||||
let tag = tag as AnyObject
|
||||
|
|
@ -102,164 +130,350 @@ final class ModeAndSizeComponent: Component {
|
|||
}
|
||||
|
||||
init() {
|
||||
self.backgroundNode = NavigationBackgroundNode(color: UIColor(rgb: 0x888888, alpha: 0.3))
|
||||
self.node = SegmentedControlNode(theme: SegmentedControlTheme(backgroundColor: .clear, foregroundColor: UIColor(rgb: 0xffffff, alpha: 0.2), shadowColor: .black, textColor: UIColor(rgb: 0xffffff), dividerColor: UIColor(rgb: 0x505155, alpha: 0.6)), items: [], selectedIndex: 0, cornerRadius: 16.0)
|
||||
|
||||
self.knob = UIImageView(image: generateKnobImage())
|
||||
|
||||
super.init(frame: CGRect())
|
||||
|
||||
self.backgroundView.backgroundColor = UIColor(rgb: 0xffffff, alpha: 0.09)
|
||||
self.backgroundView.layer.cornerRadius = 22.0
|
||||
|
||||
self.layer.allowsGroupOpacity = true
|
||||
|
||||
self.addSubview(self.backgroundNode.view)
|
||||
self.addSubview(self.node.view)
|
||||
self.addSubview(self.knob)
|
||||
|
||||
self.backgroundNode.layer.mask = self.maskLayer
|
||||
self.scrollView.delaysContentTouches = false
|
||||
self.scrollView.canCancelContentTouches = true
|
||||
self.scrollView.contentInsetAdjustmentBehavior = .never
|
||||
self.scrollView.automaticallyAdjustsScrollIndicatorInsets = false
|
||||
self.scrollView.showsVerticalScrollIndicator = false
|
||||
self.scrollView.showsHorizontalScrollIndicator = false
|
||||
self.scrollView.alwaysBounceHorizontal = false
|
||||
self.scrollView.alwaysBounceVertical = false
|
||||
self.scrollView.scrollsToTop = false
|
||||
self.scrollView.clipsToBounds = true
|
||||
self.scrollView.delegate = self
|
||||
self.scrollView.disablesInteractiveTransitionGestureRecognizerNow = { [weak self] in
|
||||
guard let self else {
|
||||
return false
|
||||
}
|
||||
return self.scrollView.contentOffset.x > .ulpOfOne
|
||||
}
|
||||
|
||||
let pressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handlePress(_:)))
|
||||
pressGestureRecognizer.minimumPressDuration = 0.01
|
||||
pressGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(pressGestureRecognizer)
|
||||
self.selectedScrollView.clipsToBounds = true
|
||||
self.selectedScrollView.isUserInteractionEnabled = false
|
||||
|
||||
let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:)))
|
||||
panGestureRecognizer.delegate = self
|
||||
self.addGestureRecognizer(panGestureRecognizer)
|
||||
self.addSubview(self.backgroundView)
|
||||
self.backgroundView.addSubview(self.backgroundContainer)
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
preconditionFailure()
|
||||
}
|
||||
|
||||
@objc func handlePress(_ gestureRecognizer: UILongPressGestureRecognizer) {
|
||||
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
|
||||
guard self.frame.width > 0.0, case .began = gestureRecognizer.state else {
|
||||
return
|
||||
}
|
||||
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
|
||||
self.updated(value)
|
||||
private var animatedOut = false
|
||||
func animateOutToEditor(transition: ComponentTransition) {
|
||||
self.animatedOut = true
|
||||
|
||||
transition.setAlpha(view: self.backgroundView, alpha: 0.0)
|
||||
transition.setSublayerTransform(view: self, transform: CATransform3DMakeTranslation(0.0, -buttonSize.height, 0.0))
|
||||
}
|
||||
|
||||
@objc func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) {
|
||||
switch gestureRecognizer.state {
|
||||
case .changed:
|
||||
let location = gestureRecognizer.location(in: self).offsetBy(dx: -12.0, dy: 0.0)
|
||||
guard self.frame.width > 0.0 else {
|
||||
return
|
||||
func animateInFromEditor(transition: ComponentTransition) {
|
||||
self.animatedOut = false
|
||||
|
||||
transition.setAlpha(view: self.backgroundView, alpha: 1.0)
|
||||
transition.setSublayerTransform(view: self, transform: CATransform3DIdentity)
|
||||
}
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
return self.backgroundView.frame.contains(point)
|
||||
}
|
||||
|
||||
private func item(at point: CGPoint, in view: UIView) -> AnyHashable? {
|
||||
var closestItem: (AnyHashable, CGFloat)?
|
||||
for (id, itemView) in self.itemViews {
|
||||
let itemFrame = itemView.convert(itemView.bounds, to: view)
|
||||
if itemFrame.contains(point) {
|
||||
return id
|
||||
} else {
|
||||
let distance = abs(point.x - itemFrame.midX)
|
||||
if let closestItemValue = closestItem {
|
||||
if closestItemValue.1 > distance {
|
||||
closestItem = (id, distance)
|
||||
}
|
||||
} else {
|
||||
closestItem = (id, distance)
|
||||
}
|
||||
}
|
||||
}
|
||||
return closestItem?.0
|
||||
}
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
if self.ignoreScrolling {
|
||||
return
|
||||
}
|
||||
self.updateScrolling(transition: .immediate)
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if gestureRecognizer === self.tabSelectionRecognizer && otherGestureRecognizer === self.scrollView.panGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
if otherGestureRecognizer === self.tabSelectionRecognizer && gestureRecognizer === self.scrollView.panGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc private func onTabSelectionGesture(_ recognizer: TabSelectionRecognizer) {
|
||||
guard let component = self.component else {
|
||||
return
|
||||
}
|
||||
let location = recognizer.location(in: self)
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
if let itemId = self.item(at: location, in: self), let itemView = self.itemViews[itemId] {
|
||||
let startX = itemView.frame.minX - 4.0
|
||||
self.selectionGestureState = (startX, startX, itemId)
|
||||
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
||||
}
|
||||
case .changed:
|
||||
if var selectionGestureState = self.selectionGestureState {
|
||||
let translation = recognizer.translation(in: self)
|
||||
if !component.isTablet && self.scrollView.isScrollEnabled && abs(translation.x) > 6.0 && abs(translation.x) > abs(translation.y) {
|
||||
self.selectionGestureState = nil
|
||||
recognizer.state = .cancelled
|
||||
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
||||
return
|
||||
}
|
||||
selectionGestureState.currentX = selectionGestureState.startX + recognizer.translation(in: self).x
|
||||
if let itemId = self.item(at: location, in: self) {
|
||||
selectionGestureState.itemId = itemId
|
||||
}
|
||||
self.selectionGestureState = selectionGestureState
|
||||
self.state?.updated(transition: .immediate, isLocal: true)
|
||||
}
|
||||
let value = max(0.0, min(1.0, location.x / (self.frame.width - 24.0)))
|
||||
self.updated(value)
|
||||
case .ended, .cancelled:
|
||||
self.released()
|
||||
if let selectionGestureState = self.selectionGestureState {
|
||||
self.selectionGestureState = nil
|
||||
if case .ended = recognizer.state {
|
||||
guard let item = component.availableModes.first(where: { AnyHashable($0.rawValue) == selectionGestureState.itemId }) else {
|
||||
return
|
||||
}
|
||||
component.updatedMode(item)
|
||||
}
|
||||
self.state?.updated(transition: .spring(duration: 0.4), isLocal: true)
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if let isEditing = self.isEditing, let isControlEnabled = self.isControlEnabled {
|
||||
return isEditing && isControlEnabled
|
||||
private func updateScrolling(transition: ComponentTransition) {
|
||||
guard let component = self.component, let liquidLensView = self.liquidLensView, let layoutData = self.layoutData else {
|
||||
return
|
||||
}
|
||||
|
||||
let contentOffsetX = layoutData.isTablet ? 0.0 : self.scrollView.bounds.minX
|
||||
var lensSelection = (origin: layoutData.selectedFrame.origin, size: layoutData.selectedFrame.size)
|
||||
if let selectionGestureState = self.selectionGestureState, !layoutData.isTablet {
|
||||
lensSelection.origin = CGPoint(x: selectionGestureState.currentX, y: 0.0)
|
||||
}
|
||||
|
||||
if layoutData.isTablet {
|
||||
lensSelection.size.width = layoutData.containerSize.width
|
||||
} else {
|
||||
return false
|
||||
lensSelection.origin.x -= contentOffsetX
|
||||
lensSelection.origin.y = 0.0
|
||||
lensSelection.size.height = layoutData.containerSize.height
|
||||
}
|
||||
|
||||
let maxSelectionOriginX = max(0.0, layoutData.containerSize.width - lensSelection.size.width)
|
||||
transition.setFrame(view: self.selectedScrollView, frame: CGRect(origin: .zero, size: layoutData.containerSize))
|
||||
transition.setBounds(view: self.selectedScrollView, bounds: CGRect(origin: CGPoint(x: contentOffsetX, y: 0.0), size: layoutData.containerSize))
|
||||
|
||||
liquidLensView.update(size: layoutData.containerSize, cornerRadius: layoutData.cornerRadius, selectionOrigin: CGPoint(x: max(0.0, min(lensSelection.origin.x, maxSelectionOriginX)), y: lensSelection.origin.y), selectionSize: lensSelection.size, inset: 3.0, isDark: true, isLifted: self.selectionGestureState != nil && !layoutData.isTablet, isCollapsed: false, transition: transition)
|
||||
self.backgroundContainer.update(size: layoutData.containerSize, isDark: true, transition: .immediate)
|
||||
|
||||
self.scrollView.isScrollEnabled = !component.isTablet && self.scrollView.contentSize.width > self.scrollView.bounds.width + .ulpOfOne
|
||||
}
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func animateIn() {
|
||||
self.backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
self.node.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
|
||||
}
|
||||
|
||||
func animateOut() {
|
||||
self.node.alpha = 0.0
|
||||
self.node.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
|
||||
self.backgroundNode.alpha = 0.0
|
||||
self.backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3)
|
||||
}
|
||||
|
||||
func update(component: ModeAndSizeComponent, availableSize: CGSize, transition: ComponentTransition) -> CGSize {
|
||||
self.component = component
|
||||
|
||||
self.updated = component.sizeUpdated
|
||||
self.released = component.sizeReleased
|
||||
|
||||
let previousIsEditing = self.isEditing
|
||||
self.isEditing = component.isEditing
|
||||
self.isControlEnabled = component.isEnabled
|
||||
|
||||
if component.isEditing {
|
||||
self.sliderWidth = availableSize.width
|
||||
}
|
||||
|
||||
self.node.items = component.values.map { SegmentedControlItem(title: $0) }
|
||||
self.node.setSelectedIndex(component.selectedIndex, animated: !transition.animation.isImmediate)
|
||||
let selectionChanged = component.selectionChanged
|
||||
self.node.selectedIndexChanged = { [weak self] index in
|
||||
self?.window?.endEditing(true)
|
||||
selectionChanged(index)
|
||||
}
|
||||
|
||||
let nodeSize = self.node.updateLayout(.stretchToFill(width: availableSize.width + component.rightInset), transition: transition.containedViewLayoutTransition)
|
||||
let size = CGSize(width: availableSize.width, height: nodeSize.height)
|
||||
transition.setFrame(view: self.node.view, frame: CGRect(origin: CGPoint(), size: nodeSize))
|
||||
|
||||
var isDismissingEditing = false
|
||||
if component.isEditing != previousIsEditing && !component.isEditing {
|
||||
isDismissingEditing = true
|
||||
}
|
||||
|
||||
self.knob.alpha = component.isEditing ? 1.0 : 0.0
|
||||
if !isDismissingEditing {
|
||||
self.knob.frame = CGRect(origin: CGPoint(x: -12.0 + floorToScreenPixels((self.sliderWidth + 24.0 - self.knob.frame.size.width) * component.sizeValue), y: floorToScreenPixels((size.height - self.knob.frame.size.height) / 2.0)), size: self.knob.frame.size)
|
||||
}
|
||||
|
||||
if component.isEditing != previousIsEditing {
|
||||
let containedTransition = transition.containedViewLayoutTransition
|
||||
let maskPath: UIBezierPath
|
||||
if component.isEditing {
|
||||
maskPath = generateMaskPath(size: size, leftRadius: 2.0, rightRadius: 11.5)
|
||||
let selectionFrame = self.node.animateSelection(to: self.knob.center, transition: containedTransition)
|
||||
containedTransition.animateFrame(layer: self.knob.layer, from: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
|
||||
|
||||
self.knob.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
func update(component: ModeComponent, availableSize: CGSize, state: EmptyComponentState, transition: ComponentTransition) -> CGSize {
|
||||
let previousComponent = self.component
|
||||
self.component = component
|
||||
self.state = state
|
||||
|
||||
let isTablet = component.isTablet
|
||||
|
||||
let liquidLensView: LiquidLensView
|
||||
if let current = self.liquidLensView {
|
||||
liquidLensView = current
|
||||
} else {
|
||||
liquidLensView = LiquidLensView(kind: isTablet ? .noContainer : .externalContainer)
|
||||
self.liquidLensView = liquidLensView
|
||||
self.backgroundContainer.contentView.addSubview(liquidLensView)
|
||||
liquidLensView.contentView.addSubview(self.scrollView)
|
||||
liquidLensView.selectedContentView.addSubview(self.selectedScrollView)
|
||||
|
||||
let tabSelectionRecognizer = TabSelectionRecognizer(target: self, action: #selector(self.onTabSelectionGesture(_:)))
|
||||
tabSelectionRecognizer.delegate = self
|
||||
tabSelectionRecognizer.cancelsTouchesInView = false
|
||||
self.tabSelectionRecognizer = tabSelectionRecognizer
|
||||
liquidLensView.addGestureRecognizer(tabSelectionRecognizer)
|
||||
}
|
||||
if self.scrollView.superview == nil {
|
||||
liquidLensView.contentView.addSubview(self.scrollView)
|
||||
}
|
||||
if self.selectedScrollView.superview == nil {
|
||||
liquidLensView.selectedContentView.addSubview(self.selectedScrollView)
|
||||
}
|
||||
|
||||
self.backgroundView.backgroundColor = component.isTablet ? .clear : UIColor(rgb: 0xffffff, alpha: 0.11)
|
||||
|
||||
var inset: CGFloat = 23.0
|
||||
let spacing: CGFloat
|
||||
if isTablet {
|
||||
spacing = 9.0
|
||||
} else {
|
||||
if availableSize.width < 200.0 {
|
||||
inset = 20.0
|
||||
spacing = 24.0
|
||||
} else {
|
||||
maskPath = generateMaskPath(size: size, leftRadius: 16.0, rightRadius: 16.0)
|
||||
if previousIsEditing != nil {
|
||||
let selectionFrame = self.node.animateSelection(from: self.knob.center, transition: containedTransition)
|
||||
containedTransition.animateFrame(layer: self.knob.layer, from: self.knob.frame, to: selectionFrame.insetBy(dx: -9.0, dy: -9.0))
|
||||
self.knob.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2)
|
||||
spacing = 30.0
|
||||
}
|
||||
}
|
||||
|
||||
var i = 0
|
||||
var itemFrame = CGRect(origin: isTablet ? .zero : CGPoint(x: inset, y: 0.0), size: buttonSize)
|
||||
var selectedFrame = itemFrame
|
||||
|
||||
var validKeys: Set<AnyHashable> = Set()
|
||||
for mode in component.availableModes {
|
||||
let id = mode.rawValue
|
||||
validKeys.insert(id)
|
||||
|
||||
let itemView: ItemView
|
||||
let selectedItemView: ItemView
|
||||
if let current = self.itemViews[id], let currentSelected = self.selectedItemViews[id] {
|
||||
itemView = current
|
||||
selectedItemView = currentSelected
|
||||
} else {
|
||||
itemView = ItemView()
|
||||
itemView.isUserInteractionEnabled = false
|
||||
self.itemViews[id] = itemView
|
||||
|
||||
selectedItemView = ItemView()
|
||||
selectedItemView.isUserInteractionEnabled = false
|
||||
self.selectedItemViews[id] = selectedItemView
|
||||
}
|
||||
if itemView.superview !== self.scrollView {
|
||||
self.scrollView.addSubview(itemView)
|
||||
}
|
||||
if selectedItemView.superview !== self.selectedScrollView {
|
||||
self.selectedScrollView.addSubview(selectedItemView)
|
||||
}
|
||||
|
||||
let itemSize = itemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: false, tintColor: component.tintColor)
|
||||
itemView.bounds = CGRect(origin: .zero, size: itemSize)
|
||||
|
||||
let _ = selectedItemView.update(isTablet: component.isTablet, value: mode.title(strings: component.strings), selected: true, tintColor: component.tintColor)
|
||||
selectedItemView.bounds = CGRect(origin: .zero, size: itemSize)
|
||||
|
||||
itemFrame = CGRect(origin: itemFrame.origin, size: itemSize)
|
||||
|
||||
if mode == component.currentMode {
|
||||
selectedFrame = itemFrame
|
||||
}
|
||||
|
||||
if isTablet {
|
||||
itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY)
|
||||
selectedItemView.center = itemView.center
|
||||
itemFrame = itemFrame.offsetBy(dx: 0.0, dy: tabletButtonSize.height + spacing)
|
||||
} else {
|
||||
itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY)
|
||||
selectedItemView.center = itemView.center
|
||||
itemFrame = itemFrame.offsetBy(dx: itemFrame.width + spacing, dy: 0.0)
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
|
||||
var removeKeys: [AnyHashable] = []
|
||||
for (id, itemView) in self.itemViews {
|
||||
if !validKeys.contains(id) {
|
||||
removeKeys.append(id)
|
||||
|
||||
transition.setAlpha(view: itemView, alpha: 0.0, completion: { _ in
|
||||
itemView.removeFromSuperview()
|
||||
})
|
||||
|
||||
if let selectedItemView = self.selectedItemViews[id] {
|
||||
transition.setAlpha(view: selectedItemView, alpha: 0.0, completion: { _ in
|
||||
selectedItemView.removeFromSuperview()
|
||||
})
|
||||
}
|
||||
}
|
||||
transition.setShapeLayerPath(layer: self.maskLayer, path: maskPath.cgPath)
|
||||
}
|
||||
|
||||
transition.setFrame(layer: self.maskLayer, frame: CGRect(origin: .zero, size: nodeSize))
|
||||
for id in removeKeys {
|
||||
self.itemViews.removeValue(forKey: id)
|
||||
self.selectedItemViews.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
transition.setFrame(view: self.backgroundNode.view, frame: CGRect(origin: CGPoint(), size: size))
|
||||
self.backgroundNode.update(size: size, transition: transition.containedViewLayoutTransition)
|
||||
let totalSize: CGSize
|
||||
let size: CGSize
|
||||
let contentSize: CGSize
|
||||
var cornerRadius: CGFloat?
|
||||
if isTablet {
|
||||
totalSize = CGSize(width: availableSize.width, height: tabletButtonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1))
|
||||
size = CGSize(width: availableSize.width, height: availableSize.height)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: .zero, size: totalSize))
|
||||
contentSize = totalSize
|
||||
cornerRadius = 20.0
|
||||
} else {
|
||||
size = CGSize(width: availableSize.width, height: buttonSize.height)
|
||||
totalSize = CGSize(width: itemFrame.minX - spacing + inset, height: buttonSize.height)
|
||||
let visibleSize = CGSize(width: min(availableSize.width, totalSize.width), height: totalSize.height)
|
||||
transition.setFrame(view: self.backgroundView, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - visibleSize.width) / 2.0), y: 0.0), size: visibleSize))
|
||||
contentSize = totalSize
|
||||
}
|
||||
|
||||
if let screenTransition = transition.userData(DrawingScreenTransition.self) {
|
||||
switch screenTransition {
|
||||
case .animateIn:
|
||||
self.animateIn()
|
||||
case .animateOut:
|
||||
self.animateOut()
|
||||
let containerFrame = CGRect(origin: .zero, size: self.backgroundView.frame.size)
|
||||
transition.setFrame(view: self.backgroundContainer, frame: containerFrame)
|
||||
transition.setFrame(view: liquidLensView, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: containerFrame.size))
|
||||
|
||||
let scrollViewFrame = CGRect(origin: .zero, size: containerFrame.size)
|
||||
transition.setFrame(view: self.scrollView, frame: scrollViewFrame)
|
||||
if self.scrollView.contentSize != contentSize {
|
||||
self.scrollView.contentSize = contentSize
|
||||
}
|
||||
self.scrollView.isScrollEnabled = !isTablet && contentSize.width > scrollViewFrame.width + .ulpOfOne
|
||||
|
||||
self.layoutData = LayoutData(containerSize: containerFrame.size, selectedFrame: selectedFrame.insetBy(dx: -inset, dy: 3.0), cornerRadius: cornerRadius, isTablet: isTablet)
|
||||
|
||||
self.ignoreScrolling = true
|
||||
var scrollViewBounds = CGRect(origin: self.scrollView.bounds.origin, size: scrollViewFrame.size)
|
||||
let maxContentOffsetX = max(0.0, contentSize.width - scrollViewFrame.width)
|
||||
let shouldFocusOnSelectedItem = previousComponent?.currentMode != component.currentMode || previousComponent?.availableModes != component.availableModes || self.scrollView.bounds.size != scrollViewFrame.size
|
||||
if self.scrollView.isScrollEnabled && shouldFocusOnSelectedItem {
|
||||
let scrollLookahead = min(60.0, scrollViewBounds.width * 0.25)
|
||||
if scrollViewBounds.minX + scrollViewBounds.width - scrollLookahead < selectedFrame.maxX {
|
||||
scrollViewBounds.origin.x = selectedFrame.maxX - scrollViewBounds.width + scrollLookahead
|
||||
}
|
||||
if scrollViewBounds.minX > selectedFrame.minX - scrollLookahead {
|
||||
scrollViewBounds.origin.x = selectedFrame.minX - scrollLookahead
|
||||
}
|
||||
}
|
||||
scrollViewBounds.origin.x = max(0.0, min(scrollViewBounds.origin.x, maxContentOffsetX))
|
||||
transition.setBounds(view: self.scrollView, bounds: scrollViewBounds)
|
||||
self.ignoreScrolling = false
|
||||
|
||||
self.updateScrolling(transition: transition)
|
||||
|
||||
return size
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeView() -> View {
|
||||
return View()
|
||||
}
|
||||
|
||||
func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment<Empty>, transition: ComponentTransition) -> CGSize {
|
||||
return view.update(component: self, availableSize: availableSize, transition: transition)
|
||||
return view.update(component: self, availableSize: availableSize, state: state, transition: transition)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -675,6 +675,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
super.didLoad()
|
||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:)))
|
||||
backwardLongPressGestureRecognizer.minimumPressDuration = 0.3
|
||||
|
|
@ -1356,7 +1357,7 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
speed: settingsButtonState.speed,
|
||||
quality: settingsButtonState.quality,
|
||||
isOpen: false
|
||||
))),
|
||||
)), insets: .zero),
|
||||
action: { [weak self] in
|
||||
guard let self, let buttonPanelView = self.buttonPanel.view as? GlassControlPanelComponent.View else {
|
||||
return
|
||||
|
|
@ -1819,12 +1820,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
if availableOpenInOptions(context: strongSelf.context, item: item).count > 1 {
|
||||
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
|
||||
let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: defaultDarkColorPresentationTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
}
|
||||
})
|
||||
strongSelf.controllerInteraction?.presentController(openInController, nil)
|
||||
strongSelf.controllerInteraction?.pushController(openInController)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
|
|
@ -2124,12 +2125,12 @@ final class ChatItemGalleryFooterContentNode: GalleryFooterContentNode, ASScroll
|
|||
if availableOpenInOptions(context: self.context, item: item).count > 1 {
|
||||
preferredAction = .custom(action: ShareControllerAction(title: presentationData.strings.Conversation_FileOpenIn, action: { [weak self] in
|
||||
if let strongSelf = self {
|
||||
let openInController = OpenInActionSheetController(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
|
||||
let openInController = OpenInOptionsScreen(context: strongSelf.context, forceTheme: forceTheme, item: item, additionalAction: nil, openUrl: { [weak self] url in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: nil, dismissInput: {})
|
||||
}
|
||||
})
|
||||
strongSelf.controllerInteraction?.presentController(openInController, nil)
|
||||
strongSelf.controllerInteraction?.pushController(openInController)
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ public final class GalleryThumbnailContainerNode: ASDisplayNode, ASScrollViewDel
|
|||
self.scrollNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:))))
|
||||
self.scrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.scrollNode.view.showsVerticalScrollIndicator = false
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
self.addSubnode(self.scrollNode)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -419,6 +419,9 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
|
|||
adView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
|
||||
adView.layer.animatePosition(from: .zero, to: CGPoint(x: 0.0, y: 64.0), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true, completion: { _ in
|
||||
adView.removeFromSuperview()
|
||||
if self.adView.view === adView {
|
||||
self.adView = ComponentView<Empty>()
|
||||
}
|
||||
Queue.mainQueue().after(0.1) {
|
||||
adView.layer.removeAllAnimations()
|
||||
}
|
||||
|
|
@ -444,7 +447,7 @@ private final class UniversalVideoGalleryItemOverlayNode: GalleryOverlayContentN
|
|||
return result
|
||||
}
|
||||
}
|
||||
if let adView = self.adView.view, adView.frame.contains(point) {
|
||||
if let adView = self.adView.view, adView.superview === self.view, !self.isAnimatingOut, adView.frame.contains(point) {
|
||||
return super.hitTest(point, with: event)
|
||||
}
|
||||
return nil
|
||||
|
|
@ -3892,12 +3895,12 @@ final class UniversalVideoGalleryItemNode: ZoomableContentGalleryItemNode {
|
|||
if !presentationData.theme.overallDarkAppearance {
|
||||
presentationData = presentationData.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
}
|
||||
let actionSheet = OpenInActionSheetController(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
|
||||
let actionSheet = OpenInOptionsScreen(context: strongSelf.context, forceTheme: presentationData.theme, item: item, openUrl: { [weak self] url in
|
||||
if let strongSelf = self {
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: strongSelf.baseNavigationController(), dismissInput: {})
|
||||
}
|
||||
})
|
||||
controller.present(actionSheet, in: .window(.root))
|
||||
controller.push(actionSheet)
|
||||
}
|
||||
})))
|
||||
break
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, ASScroll
|
|||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
self.wrappingScrollNode.view.scrollsToTop = false
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
|
|
|||
|
|
@ -140,6 +140,7 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
self.addSubnode(self.navigationBar)
|
||||
self.scrollNode.view.delaysContentTouches = false
|
||||
self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
|
||||
self.scrollNode.view.scrollsToTop = false
|
||||
|
||||
self.navigationBar.back = navigateBack
|
||||
self.navigationBar.share = { [weak self] in
|
||||
|
|
@ -1789,12 +1790,12 @@ final class InstantPageControllerNode: ASDisplayNode, ASScrollViewDelegate {
|
|||
|
||||
private func openUrlIn(_ url: InstantPageUrlItem) {
|
||||
let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
|
||||
let actionSheet = OpenInActionSheetController(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
|
||||
let actionSheet = OpenInOptionsScreen(context: self.context, item: .url(url: url.url), openUrl: { [weak self] url in
|
||||
if let strongSelf = self, let navigationController = strongSelf.getNavigationController() {
|
||||
strongSelf.context.sharedContext.openExternalUrl(context: strongSelf.context, urlContext: .generic, url: url, forceExternal: true, presentationData: presentationData, navigationController: navigationController, dismissInput: {})
|
||||
}
|
||||
})
|
||||
self.present(actionSheet, nil)
|
||||
self.pushController(actionSheet)
|
||||
}
|
||||
|
||||
private func mediasFromItems(_ items: [InstantPageItem]) -> [InstantPageMedia] {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,7 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, ASScrollVie
|
|||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
self.wrappingScrollNode.view.scrollsToTop = false
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
|
|
|||
|
|
@ -410,14 +410,7 @@ class ItemListStickerPackItemNode: ItemListRevealOptionsItemNode {
|
|||
|
||||
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||
|
||||
let verticalInset: CGFloat
|
||||
switch item.systemStyle {
|
||||
case .glass:
|
||||
verticalInset = 13.0
|
||||
case .legacy:
|
||||
verticalInset = 11.0
|
||||
}
|
||||
|
||||
let verticalInset: CGFloat = 11.0
|
||||
let titleSpacing: CGFloat = 2.0
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ swift_library(
|
|||
"//submodules/TelegramUI/Components/TextNodeWithEntities",
|
||||
"//submodules/TelegramUI/Components/ListItemComponentAdaptor",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/GlassControls",
|
||||
"//submodules/TelegramUI/Components/HorizontalTabsComponent",
|
||||
],
|
||||
visibility = [
|
||||
|
|
|
|||
|
|
@ -689,6 +689,16 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
|
|||
}
|
||||
}
|
||||
|
||||
public func itemNode(forTag tag: ItemListItemTag) -> ListViewItemNode? {
|
||||
var result: ListViewItemNode?
|
||||
self.forEachItemNode { itemNode in
|
||||
if result == nil, let taggedItemNode = itemNode as? ItemListItemNode, let itemTag = taggedItemNode.tag, itemTag.isEqual(to: tag) {
|
||||
result = itemNode
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func ensureItemNodeVisible(_ itemNode: ListViewItemNode, animated: Bool = true, overflow: CGFloat = 0.0, atTop: Bool = false, curve: ListViewAnimationCurve = .Default(duration: 0.25)) {
|
||||
self.controllerNode.listNode.ensureItemNodeVisible(itemNode, animated: animated, overflow: overflow, atTop: atTop, curve: curve)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ import SwiftSignalKit
|
|||
import TelegramCore
|
||||
import TelegramPresentationData
|
||||
import MergeLists
|
||||
import ComponentFlow
|
||||
import GlassControls
|
||||
|
||||
public protocol ItemListHeaderItemNode: AnyObject {
|
||||
func updateTheme(theme: PresentationTheme)
|
||||
|
|
@ -255,7 +257,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
private var emptyStateItem: ItemListControllerEmptyStateItem?
|
||||
private var emptyStateNode: ItemListControllerEmptyStateItemNode?
|
||||
|
||||
private var toolbarNode: ToolbarNode?
|
||||
private var toolbar: ComponentView<Empty>?
|
||||
|
||||
private var searchItem: ItemListControllerSearch?
|
||||
private var searchNode: ItemListControllerSearchNode?
|
||||
|
|
@ -654,7 +656,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
insets.bottom = max(insets.bottom, additionalInsets.bottom)
|
||||
|
||||
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
if layout.size.width >= 375.0 {
|
||||
if layout.size.width >= 320.0 {
|
||||
insets.left += inset
|
||||
insets.right += inset
|
||||
}
|
||||
|
|
@ -666,60 +668,165 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
self.listNodeContainer.insertSubnode(self.leftOverlayNode, aboveSubnode: self.listNode)
|
||||
}
|
||||
|
||||
if let toolbarItem = self.toolbarItem {
|
||||
var tabBarHeight: CGFloat
|
||||
let bottomInset: CGFloat = insets.bottom
|
||||
if !layout.safeInsets.left.isZero {
|
||||
tabBarHeight = 34.0 + bottomInset
|
||||
insets.bottom += 34.0
|
||||
if let toolbarData = self.toolbarItem, let theme = self.theme {
|
||||
var panelsBottomInset: CGFloat = layout.insets(options: []).bottom
|
||||
if layout.metrics.widthClass == .regular, let inputHeight = layout.inputHeight, inputHeight != 0.0 {
|
||||
panelsBottomInset = inputHeight + 8.0
|
||||
}
|
||||
if panelsBottomInset == 0.0 {
|
||||
panelsBottomInset = 8.0
|
||||
} else {
|
||||
tabBarHeight = 49.0 + bottomInset
|
||||
insets.bottom += 49.0
|
||||
panelsBottomInset = max(panelsBottomInset, 8.0)
|
||||
}
|
||||
|
||||
let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
|
||||
let sideInset: CGFloat = 20.0
|
||||
let toolbarHeight = 44.0
|
||||
let toolbarFrame = CGRect(origin: CGPoint(x: sideInset, y: layout.size.height - panelsBottomInset - toolbarHeight), size: CGSize(width: layout.size.width - sideInset * 2.0, height: toolbarHeight))
|
||||
|
||||
if let toolbarNode = self.toolbarNode {
|
||||
transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
|
||||
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition)
|
||||
} else if let theme = self.theme {
|
||||
let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true)
|
||||
toolbarNode.frame = toolbarFrame
|
||||
toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate)
|
||||
self.addSubnode(toolbarNode)
|
||||
self.toolbarNode = toolbarNode
|
||||
if case let .animated(duration, curve) = transition {
|
||||
toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true)
|
||||
let toolbar: ComponentView<Empty>
|
||||
var toolbarTransition = ComponentTransition(transition)
|
||||
if let current = self.toolbar {
|
||||
toolbar = current
|
||||
} else {
|
||||
toolbar = ComponentView()
|
||||
self.toolbar = toolbar
|
||||
toolbarTransition = .immediate
|
||||
}
|
||||
|
||||
let _ = toolbar.update(
|
||||
transition: toolbarTransition,
|
||||
component: AnyComponent(GlassControlPanelComponent(
|
||||
theme: theme,
|
||||
leftItem: toolbarData.toolbar.leftAction.flatMap { value in
|
||||
return GlassControlPanelComponent.Item(
|
||||
items: [GlassControlGroupComponent.Item(
|
||||
id: "left_" + value.title,
|
||||
content: .text(value.title),
|
||||
action: value.isEnabled ? { [weak self] in
|
||||
guard let self, let toolbarData = self.toolbarItem else {
|
||||
return
|
||||
}
|
||||
toolbarData.actions[0].action()
|
||||
} : nil
|
||||
)],
|
||||
background: .panel
|
||||
)
|
||||
},
|
||||
centralItem: toolbarData.toolbar.middleAction.flatMap { value in
|
||||
return GlassControlPanelComponent.Item(
|
||||
items: [GlassControlGroupComponent.Item(
|
||||
id: "right_" + value.title,
|
||||
content: .text(value.title),
|
||||
action: value.isEnabled ? { [weak self] in
|
||||
guard let self, let toolbarData = self.toolbarItem else {
|
||||
return
|
||||
}
|
||||
if toolbarData.actions.count == 1 {
|
||||
toolbarData.actions[0].action()
|
||||
} else if toolbarData.actions.count == 3 {
|
||||
toolbarData.actions[1].action()
|
||||
}
|
||||
} : nil
|
||||
)],
|
||||
background: .panel
|
||||
)
|
||||
},
|
||||
rightItem: toolbarData.toolbar.rightAction.flatMap { value in
|
||||
return GlassControlPanelComponent.Item(
|
||||
items: [GlassControlGroupComponent.Item(
|
||||
id: "right_" + value.title,
|
||||
content: .text(value.title),
|
||||
action: value.isEnabled ? { [weak self] in
|
||||
guard let self, let toolbarData = self.toolbarItem else {
|
||||
return
|
||||
}
|
||||
if toolbarData.actions.count == 2 {
|
||||
toolbarData.actions[1].action()
|
||||
} else if toolbarData.actions.count == 3 {
|
||||
toolbarData.actions[2].action()
|
||||
}
|
||||
} : nil
|
||||
)],
|
||||
background: .panel
|
||||
)
|
||||
},
|
||||
centerAlignmentIfPossible: true
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: toolbarFrame.size
|
||||
)
|
||||
|
||||
if let toolbarView = toolbar.view {
|
||||
if toolbarView.superview == nil {
|
||||
self.view.addSubview(toolbarView)
|
||||
toolbarView.alpha = 0.0
|
||||
}
|
||||
toolbarTransition.setFrame(view: toolbarView, frame: toolbarFrame)
|
||||
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 1.0)
|
||||
}
|
||||
|
||||
self.toolbarNode?.left = {
|
||||
toolbarItem.actions[0].action()
|
||||
}
|
||||
self.toolbarNode?.right = {
|
||||
if toolbarItem.actions.count == 2 {
|
||||
toolbarItem.actions[1].action()
|
||||
} else if toolbarItem.actions.count == 3 {
|
||||
toolbarItem.actions[2].action()
|
||||
}
|
||||
}
|
||||
self.toolbarNode?.middle = {
|
||||
if toolbarItem.actions.count == 1 {
|
||||
toolbarItem.actions[0].action()
|
||||
} else if toolbarItem.actions.count == 3 {
|
||||
toolbarItem.actions[1].action()
|
||||
}
|
||||
}
|
||||
} else if let toolbarNode = self.toolbarNode {
|
||||
self.toolbarNode = nil
|
||||
if case let .animated(duration, curve) = transition {
|
||||
toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in
|
||||
toolbarNode?.removeFromSupernode()
|
||||
} else if let toolbar = self.toolbar {
|
||||
self.toolbar = nil
|
||||
if let toolbarView = toolbar.view {
|
||||
ComponentTransition(transition).setAlpha(view: toolbarView, alpha: 0.0, completion: { [weak toolbarView] _ in
|
||||
toolbarView?.removeFromSuperview()
|
||||
})
|
||||
} else {
|
||||
toolbarNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
// if let toolbarItem = self.toolbarItem {
|
||||
// var tabBarHeight: CGFloat
|
||||
// let bottomInset: CGFloat = insets.bottom
|
||||
// if !layout.safeInsets.left.isZero {
|
||||
// tabBarHeight = 34.0 + bottomInset
|
||||
// insets.bottom += 34.0
|
||||
// } else {
|
||||
// tabBarHeight = 49.0 + bottomInset
|
||||
// insets.bottom += 49.0
|
||||
// }
|
||||
//
|
||||
// let toolbarFrame = CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - tabBarHeight), size: CGSize(width: layout.size.width, height: tabBarHeight))
|
||||
//
|
||||
// if let toolbarNode = self.toolbarNode {
|
||||
// transition.updateFrame(node: toolbarNode, frame: toolbarFrame)
|
||||
// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: transition)
|
||||
// } else if let theme = self.theme {
|
||||
// let toolbarNode = ToolbarNode(theme: ToolbarTheme(rootControllerTheme: theme), displaySeparator: true)
|
||||
// toolbarNode.frame = toolbarFrame
|
||||
// toolbarNode.updateLayout(size: toolbarFrame.size, leftInset: layout.safeInsets.left, rightInset: layout.safeInsets.right, additionalSideInsets: layout.additionalInsets, bottomInset: layout.intrinsicInsets.bottom, toolbar: toolbarItem.toolbar, transition: .immediate)
|
||||
// self.addSubnode(toolbarNode)
|
||||
// self.toolbarNode = toolbarNode
|
||||
// if case let .animated(duration, curve) = transition {
|
||||
// toolbarNode.layer.animatePosition(from: CGPoint(x: 0.0, y: toolbarFrame.height), to: CGPoint(), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, additive: true)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// self.toolbarNode?.left = {
|
||||
// toolbarItem.actions[0].action()
|
||||
// }
|
||||
// self.toolbarNode?.right = {
|
||||
// if toolbarItem.actions.count == 2 {
|
||||
// toolbarItem.actions[1].action()
|
||||
// } else if toolbarItem.actions.count == 3 {
|
||||
// toolbarItem.actions[2].action()
|
||||
// }
|
||||
// }
|
||||
// self.toolbarNode?.middle = {
|
||||
// if toolbarItem.actions.count == 1 {
|
||||
// toolbarItem.actions[0].action()
|
||||
// } else if toolbarItem.actions.count == 3 {
|
||||
// toolbarItem.actions[1].action()
|
||||
// }
|
||||
// }
|
||||
// } else if let toolbarNode = self.toolbarNode {
|
||||
// self.toolbarNode = nil
|
||||
// if case let .animated(duration, curve) = transition {
|
||||
// toolbarNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: toolbarNode.frame.size.height), duration: duration, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: false, additive: true, completion: { [weak toolbarNode] _ in
|
||||
// toolbarNode?.removeFromSupernode()
|
||||
// })
|
||||
// } else {
|
||||
// toolbarNode.removeFromSupernode()
|
||||
// }
|
||||
// }
|
||||
|
||||
if let headerItemNode = self.headerItemNode {
|
||||
let headerHeight = headerItemNode.updateLayout(layout: layout, transition: transition)
|
||||
|
|
@ -977,7 +1084,7 @@ open class ItemListControllerNode: ASDisplayNode, ASGestureRecognizerDelegate {
|
|||
insets.bottom = footerHeight
|
||||
|
||||
let inset = max(16.0, floor((layout.size.width - 674.0) / 2.0))
|
||||
if layout.size.width >= 375.0 {
|
||||
if layout.size.width >= 320.0 {
|
||||
insets.left += inset
|
||||
insets.right += inset
|
||||
}
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ public func itemListNeighborsGroupedInsets(_ neighbors: ItemListNeighbors, _ par
|
|||
}
|
||||
|
||||
public func itemListHasRoundedBlockLayout(_ params: ListViewItemLayoutParams) -> Bool {
|
||||
return params.width >= 350.0
|
||||
return params.width >= 320.0
|
||||
}
|
||||
|
||||
public final class ItemListPresentationData: Equatable {
|
||||
|
|
|
|||
|
|
@ -74,14 +74,20 @@ public struct ItemListRevealOption: Equatable {
|
|||
}
|
||||
|
||||
private let titleFont = Font.regular(11.0)
|
||||
private let iconlessTitleFont = Font.regular(13.0)
|
||||
|
||||
private let optionSpacing: CGFloat = 10.0
|
||||
private let optionEdgeInset: CGFloat = 10.0
|
||||
private let optionTitleSpacing: CGFloat = 4.0
|
||||
private let optionRevealStartOverlap: CGFloat = 12.0
|
||||
private let optionRevealEndDistance: CGFloat = 10.0
|
||||
private let optionExpandedActivationWidthFactor: CGFloat = 5.0
|
||||
private let optionExpandedActivationWidthFactor: CGFloat = 3.0
|
||||
private let optionExpandedTransitionDistance: CGFloat = 16.0
|
||||
private let optionIconlessTitleHorizontalInset: CGFloat = 10.0
|
||||
private let optionIconAnimationResponse: CGFloat = 18.0
|
||||
private let optionIconAnimationSnapDistance: CGFloat = 0.5
|
||||
private let optionIconAnimationSnapSize: CGFloat = 0.5
|
||||
private let optionIconAnimationSnapAlpha: CGFloat = 0.01
|
||||
|
||||
private struct ItemListRevealOptionLayoutMetrics {
|
||||
let shapeSize: CGSize
|
||||
|
|
@ -122,6 +128,10 @@ private func clampToUnitInterval(_ value: CGFloat) -> CGFloat {
|
|||
return max(0.0, min(1.0, value))
|
||||
}
|
||||
|
||||
private func frameCenter(_ frame: CGRect) -> CGPoint {
|
||||
return CGPoint(x: frame.midX, y: frame.midY)
|
||||
}
|
||||
|
||||
private final class ItemListRevealOptionNode: ASDisplayNode {
|
||||
private let contentContainerNode: ASDisplayNode
|
||||
private let backgroundNode: ASDisplayNode
|
||||
|
|
@ -131,10 +141,21 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
private let animationNode: SimpleAnimationNode?
|
||||
|
||||
private let enableAnimations: Bool
|
||||
private let displaysTitleInsidePill: Bool
|
||||
|
||||
private var animationScale: CGFloat = 1.0
|
||||
private var animationNodeOffset: CGFloat = 0.0
|
||||
private var animationNodeFlip = false
|
||||
|
||||
private var iconAnimationLink: SharedDisplayLinkDriver.Link?
|
||||
private weak var manuallyAnimatedIconNode: ASDisplayNode?
|
||||
private var currentIconCenter: CGPoint?
|
||||
private var targetIconCenter: CGPoint?
|
||||
private var currentIconSize: CGSize?
|
||||
private var targetIconSize: CGSize?
|
||||
private var currentTitleAlpha: CGFloat?
|
||||
private var targetTitleAlpha: CGFloat?
|
||||
|
||||
private var didApplyLayout = false
|
||||
var isExpanded: Bool = false
|
||||
|
||||
|
|
@ -150,7 +171,15 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
self.titleNode = ASTextNode()
|
||||
self.titleNode.maximumNumberOfLines = 1
|
||||
self.titleNode.truncationMode = .byTruncatingTail
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: titleFont, textColor: textColor)
|
||||
|
||||
let displaysTitleInsidePill: Bool
|
||||
if case .none = icon {
|
||||
displaysTitleInsidePill = true
|
||||
} else {
|
||||
displaysTitleInsidePill = false
|
||||
}
|
||||
self.displaysTitleInsidePill = displaysTitleInsidePill
|
||||
self.titleNode.attributedText = NSAttributedString(string: title, font: displaysTitleInsidePill ? iconlessTitleFont : titleFont, textColor: displaysTitleInsidePill ? iconColor : textColor)
|
||||
|
||||
self.enableAnimations = enableAnimations
|
||||
|
||||
|
|
@ -201,6 +230,10 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
self.highlightNode.backgroundColor = color.withMultipliedBrightnessBy(0.9)
|
||||
}
|
||||
|
||||
deinit {
|
||||
self.stopManualIconAnimation()
|
||||
}
|
||||
|
||||
func setHighlighted(_ highlighted: Bool) {
|
||||
if highlighted {
|
||||
self.contentContainerNode.insertSubnode(self.highlightNode, aboveSubnode: self.backgroundNode)
|
||||
|
|
@ -214,25 +247,150 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
|
||||
func resetAnimation() {
|
||||
self.animationNode?.reset()
|
||||
self.stopManualIconAnimation()
|
||||
}
|
||||
|
||||
private func currentIconPresentationFrame(iconNode: ASDisplayNode) -> CGRect {
|
||||
return iconNode.layer.presentation()?.frame ?? iconNode.frame
|
||||
}
|
||||
|
||||
private func currentTitlePresentationAlpha() -> CGFloat {
|
||||
if let presentation = self.titleNode.layer.presentation() {
|
||||
return CGFloat(presentation.opacity)
|
||||
} else {
|
||||
return self.titleNode.alpha
|
||||
}
|
||||
}
|
||||
|
||||
private func applyManualIconState(iconNode: ASDisplayNode, center: CGPoint, size: CGSize, titleAlpha: CGFloat) {
|
||||
iconNode.frame = CGRect(origin: CGPoint(x: floorToScreenPixels(center.x - size.width / 2.0), y: floorToScreenPixels(center.y - size.height / 2.0)), size: size)
|
||||
self.titleNode.alpha = titleAlpha
|
||||
}
|
||||
|
||||
private func isManualIconAnimationAtTarget(center: CGPoint, size: CGSize, titleAlpha: CGFloat) -> Bool {
|
||||
guard let targetIconCenter = self.targetIconCenter, let targetIconSize = self.targetIconSize, let targetTitleAlpha = self.targetTitleAlpha else {
|
||||
return true
|
||||
}
|
||||
let centerDeltaX = targetIconCenter.x - center.x
|
||||
let centerDeltaY = targetIconCenter.y - center.y
|
||||
let centerDistance = sqrt(centerDeltaX * centerDeltaX + centerDeltaY * centerDeltaY)
|
||||
let sizeDistance = max(abs(targetIconSize.width - size.width), abs(targetIconSize.height - size.height))
|
||||
let alphaDistance = abs(targetTitleAlpha - titleAlpha)
|
||||
return centerDistance <= optionIconAnimationSnapDistance && sizeDistance <= optionIconAnimationSnapSize && alphaDistance <= optionIconAnimationSnapAlpha
|
||||
}
|
||||
|
||||
private func stopManualIconAnimation() {
|
||||
self.iconAnimationLink?.isPaused = true
|
||||
self.iconAnimationLink?.invalidate()
|
||||
self.iconAnimationLink = nil
|
||||
self.manuallyAnimatedIconNode = nil
|
||||
self.currentIconCenter = nil
|
||||
self.targetIconCenter = nil
|
||||
self.currentIconSize = nil
|
||||
self.targetIconSize = nil
|
||||
self.currentTitleAlpha = nil
|
||||
self.targetTitleAlpha = nil
|
||||
}
|
||||
|
||||
private func updateManualIconAnimation(iconNode: ASDisplayNode, targetFrame: CGRect, targetTitleAlpha: CGFloat, forceImmediate: Bool) {
|
||||
iconNode.layer.removeAnimation(forKey: "position")
|
||||
iconNode.layer.removeAnimation(forKey: "bounds")
|
||||
self.titleNode.layer.removeAnimation(forKey: "opacity")
|
||||
|
||||
let targetCenter = frameCenter(targetFrame)
|
||||
let targetSize = targetFrame.size
|
||||
|
||||
if self.manuallyAnimatedIconNode !== iconNode || self.currentIconCenter == nil || self.currentIconSize == nil || self.currentTitleAlpha == nil {
|
||||
let currentFrame = self.currentIconPresentationFrame(iconNode: iconNode)
|
||||
self.currentIconCenter = frameCenter(currentFrame)
|
||||
self.currentIconSize = currentFrame.size
|
||||
self.currentTitleAlpha = self.currentTitlePresentationAlpha()
|
||||
self.manuallyAnimatedIconNode = iconNode
|
||||
}
|
||||
|
||||
self.targetIconCenter = targetCenter
|
||||
self.targetIconSize = targetSize
|
||||
self.targetTitleAlpha = targetTitleAlpha
|
||||
|
||||
if forceImmediate {
|
||||
self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha)
|
||||
self.stopManualIconAnimation()
|
||||
return
|
||||
}
|
||||
|
||||
if let currentIconCenter = self.currentIconCenter, let currentIconSize = self.currentIconSize, let currentTitleAlpha = self.currentTitleAlpha, self.isManualIconAnimationAtTarget(center: currentIconCenter, size: currentIconSize, titleAlpha: currentTitleAlpha) {
|
||||
self.applyManualIconState(iconNode: iconNode, center: targetCenter, size: targetSize, titleAlpha: targetTitleAlpha)
|
||||
self.stopManualIconAnimation()
|
||||
return
|
||||
}
|
||||
|
||||
if self.iconAnimationLink == nil {
|
||||
self.iconAnimationLink = SharedDisplayLinkDriver.shared.add(framesPerSecond: .max, { [weak self] deltaTime in
|
||||
self?.tickManualIconAnimation(deltaTime: deltaTime)
|
||||
})
|
||||
self.iconAnimationLink?.isPaused = false
|
||||
}
|
||||
}
|
||||
|
||||
private func tickManualIconAnimation(deltaTime: CGFloat) {
|
||||
guard let iconNode = self.manuallyAnimatedIconNode, let currentIconCenter = self.currentIconCenter, let targetIconCenter = self.targetIconCenter, let currentIconSize = self.currentIconSize, let targetIconSize = self.targetIconSize, let currentTitleAlpha = self.currentTitleAlpha, let targetTitleAlpha = self.targetTitleAlpha else {
|
||||
self.stopManualIconAnimation()
|
||||
return
|
||||
}
|
||||
|
||||
let clampedDeltaTime = min(0.05, max(0.0, deltaTime))
|
||||
let progress = 1.0 - exp(-clampedDeltaTime * optionIconAnimationResponse)
|
||||
let updatedCenter = CGPoint(
|
||||
x: currentIconCenter.x + (targetIconCenter.x - currentIconCenter.x) * progress,
|
||||
y: currentIconCenter.y + (targetIconCenter.y - currentIconCenter.y) * progress
|
||||
)
|
||||
let updatedSize = CGSize(
|
||||
width: currentIconSize.width + (targetIconSize.width - currentIconSize.width) * progress,
|
||||
height: currentIconSize.height + (targetIconSize.height - currentIconSize.height) * progress
|
||||
)
|
||||
let updatedTitleAlpha = currentTitleAlpha + (targetTitleAlpha - currentTitleAlpha) * progress
|
||||
|
||||
if self.isManualIconAnimationAtTarget(center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha) {
|
||||
self.applyManualIconState(iconNode: iconNode, center: targetIconCenter, size: targetIconSize, titleAlpha: targetTitleAlpha)
|
||||
self.stopManualIconAnimation()
|
||||
} else {
|
||||
self.currentIconCenter = updatedCenter
|
||||
self.currentIconSize = updatedSize
|
||||
self.currentTitleAlpha = updatedTitleAlpha
|
||||
self.applyManualIconState(iconNode: iconNode, center: updatedCenter, size: updatedSize, titleAlpha: updatedTitleAlpha)
|
||||
}
|
||||
}
|
||||
|
||||
func updateLayout(isLeft: Bool, isPrimary: Bool, metrics: ItemListRevealOptionLayoutMetrics, revealProgress: CGFloat, overswipeProgress: CGFloat, expandedProgress: CGFloat, isStretched: Bool, isExpanded: Bool, transition: ContainedViewLayoutTransition) {
|
||||
let didApplyLayout = self.didApplyLayout
|
||||
let bounds = CGRect(origin: CGPoint(), size: self.bounds.size)
|
||||
transition.updateFrame(node: self.contentContainerNode, frame: bounds)
|
||||
|
||||
let contentHeight = metrics.contentHeight
|
||||
let shapeY = floor((bounds.height - contentHeight) / 2.0)
|
||||
let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
let pillSize: CGSize
|
||||
if self.displaysTitleInsidePill {
|
||||
let pillWidth = max(metrics.shapeSize.width, min(metrics.slotWidth, titleSize.width + optionIconlessTitleHorizontalInset * 2.0))
|
||||
pillSize = CGSize(width: pillWidth, height: metrics.shapeSize.height)
|
||||
} else {
|
||||
pillSize = metrics.shapeSize
|
||||
}
|
||||
let shapeY: CGFloat
|
||||
if self.displaysTitleInsidePill {
|
||||
shapeY = floor((bounds.height - pillSize.height) / 2.0)
|
||||
} else {
|
||||
shapeY = floor((bounds.height - metrics.contentHeight) / 2.0)
|
||||
}
|
||||
|
||||
let shapeFrameX: CGFloat
|
||||
if isStretched {
|
||||
shapeFrameX = isLeft ? 0.0 : bounds.width - metrics.shapeSize.width
|
||||
shapeFrameX = isLeft ? 0.0 : bounds.width - pillSize.width
|
||||
} else {
|
||||
shapeFrameX = metrics.slotShapeInset
|
||||
shapeFrameX = floor((metrics.slotWidth - pillSize.width) / 2.0)
|
||||
}
|
||||
let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: metrics.shapeSize)
|
||||
let shapeFrame = CGRect(origin: CGPoint(x: shapeFrameX, y: shapeY), size: pillSize)
|
||||
let backgroundFrame: CGRect
|
||||
if isStretched {
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: metrics.shapeSize.height))
|
||||
backgroundFrame = CGRect(origin: CGPoint(x: 0.0, y: shapeY), size: CGSize(width: bounds.width, height: pillSize.height))
|
||||
} else {
|
||||
backgroundFrame = shapeFrame
|
||||
}
|
||||
|
|
@ -244,7 +402,6 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
|
||||
self.isExpanded = isExpanded
|
||||
self.didApplyLayout = true
|
||||
let titleSize = self.titleNode.measure(CGSize(width: metrics.titleWidth, height: CGFloat.greatestFiniteMagnitude))
|
||||
let contentAlpha: CGFloat
|
||||
if isPrimary {
|
||||
contentAlpha = revealProgress
|
||||
|
|
@ -255,8 +412,8 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
transition.updateAlpha(node: self.contentContainerNode, alpha: contentAlpha)
|
||||
transition.updateTransform(node: self.contentContainerNode, transform: CGAffineTransform(scaleX: contentScale, y: contentScale))
|
||||
|
||||
let titleAlpha: CGFloat = isPrimary ? (1.0 - expandedProgress) : 1.0
|
||||
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
|
||||
let titleAlpha: CGFloat = isPrimary && !self.displaysTitleInsidePill ? (1.0 - expandedProgress) : 1.0
|
||||
var didApplyManualIconAnimation = false
|
||||
|
||||
let centeredIconCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
|
||||
let iconCenterX: CGFloat
|
||||
|
|
@ -281,7 +438,13 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
imageSize = CGSize(width: floorToScreenPixels(imageSize.width * imageScale), height: floorToScreenPixels(imageSize.height * imageScale))
|
||||
}
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - imageSize.width / 2.0), y: floorToScreenPixels(iconCenterY - imageSize.height / 2.0) + 6.0 + self.animationNodeOffset), size: imageSize)
|
||||
transition.updateFrame(node: animationNode, frame: iconFrame)
|
||||
|
||||
if isPrimary {
|
||||
didApplyManualIconAnimation = true
|
||||
self.updateManualIconAnimation(iconNode: animationNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne)
|
||||
} else {
|
||||
transition.updateFrame(node: animationNode, frame: iconFrame)
|
||||
}
|
||||
if self.enableAnimations {
|
||||
if revealProgress >= 0.4 {
|
||||
animationNode.play()
|
||||
|
|
@ -297,11 +460,26 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
fittedSize = CGSize(width: floorToScreenPixels(fittedSize.width * imageScale), height: floorToScreenPixels(fittedSize.height * imageScale))
|
||||
}
|
||||
let iconFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(iconCenterX - fittedSize.width / 2.0), y: floorToScreenPixels(iconCenterY - fittedSize.height / 2.0)), size: fittedSize)
|
||||
transition.updateFrame(node: iconNode, frame: iconFrame)
|
||||
if isPrimary {
|
||||
didApplyManualIconAnimation = true
|
||||
self.updateManualIconAnimation(iconNode: iconNode, targetFrame: iconFrame, targetTitleAlpha: titleAlpha, forceImmediate: !didApplyLayout || revealProgress < CGFloat.ulpOfOne)
|
||||
} else {
|
||||
transition.updateFrame(node: iconNode, frame: iconFrame)
|
||||
}
|
||||
}
|
||||
|
||||
let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
|
||||
let titleFrame = CGRect(origin: CGPoint(x: floor(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize)
|
||||
if !didApplyManualIconAnimation {
|
||||
self.stopManualIconAnimation()
|
||||
transition.updateAlpha(node: self.titleNode, alpha: titleAlpha)
|
||||
}
|
||||
|
||||
let titleFrame: CGRect
|
||||
if self.displaysTitleInsidePill {
|
||||
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(backgroundFrame.midX - titleSize.width / 2.0), y: floorToScreenPixels(backgroundFrame.midY - titleSize.height / 2.0)), size: titleSize)
|
||||
} else {
|
||||
let titleCenterX = isPrimary ? backgroundFrame.midX : shapeFrame.midX
|
||||
titleFrame = CGRect(origin: CGPoint(x: floorToScreenPixels(titleCenterX - titleSize.width / 2.0), y: shapeFrame.maxY + optionTitleSpacing), size: titleSize)
|
||||
}
|
||||
transition.updateFrame(node: self.titleNode, frame: titleFrame)
|
||||
}
|
||||
|
||||
|
|
@ -315,6 +493,8 @@ private final class ItemListRevealOptionNode: ASDisplayNode {
|
|||
public final class ItemListRevealOptionsNode: ASDisplayNode {
|
||||
private let optionSelected: (ItemListRevealOption) -> Void
|
||||
private let tapticAction: () -> Void
|
||||
private let clippingContainerNode: ASDisplayNode
|
||||
private let optionsContainerNode: ASDisplayNode
|
||||
|
||||
private var options: [ItemListRevealOption] = []
|
||||
private var isLeft: Bool = false
|
||||
|
|
@ -326,8 +506,14 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
public init(optionSelected: @escaping (ItemListRevealOption) -> Void, tapticAction: @escaping () -> Void) {
|
||||
self.optionSelected = optionSelected
|
||||
self.tapticAction = tapticAction
|
||||
self.clippingContainerNode = ASDisplayNode()
|
||||
self.optionsContainerNode = ASDisplayNode()
|
||||
|
||||
super.init()
|
||||
|
||||
self.clippingContainerNode.clipsToBounds = true
|
||||
self.addSubnode(self.clippingContainerNode)
|
||||
self.clippingContainerNode.addSubnode(self.optionsContainerNode)
|
||||
}
|
||||
|
||||
override public func didLoad() {
|
||||
|
|
@ -363,11 +549,11 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
}
|
||||
if isLeft {
|
||||
for node in self.optionNodes.reversed() {
|
||||
self.addSubnode(node)
|
||||
self.optionsContainerNode.addSubnode(node)
|
||||
}
|
||||
} else {
|
||||
for node in self.optionNodes {
|
||||
self.addSubnode(node)
|
||||
self.optionsContainerNode.addSubnode(node)
|
||||
}
|
||||
}
|
||||
self.invalidateCalculatedLayout()
|
||||
|
|
@ -402,6 +588,16 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
let primaryIndex = self.isLeft ? 0 : self.optionNodes.count - 1
|
||||
let stride = metrics.shapeSize.width + optionSpacing
|
||||
|
||||
let clippingFrameX: CGFloat
|
||||
if self.isLeft {
|
||||
clippingFrameX = max(0.0, size.width - revealedDistance)
|
||||
} else {
|
||||
clippingFrameX = 0.0
|
||||
}
|
||||
let clippingFrame = CGRect(origin: CGPoint(x: clippingFrameX, y: 0.0), size: CGSize(width: revealedDistance, height: size.height))
|
||||
transition.updateFrame(node: self.clippingContainerNode, frame: clippingFrame)
|
||||
transition.updateFrame(node: self.optionsContainerNode, frame: CGRect(origin: CGPoint(x: -clippingFrameX, y: 0.0), size: CGSize(width: max(size.width, revealedDistance), height: size.height)))
|
||||
|
||||
let animated = transition.isAnimated
|
||||
var completionCount = self.optionNodes.count
|
||||
let intermediateCompletion = {
|
||||
|
|
@ -419,12 +615,8 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
let isStretched = isPrimary && overswipeDistance > CGFloat.ulpOfOne
|
||||
let isExpanded = isPrimary && overswipeDistance > expandedActivationDistance
|
||||
let expandedProgress: CGFloat = isExpanded ? 1.0 : 0.0
|
||||
var nodeTransition = transition
|
||||
if node.hasAppliedLayout && node.isExpanded != isExpanded {
|
||||
nodeTransition = transition.isAnimated ? transition : .animated(duration: 0.2, curve: .easeInOut)
|
||||
if !transition.isAnimated {
|
||||
self.tapticAction()
|
||||
}
|
||||
if node.hasAppliedLayout && node.isExpanded != isExpanded && !transition.isAnimated {
|
||||
self.tapticAction()
|
||||
}
|
||||
|
||||
let baseCircleFrame: CGRect
|
||||
|
|
@ -477,7 +669,7 @@ public final class ItemListRevealOptionsNode: ASDisplayNode {
|
|||
intermediateCompletion()
|
||||
})
|
||||
|
||||
node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: nodeTransition)
|
||||
node.updateLayout(isLeft: self.isLeft, isPrimary: isPrimary, metrics: metrics, revealProgress: revealProgress, overswipeProgress: overswipeProgress, expandedProgress: expandedProgress, isStretched: isStretched, isExpanded: isExpanded, transition: transition)
|
||||
|
||||
if self.isLeft {
|
||||
i -= 1
|
||||
|
|
|
|||
|
|
@ -74,7 +74,33 @@ public class ItemListDisclosureItem: ListViewItem, ItemListItem, ListItemCompone
|
|||
public let tag: ItemListItemTag?
|
||||
public let shimmeringIndex: Int?
|
||||
|
||||
public init(presentationData: ItemListPresentationData, systemStyle: ItemListSystemStyle = .legacy, icon: UIImage? = nil, context: AccountContext? = nil, iconPeer: EnginePeer? = nil, title: String, attributedTitle: NSAttributedString? = nil, enabled: Bool = true, titleColor: ItemListDisclosureItemTitleColor = .primary, titleFont: ItemListDisclosureItemTitleFont = .regular, titleIcon: UIImage? = nil, titleBadge: String? = nil, label: String, attributedLabel: NSAttributedString? = nil, labelStyle: ItemListDisclosureLabelStyle = .text, additionalDetailLabel: String? = nil, additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic, sectionId: ItemListSectionId, style: ItemListStyle, disclosureStyle: ItemListDisclosureStyle = .arrow, noInsets: Bool = false, action: (() -> Void)?, clearHighlightAutomatically: Bool = true, tag: ItemListItemTag? = nil, shimmeringIndex: Int? = nil) {
|
||||
public init(
|
||||
presentationData: ItemListPresentationData,
|
||||
systemStyle: ItemListSystemStyle = .legacy,
|
||||
icon: UIImage? = nil,
|
||||
context: AccountContext? = nil,
|
||||
iconPeer: EnginePeer? = nil,
|
||||
title: String,
|
||||
attributedTitle: NSAttributedString? = nil,
|
||||
enabled: Bool = true,
|
||||
titleColor: ItemListDisclosureItemTitleColor = .primary,
|
||||
titleFont: ItemListDisclosureItemTitleFont = .regular,
|
||||
titleIcon: UIImage? = nil,
|
||||
titleBadge: String? = nil,
|
||||
label: String,
|
||||
attributedLabel: NSAttributedString? = nil,
|
||||
labelStyle: ItemListDisclosureLabelStyle = .text,
|
||||
additionalDetailLabel: String? = nil,
|
||||
additionalDetailLabelColor: ItemListDisclosureItemDetailLabelColor = .generic,
|
||||
sectionId: ItemListSectionId,
|
||||
style: ItemListStyle,
|
||||
disclosureStyle: ItemListDisclosureStyle = .arrow,
|
||||
noInsets: Bool = false,
|
||||
action: (() -> Void)?,
|
||||
clearHighlightAutomatically: Bool = true,
|
||||
tag: ItemListItemTag? = nil,
|
||||
shimmeringIndex: Int? = nil
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.systemStyle = systemStyle
|
||||
self.icon = icon
|
||||
|
|
@ -709,7 +735,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
|||
centralContentHeight += additionalDetailLabelInfo.0.size.height
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floor((height - centralContentHeight) / 2.0)), size: titleLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: floorToScreenPixels((height - centralContentHeight) / 2.0) + 1.0), size: titleLayout.size)
|
||||
strongSelf.titleNode.textNode.frame = titleFrame
|
||||
|
||||
if let updateBadgeImage = updatedLabelBadgeImage {
|
||||
|
|
@ -739,7 +765,7 @@ public class ItemListDisclosureItemNode: ListViewItemNode, ItemListItemNode {
|
|||
case .detailText, .multilineDetailText:
|
||||
labelFrame = CGRect(origin: CGPoint(x: leftInset, y: titleFrame.maxY + titleSpacing), size: labelLayout.size)
|
||||
default:
|
||||
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floor((height - labelLayout.size.height) / 2.0)), size: labelLayout.size)
|
||||
labelFrame = CGRect(origin: CGPoint(x: params.width - rightInset - labelLayout.size.width, y: floorToScreenPixels((height - labelLayout.size.height) / 2.0) + 1.0), size: labelLayout.size)
|
||||
}
|
||||
strongSelf.labelNode.frame = labelFrame
|
||||
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
private var enableAnimations: Bool = true
|
||||
|
||||
private var initialRevealOffset: CGFloat = 0.0
|
||||
private var hasActiveRevealGestureOffset: Bool = false
|
||||
public private(set) var revealOffset: CGFloat = 0.0
|
||||
|
||||
private var recognizer: ItemListRevealOptionsGestureRecognizer?
|
||||
|
|
@ -190,17 +191,22 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
|
||||
@objc private func revealTapGesture(_ recognizer: UITapGestureRecognizer) {
|
||||
if case .ended = recognizer.state {
|
||||
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
||||
self.updateRevealOffsetInternal(offset: 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)))
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
}
|
||||
|
||||
private func revealSpringCurve(initialVelocity: CGFloat = 0.0) -> ContainedViewLayoutTransitionCurve {
|
||||
return .customSpring(mass: 2.0, stiffness: 200.0, damping: 100.0, initialVelocity: initialVelocity)
|
||||
}
|
||||
|
||||
@objc private func revealGesture(_ recognizer: ItemListRevealOptionsGestureRecognizer) {
|
||||
guard let (size, _, _) = self.validLayout else {
|
||||
return
|
||||
}
|
||||
switch recognizer.state {
|
||||
case .began:
|
||||
self.hasActiveRevealGestureOffset = !self.revealOffset.isZero
|
||||
if let leftRevealNode = self.leftRevealNode {
|
||||
let revealSize = leftRevealNode.bounds.size
|
||||
let location = recognizer.location(in: self.view)
|
||||
|
|
@ -232,16 +238,26 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
if self.leftRevealNode == nil && CGFloat(0.0).isLess(than: translation.x) {
|
||||
self.setupAndAddLeftRevealNode()
|
||||
self.revealOptionsInteractivelyOpened()
|
||||
self.hasActiveRevealGestureOffset = true
|
||||
} else if self.rightRevealNode == nil && translation.x.isLess(than: 0.0) {
|
||||
self.setupAndAddRightRevealNode()
|
||||
self.revealOptionsInteractivelyOpened()
|
||||
self.hasActiveRevealGestureOffset = true
|
||||
}
|
||||
if !translation.x.isZero {
|
||||
self.hasActiveRevealGestureOffset = true
|
||||
}
|
||||
self.updateRevealOffsetInternal(offset: translation.x, transition: .immediate)
|
||||
if self.leftRevealNode == nil && self.rightRevealNode == nil {
|
||||
if self.leftRevealNode == nil && self.rightRevealNode == nil && !self.hasActiveRevealGestureOffset {
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
case .ended, .cancelled:
|
||||
let hasActiveRevealGestureOffset = self.hasActiveRevealGestureOffset
|
||||
self.hasActiveRevealGestureOffset = false
|
||||
guard let recognizer = self.recognizer else {
|
||||
if hasActiveRevealGestureOffset {
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -270,7 +286,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
reveal = false
|
||||
selectedOption = self.revealOptions.left.first
|
||||
} else {
|
||||
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
||||
self.updateRevealOffsetInternal(offset: reveal ?revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)))
|
||||
}
|
||||
|
||||
if let selectedOption = selectedOption {
|
||||
|
|
@ -305,7 +321,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
reveal = false
|
||||
selectedOption = self.revealOptions.right.last
|
||||
} else {
|
||||
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.3, curve: .spring))
|
||||
self.updateRevealOffsetInternal(offset: reveal ? -revealSize.width : 0.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)))
|
||||
}
|
||||
|
||||
if let selectedOption = selectedOption {
|
||||
|
|
@ -315,6 +331,8 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
}
|
||||
} else if hasActiveRevealGestureOffset {
|
||||
self.revealOptionsInteractivelyClosed()
|
||||
}
|
||||
default:
|
||||
break
|
||||
|
|
@ -473,7 +491,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
}
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if animated {
|
||||
transition = .animated(duration: 0.3, curve: .spring)
|
||||
transition = .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0))
|
||||
} else {
|
||||
transition = .immediate
|
||||
}
|
||||
|
|
@ -495,7 +513,7 @@ open class ItemListRevealOptionsItemNode: ListViewItemNode, ASGestureRecognizerD
|
|||
open func animateRevealOptionsFill(completion: (() -> Void)? = nil) {
|
||||
if let validLayout = self.validLayout {
|
||||
self.layer.allowsGroupOpacity = true
|
||||
self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.3, curve: .spring), completion: {
|
||||
self.updateRevealOffsetInternal(offset: -validLayout.0.width - 74.0, transition: .animated(duration: 0.6, curve: self.revealSpringCurve(initialVelocity: 0.0)), completion: {
|
||||
self.layer.allowsGroupOpacity = false
|
||||
completion?()
|
||||
})
|
||||
|
|
|
|||
|
|
@ -174,12 +174,15 @@ public class InfoItemNode: ListViewItemNode {
|
|||
|
||||
self.labelNode = TextNode()
|
||||
self.labelNode.isUserInteractionEnabled = false
|
||||
self.labelNode.displaysAsynchronously = false
|
||||
|
||||
self.titleNode = TextNode()
|
||||
self.titleNode.isUserInteractionEnabled = false
|
||||
self.titleNode.displaysAsynchronously = false
|
||||
|
||||
self.textNode = TextNode()
|
||||
self.textNode.isUserInteractionEnabled = false
|
||||
self.textNode.displaysAsynchronously = false
|
||||
|
||||
self.activateArea = AccessibilityAreaNode()
|
||||
self.activateArea.accessibilityTraits = .staticText
|
||||
|
|
|
|||
|
|
@ -171,6 +171,10 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
|
|||
public var tag: ItemListItemTag? {
|
||||
return self.item?.tag
|
||||
}
|
||||
|
||||
public var contextSourceView: UIView {
|
||||
return self.textNode?.view ?? self.switchNode.view
|
||||
}
|
||||
|
||||
public init(type: ItemListSwitchItemNodeType) {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
|
|
@ -251,6 +255,20 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
|
|||
})
|
||||
})
|
||||
}
|
||||
|
||||
public func updateHasContextMenu(hasContextMenu: Bool) {
|
||||
let transition: ContainedViewLayoutTransition
|
||||
if hasContextMenu {
|
||||
transition = .immediate
|
||||
} else {
|
||||
transition = .animated(duration: 0.3, curve: .easeInOut)
|
||||
}
|
||||
if let textNode = self.textNode {
|
||||
transition.updateAlpha(node: textNode, alpha: hasContextMenu ? 0.5 : 1.0)
|
||||
} else {
|
||||
transition.updateAlpha(node: self.switchNode, alpha: hasContextMenu ? 0.5 : 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
func asyncLayout() -> (_ item: ItemListSwitchItem, _ params: ListViewItemLayoutParams, _ insets: ItemListNeighbors) -> (ListViewItemNodeLayout, (Bool) -> Void) {
|
||||
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
|
||||
|
|
@ -488,7 +506,7 @@ public class ItemListSwitchItemNode: ListViewItemNode, ItemListItemNode {
|
|||
transition.updateFrame(node: strongSelf.bottomStripeNode, frame: CGRect(origin: CGPoint(x: bottomStripeInset, y: contentSize.height - separatorHeight), size: CGSize(width: layoutSize.width - params.rightInset - bottomStripeInset - separatorRightInset, height: separatorHeight)))
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset), size: titleLayout.size)
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: topInset + 1.0), size: titleLayout.size)
|
||||
transition.updatePosition(node: strongSelf.titleNode, position: titleFrame.origin)
|
||||
strongSelf.titleNode.bounds = CGRect(origin: CGPoint(), size: titleFrame.size)
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,7 @@ final class JoinLinkPreviewControllerNode: ViewControllerTracingNode, ASScrollVi
|
|||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
self.wrappingScrollNode.view.scrollsToTop = false
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
|
|
|||
|
|
@ -133,6 +133,7 @@ final class JoinLinkPreviewPeerContentNode: ASDisplayNode, ShareContentContainer
|
|||
self.descriptionNode.textAlignment = .center
|
||||
self.peersScrollNode = ASScrollNode()
|
||||
self.peersScrollNode.view.showsHorizontalScrollIndicator = false
|
||||
self.peersScrollNode.view.scrollsToTop = false
|
||||
|
||||
self.actionButtonNode = SolidRoundedButtonNode(theme: SolidRoundedButtonTheme(theme: theme), height: 52.0, cornerRadius: 11.0)
|
||||
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ final class LanguageLinkPreviewControllerNode: ViewControllerTracingNode, ASScro
|
|||
self.wrappingScrollNode.view.alwaysBounceVertical = true
|
||||
self.wrappingScrollNode.view.delaysContentTouches = false
|
||||
self.wrappingScrollNode.view.canCancelContentTouches = true
|
||||
self.wrappingScrollNode.view.scrollsToTop = false
|
||||
|
||||
self.dimNode = ASDisplayNode()
|
||||
self.dimNode.backgroundColor = UIColor(white: 0.0, alpha: 0.5)
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
|
||||
@property (nonatomic, copy) void (^captionSet)(id<TGModernGalleryItem>, NSAttributedString *);
|
||||
@property (nonatomic, copy) void (^donePressed)(id<TGModernGalleryItem>);
|
||||
@property (nonatomic, copy) void (^doneLongPressed)(id<TGModernGalleryItem>);
|
||||
@property (nonatomic, copy) void (^doneLongPressed)(id<TGModernGalleryItem>, UIView *);
|
||||
|
||||
@property (nonatomic, copy) void (^photoStripItemSelected)(NSInteger index);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <CoreMedia/CoreMedia.h>
|
||||
#import <LegacyComponents/TGPhotoToolbarViewProtocol.h>
|
||||
|
||||
@class TGPaintingData;
|
||||
@class TGStickerMaskDescription;
|
||||
|
|
@ -142,6 +143,8 @@
|
|||
|
||||
@property (nonatomic, copy) id<TGCaptionPanelView> _Nullable(^ _Nullable captionPanelView)(void);
|
||||
@property (nonatomic, copy) id<TGLivePhotoButton> _Nullable(^ _Nullable livePhotoButton)(void);
|
||||
@property (nonatomic, copy) UIView<TGPhotoToolbarViewProtocol> *_Nullable(^ _Nullable photoToolbarView)(TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, bool hasSendStarsButton);
|
||||
@property (nonatomic, copy) bool (^ _Nullable presentMediaPickerSendActionMenu)(UIView * _Nonnull sourceView, bool canSendSilently, bool canSendWhenOnline, bool canSchedule, bool reminder, bool hasTimer, void (^ _Nonnull sendSilently)(void), void (^ _Nonnull sendWhenOnline)(void), void (^ _Nonnull schedule)(void), void (^ _Nonnull sendWithTimer)(void));
|
||||
|
||||
@property (nonatomic, copy) void (^ _Nullable editCover)(CGSize dimensions, void(^_Nonnull completion)(UIImage * _Nonnull));
|
||||
|
||||
|
|
|
|||
|
|
@ -1,44 +1,14 @@
|
|||
#import <LegacyComponents/TGPhotoEditorButton.h>
|
||||
#import <LegacyComponents/TGPhotoToolbarViewProtocol.h>
|
||||
|
||||
#import <LegacyComponents/LegacyComponentsContext.h>
|
||||
|
||||
@protocol TGPhotoPaintStickersContext;
|
||||
|
||||
typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) {
|
||||
TGPhotoEditorNoneTab = 0,
|
||||
TGPhotoEditorCropTab = 1 << 0,
|
||||
TGPhotoEditorRotateTab = 1 << 1,
|
||||
TGPhotoEditorMirrorTab = 1 << 2,
|
||||
TGPhotoEditorPaintTab = 1 << 3,
|
||||
TGPhotoEditorEraserTab = 1 << 4,
|
||||
TGPhotoEditorStickerTab = 1 << 5,
|
||||
TGPhotoEditorTextTab = 1 << 6,
|
||||
TGPhotoEditorToolsTab = 1 << 7,
|
||||
TGPhotoEditorQualityTab = 1 << 8,
|
||||
TGPhotoEditorTimerTab = 1 << 9,
|
||||
TGPhotoEditorAspectRatioTab = 1 << 10,
|
||||
TGPhotoEditorTintTab = 1 << 11,
|
||||
TGPhotoEditorBlurTab = 1 << 12,
|
||||
TGPhotoEditorCurvesTab = 1 << 13
|
||||
};
|
||||
|
||||
typedef enum
|
||||
{
|
||||
TGPhotoEditorBackButtonBack,
|
||||
TGPhotoEditorBackButtonCancel
|
||||
} TGPhotoEditorBackButton;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
TGPhotoEditorDoneButtonSend,
|
||||
TGPhotoEditorDoneButtonCheck,
|
||||
TGPhotoEditorDoneButtonDone,
|
||||
TGPhotoEditorDoneButtonSchedule
|
||||
} TGPhotoEditorDoneButton;
|
||||
|
||||
@interface TGPhotoToolbarView : UIView
|
||||
@interface TGPhotoToolbarView : UIView <TGPhotoToolbarViewProtocol>
|
||||
|
||||
@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation;
|
||||
@property (nonatomic, assign) CGFloat bottomInset;
|
||||
|
||||
@property (nonatomic, readonly) UIButton *doneButton;
|
||||
|
||||
|
|
@ -79,8 +49,12 @@ typedef enum
|
|||
|
||||
- (void)setActiveTab:(TGPhotoEditorTab)tab;
|
||||
|
||||
- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset;
|
||||
- (void)setTimerButtonValue:(NSInteger)value;
|
||||
|
||||
- (void)setInfoString:(NSString *)string;
|
||||
|
||||
- (UIView *)viewForTab:(TGPhotoEditorTab)tab;
|
||||
- (TGPhotoEditorButton *)buttonForTab:(TGPhotoEditorTab)tab;
|
||||
|
||||
@end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,83 @@
|
|||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_OPTIONS(NSUInteger, TGPhotoEditorTab) {
|
||||
TGPhotoEditorNoneTab = 0,
|
||||
TGPhotoEditorCropTab = 1 << 0,
|
||||
TGPhotoEditorRotateTab = 1 << 1,
|
||||
TGPhotoEditorMirrorTab = 1 << 2,
|
||||
TGPhotoEditorPaintTab = 1 << 3,
|
||||
TGPhotoEditorEraserTab = 1 << 4,
|
||||
TGPhotoEditorStickerTab = 1 << 5,
|
||||
TGPhotoEditorTextTab = 1 << 6,
|
||||
TGPhotoEditorToolsTab = 1 << 7,
|
||||
TGPhotoEditorQualityTab = 1 << 8,
|
||||
TGPhotoEditorTimerTab = 1 << 9,
|
||||
TGPhotoEditorAspectRatioTab = 1 << 10,
|
||||
TGPhotoEditorTintTab = 1 << 11,
|
||||
TGPhotoEditorBlurTab = 1 << 12,
|
||||
TGPhotoEditorCurvesTab = 1 << 13
|
||||
};
|
||||
|
||||
typedef enum
|
||||
{
|
||||
TGPhotoEditorBackButtonBack,
|
||||
TGPhotoEditorBackButtonCancel
|
||||
} TGPhotoEditorBackButton;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
TGPhotoEditorDoneButtonSend,
|
||||
TGPhotoEditorDoneButtonCheck,
|
||||
TGPhotoEditorDoneButtonDone,
|
||||
TGPhotoEditorDoneButtonSchedule
|
||||
} TGPhotoEditorDoneButton;
|
||||
|
||||
@protocol TGPhotoToolbarViewProtocol <NSObject>
|
||||
|
||||
@property (nonatomic, assign) UIInterfaceOrientation interfaceOrientation;
|
||||
@property (nonatomic, assign) CGFloat bottomInset;
|
||||
|
||||
@property (nonatomic, readonly) UIView *doneButton;
|
||||
|
||||
@property (nonatomic, copy) void(^cancelPressed)(void);
|
||||
@property (nonatomic, copy) void(^donePressed)(void);
|
||||
@property (nonatomic, copy) void(^doneLongPressed)(id sender);
|
||||
@property (nonatomic, copy) void(^tabPressed)(TGPhotoEditorTab tab);
|
||||
|
||||
@property (nonatomic, readonly) CGRect cancelButtonFrame;
|
||||
@property (nonatomic, readonly) CGRect doneButtonFrame;
|
||||
|
||||
@property (nonatomic, assign) TGPhotoEditorBackButton backButtonType;
|
||||
@property (nonatomic, assign) TGPhotoEditorDoneButton doneButtonType;
|
||||
|
||||
@property (nonatomic, assign) int64_t sendPaidMessageStars;
|
||||
|
||||
@property (nonatomic, readonly) TGPhotoEditorTab currentTabs;
|
||||
|
||||
- (void)transitionInAnimated:(bool)animated;
|
||||
- (void)transitionInAnimated:(bool)animated transparent:(bool)transparent;
|
||||
- (void)transitionOutAnimated:(bool)animated;
|
||||
- (void)transitionOutAnimated:(bool)animated transparent:(bool)transparent hideOnCompletion:(bool)hideOnCompletion;
|
||||
|
||||
- (void)setDoneButtonEnabled:(bool)enabled animated:(bool)animated;
|
||||
- (void)setEditButtonsEnabled:(bool)enabled animated:(bool)animated;
|
||||
- (void)setEditButtonsHidden:(bool)hidden animated:(bool)animated;
|
||||
- (void)setEditButtonsHighlighted:(TGPhotoEditorTab)buttons;
|
||||
- (void)setEditButtonsDisabled:(TGPhotoEditorTab)buttons;
|
||||
|
||||
- (void)setCenterButtonsHidden:(bool)hidden animated:(bool)animated;
|
||||
- (void)setAllButtonsHidden:(bool)hidden animated:(bool)animated;
|
||||
- (void)setCancelDoneButtonsHidden:(bool)hidden animated:(bool)animated;
|
||||
|
||||
- (void)setToolbarTabs:(TGPhotoEditorTab)tabs animated:(bool)animated;
|
||||
- (void)setActiveTab:(TGPhotoEditorTab)tab;
|
||||
|
||||
- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset;
|
||||
- (void)setTimerButtonValue:(NSInteger)value;
|
||||
|
||||
- (void)setInfoString:(NSString *)string;
|
||||
|
||||
- (UIView *)viewForTab:(TGPhotoEditorTab)tab;
|
||||
|
||||
@end
|
||||
|
|
@ -1548,7 +1548,7 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
__weak TGModernGalleryController *weakGalleryController = galleryController;
|
||||
__weak TGMediaPickerGalleryModel *weakModel = model;
|
||||
|
||||
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) {
|
||||
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) {
|
||||
__strong TGCameraController *strongSelf = weakSelf;
|
||||
__strong TGMediaPickerGalleryModel *strongModel = weakModel;
|
||||
if (strongSelf == nil || !(strongSelf.hasSilentPosting || strongSelf.hasSchedule) || strongSelf->_shortcut)
|
||||
|
|
@ -1720,6 +1720,21 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
|
|||
[strongSelf _dismissTransitionForResultController:strongController];
|
||||
});
|
||||
};
|
||||
if (sourceView != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf.stickersContext.presentMediaPickerSendActionMenu(sourceView, strongSelf->_hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, strongSelf->_reminder, strongSelf->_hasTimer, ^{
|
||||
if (controller.sendSilently != nil)
|
||||
controller.sendSilently();
|
||||
}, ^{
|
||||
if (controller.sendWhenOnline != nil)
|
||||
controller.sendWhenOnline();
|
||||
}, ^{
|
||||
if (controller.schedule != nil)
|
||||
controller.schedule();
|
||||
}, ^{
|
||||
if (controller.sendWithTimer != nil)
|
||||
controller.sendWithTimer();
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
id<LegacyComponentsOverlayWindowManager> windowManager = nil;
|
||||
windowManager = [strongSelf->_context makeOverlayWindowManager];
|
||||
|
|
|
|||
|
|
@ -42,6 +42,19 @@
|
|||
#import <LegacyComponents/TGTooltipView.h>
|
||||
|
||||
#import <LegacyComponents/TGPhotoCaptionInputMixin.h>
|
||||
#import <LegacyComponents/TGPhotoPaintStickersContext.h>
|
||||
|
||||
static UIView<TGPhotoToolbarViewProtocol> *TGMediaPickerCreatePhotoToolbarView(id<LegacyComponentsContext> context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id<TGPhotoPaintStickersContext> stickersContext, bool hasSendStarsButton)
|
||||
{
|
||||
if (stickersContext.photoToolbarView != nil)
|
||||
{
|
||||
UIView<TGPhotoToolbarViewProtocol> *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, hasSendStarsButton);
|
||||
if (toolbarView != nil)
|
||||
return toolbarView;
|
||||
}
|
||||
|
||||
return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:hasSendStarsButton ? stickersContext : nil];
|
||||
}
|
||||
|
||||
static TGMediaAsset *TGMediaPickerGalleryLivePhotoAsset(id<TGMediaEditableItem> editableMediaItem)
|
||||
{
|
||||
|
|
@ -101,8 +114,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
|
||||
UIView *_wrapperView;
|
||||
UIView *_headerWrapperView;
|
||||
TGPhotoToolbarView *_portraitToolbarView;
|
||||
TGPhotoToolbarView *_landscapeToolbarView;
|
||||
UIView<TGPhotoToolbarViewProtocol> *_portraitToolbarView;
|
||||
UIView<TGPhotoToolbarViewProtocol> *_landscapeToolbarView;
|
||||
|
||||
UIImageView *_arrowView;
|
||||
UILabel *_recipientLabel;
|
||||
|
|
@ -224,8 +237,10 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
return;
|
||||
|
||||
[strongSelf.window endEditing:true];
|
||||
if (strongSelf->_doneLongPressed != nil)
|
||||
strongSelf->_doneLongPressed(strongSelf->_currentItem);
|
||||
if (strongSelf->_doneLongPressed != nil) {
|
||||
UIView *sourceView = [sender isKindOfClass:[UIView class]] ? (UIView *)sender : nil;
|
||||
strongSelf->_doneLongPressed(strongSelf->_currentItem, sourceView);
|
||||
}
|
||||
|
||||
[[NSUserDefaults standardUserDefaults] setObject:@(3) forKey:@"TG_displayedMediaTimerTooltip_v3"];
|
||||
};
|
||||
|
|
@ -471,13 +486,13 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
|
||||
TGPhotoEditorDoneButton doneButton = isScheduledMessages ? TGPhotoEditorDoneButtonSchedule : TGPhotoEditorDoneButtonSend;
|
||||
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:editingContext.sendPaidMessageStars > 0 ? stickersContext : nil];
|
||||
_portraitToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, editingContext.sendPaidMessageStars > 0);
|
||||
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
_portraitToolbarView.donePressed = toolbarDonePressed;
|
||||
_portraitToolbarView.doneLongPressed = toolbarDoneLongPressed;
|
||||
[_wrapperView addSubview:_portraitToolbarView];
|
||||
|
||||
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:TGPhotoEditorBackButtonBack doneButton:doneButton solidBackground:false stickersContext:nil];
|
||||
_landscapeToolbarView = TGMediaPickerCreatePhotoToolbarView(_context, TGPhotoEditorBackButtonBack, doneButton, false, stickersContext, false);
|
||||
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
_landscapeToolbarView.donePressed = toolbarDonePressed;
|
||||
_landscapeToolbarView.doneLongPressed = toolbarDoneLongPressed;
|
||||
|
|
@ -641,17 +656,17 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
- (UIView *)timerButton
|
||||
{
|
||||
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
|
||||
return [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab];
|
||||
return [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab];
|
||||
else
|
||||
return [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab];
|
||||
return [_landscapeToolbarView viewForTab:TGPhotoEditorTimerTab];
|
||||
}
|
||||
|
||||
- (UIView *)qualityButton
|
||||
{
|
||||
if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation))
|
||||
return [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
return [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab];
|
||||
else
|
||||
return [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
return [_landscapeToolbarView viewForTab:TGPhotoEditorQualityTab];
|
||||
}
|
||||
|
||||
- (void)setSelectedItemsModel:(TGMediaPickerGallerySelectedItemsModel *)selectedItemsModel
|
||||
|
|
@ -1188,19 +1203,15 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
|
||||
_muteButton.selected = adjustments.sendAsGif;
|
||||
|
||||
TGPhotoEditorButton *qualityButton = [_portraitToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
UIView *qualityButton = [_portraitToolbarView viewForTab:TGPhotoEditorQualityTab];
|
||||
if (qualityButton != nil)
|
||||
{
|
||||
bool isPhoto = [_currentItemView isKindOfClass:[TGMediaPickerGalleryPhotoItemView class]] || [_currentItem isKindOfClass:[TGCameraCapturedPhoto class]];
|
||||
bool isHd = false;
|
||||
TGMediaVideoConversionPreset preset = TGMediaVideoConversionPresetCompressedMedium;
|
||||
if (isPhoto) {
|
||||
bool isHd = _editingContext.isHighQualityPhoto;
|
||||
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:isHd filled: false];
|
||||
qualityButton.iconImage = icon;
|
||||
|
||||
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
qualityButton.iconImage = icon;
|
||||
isHd = _editingContext.isHighQualityPhoto;
|
||||
} else {
|
||||
TGMediaVideoConversionPreset preset = 0;
|
||||
TGMediaVideoConversionPreset adjustmentsPreset = TGMediaVideoConversionPresetCompressedDefault;
|
||||
if ([adjustments isKindOfClass:[TGMediaVideoEditAdjustments class]])
|
||||
adjustmentsPreset = ((TGMediaVideoEditAdjustments *)adjustments).preset;
|
||||
|
|
@ -1221,28 +1232,19 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
TGMediaVideoConversionPreset bestPreset = [TGMediaVideoConverter bestAvailablePresetForDimensions:dimensions];
|
||||
if (preset > bestPreset)
|
||||
preset = bestPreset;
|
||||
|
||||
UIImage *icon = [TGPhotoEditorInterfaceAssets qualityIconForPreset:preset];
|
||||
qualityButton.iconImage = icon;
|
||||
|
||||
qualityButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorQualityTab];
|
||||
qualityButton.iconImage = icon;
|
||||
}
|
||||
|
||||
[_portraitToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset];
|
||||
[_landscapeToolbarView setQualityButtonIsPhoto:isPhoto highQuality:isHd videoPreset:preset];
|
||||
}
|
||||
|
||||
TGPhotoEditorButton *timerButton = [_portraitToolbarView buttonForTab:TGPhotoEditorTimerTab];
|
||||
UIView *timerButton = [_portraitToolbarView viewForTab:TGPhotoEditorTimerTab];
|
||||
if (timerButton != nil)
|
||||
{
|
||||
NSInteger value = [timer integerValue];
|
||||
|
||||
UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0];
|
||||
UIImage *icon = [TGPhotoEditorInterfaceAssets timerIconForValue:value];
|
||||
[timerButton setIconImage:defaultIcon activeIconImage:icon];
|
||||
|
||||
TGPhotoEditorButton *landscapeTimerButton = [_landscapeToolbarView buttonForTab:TGPhotoEditorTimerTab];
|
||||
|
||||
timerButton = landscapeTimerButton;
|
||||
[timerButton setIconImage:defaultIcon activeIconImage:icon];
|
||||
[_portraitToolbarView setTimerButtonValue:value];
|
||||
[_landscapeToolbarView setTimerButtonValue:value];
|
||||
|
||||
if (value > 0)
|
||||
highlightedButtons |= TGPhotoEditorTimerTab;
|
||||
|
|
@ -1413,6 +1415,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
_coverButton.alpha = alpha;
|
||||
_arrowView.alpha = alpha * 0.6f;
|
||||
_recipientLabel.alpha = alpha * 0.6;
|
||||
_captionMixin.livePhotoButtonView.alpha = alpha;
|
||||
} completion:^(BOOL finished)
|
||||
{
|
||||
if (finished)
|
||||
|
|
@ -1420,6 +1423,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
_checkButton.userInteractionEnabled = !hidden;
|
||||
_muteButton.userInteractionEnabled = !hidden;
|
||||
_coverButton.userInteractionEnabled = !hidden;
|
||||
_captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden;
|
||||
}
|
||||
}];
|
||||
|
||||
|
|
@ -1448,6 +1452,9 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
|
||||
_arrowView.alpha = alpha * 0.6f;
|
||||
_recipientLabel.alpha = alpha * 0.6;
|
||||
|
||||
_captionMixin.livePhotoButtonView.alpha = alpha;
|
||||
_captionMixin.livePhotoButtonView.userInteractionEnabled = !hidden;
|
||||
}
|
||||
|
||||
if (hidden)
|
||||
|
|
@ -1735,11 +1742,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
{
|
||||
[self setSelectionInterfaceHidden:true animated:true];
|
||||
|
||||
[UIView animateWithDuration:0.2 animations:^
|
||||
[UIView animateWithDuration:0.3 animations:^
|
||||
{
|
||||
_captionMixin.inputPanelView.alpha = 0.0f;
|
||||
_portraitToolbarView.doneButton.alpha = 0.0f;
|
||||
_landscapeToolbarView.doneButton.alpha = 0.0f;
|
||||
_portraitToolbarView.alpha = 0.0f;
|
||||
_landscapeToolbarView.alpha = 0.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
@ -1747,11 +1754,11 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
{
|
||||
[self setSelectionInterfaceHidden:false animated:true];
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^
|
||||
[UIView animateWithDuration:0.2 animations:^
|
||||
{
|
||||
_captionMixin.inputPanelView.alpha = 1.0f;
|
||||
_portraitToolbarView.doneButton.alpha = 1.0f;
|
||||
_landscapeToolbarView.doneButton.alpha = 1.0f;
|
||||
_portraitToolbarView.alpha = 1.0f;
|
||||
_landscapeToolbarView.alpha = 1.0f;
|
||||
}];
|
||||
}
|
||||
|
||||
|
|
@ -1861,7 +1868,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
break;
|
||||
|
||||
default:
|
||||
frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height);
|
||||
frame = CGRectMake(screenEdges.left + 5, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 26 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 74.0 : 0.0), _muteButton.frame.size.width, _muteButton.frame.size.height);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -1972,7 +1979,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
break;
|
||||
|
||||
default:
|
||||
frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 45 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44);
|
||||
frame = CGRectMake(screenEdges.right - 46 - _safeAreaInset.right - buttonInset, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - panelInset - (hasHeaderView ? 64.0 : 0.0), 44, 44);
|
||||
break;
|
||||
}
|
||||
|
||||
|
|
@ -2085,6 +2092,8 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
CGFloat screenSide = MAX(screenSize.width, screenSize.height);
|
||||
UIEdgeInsets screenEdges = UIEdgeInsetsZero;
|
||||
|
||||
_portraitToolbarView.bottomInset = _safeAreaInset.bottom;
|
||||
|
||||
if (TGIsPad())
|
||||
{
|
||||
_landscapeToolbarView.hidden = true;
|
||||
|
|
@ -2118,7 +2127,7 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
_coverTitleLabel.frame = CGRectMake(screenEdges.left + floor((self.frame.size.width - _coverTitleLabel.frame.size.width) / 2.0), coverTitleTopY + 26, _coverTitleLabel.frame.size.width, _coverTitleLabel.frame.size.height);
|
||||
|
||||
UIEdgeInsets captionEdgeInsets = screenEdges;
|
||||
captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height;
|
||||
captionEdgeInsets.bottom = _portraitToolbarView.frame.size.height + 10.0;
|
||||
[_captionMixin updateLayoutWithFrame:self.bounds edgeInsets:captionEdgeInsets animated:false];
|
||||
|
||||
switch (orientation)
|
||||
|
|
@ -2157,14 +2166,14 @@ static TGMediaLivePhotoMode TGMediaPickerGalleryResolvedLivePhotoMode(NSNumber *
|
|||
{
|
||||
[UIView performWithoutAnimation:^
|
||||
{
|
||||
_photoCounterButton.frame = CGRectMake(screenEdges.right - 56 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 40 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38);
|
||||
_photoCounterButton.frame = CGRectMake(screenEdges.right - 64 - _safeAreaInset.right, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - 50 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), 64, 38);
|
||||
|
||||
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 54 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
|
||||
_selectedPhotosView.frame = CGRectMake(screenEdges.left + 4, screenEdges.bottom - TGPhotoEditorToolbarSize - [_captionMixin.inputPanel baseHeight] - photosViewSize - 64 - _safeAreaInset.bottom - (hasHeaderView ? 46.0 : 0.0), self.frame.size.width - 4 * 2 - _safeAreaInset.right, photosViewSize);
|
||||
}];
|
||||
|
||||
_landscapeToolbarView.frame = CGRectMake(_landscapeToolbarView.frame.origin.x, screenEdges.top, TGPhotoEditorToolbarSize, self.frame.size.height);
|
||||
|
||||
_headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 64.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0);
|
||||
_headerWrapperView.frame = CGRectMake(screenEdges.left, _portraitToolbarView.frame.origin.y - 74.0 - [_captionMixin.inputPanel baseHeight], self.frame.size.width, 72.0);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -142,7 +142,7 @@
|
|||
strongSelf.completeWithItem(item, false, 0);
|
||||
};
|
||||
|
||||
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item) {
|
||||
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView) {
|
||||
__strong TGMediaPickerModernGalleryMixin *strongSelf = weakSelf;
|
||||
if (strongSelf == nil || !(hasSilentPosting || hasSchedule))
|
||||
return;
|
||||
|
|
@ -236,6 +236,19 @@
|
|||
strongSelf.completeWithItem(item, false, 0);
|
||||
});
|
||||
};
|
||||
if (sourceView != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu != nil && strongSelf->_stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, effectiveHasSchedule, effectiveHasSchedule, reminder, false, ^{
|
||||
if (controller.sendSilently != nil)
|
||||
controller.sendSilently();
|
||||
}, ^{
|
||||
if (controller.sendWhenOnline != nil)
|
||||
controller.sendWhenOnline();
|
||||
}, ^{
|
||||
if (controller.schedule != nil)
|
||||
controller.schedule();
|
||||
}, ^{
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
TGOverlayControllerWindow *controllerWindow = [[TGOverlayControllerWindow alloc] initWithManager:[strongSelf->_context makeOverlayWindowManager] parentController:strongSelf->_parentController contentController:controller];
|
||||
controllerWindow.hidden = false;
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
- (void)rotate;
|
||||
- (void)mirror;
|
||||
- (void)aspectRatioButtonPressed;
|
||||
- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView;
|
||||
|
||||
- (void)setImage:(UIImage *)image;
|
||||
- (void)setSnapshotImage:(UIImage *)snapshotImage;
|
||||
|
|
|
|||
|
|
@ -22,8 +22,6 @@
|
|||
#import "TGPhotoCropView.h"
|
||||
#import <LegacyComponents/TGModernButton.h>
|
||||
|
||||
#import <LegacyComponents/TGMenuSheetController.h>
|
||||
|
||||
const CGFloat TGPhotoCropButtonsWrapperSize = 61.0f;
|
||||
const CGSize TGPhotoCropAreaInsetSize = { 9, 9 };
|
||||
|
||||
|
|
@ -535,6 +533,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
|
|||
}
|
||||
|
||||
- (void)aspectRatioButtonPressed
|
||||
{
|
||||
[self aspectRatioButtonPressedWithSourceView:nil];
|
||||
}
|
||||
|
||||
- (void)aspectRatioButtonPressedWithSourceView:(UIView *)sourceView
|
||||
{
|
||||
if (_cropView.isAnimating)
|
||||
return;
|
||||
|
|
@ -547,16 +550,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
|
|||
{
|
||||
[_cropView performConfirmAnimated:true];
|
||||
|
||||
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
|
||||
controller.dismissesByOutsideTap = true;
|
||||
controller.hasSwipeGesture = true;
|
||||
__weak TGMenuSheetController *weakController = controller;
|
||||
__weak TGPhotoCropController *weakSelf = self;
|
||||
|
||||
void (^action)(NSString *) = ^(NSString *ratioString)
|
||||
{
|
||||
__strong TGPhotoCropController *strongSelf = weakSelf;
|
||||
__strong TGMenuSheetController *strongController = weakController;
|
||||
if (strongSelf == nil)
|
||||
return;
|
||||
|
||||
|
|
@ -569,7 +567,7 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
|
|||
else
|
||||
{
|
||||
aspectRatio = [ratioString floatValue];
|
||||
if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight)
|
||||
if (strongSelf->_cropView.cropOrientation == UIImageOrientationLeft || strongSelf->_cropView.cropOrientation == UIImageOrientationRight)
|
||||
aspectRatio = 1.0f / aspectRatio;
|
||||
}
|
||||
|
||||
|
|
@ -588,13 +586,11 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
|
|||
else
|
||||
TGDispatchAfter(0.1f, dispatch_get_main_queue(), setAspectRatioBlock);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
[strongController dismissAnimated:true];
|
||||
};
|
||||
|
||||
NSMutableArray *items = [[NSMutableArray alloc] init];
|
||||
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(TGPhotoCropOriginalAspectRatio); }]];
|
||||
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action(@"1.0"); }]];
|
||||
NSMutableArray *actions = [[NSMutableArray alloc] init];
|
||||
[actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioOriginal") action:TGPhotoCropOriginalAspectRatio]];
|
||||
[actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.CropAspectRatioSquare") action:@"1.0"]];
|
||||
|
||||
CGSize croppedImageSize = _cropView.cropRect.size;
|
||||
if (_cropView.cropOrientation == UIImageOrientationLeft || _cropView.cropOrientation == UIImageOrientationRight)
|
||||
|
|
@ -634,26 +630,14 @@ NSString * const TGPhotoCropOriginalAspectRatio = @"original";
|
|||
|
||||
ratio = heightComponent / widthComponent;
|
||||
|
||||
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^{ action([NSString stringWithFormat:@"%f", ratio]); }]];
|
||||
[actions addObject:[[LegacyComponentsActionSheetAction alloc] initWithTitle:[NSString stringWithFormat:@"%d:%d", (int)widthComponent, (int)heightComponent] action:[NSString stringWithFormat:@"%f", ratio]]];
|
||||
}
|
||||
|
||||
[items addObject:[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^
|
||||
[_context presentActionSheet:actions view:(sourceView ?: self.view) completion:^(LegacyComponentsActionSheetAction *selectedAction)
|
||||
{
|
||||
__strong TGMenuSheetController *strongController = weakController;
|
||||
if (strongController != nil)
|
||||
[strongController dismissAnimated:true];
|
||||
}]];
|
||||
|
||||
[controller setItemViews:items];
|
||||
// controller.sourceRect = ^CGRect
|
||||
// {
|
||||
// __strong TGPhotoCropController *strongSelf = weakSelf;
|
||||
// if (strongSelf != nil)
|
||||
// return [strongSelf.view convertRect:strongSelf->_aspectRatioButton.frame fromView:strongSelf->_aspectRatioButton.superview];
|
||||
//
|
||||
// return CGRectZero;
|
||||
// };
|
||||
[controller presentInViewController:self.parentViewController sourceView:self.view animated:true];
|
||||
if (selectedAction != nil)
|
||||
action(selectedAction.action);
|
||||
}];
|
||||
}
|
||||
|
||||
[self _updateTabs];
|
||||
|
|
|
|||
|
|
@ -169,25 +169,7 @@
|
|||
|
||||
- (void)blurButtonPressed:(TGPhotoEditorBlurTypeButton *)sender
|
||||
{
|
||||
// if (sender.tag != 0 && sender.tag == _currentType)
|
||||
// {
|
||||
// _editingIntensity = true;
|
||||
// _startIntensity = [(PGBlurToolValue *)self.value intensity];
|
||||
//
|
||||
// PGBlurToolValue *value = [(PGBlurToolValue *)self.value copy];
|
||||
// value.editingIntensity = true;
|
||||
//
|
||||
// _value = value;
|
||||
//
|
||||
// if (self.valueChanged != nil)
|
||||
// self.valueChanged(value);
|
||||
//
|
||||
// [self setIntensitySliderHidden:false animated:true];
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
[self setSelectedBlurType:(PGBlurToolType)sender.tag update:true];
|
||||
// }
|
||||
[self setSelectedBlurType:(PGBlurToolType)sender.tag update:true];
|
||||
}
|
||||
|
||||
- (void)setValue:(id)value
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
#import <LegacyComponents/TGMediaVideoConverter.h>
|
||||
|
||||
#import <LegacyComponents/TGPhotoToolbarView.h>
|
||||
#import <LegacyComponents/TGPhotoPaintStickersContext.h>
|
||||
#import "TGPhotoEditorPreviewView.h"
|
||||
|
||||
#import <LegacyComponents/TGMenuView.h>
|
||||
|
|
@ -46,11 +47,21 @@
|
|||
#import "TGMediaPickerGalleryVideoScrubber.h"
|
||||
#import "TGMediaPickerGalleryVideoScrubberThumbnailView.h"
|
||||
|
||||
#import <LegacyComponents/TGMenuSheetController.h>
|
||||
|
||||
#import <LegacyComponents/AVURLAsset+TGMediaItem.h>
|
||||
#import <LegacyComponents/TGCameraCapturedVideo.h>
|
||||
|
||||
static UIView<TGPhotoToolbarViewProtocol> *TGPhotoEditorCreatePhotoToolbarView(id<LegacyComponentsContext> context, TGPhotoEditorBackButton backButton, TGPhotoEditorDoneButton doneButton, bool solidBackground, id<TGPhotoPaintStickersContext> stickersContext)
|
||||
{
|
||||
if (stickersContext.photoToolbarView != nil)
|
||||
{
|
||||
UIView<TGPhotoToolbarViewProtocol> *toolbarView = stickersContext.photoToolbarView(backButton, doneButton, solidBackground, false);
|
||||
if (toolbarView != nil)
|
||||
return toolbarView;
|
||||
}
|
||||
|
||||
return [[TGPhotoToolbarView alloc] initWithContext:context backButton:backButton doneButton:doneButton solidBackground:solidBackground stickersContext:nil];
|
||||
}
|
||||
|
||||
@interface TGPhotoEditorController () <TGViewControllerNavigationBarAppearance, TGMediaPickerGalleryVideoScrubberDataSource, TGMediaPickerGalleryVideoScrubberDelegate, UIDocumentInteractionControllerDelegate>
|
||||
{
|
||||
bool _switchingTab;
|
||||
|
|
@ -64,8 +75,8 @@
|
|||
UIView *_containerView;
|
||||
UIView *_wrapperView;
|
||||
UIView *_transitionWrapperView;
|
||||
TGPhotoToolbarView *_portraitToolbarView;
|
||||
TGPhotoToolbarView *_landscapeToolbarView;
|
||||
UIView<TGPhotoToolbarViewProtocol> *_portraitToolbarView;
|
||||
UIView<TGPhotoToolbarViewProtocol> *_landscapeToolbarView;
|
||||
TGPhotoEditorPreviewView *_previewView;
|
||||
PGPhotoEditorView *_fullPreviewView;
|
||||
UIView<TGPhotoDrawingEntitiesView> *_fullEntitiesView;
|
||||
|
|
@ -285,16 +296,27 @@
|
|||
|
||||
case TGPhotoEditorRotateTab:
|
||||
case TGPhotoEditorMirrorTab:
|
||||
case TGPhotoEditorAspectRatioTab:
|
||||
if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]] || [strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]])
|
||||
[strongSelf->_currentTabController handleTabAction:tab];
|
||||
break;
|
||||
|
||||
case TGPhotoEditorAspectRatioTab:
|
||||
if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoCropController class]])
|
||||
{
|
||||
UIView<TGPhotoToolbarViewProtocol> *toolbarView = UIInterfaceOrientationIsPortrait(strongSelf.effectiveOrientation) ? strongSelf->_portraitToolbarView : strongSelf->_landscapeToolbarView;
|
||||
[(TGPhotoCropController *)strongSelf->_currentTabController aspectRatioButtonPressedWithSourceView:[toolbarView viewForTab:TGPhotoEditorAspectRatioTab]];
|
||||
}
|
||||
else if ([strongSelf->_currentTabController isKindOfClass:[TGPhotoAvatarPreviewController class]])
|
||||
{
|
||||
[strongSelf->_currentTabController handleTabAction:tab];
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
TGPhotoEditorBackButton backButton = TGPhotoEditorBackButtonCancel;
|
||||
TGPhotoEditorDoneButton doneButton = TGPhotoEditorDoneButtonCheck;
|
||||
_portraitToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
|
||||
_portraitToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext);
|
||||
[_portraitToolbarView setToolbarTabs:_availableTabs animated:false];
|
||||
[_portraitToolbarView setActiveTab:_currentTab];
|
||||
_portraitToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
|
|
@ -303,7 +325,7 @@
|
|||
_portraitToolbarView.tabPressed = toolbarTabPressed;
|
||||
[_wrapperView addSubview:_portraitToolbarView];
|
||||
|
||||
_landscapeToolbarView = [[TGPhotoToolbarView alloc] initWithContext:_context backButton:backButton doneButton:doneButton solidBackground:true stickersContext:nil];
|
||||
_landscapeToolbarView = TGPhotoEditorCreatePhotoToolbarView(_context, backButton, doneButton, true, _stickersContext);
|
||||
[_landscapeToolbarView setToolbarTabs:_availableTabs animated:false];
|
||||
[_landscapeToolbarView setActiveTab:_currentTab];
|
||||
_landscapeToolbarView.cancelPressed = toolbarCancelPressed;
|
||||
|
|
@ -1974,34 +1996,12 @@
|
|||
|
||||
if ((_initialAdjustments == nil && (![editorValues isDefaultValuesForAvatar:[self presentedForAvatarCreation]] || editorValues.cropOrientation != UIImageOrientationUp)) || (_initialAdjustments != nil && ![editorValues isEqual:_initialAdjustments]))
|
||||
{
|
||||
TGMenuSheetController *controller = [[TGMenuSheetController alloc] initWithContext:_context dark:false];
|
||||
controller.dismissesByOutsideTap = true;
|
||||
controller.narrowInLandscape = true;
|
||||
__weak TGMenuSheetController *weakController = controller;
|
||||
|
||||
NSArray *items = @
|
||||
NSArray *actions = @
|
||||
[
|
||||
[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") type:TGMenuSheetButtonTypeDefault fontSize:20.0 action:^
|
||||
{
|
||||
__strong TGMenuSheetController *strongController = weakController;
|
||||
if (strongController == nil)
|
||||
return;
|
||||
|
||||
[strongController dismissAnimated:true manual:false completion:^
|
||||
{
|
||||
dismiss();
|
||||
}];
|
||||
}],
|
||||
[[TGMenuSheetButtonItemView alloc] initWithTitle:TGLocalized(@"Common.Cancel") type:TGMenuSheetButtonTypeCancel fontSize:20.0 action:^
|
||||
{
|
||||
__strong TGMenuSheetController *strongController = weakController;
|
||||
if (strongController != nil)
|
||||
[strongController dismissAnimated:true];
|
||||
}]
|
||||
[[LegacyComponentsActionSheetAction alloc] initWithTitle:TGLocalized(@"PhotoEditor.DiscardChanges") action:@"discard" type:LegacyComponentsActionSheetActionTypeDestructive]
|
||||
];
|
||||
|
||||
[controller setItemViews:items];
|
||||
controller.sourceRect = ^
|
||||
[_context presentActionSheet:actions view:self.view sourceRect:^CGRect
|
||||
{
|
||||
__strong TGPhotoEditorController *strongSelf = weakSelf;
|
||||
if (strongSelf == nil)
|
||||
|
|
@ -2011,8 +2011,11 @@
|
|||
return [strongSelf.view convertRect:strongSelf->_portraitToolbarView.cancelButtonFrame fromView:strongSelf->_portraitToolbarView];
|
||||
else
|
||||
return [strongSelf.view convertRect:strongSelf->_landscapeToolbarView.cancelButtonFrame fromView:strongSelf->_landscapeToolbarView];
|
||||
};
|
||||
[controller presentInViewController:self sourceView:self.view animated:true];
|
||||
} completion:^(LegacyComponentsActionSheetAction *selectedAction)
|
||||
{
|
||||
if ([selectedAction.action isEqualToString:@"discard"])
|
||||
dismiss();
|
||||
}];
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -30,11 +30,11 @@
|
|||
|
||||
+ (UIColor *)accentColor
|
||||
{
|
||||
TGMediaAssetsPallete *pallete = nil;
|
||||
if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)])
|
||||
pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete];
|
||||
|
||||
return pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff);
|
||||
// TGMediaAssetsPallete *pallete = nil;
|
||||
// if ([[LegacyComponentsGlobals provider] respondsToSelector:@selector(mediaAssetsPallete)])
|
||||
// pallete = [[LegacyComponentsGlobals provider] mediaAssetsPallete];
|
||||
//
|
||||
return UIColorRGB(0xffd300); //pallete.maybeAccentColor ?: UIColorRGB(0x65b3ff);
|
||||
}
|
||||
|
||||
+ (UIColor *)panelBackgroundColor
|
||||
|
|
@ -374,7 +374,7 @@
|
|||
|
||||
+ (UIFont *)editorItemTitleFont
|
||||
{
|
||||
return [TGFont systemFontOfSize:14];
|
||||
return [TGFont boldSystemFontOfSize:13]; //[TGFont systemFontOfSize:1];
|
||||
}
|
||||
|
||||
+ (UIColor *)filterSelectionColor
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@
|
|||
|
||||
bool _animatingCancelDoneButtons;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation TGPhotoToolbarView
|
||||
|
|
@ -635,6 +636,34 @@
|
|||
return nil;
|
||||
}
|
||||
|
||||
- (UIView *)viewForTab:(TGPhotoEditorTab)tab
|
||||
{
|
||||
return [self buttonForTab:tab];
|
||||
}
|
||||
|
||||
- (void)setQualityButtonIsPhoto:(bool)isPhoto highQuality:(bool)highQuality videoPreset:(NSInteger)videoPreset
|
||||
{
|
||||
TGPhotoEditorButton *qualityButton = [self buttonForTab:TGPhotoEditorQualityTab];
|
||||
if (qualityButton == nil)
|
||||
return;
|
||||
|
||||
if (isPhoto)
|
||||
qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForHighQuality:highQuality filled:false];
|
||||
else
|
||||
qualityButton.iconImage = [TGPhotoEditorInterfaceAssets qualityIconForPreset:(TGMediaVideoConversionPreset)videoPreset];
|
||||
}
|
||||
|
||||
- (void)setTimerButtonValue:(NSInteger)value
|
||||
{
|
||||
TGPhotoEditorButton *timerButton = [self buttonForTab:TGPhotoEditorTimerTab];
|
||||
if (timerButton == nil)
|
||||
return;
|
||||
|
||||
UIImage *defaultIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:0];
|
||||
UIImage *activeIcon = [TGPhotoEditorInterfaceAssets timerIconForValue:value];
|
||||
[timerButton setIconImage:defaultIcon activeIconImage:activeIcon];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
CGRect backgroundFrame = self.bounds;
|
||||
|
|
|
|||
|
|
@ -236,7 +236,7 @@
|
|||
|
||||
[strongController dismissWhenReadyAnimated:true];
|
||||
};
|
||||
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item)
|
||||
model.interfaceView.doneLongPressed = ^(TGMediaPickerGalleryItem *item, UIView *sourceView)
|
||||
{
|
||||
__strong TGModernGalleryController *strongController = weakGalleryController;
|
||||
__strong TGMediaPickerGalleryModel *strongModel = weakModel;
|
||||
|
|
@ -286,6 +286,19 @@
|
|||
complete(silentPosting, time);
|
||||
});
|
||||
};
|
||||
if (sourceView != nil && stickersContext.presentMediaPickerSendActionMenu != nil && stickersContext.presentMediaPickerSendActionMenu(sourceView, hasSilentPosting, hasSchedule, hasSchedule, reminder, false, ^{
|
||||
if (sendController.sendSilently != nil)
|
||||
sendController.sendSilently();
|
||||
}, ^{
|
||||
if (sendController.sendWhenOnline != nil)
|
||||
sendController.sendWhenOnline();
|
||||
}, ^{
|
||||
if (sendController.schedule != nil)
|
||||
sendController.schedule();
|
||||
}, ^{
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
[strongController presentViewController:sendController animated:false completion:nil];
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ swift_library(
|
|||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/LegacyUI:LegacyUI",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/MimeTypes:MimeTypes",
|
||||
"//submodules/LocalMediaResources:LocalMediaResources",
|
||||
"//submodules/SearchPeerMembers:SearchPeerMembers",
|
||||
|
|
|
|||
|
|
@ -166,6 +166,7 @@ public func legacyMediaEditor(
|
|||
snapshots: [UIView],
|
||||
transitionCompletion: (() -> Void)?,
|
||||
getCaptionPanelView: @escaping () -> TGCaptionPanelView?,
|
||||
photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)? = nil,
|
||||
hasSilentPosting: Bool = false,
|
||||
hasSchedule: Bool = false,
|
||||
reminder: Bool = false,
|
||||
|
|
@ -191,6 +192,7 @@ public func legacyMediaEditor(
|
|||
paintStickersContext.captionPanelView = {
|
||||
return getCaptionPanelView()
|
||||
}
|
||||
paintStickersContext.photoToolbarView = photoToolbarView
|
||||
|
||||
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
|
||||
let recipientName: String
|
||||
|
|
@ -208,15 +210,28 @@ public func legacyMediaEditor(
|
|||
legacyController.blocksBackgroundWhenInOverlay = true
|
||||
legacyController.acceptsFocusWhenInOverlay = true
|
||||
legacyController.statusBar.statusBarStyle = .Ignore
|
||||
paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: presentationData, presentInGlobalOverlay: { [weak legacyController] controller in
|
||||
if let legacyController {
|
||||
legacyController.presentInGlobalOverlay(controller)
|
||||
} else if let mainWindow = context.sharedContext.mainWindow {
|
||||
mainWindow.presentInGlobalOverlay(controller)
|
||||
} else {
|
||||
context.sharedContext.presentGlobalController(controller, nil)
|
||||
}
|
||||
})
|
||||
legacyController.controllerLoaded = { [weak legacyController] in
|
||||
legacyController?.view.disablesInteractiveTransitionGestureRecognizer = true
|
||||
}
|
||||
|
||||
legacyController.presentationCompleted = {
|
||||
Queue.mainQueue().after(0.1) {
|
||||
transitionCompletion?()
|
||||
}
|
||||
}
|
||||
|
||||
let schedulePicker: (Bool, @escaping (Int32, Bool) -> Void) -> Void = { media, done in
|
||||
presentSchedulePicker(media, done)
|
||||
}
|
||||
let appeared: () -> Void = {
|
||||
transitionCompletion?()
|
||||
}
|
||||
let completion: (TGMediaEditableItem, TGMediaEditingContext, Bool, Int32) -> Void = { result, editingContext, silentPosting, scheduleTime in
|
||||
let nativeGenerator = legacyAssetPickerItemGenerator()
|
||||
|
|
@ -235,64 +250,30 @@ public func legacyMediaEditor(
|
|||
|
||||
legacyController.enableSizeClassSignal = true
|
||||
|
||||
if isGif {
|
||||
let galleryController = TGPhotoVideoEditor.controller(
|
||||
with: legacyController.context,
|
||||
caption: initialCaption,
|
||||
withItem: item,
|
||||
paint: mode == .draw,
|
||||
adjustments: mode == .adjustments,
|
||||
recipientName: recipientName,
|
||||
stickersContext: paintStickersContext,
|
||||
from: .zero,
|
||||
mainSnapshot: nil,
|
||||
snapshots: snapshots as [Any],
|
||||
immediate: transitionCompletion != nil,
|
||||
activateInput: mode == .caption,
|
||||
isGif: true,
|
||||
hasSilentPosting: hasSilentPosting,
|
||||
hasSchedule: hasSchedule,
|
||||
reminder: reminder,
|
||||
presentSchedulePicker: schedulePicker,
|
||||
appeared: appeared,
|
||||
completion: completion,
|
||||
dismissed: dismissed
|
||||
)
|
||||
legacyController.bind(controller: galleryController)
|
||||
present(legacyController, nil)
|
||||
} else {
|
||||
let emptyController = LegacyEmptyController(context: legacyController.context)!
|
||||
emptyController.navigationBarShouldBeHidden = true
|
||||
let navigationController = makeLegacyNavigationController(rootController: emptyController)
|
||||
navigationController.setNavigationBarHidden(true, animated: false)
|
||||
legacyController.bind(controller: navigationController)
|
||||
|
||||
present(legacyController, nil)
|
||||
|
||||
TGPhotoVideoEditor.present(
|
||||
with: legacyController.context,
|
||||
controller: emptyController,
|
||||
caption: initialCaption,
|
||||
withItem: item,
|
||||
paint: mode == .draw,
|
||||
adjustments: mode == .adjustments,
|
||||
recipientName: recipientName,
|
||||
stickersContext: paintStickersContext,
|
||||
from: .zero,
|
||||
mainSnapshot: nil,
|
||||
snapshots: snapshots as [Any],
|
||||
immediate: transitionCompletion != nil,
|
||||
activateInput: mode == .caption,
|
||||
isGif: false,
|
||||
hasSilentPosting: hasSilentPosting,
|
||||
hasSchedule: hasSchedule,
|
||||
reminder: reminder,
|
||||
presentSchedulePicker: schedulePicker,
|
||||
appeared: appeared,
|
||||
completion: completion,
|
||||
dismissed: dismissed
|
||||
)
|
||||
}
|
||||
let galleryController = TGPhotoVideoEditor.controller(
|
||||
with: legacyController.context,
|
||||
caption: initialCaption,
|
||||
withItem: item,
|
||||
paint: mode == .draw,
|
||||
adjustments: mode == .adjustments,
|
||||
recipientName: recipientName,
|
||||
stickersContext: paintStickersContext,
|
||||
from: .zero,
|
||||
mainSnapshot: nil,
|
||||
snapshots: snapshots as [Any],
|
||||
immediate: transitionCompletion != nil,
|
||||
activateInput: mode == .caption,
|
||||
isGif: isGif,
|
||||
hasSilentPosting: hasSilentPosting,
|
||||
hasSchedule: hasSchedule,
|
||||
reminder: reminder,
|
||||
presentSchedulePicker: schedulePicker,
|
||||
appeared: appeared,
|
||||
completion: completion,
|
||||
dismissed: dismissed
|
||||
)
|
||||
legacyController.bind(controller: galleryController)
|
||||
present(legacyController, nil)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -389,6 +370,15 @@ public func legacyAttachmentMenu(
|
|||
}
|
||||
|
||||
let paintStickersContext = LegacyPaintStickersContext(context: context)
|
||||
paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context, presentationData: updatedPresentationData.initial, presentInGlobalOverlay: { [weak parentController] controller in
|
||||
if let parentController {
|
||||
parentController.presentInGlobalOverlay(controller)
|
||||
} else if let mainWindow = context.sharedContext.mainWindow {
|
||||
mainWindow.presentInGlobalOverlay(controller)
|
||||
} else {
|
||||
context.sharedContext.presentGlobalController(controller, nil)
|
||||
}
|
||||
})
|
||||
paintStickersContext.captionPanelView = {
|
||||
return getCaptionPanelView()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public func guessMimeTypeByFileExtension(_ ext: String) -> String {
|
|||
|
||||
public func configureLegacyAssetPicker(_ controller: TGMediaAssetsController, context: AccountContext, peer: EngineRawPeer, chatLocation: ChatLocation, captionsEnabled: Bool = true, storeCreatedAssets: Bool = true, showFileTooltip: Bool = false, initialCaption: NSAttributedString, hasSchedule: Bool, presentWebSearch: (() -> Void)?, presentSelectionLimitExceeded: @escaping () -> Void, presentSchedulePicker: @escaping (Bool, @escaping (Int32, Bool) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?) {
|
||||
let paintStickersContext = LegacyPaintStickersContext(context: context)
|
||||
paintStickersContext.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context)
|
||||
paintStickersContext.captionPanelView = {
|
||||
return getCaptionPanelView()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import SolidRoundedButtonNode
|
|||
import MediaEditor
|
||||
import DrawingUI
|
||||
import TelegramPresentationData
|
||||
import ContextUI
|
||||
import AnimatedCountLabelNode
|
||||
import CoreMedia
|
||||
|
||||
|
|
@ -571,15 +572,118 @@ public final class LegacyPaintEntityRenderer: NSObject, TGPhotoPaintEntityRender
|
|||
}
|
||||
}
|
||||
|
||||
public typealias LegacyMediaPickerSendActionMenuPresenter = (
|
||||
UIView,
|
||||
Bool,
|
||||
Bool,
|
||||
Bool,
|
||||
Bool,
|
||||
Bool,
|
||||
@escaping () -> Void,
|
||||
@escaping () -> Void,
|
||||
@escaping () -> Void,
|
||||
@escaping () -> Void
|
||||
) -> Bool
|
||||
|
||||
private final class LegacyMediaPickerSendActionMenuReferenceContentSource: ContextReferenceContentSource {
|
||||
private weak var sourceView: UIView?
|
||||
|
||||
init(sourceView: UIView) {
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
guard let sourceView = self.sourceView else {
|
||||
return nil
|
||||
}
|
||||
return ContextControllerReferenceViewInfo(referenceView: sourceView, contentAreaInScreenSpace: UIScreen.main.bounds)
|
||||
}
|
||||
}
|
||||
|
||||
public func makeLegacyMediaPickerSendActionMenuPresenter(
|
||||
context: AccountContext,
|
||||
presentationData: PresentationData? = nil,
|
||||
presentInGlobalOverlay: ((ViewController) -> Void)? = nil
|
||||
) -> LegacyMediaPickerSendActionMenuPresenter {
|
||||
return { sourceView, canSendSilently, canSendWhenOnline, canSchedule, reminder, hasTimer, sendSilently, sendWhenOnline, schedule, sendWithTimer in
|
||||
guard sourceView.window != nil else {
|
||||
return false
|
||||
}
|
||||
|
||||
let presentationData = (presentationData ?? context.sharedContext.currentPresentationData.with { $0 }).withUpdated(theme: defaultDarkPresentationTheme)
|
||||
var items: [ContextMenuItem] = []
|
||||
|
||||
if canSendSilently {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendSilently, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/SilentIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
sendSilently()
|
||||
})))
|
||||
}
|
||||
|
||||
if canSendWhenOnline {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_SendMessage_SendWhenOnline, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/WhenOnlineIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
sendWhenOnline()
|
||||
})))
|
||||
}
|
||||
|
||||
if canSchedule {
|
||||
items.append(.action(ContextMenuActionItem(text: reminder ? presentationData.strings.Conversation_SendMessage_SetReminder : presentationData.strings.Conversation_SendMessage_ScheduleMessage, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Input/Menu/ScheduleIcon"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
schedule()
|
||||
})))
|
||||
}
|
||||
|
||||
if hasTimer {
|
||||
items.append(.action(ContextMenuActionItem(text: presentationData.strings.Conversation_Timer_Send, icon: { theme in
|
||||
return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Timer"), color: theme.contextMenu.primaryColor)
|
||||
}, action: { _, f in
|
||||
f(.default)
|
||||
sendWithTimer()
|
||||
})))
|
||||
}
|
||||
|
||||
guard !items.isEmpty else {
|
||||
return false
|
||||
}
|
||||
|
||||
let contextController = makeContextController(
|
||||
presentationData: presentationData,
|
||||
source: .reference(LegacyMediaPickerSendActionMenuReferenceContentSource(sourceView: sourceView)),
|
||||
items: .single(ContextController.Items(content: .list(items))),
|
||||
gesture: nil
|
||||
)
|
||||
if let presentInGlobalOverlay {
|
||||
presentInGlobalOverlay(contextController)
|
||||
} else if let mainWindow = context.sharedContext.mainWindow {
|
||||
mainWindow.presentInGlobalOverlay(contextController)
|
||||
} else {
|
||||
context.sharedContext.presentGlobalController(contextController, nil)
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersContext {
|
||||
public var captionPanelView: (() -> TGCaptionPanelView?)?
|
||||
public var livePhotoButton: (() -> TGLivePhotoButton?)?
|
||||
public var photoToolbarView: ((TGPhotoEditorBackButton, TGPhotoEditorDoneButton, Bool, Bool) -> (UIView & TGPhotoToolbarViewProtocol)?)?
|
||||
public var presentMediaPickerSendActionMenu: LegacyMediaPickerSendActionMenuPresenter?
|
||||
public var editCover: ((CGSize, @escaping (UIImage) -> Void) -> Void)?
|
||||
|
||||
|
||||
private let context: AccountContext
|
||||
|
||||
|
||||
public init(context: AccountContext) {
|
||||
self.context = context
|
||||
super.init()
|
||||
|
||||
self.presentMediaPickerSendActionMenu = makeLegacyMediaPickerSendActionMenuPresenter(context: context)
|
||||
}
|
||||
|
||||
class LegacyDrawingAdapter: NSObject, TGPhotoDrawingAdapter {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ swift_library(
|
|||
"//submodules/SSignalKit/SwiftSignalKit:SwiftSignalKit",
|
||||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/AsyncDisplayKit:AsyncDisplayKit",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/Display:Display",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/TelegramAudio:TelegramAudio",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import Foundation
|
||||
import UIKit
|
||||
import ContextUI
|
||||
import Display
|
||||
import SSignalKit
|
||||
import SwiftSignalKit
|
||||
|
|
@ -28,6 +29,18 @@ private func passControllerAppearanceAnimated(in: Bool, presentation: LegacyCont
|
|||
}
|
||||
}
|
||||
|
||||
private final class LegacyActionSheetContextReferenceContentSource: ContextReferenceContentSource {
|
||||
private let sourceView: UIView
|
||||
|
||||
init(sourceView: UIView) {
|
||||
self.sourceView = sourceView
|
||||
}
|
||||
|
||||
func transitionInfo() -> ContextControllerReferenceViewInfo? {
|
||||
return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top)
|
||||
}
|
||||
}
|
||||
|
||||
private final class LegacyComponentsOverlayWindowManagerImpl: NSObject, LegacyComponentsOverlayWindowManager {
|
||||
private weak var contentController: UIViewController?
|
||||
private weak var parentController: ViewController?
|
||||
|
|
@ -260,11 +273,80 @@ public final class LegacyControllerContext: NSObject, LegacyComponentsContext {
|
|||
}
|
||||
|
||||
public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) {
|
||||
|
||||
self.presentActionSheet(actions, view: view, sourceRect: nil, completion: completion)
|
||||
}
|
||||
|
||||
public func presentActionSheet(_ actions: [LegacyComponentsActionSheetAction]!, view: UIView!, sourceRect: (() -> CGRect)!, completion: ((LegacyComponentsActionSheetAction?) -> Void)!) {
|
||||
guard let controller = self.controller, let view = view else {
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let presentationData: PresentationData
|
||||
if let context = legacyContextGet() {
|
||||
presentationData = context.sharedContext.currentPresentationData.with { $0 }.withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
} else {
|
||||
presentationData = defaultPresentationData().withUpdated(theme: defaultDarkColorPresentationTheme)
|
||||
}
|
||||
|
||||
let anchorView: UIView?
|
||||
let referenceView: UIView
|
||||
if let sourceRect = sourceRect {
|
||||
let anchor = UIView(frame: sourceRect())
|
||||
anchor.isUserInteractionEnabled = false
|
||||
anchor.backgroundColor = .clear
|
||||
view.addSubview(anchor)
|
||||
anchorView = anchor
|
||||
referenceView = anchor
|
||||
} else {
|
||||
anchorView = nil
|
||||
referenceView = view
|
||||
}
|
||||
|
||||
var didSelectAction = false
|
||||
var items: [ContextMenuItem] = []
|
||||
for legacyAction in actions ?? [] {
|
||||
if legacyAction.type == LegacyComponentsActionSheetActionTypeCancel {
|
||||
continue
|
||||
}
|
||||
guard let title = legacyAction.title else {
|
||||
continue
|
||||
}
|
||||
let textColor: ContextMenuActionItemTextColor = legacyAction.type == LegacyComponentsActionSheetActionTypeDestructive ? .destructive : .primary
|
||||
items.append(.action(ContextMenuActionItem(text: title, textColor: textColor, icon: { _ in
|
||||
return nil
|
||||
}, action: { actionContext in
|
||||
didSelectAction = true
|
||||
if let contextController = actionContext.controller {
|
||||
contextController.dismiss(result: .default, completion: {
|
||||
completion?(legacyAction)
|
||||
})
|
||||
} else {
|
||||
anchorView?.removeFromSuperview()
|
||||
completion?(legacyAction)
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
if items.isEmpty {
|
||||
anchorView?.removeFromSuperview()
|
||||
completion?(nil)
|
||||
return
|
||||
}
|
||||
|
||||
let contextController = makeContextController(
|
||||
context: legacyContextGet(),
|
||||
presentationData: presentationData,
|
||||
source: .reference(LegacyActionSheetContextReferenceContentSource(sourceView: referenceView)),
|
||||
items: .single(ContextController.Items(content: .list(items)))
|
||||
)
|
||||
contextController.dismissed = { [weak anchorView] in
|
||||
anchorView?.removeFromSuperview()
|
||||
if !didSelectAction {
|
||||
completion?(nil)
|
||||
}
|
||||
}
|
||||
controller.present(contextController, in: .window(.root))
|
||||
}
|
||||
|
||||
public func presentTooltip(_ text: String!, icon: UIImage!, sourceRect: CGRect) {
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import AsyncDisplayKit
|
|||
import Display
|
||||
import TelegramPresentationData
|
||||
|
||||
private let compactInfinityFont = Font.with(size: 14.0, design: .round, weight: .bold)
|
||||
private let compactTextFont = Font.with(size: 12.0, design: .round, weight: .bold)
|
||||
private let compactSmallTextFont = Font.with(size: 10.0, design: .round, weight: .bold)
|
||||
|
||||
private let infinityFont = Font.with(size: 15.0, design: .round, weight: .bold)
|
||||
private let textFont = Font.with(size: 13.0, design: .round, weight: .bold)
|
||||
private let smallTextFont = Font.with(size: 11.0, design: .round, weight: .bold)
|
||||
|
|
@ -134,11 +138,11 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode {
|
|||
|
||||
let font: UIFont
|
||||
if parameters.string == "∞" {
|
||||
font = infinityFont
|
||||
font = bounds.width < 28.0 ? compactInfinityFont : infinityFont
|
||||
} else if parameters.string.count > 2 {
|
||||
font = smallTextFont
|
||||
font = bounds.width < 28.0 ? compactSmallTextFont : smallTextFont
|
||||
} else {
|
||||
font = textFont
|
||||
font = bounds.width < 28.0 ? compactTextFont : textFont
|
||||
}
|
||||
|
||||
let attributes: [NSAttributedString.Key: Any] = [.font: font, .foregroundColor: parameters.foregroundColor]
|
||||
|
|
@ -147,7 +151,7 @@ public final class ChatMessageLiveLocationTimerNode: ASDisplayNode {
|
|||
|
||||
var offset = CGPoint()
|
||||
if parameters.string == "∞" {
|
||||
offset = CGPoint(x: 1.0, y: -1.0)
|
||||
offset = bounds.width < 28.0 ? CGPoint(x: 1.0 - UIScreenPixel, y: 0.0) : CGPoint(x: 1.0, y: -1.0)
|
||||
} else if parameters.string.count > 2 {
|
||||
offset = CGPoint(x: 0.0, y: UIScreenPixel)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ swift_library(
|
|||
"//submodules/TelegramCore:TelegramCore",
|
||||
"//submodules/TelegramPresentationData:TelegramPresentationData",
|
||||
"//submodules/LegacyComponents:LegacyComponents",
|
||||
"//submodules/ContextUI:ContextUI",
|
||||
"//submodules/ShareController:ShareController",
|
||||
"//submodules/AccountContext:AccountContext",
|
||||
"//submodules/OpenInExternalAppUI:OpenInExternalAppUI",
|
||||
|
|
@ -41,14 +42,20 @@ swift_library(
|
|||
"//submodules/TelegramNotices:TelegramNotices",
|
||||
"//submodules/TooltipUI:TooltipUI",
|
||||
"//submodules/UndoUI:UndoUI",
|
||||
"//submodules/Weather",
|
||||
"//submodules/AttachmentUI:AttachmentUI",
|
||||
"//submodules/AnimatedStickerNode:AnimatedStickerNode",
|
||||
"//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode",
|
||||
"//submodules/ComponentFlow",
|
||||
"//submodules/Components/BundleIconComponent",
|
||||
"//submodules/Components/SheetComponent",
|
||||
"//submodules/Components/ViewControllerComponent",
|
||||
"//submodules/TelegramUI/Components/AnimatedTextComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBackgroundComponent",
|
||||
"//submodules/TelegramUI/Components/GlassBarButtonComponent",
|
||||
"//submodules/TelegramUI/Components/GlassControls",
|
||||
"//submodules/TelegramUI/Components/LottieComponent",
|
||||
"//submodules/TelegramUI/Components/LottieComponentResourceContent",
|
||||
"//submodules/TelegramUI/Components/EdgeEffect",
|
||||
"//submodules/TelegramUI/Components/SearchInputPanelComponent",
|
||||
"//submodules/TelegramUI/Components/ButtonComponent",
|
||||
|
|
|
|||
|
|
@ -141,16 +141,18 @@ final class LocationActionListItem: ListViewItem {
|
|||
let title: String
|
||||
let subtitle: String
|
||||
let icon: LocationActionListItemIcon
|
||||
let isOpaque: Bool
|
||||
let beginTimeAndTimeout: (Double, Double)?
|
||||
let action: () -> Void
|
||||
let highlighted: (Bool) -> Void
|
||||
|
||||
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) {
|
||||
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, title: String, subtitle: String, icon: LocationActionListItemIcon, isOpaque: Bool = true, beginTimeAndTimeout: (Double, Double)?, action: @escaping () -> Void, highlighted: @escaping (Bool) -> Void = { _ in }) {
|
||||
self.presentationData = presentationData
|
||||
self.engine = engine
|
||||
self.title = title
|
||||
self.subtitle = subtitle
|
||||
self.icon = icon
|
||||
self.isOpaque = isOpaque
|
||||
self.beginTimeAndTimeout = beginTimeAndTimeout
|
||||
self.action = action
|
||||
self.highlighted = highlighted
|
||||
|
|
@ -216,6 +218,7 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.clipsToBounds = true
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
self.iconNode = ASImageNode()
|
||||
|
|
@ -232,6 +235,21 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
self.addSubnode(self.venueIconNode)
|
||||
}
|
||||
|
||||
func liveLocationContextSourceView(extend: Bool) -> UIView? {
|
||||
guard let icon = self.item?.icon else {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch icon {
|
||||
case .liveLocation:
|
||||
return extend ? nil : self.view
|
||||
case .extendLiveLocation:
|
||||
return extend ? self.view : nil
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
|
||||
if let item = self.item {
|
||||
let makeLayout = self.asyncLayout()
|
||||
|
|
@ -278,7 +296,7 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
let iconLayout = self.venueIconNode.asyncLayout()
|
||||
|
||||
return { [weak self] item, params, hasSeparator in
|
||||
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||
let leftInset: CGFloat = (item.isOpaque ? 65.0 : 72.0 ) + params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
let verticalInset: CGFloat = 8.0
|
||||
let iconSize: CGFloat = 40.0
|
||||
|
|
@ -292,7 +310,7 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
let subtitleAttributedString = NSAttributedString(string: item.subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let titleSpacing: CGFloat = 0.0
|
||||
let bottomInset: CGFloat = hasSeparator ? 0.0 : 4.0
|
||||
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset)
|
||||
if hasSeparator {
|
||||
|
|
@ -300,6 +318,8 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
}
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
var hasSeparator = hasSeparator
|
||||
|
||||
return (nodeLayout, { [weak self] in
|
||||
var updatedTheme: PresentationTheme?
|
||||
if currentItem?.presentationData.theme !== item.presentationData.theme {
|
||||
|
|
@ -315,11 +335,15 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
if item.isOpaque {
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
} else {
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
}
|
||||
}
|
||||
|
||||
var arguments: TransformImageCustomArguments?
|
||||
|
|
@ -394,15 +418,34 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
let topHighlightInset: CGFloat = separatorHeight
|
||||
let separatorRightInset: CGFloat = 16.0
|
||||
|
||||
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
|
||||
let contentLeftInset: CGFloat = item.isOpaque ? 0.0 : 7.0
|
||||
|
||||
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 15.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0)), size: CGSize(width: iconSize, height: iconSize))
|
||||
strongSelf.iconNode.frame = iconNodeFrame
|
||||
strongSelf.venueIconNode.frame = iconNodeFrame
|
||||
|
||||
strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0))
|
||||
strongSelf.wavesNode?.frame = CGRect(origin: CGPoint(x: params.leftInset + 11.0 + contentLeftInset, y: floorToScreenPixels((contentSize.height - bottomInset - iconSize) / 2.0) - 4.0), size: CGSize(width: 48.0, height: 48.0))
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
|
||||
|
||||
let highlightFrame: CGRect
|
||||
let highlightCornerRadius: CGFloat
|
||||
if item.isOpaque {
|
||||
highlightFrame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
|
||||
highlightCornerRadius = 0.0
|
||||
} else {
|
||||
highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0))
|
||||
highlightCornerRadius = highlightFrame.height * 0.5
|
||||
}
|
||||
|
||||
strongSelf.highlightedBackgroundNode.frame = highlightFrame
|
||||
strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight))
|
||||
|
||||
if !item.isOpaque {
|
||||
hasSeparator = false
|
||||
}
|
||||
strongSelf.backgroundNode.isHidden = !item.isOpaque
|
||||
strongSelf.separatorNode.isHidden = !hasSeparator
|
||||
|
||||
if let (beginTimestamp, timeout) = item.beginTimeAndTimeout {
|
||||
|
|
@ -414,9 +457,9 @@ final class LocationActionListItemNode: ListViewItemNode {
|
|||
strongSelf.addSubnode(timerNode)
|
||||
strongSelf.timerNode = timerNode
|
||||
}
|
||||
let timerSize = CGSize(width: 28.0, height: 28.0)
|
||||
let timerSize = CGSize(width: 24.0, height: 24.0)
|
||||
timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: beginTimestamp, timeout: Int32(timeout) == liveLocationIndefinitePeriod ? -1.0 : timeout, strings: item.presentationData.strings)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 15.0 - contentLeftInset - timerSize.width, y: floorToScreenPixels((contentSize.height - timerSize.height) / 2.0) - 2.0), size: timerSize)
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
timerNode.removeFromSupernode()
|
||||
|
|
|
|||
|
|
@ -177,6 +177,7 @@ public class LocationPinAnnotationView: MKAnnotationView {
|
|||
var hasPulse = false
|
||||
|
||||
var headingKvoToken: NSKeyValueObservation?
|
||||
private var mapHeading: CGFloat = 0.0
|
||||
|
||||
override public class var layerClass: AnyClass {
|
||||
return LocationPinAnnotationLayer.self
|
||||
|
|
@ -291,12 +292,10 @@ public class LocationPinAnnotationView: MKAnnotationView {
|
|||
headingKvoToken.invalidate()
|
||||
}
|
||||
|
||||
self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, change) in
|
||||
guard let heading = change.newValue else {
|
||||
return
|
||||
}
|
||||
self?.updateHeading(heading)
|
||||
self.headingKvoToken = annotation.observe(\.heading, options: .new) { [weak self] (_, _) in
|
||||
self?.updateHeading()
|
||||
}
|
||||
self.updateHeading()
|
||||
}
|
||||
else if let peer = annotation.peer {
|
||||
self.iconNode.isHidden = true
|
||||
|
|
@ -310,7 +309,7 @@ public class LocationPinAnnotationView: MKAnnotationView {
|
|||
self.headingKvoToken = nil
|
||||
headingKvoToken.invalidate()
|
||||
}
|
||||
self.updateHeading(nil)
|
||||
self.updateHeading()
|
||||
} else if let location = annotation.location {
|
||||
let venueType = location.venue?.type ?? ""
|
||||
let color = venueType.isEmpty ? annotation.theme.list.itemAccentColor : venueIconColor(type: venueType)
|
||||
|
|
@ -347,16 +346,23 @@ public class LocationPinAnnotationView: MKAnnotationView {
|
|||
self.headingKvoToken = nil
|
||||
headingKvoToken.invalidate()
|
||||
}
|
||||
self.updateHeading(nil)
|
||||
self.updateHeading()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func updateHeading(_ heading: NSNumber?) {
|
||||
if let heading = heading?.int32Value {
|
||||
func updateMapHeading(_ mapHeading: CGFloat) {
|
||||
if self.mapHeading != mapHeading {
|
||||
self.mapHeading = mapHeading
|
||||
self.updateHeading()
|
||||
}
|
||||
}
|
||||
|
||||
private func updateHeading() {
|
||||
if let heading = (self.annotation as? LocationPinAnnotation)?.heading?.doubleValue {
|
||||
self.arrowNode.isHidden = false
|
||||
self.arrowNode.transform = CATransform3DMakeRotation(CGFloat(heading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
self.arrowNode.transform = CATransform3DMakeRotation((CGFloat(heading) - self.mapHeading) / 180.0 * CGFloat.pi, 0.0, 0.0, 1.0)
|
||||
} else {
|
||||
self.arrowNode.isHidden = true
|
||||
self.arrowNode.transform = CATransform3DIdentity
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -7,9 +7,10 @@ import TelegramCore
|
|||
import TelegramPresentationData
|
||||
import ItemListUI
|
||||
import LocationResources
|
||||
import AppBundle
|
||||
import SolidRoundedButtonNode
|
||||
import ShimmerEffect
|
||||
import ComponentFlow
|
||||
import ButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
public final class LocationInfoListItem: ListViewItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
|
|
@ -18,27 +19,35 @@ public final class LocationInfoListItem: ListViewItem {
|
|||
let address: String?
|
||||
let distance: String?
|
||||
let drivingTime: ExpectedTravelTime
|
||||
let transitTime: ExpectedTravelTime
|
||||
let walkingTime: ExpectedTravelTime
|
||||
let hasEta: Bool
|
||||
let action: () -> Void
|
||||
let drivingAction: () -> Void
|
||||
let transitAction: () -> Void
|
||||
let walkingAction: () -> Void
|
||||
|
||||
public init(presentationData: ItemListPresentationData, engine: TelegramEngine, location: TelegramMediaMap, address: String?, distance: String?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, hasEta: Bool, action: @escaping () -> Void, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
|
||||
public init(
|
||||
presentationData: ItemListPresentationData,
|
||||
engine: TelegramEngine,
|
||||
location: TelegramMediaMap,
|
||||
address: String?,
|
||||
distance: String?,
|
||||
drivingTime: ExpectedTravelTime,
|
||||
walkingTime: ExpectedTravelTime,
|
||||
hasEta: Bool,
|
||||
action: @escaping () -> Void,
|
||||
drivingAction: @escaping () -> Void,
|
||||
walkingAction: @escaping () -> Void
|
||||
) {
|
||||
self.presentationData = presentationData
|
||||
self.engine = engine
|
||||
self.location = location
|
||||
self.address = address
|
||||
self.distance = distance
|
||||
self.drivingTime = drivingTime
|
||||
self.transitTime = transitTime
|
||||
self.walkingTime = walkingTime
|
||||
self.hasEta = hasEta
|
||||
self.action = action
|
||||
self.drivingAction = drivingAction
|
||||
self.transitAction = transitAction
|
||||
self.walkingAction = walkingAction
|
||||
}
|
||||
|
||||
|
|
@ -76,31 +85,26 @@ public final class LocationInfoListItem: ListViewItem {
|
|||
}
|
||||
|
||||
public final class LocationInfoListItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private var titleNode: TextNode?
|
||||
private var subtitleNode: TextNode?
|
||||
private let venueIconNode: TransformImageNode
|
||||
private let buttonNode: HighlightableButtonNode
|
||||
|
||||
private var placeholderNode: ShimmerEffectNode?
|
||||
private var drivingButtonNode: SolidRoundedButtonNode?
|
||||
private var transitButtonNode: SolidRoundedButtonNode?
|
||||
private var walkingButtonNode: SolidRoundedButtonNode?
|
||||
private let drivingButton = ComponentView<Empty>()
|
||||
private let walkingButton = ComponentView<Empty>()
|
||||
|
||||
private var item: LocationInfoListItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
private var absoluteLocation: (CGRect, CGSize)?
|
||||
|
||||
required public init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
self.buttonNode = HighlightableButtonNode()
|
||||
self.venueIconNode = TransformImageNode()
|
||||
self.venueIconNode.isUserInteractionEnabled = false
|
||||
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
//self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.buttonNode)
|
||||
self.addSubnode(self.venueIconNode)
|
||||
|
||||
|
|
@ -145,14 +149,15 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
let iconLayout = self.venueIconNode.asyncLayout()
|
||||
|
||||
return { [weak self] item, params in
|
||||
let leftInset: CGFloat = 75.0 + params.leftInset
|
||||
let leftInset: CGFloat = 78.0 + params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
let verticalInset: CGFloat = 14.0
|
||||
let iconSize: CGFloat = 48.0
|
||||
let inset: CGFloat = 15.0
|
||||
let iconSize: CGFloat = 40.0
|
||||
let directionsButtonHeight: CGFloat = 52.0
|
||||
let directionsTopInset: CGFloat = 18.0
|
||||
|
||||
let titleFont = Font.medium(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 14.0 / 17.0))
|
||||
let titleFont = Font.semibold(item.presentationData.fontSize.itemListBaseFontSize)
|
||||
let subtitleFont = Font.regular(floor(item.presentationData.fontSize.itemListBaseFontSize * 15.0 / 17.0))
|
||||
|
||||
let title: String
|
||||
let subtitle: String
|
||||
|
|
@ -179,10 +184,11 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 15.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let bottomInset: CGFloat = 4.0
|
||||
let titleSpacing: CGFloat = 0.0
|
||||
let bottomInset: CGFloat = 16.0
|
||||
let textContentSize = verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + bottomInset
|
||||
let contentSize = CGSize(width: params.width, height: item.hasEta ? max(100.0, textContentSize) : textContentSize)
|
||||
let etaContentSize = verticalInset + titleLayout.size.height + titleSpacing + subtitleLayout.size.height + directionsTopInset + directionsButtonHeight + bottomInset
|
||||
let contentSize = CGSize(width: params.width, height: item.hasEta ? max(etaContentSize, textContentSize) : textContentSize)
|
||||
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
|
||||
|
||||
return (nodeLayout, { [weak self] in
|
||||
|
|
@ -200,13 +206,7 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
if let strongSelf = self {
|
||||
strongSelf.item = item
|
||||
strongSelf.layoutParams = params
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
}
|
||||
|
||||
strongSelf.backgroundNode.isHidden = params.isStandalone
|
||||
|
||||
|
||||
let arguments = VenueIconArguments(defaultBackgroundColor: item.presentationData.theme.chat.inputPanel.actionControlFillColor, defaultForegroundColor: item.presentationData.theme.chat.inputPanel.actionControlForegroundColor)
|
||||
if let updatedLocation = updatedLocation {
|
||||
strongSelf.venueIconNode.setSignal(venueIcon(engine: item.engine, type: updatedLocation.venue?.type ?? "", background: true))
|
||||
|
|
@ -229,107 +229,126 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
strongSelf.addSubnode(subtitleNode)
|
||||
}
|
||||
|
||||
let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
|
||||
if strongSelf.drivingButtonNode == nil {
|
||||
strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.drivingButtonNode?.iconSpacing = 5.0
|
||||
strongSelf.drivingButtonNode?.alpha = 0.0
|
||||
strongSelf.drivingButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.drivingButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.drivingAction()
|
||||
}
|
||||
}
|
||||
strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
|
||||
strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.transitButtonNode?.iconSpacing = 2.0
|
||||
strongSelf.transitButtonNode?.alpha = 0.0
|
||||
strongSelf.transitButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.transitButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.transitAction()
|
||||
}
|
||||
}
|
||||
strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
|
||||
strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.walkingButtonNode?.iconSpacing = 2.0
|
||||
strongSelf.walkingButtonNode?.alpha = 0.0
|
||||
strongSelf.walkingButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.walkingButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.walkingAction()
|
||||
}
|
||||
}
|
||||
strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
} else if let _ = updatedTheme {
|
||||
strongSelf.drivingButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
|
||||
strongSelf.transitButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
|
||||
strongSelf.walkingButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
|
||||
titleNode.frame = titleFrame
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)
|
||||
subtitleNode.frame = subtitleFrame
|
||||
|
||||
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + inset, y: 10.0), size: CGSize(width: iconSize, height: iconSize))
|
||||
let iconNodeFrame = CGRect(origin: CGPoint(x: params.leftInset + 26.0, y: 14.0), size: CGSize(width: iconSize, height: iconSize))
|
||||
strongSelf.venueIconNode.frame = iconNodeFrame
|
||||
|
||||
var directionsWidth: CGFloat = 93.0
|
||||
let glassInset: CGFloat = 6.0
|
||||
let buttonSideInset: CGFloat = 30.0
|
||||
let buttonSpacing: CGFloat = 10.0
|
||||
var directionsWidth: CGFloat = floorToScreenPixels((params.width - glassInset * 2.0 - buttonSideInset * 2.0 - buttonSpacing) / 2.0)
|
||||
|
||||
if item.hasEta {
|
||||
if item.drivingTime == .unknown && item.transitTime == .unknown && item.walkingTime == .unknown {
|
||||
strongSelf.drivingButtonNode?.icon = nil
|
||||
strongSelf.drivingButtonNode?.title = item.presentationData.strings.Map_GetDirections
|
||||
if let drivingButtonNode = strongSelf.drivingButtonNode {
|
||||
let buttonSize = drivingButtonNode.sizeThatFits(contentSize)
|
||||
directionsWidth = buttonSize.width
|
||||
}
|
||||
let buttonBackground = ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: item.presentationData.theme.list.itemCheckColors.fillColor,
|
||||
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
)
|
||||
let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
|
||||
var drivingButtonTitle = ""
|
||||
var walkingButtonTitle = ""
|
||||
var drivingButtonHasIcon = true
|
||||
var drivingButtonVisible = false
|
||||
var walkingButtonVisible = false
|
||||
|
||||
if item.drivingTime == .unknown && item.walkingTime == .unknown {
|
||||
drivingButtonHasIcon = false
|
||||
drivingButtonTitle = item.presentationData.strings.Map_GetDirections
|
||||
drivingButtonVisible = true
|
||||
|
||||
if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime {
|
||||
strongSelf.drivingButtonNode?.alpha = 1.0
|
||||
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.drivingButton.view?.alpha = 1.0
|
||||
strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
} else {
|
||||
if case let .ready(drivingTime) = item.drivingTime {
|
||||
strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 })
|
||||
drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? ""
|
||||
drivingButtonVisible = true
|
||||
|
||||
if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime {
|
||||
strongSelf.drivingButtonNode?.alpha = 1.0
|
||||
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .ready(transitTime) = item.transitTime {
|
||||
strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 })
|
||||
|
||||
if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime {
|
||||
strongSelf.transitButtonNode?.alpha = 1.0
|
||||
strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.drivingButton.view?.alpha = 1.0
|
||||
strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .ready(walkingTime) = item.walkingTime {
|
||||
strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 })
|
||||
walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? ""
|
||||
walkingButtonVisible = true
|
||||
|
||||
if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime {
|
||||
strongSelf.walkingButtonNode?.alpha = 1.0
|
||||
strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.walkingButton.view?.alpha = 1.0
|
||||
strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let drivingButtonContent: AnyComponent<Empty>
|
||||
if drivingButtonHasIcon {
|
||||
drivingButtonContent = AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor))),
|
||||
AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor)))
|
||||
], spacing: 5.0)
|
||||
)
|
||||
} else {
|
||||
drivingButtonContent = AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(17.0), color: foregroundColor))
|
||||
}
|
||||
|
||||
let directionsSpacing: CGFloat = 8.0
|
||||
let drivingButtonSize = strongSelf.drivingButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: buttonBackground,
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("driving-\(drivingButtonHasIcon)-\(drivingButtonTitle)"),
|
||||
component: drivingButtonContent
|
||||
),
|
||||
action: { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.drivingAction()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: drivingButtonHasIcon ? directionsWidth : contentSize.width - glassInset * 2.0 - buttonSideInset * 2.0, height: directionsButtonHeight)
|
||||
)
|
||||
if !drivingButtonHasIcon {
|
||||
directionsWidth = drivingButtonSize.width
|
||||
}
|
||||
|
||||
if case .calculating = item.drivingTime, case .calculating = item.transitTime, case .calculating = item.walkingTime {
|
||||
let walkingButtonSize = strongSelf.walkingButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: buttonBackground,
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("walking-\(walkingButtonTitle)"),
|
||||
component: AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))),
|
||||
AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(17.0), color: foregroundColor)))
|
||||
], spacing: 2.0)
|
||||
)
|
||||
),
|
||||
contentInsets: UIEdgeInsets(),
|
||||
action: { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.walkingAction()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: CGSize(width: directionsWidth, height: directionsButtonHeight)
|
||||
)
|
||||
|
||||
var buttonOrigin = glassInset + buttonSideInset
|
||||
|
||||
if case .calculating = item.drivingTime, case .calculating = item.walkingTime {
|
||||
let shimmerNode: ShimmerEffectNode
|
||||
if let current = strongSelf.placeholderNode {
|
||||
shimmerNode = current
|
||||
|
|
@ -338,17 +357,17 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
strongSelf.placeholderNode = shimmerNode
|
||||
strongSelf.addSubnode(shimmerNode)
|
||||
}
|
||||
shimmerNode.frame = CGRect(origin: CGPoint(x: leftInset, y: subtitleFrame.maxY + 12.0), size: CGSize(width: contentSize.width - leftInset, height: 32.0))
|
||||
shimmerNode.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: contentSize.width - buttonOrigin * 2.0, height: directionsButtonHeight))
|
||||
if let (rect, size) = strongSelf.absoluteLocation {
|
||||
shimmerNode.updateAbsoluteRect(rect, within: size)
|
||||
}
|
||||
|
||||
var shapes: [ShimmerEffectNode.Shape] = []
|
||||
shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: 32.0))
|
||||
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0))
|
||||
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + directionsSpacing + directionsWidth + directionsSpacing, y: 0.0), width: directionsWidth, diameter: 32.0))
|
||||
shapes.append(.roundedRectLine(startPoint: CGPoint(x: 0.0, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight))
|
||||
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight))
|
||||
shapes.append(.roundedRectLine(startPoint: CGPoint(x: directionsWidth + buttonSpacing + directionsWidth + buttonSpacing, y: 0.0), width: directionsWidth, diameter: directionsButtonHeight))
|
||||
|
||||
shimmerNode.update(backgroundColor: item.presentationData.theme.list.plainBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size)
|
||||
shimmerNode.update(backgroundColor: .clear, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: shimmerNode.frame.size, mask: true)
|
||||
} else if let shimmerNode = strongSelf.placeholderNode {
|
||||
strongSelf.placeholderNode = nil
|
||||
shimmerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak shimmerNode] _ in
|
||||
|
|
@ -356,30 +375,42 @@ public final class LocationInfoListItemNode: ListViewItemNode {
|
|||
})
|
||||
}
|
||||
|
||||
let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
|
||||
var buttonOrigin = leftInset
|
||||
strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight))
|
||||
let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: drivingButtonSize.height))
|
||||
if let drivingButtonView = strongSelf.drivingButton.view {
|
||||
if drivingButtonView.superview == nil {
|
||||
strongSelf.view.addSubview(drivingButtonView)
|
||||
}
|
||||
drivingButtonView.frame = drivingButtonFrame
|
||||
if drivingButtonView.layer.animation(forKey: "opacity") == nil {
|
||||
drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
if case .ready = item.drivingTime {
|
||||
buttonOrigin += directionsWidth + directionsSpacing
|
||||
buttonOrigin += directionsWidth + buttonSpacing
|
||||
}
|
||||
|
||||
strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight))
|
||||
|
||||
if case .ready = item.transitTime {
|
||||
buttonOrigin += directionsWidth + directionsSpacing
|
||||
let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + directionsTopInset), size: CGSize(width: directionsWidth, height: walkingButtonSize.height))
|
||||
if let walkingButtonView = strongSelf.walkingButton.view {
|
||||
if walkingButtonView.superview == nil {
|
||||
strongSelf.view.addSubview(walkingButtonView)
|
||||
}
|
||||
walkingButtonView.frame = walkingButtonFrame
|
||||
if walkingButtonView.layer.animation(forKey: "opacity") == nil {
|
||||
walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight))
|
||||
} else {
|
||||
|
||||
strongSelf.drivingButton.view?.alpha = 0.0
|
||||
strongSelf.walkingButton.view?.alpha = 0.0
|
||||
if let shimmerNode = strongSelf.placeholderNode {
|
||||
strongSelf.placeholderNode = nil
|
||||
shimmerNode.removeFromSupernode()
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.buttonNode.frame = CGRect(x: 0.0, y: 0.0, width: contentSize.width, height: 72.0)
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@ import TelegramUIPreferences
|
|||
import TelegramStringFormatting
|
||||
import ItemListUI
|
||||
import LocationResources
|
||||
import AppBundle
|
||||
import AvatarNode
|
||||
import LiveLocationTimerNode
|
||||
import SolidRoundedButtonNode
|
||||
import ComponentFlow
|
||||
import ButtonComponent
|
||||
import BundleIconComponent
|
||||
|
||||
final class LocationLiveListItem: ListViewItem {
|
||||
let presentationData: ItemListPresentationData
|
||||
|
|
@ -24,17 +25,15 @@ final class LocationLiveListItem: ListViewItem {
|
|||
let distance: Double?
|
||||
|
||||
let drivingTime: ExpectedTravelTime
|
||||
let transitTime: ExpectedTravelTime
|
||||
let walkingTime: ExpectedTravelTime
|
||||
|
||||
let action: () -> Void
|
||||
let longTapAction: () -> Void
|
||||
|
||||
let drivingAction: () -> Void
|
||||
let transitAction: () -> Void
|
||||
let walkingAction: () -> Void
|
||||
|
||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, transitTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, transitAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
|
||||
public init(presentationData: ItemListPresentationData, dateTimeFormat: PresentationDateTimeFormat, nameDisplayOrder: PresentationPersonNameOrder, context: AccountContext, message: EngineMessage, distance: Double?, drivingTime: ExpectedTravelTime, walkingTime: ExpectedTravelTime, action: @escaping () -> Void, longTapAction: @escaping () -> Void = { }, drivingAction: @escaping () -> Void, walkingAction: @escaping () -> Void) {
|
||||
self.presentationData = presentationData
|
||||
self.dateTimeFormat = dateTimeFormat
|
||||
self.nameDisplayOrder = nameDisplayOrder
|
||||
|
|
@ -42,12 +41,10 @@ final class LocationLiveListItem: ListViewItem {
|
|||
self.message = message
|
||||
self.distance = distance
|
||||
self.drivingTime = drivingTime
|
||||
self.transitTime = transitTime
|
||||
self.walkingTime = walkingTime
|
||||
self.action = action
|
||||
self.longTapAction = longTapAction
|
||||
self.drivingAction = drivingAction
|
||||
self.transitAction = transitAction
|
||||
self.walkingAction = walkingAction
|
||||
}
|
||||
|
||||
|
|
@ -91,28 +88,19 @@ final class LocationLiveListItem: ListViewItem {
|
|||
|
||||
private let avatarFont = avatarPlaceholderFont(size: floor(40.0 * 16.0 / 37.0))
|
||||
final class LocationLiveListItemNode: ListViewItemNode {
|
||||
private let backgroundNode: ASDisplayNode
|
||||
private let separatorNode: ASDisplayNode
|
||||
private let highlightedBackgroundNode: ASDisplayNode
|
||||
private var titleNode: TextNode?
|
||||
private var subtitleNode: TextNode?
|
||||
private let avatarNode: AvatarNode
|
||||
private var timerNode: ChatMessageLiveLocationTimerNode?
|
||||
|
||||
private var drivingButtonNode: SolidRoundedButtonNode?
|
||||
private var transitButtonNode: SolidRoundedButtonNode?
|
||||
private var walkingButtonNode: SolidRoundedButtonNode?
|
||||
private let drivingButton = ComponentView<Empty>()
|
||||
private let walkingButton = ComponentView<Empty>()
|
||||
|
||||
private var item: LocationLiveListItem?
|
||||
private var layoutParams: ListViewItemLayoutParams?
|
||||
|
||||
required init() {
|
||||
self.backgroundNode = ASDisplayNode()
|
||||
self.backgroundNode.isLayerBacked = true
|
||||
|
||||
self.separatorNode = ASDisplayNode()
|
||||
self.separatorNode.isLayerBacked = true
|
||||
|
||||
self.highlightedBackgroundNode = ASDisplayNode()
|
||||
self.highlightedBackgroundNode.isLayerBacked = true
|
||||
|
||||
|
|
@ -121,8 +109,6 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
|
||||
super.init(layerBacked: false, rotated: false, seeThrough: false)
|
||||
|
||||
self.addSubnode(self.backgroundNode)
|
||||
self.addSubnode(self.separatorNode)
|
||||
self.addSubnode(self.avatarNode)
|
||||
}
|
||||
|
||||
|
|
@ -142,7 +128,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
if highlighted {
|
||||
self.highlightedBackgroundNode.alpha = 1.0
|
||||
if self.highlightedBackgroundNode.supernode == nil {
|
||||
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.separatorNode)
|
||||
self.insertSubnode(self.highlightedBackgroundNode, at: 0)
|
||||
}
|
||||
} else {
|
||||
if self.highlightedBackgroundNode.supernode != nil {
|
||||
|
|
@ -169,7 +155,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
let makeSubtitleLayout = TextNode.asyncLayout(self.subtitleNode)
|
||||
|
||||
return { [weak self] item, params, hasSeparator in
|
||||
let leftInset: CGFloat = 65.0 + params.leftInset
|
||||
let leftInset: CGFloat = 72.0 + params.leftInset
|
||||
let rightInset: CGFloat = params.rightInset
|
||||
let verticalInset: CGFloat = 8.0
|
||||
|
||||
|
|
@ -203,18 +189,17 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
let subtitleAttributedString = NSAttributedString(string: subtitle, font: subtitleFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor)
|
||||
let (subtitleLayout, subtitleApply) = makeSubtitleLayout(TextNodeLayoutArguments(attributedString: subtitleAttributedString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - leftInset - rightInset - 54.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
|
||||
|
||||
let titleSpacing: CGFloat = 1.0
|
||||
let titleSpacing: CGFloat = 0.0
|
||||
var contentSize = CGSize(width: params.width, height: verticalInset * 2.0 + titleLayout.size.height + titleSpacing + subtitleLayout.size.height)
|
||||
let hasEta: Bool
|
||||
var hasEta: Bool
|
||||
if case .ready = item.drivingTime {
|
||||
hasEta = true
|
||||
} else if case .ready = item.transitTime {
|
||||
hasEta = true
|
||||
} else if case .ready = item.walkingTime {
|
||||
hasEta = true
|
||||
} else {
|
||||
hasEta = false
|
||||
}
|
||||
hasEta = true
|
||||
if hasEta {
|
||||
contentSize.height += 46.0
|
||||
}
|
||||
|
|
@ -232,9 +217,7 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
strongSelf.layoutParams = params
|
||||
|
||||
if let _ = updatedTheme {
|
||||
strongSelf.separatorNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
|
||||
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
|
||||
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.contextMenu.itemHighlightedBackgroundColor
|
||||
}
|
||||
|
||||
let titleNode = titleApply()
|
||||
|
|
@ -249,72 +232,23 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
strongSelf.addSubnode(subtitleNode)
|
||||
}
|
||||
|
||||
let buttonTheme = SolidRoundedButtonTheme(theme: item.presentationData.theme)
|
||||
if strongSelf.drivingButtonNode == nil {
|
||||
strongSelf.drivingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.drivingButtonNode?.alpha = 0.0
|
||||
strongSelf.drivingButtonNode?.iconSpacing = 5.0
|
||||
strongSelf.drivingButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.drivingButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.drivingAction()
|
||||
}
|
||||
}
|
||||
strongSelf.drivingButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
|
||||
strongSelf.transitButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.transitButtonNode?.alpha = 0.0
|
||||
strongSelf.transitButtonNode?.iconSpacing = 2.0
|
||||
strongSelf.transitButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.transitButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.transitAction()
|
||||
}
|
||||
}
|
||||
strongSelf.transitButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
|
||||
strongSelf.walkingButtonNode = SolidRoundedButtonNode(icon: generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor), theme: buttonTheme, fontSize: 15.0, height: 32.0, cornerRadius: 16.0)
|
||||
strongSelf.walkingButtonNode?.alpha = 0.0
|
||||
strongSelf.walkingButtonNode?.iconSpacing = 2.0
|
||||
strongSelf.walkingButtonNode?.allowsGroupOpacity = true
|
||||
strongSelf.walkingButtonNode?.pressed = { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.walkingAction()
|
||||
}
|
||||
}
|
||||
strongSelf.walkingButtonNode.flatMap { strongSelf.addSubnode($0) }
|
||||
} else if let _ = updatedTheme {
|
||||
strongSelf.drivingButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.drivingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsDriving"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
|
||||
strongSelf.transitButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.transitButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsTransit"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
|
||||
strongSelf.walkingButtonNode?.updateTheme(buttonTheme)
|
||||
strongSelf.walkingButtonNode?.icon = generateTintedImage(image: UIImage(bundleImageName: "Location/DirectionsWalking"), color: item.presentationData.theme.list.itemCheckColors.foregroundColor)
|
||||
}
|
||||
|
||||
let titleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset), size: titleLayout.size)
|
||||
titleNode.frame = titleFrame
|
||||
|
||||
let subtitleFrame = CGRect(origin: CGPoint(x: leftInset, y: verticalInset + titleLayout.size.height + titleSpacing), size: subtitleLayout.size)
|
||||
subtitleNode.frame = subtitleFrame
|
||||
|
||||
let separatorHeight = UIScreenPixel
|
||||
let topHighlightInset: CGFloat = separatorHeight
|
||||
let separatorRightInset: CGFloat = 16.0
|
||||
let avatarSize: CGFloat = 40.0
|
||||
|
||||
if let peer = item.message.author {
|
||||
strongSelf.avatarNode.setPeer(context: item.context, theme: item.presentationData.theme, peer: peer, overrideImage: nil, emptyColor: item.presentationData.theme.list.mediaPlaceholderColor, synchronousLoad: false)
|
||||
}
|
||||
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 15.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
strongSelf.avatarNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 22.0, y: 8.0), size: CGSize(width: avatarSize, height: avatarSize))
|
||||
|
||||
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: contentSize.width, height: contentSize.height))
|
||||
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top - topHighlightInset), size: CGSize(width: contentSize.width, height: contentSize.height + topHighlightInset))
|
||||
strongSelf.separatorNode.frame = CGRect(origin: CGPoint(x: leftInset, y: nodeLayout.contentSize.height - separatorHeight), size: CGSize(width: nodeLayout.size.width - leftInset - params.rightInset - separatorRightInset, height: separatorHeight))
|
||||
strongSelf.separatorNode.isHidden = !hasSeparator
|
||||
let highlightFrame = CGRect(origin: CGPoint(x: 14.0, y: 2.0), size: CGSize(width: contentSize.width - 14.0 * 2.0, height: 52.0))
|
||||
let highlightCornerRadius = highlightFrame.height * 0.5
|
||||
strongSelf.highlightedBackgroundNode.frame = highlightFrame
|
||||
strongSelf.highlightedBackgroundNode.cornerRadius = highlightCornerRadius
|
||||
|
||||
var liveBroadcastingTimeout: Int32 = 0
|
||||
if let location = getLocation(from: item.message), let timeout = location.liveBroadcastingTimeout {
|
||||
|
|
@ -338,61 +272,134 @@ final class LocationLiveListItemNode: ListViewItemNode {
|
|||
strongSelf.addSubnode(timerNode)
|
||||
strongSelf.timerNode = timerNode
|
||||
}
|
||||
let timerSize = CGSize(width: 28.0, height: 28.0)
|
||||
let timerSize = CGSize(width: 24.0, height: 24.0)
|
||||
timerNode.update(backgroundColor: item.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), foregroundColor: item.presentationData.theme.list.itemAccentColor, textColor: item.presentationData.theme.list.itemAccentColor, beginTimestamp: Double(item.message.timestamp), timeout: Int32(liveBroadcastingTimeout) == liveLocationIndefinitePeriod ? -1.0 : Double(liveBroadcastingTimeout), strings: item.presentationData.strings)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 16.0 - timerSize.width, y: 14.0), size: timerSize)
|
||||
timerNode.frame = CGRect(origin: CGPoint(x: contentSize.width - 26.0 - timerSize.width, y: floorToScreenPixels((56.0 - timerSize.height) / 2.0)), size: timerSize)
|
||||
} else if let timerNode = strongSelf.timerNode {
|
||||
strongSelf.timerNode = nil
|
||||
timerNode.removeFromSupernode()
|
||||
}
|
||||
|
||||
let buttonBackground = ButtonComponent.Background(
|
||||
style: .glass,
|
||||
color: item.presentationData.theme.list.itemCheckColors.fillColor,
|
||||
foreground: item.presentationData.theme.list.itemCheckColors.foregroundColor,
|
||||
pressedColor: item.presentationData.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9)
|
||||
)
|
||||
let foregroundColor = item.presentationData.theme.list.itemCheckColors.foregroundColor
|
||||
var directionsSize = CGSize(width: 96.0, height: 36.0)
|
||||
let directionsSpacing: CGFloat = 8.0
|
||||
|
||||
var drivingButtonTitle = ""
|
||||
var drivingButtonHasIcon = true
|
||||
var walkingButtonTitle = ""
|
||||
var drivingButtonVisible = false
|
||||
var walkingButtonVisible = false
|
||||
|
||||
if case let .ready(drivingTime) = item.drivingTime {
|
||||
strongSelf.drivingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 })
|
||||
drivingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: drivingTime, format: { $0 }) ?? ""
|
||||
drivingButtonVisible = true
|
||||
|
||||
if let previousDrivingTime = currentItem?.drivingTime, case .calculating = previousDrivingTime {
|
||||
strongSelf.drivingButtonNode?.alpha = 1.0
|
||||
strongSelf.drivingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.drivingButton.view?.alpha = 1.0
|
||||
strongSelf.drivingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
if case let .ready(transitTime) = item.transitTime {
|
||||
strongSelf.transitButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: transitTime, format: { $0 })
|
||||
|
||||
if let previousTransitTime = currentItem?.transitTime, case .calculating = previousTransitTime {
|
||||
strongSelf.transitButtonNode?.alpha = 1.0
|
||||
strongSelf.transitButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
} else {
|
||||
drivingButtonVisible = true
|
||||
if case .unknown = item.walkingTime {
|
||||
drivingButtonHasIcon = false
|
||||
drivingButtonTitle = item.presentationData.strings.Map_GetDirections
|
||||
directionsSize.width = contentSize.width - leftInset * 2.0
|
||||
}
|
||||
}
|
||||
|
||||
if case let .ready(walkingTime) = item.walkingTime {
|
||||
strongSelf.walkingButtonNode?.title = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 })
|
||||
walkingButtonTitle = stringForEstimatedDuration(strings: item.presentationData.strings, time: walkingTime, format: { $0 }) ?? ""
|
||||
walkingButtonVisible = true
|
||||
|
||||
if let previousWalkingTime = currentItem?.walkingTime, case .calculating = previousWalkingTime {
|
||||
strongSelf.walkingButtonNode?.alpha = 1.0
|
||||
strongSelf.walkingButtonNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
strongSelf.walkingButton.view?.alpha = 1.0
|
||||
strongSelf.walkingButton.view?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
|
||||
}
|
||||
}
|
||||
|
||||
let directionsWidth: CGFloat = 93.0
|
||||
let directionsSpacing: CGFloat = 8.0
|
||||
let drivingHeight = strongSelf.drivingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
let transitHeight = strongSelf.transitButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
let walkingHeight = strongSelf.walkingButtonNode?.updateLayout(width: directionsWidth, transition: .immediate) ?? 0.0
|
||||
var drivingButtonContent: [AnyComponentWithIdentity<Empty>] = []
|
||||
if drivingButtonHasIcon {
|
||||
drivingButtonContent.append(AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsDriving", tintColor: foregroundColor))))
|
||||
}
|
||||
drivingButtonContent.append(AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: drivingButtonTitle, font: Font.semibold(14.0), color: foregroundColor))))
|
||||
|
||||
let drivingButtonSize = strongSelf.drivingButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: buttonBackground,
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("driving-\(drivingButtonTitle)"),
|
||||
component: AnyComponent(
|
||||
HStack(drivingButtonContent, spacing: 2.0)
|
||||
)
|
||||
),
|
||||
contentInsets: UIEdgeInsets(),
|
||||
action: { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.drivingAction()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: directionsSize
|
||||
)
|
||||
|
||||
let walkingButtonSize = strongSelf.walkingButton.update(
|
||||
transition: .immediate,
|
||||
component: AnyComponent(ButtonComponent(
|
||||
background: buttonBackground,
|
||||
content: AnyComponentWithIdentity(
|
||||
id: AnyHashable("walking-\(walkingButtonTitle)"),
|
||||
component: AnyComponent(
|
||||
HStack([
|
||||
AnyComponentWithIdentity(id: "icon", component: AnyComponent(BundleIconComponent(name: "Location/DirectionsWalking", tintColor: foregroundColor))),
|
||||
AnyComponentWithIdentity(id: "title", component: AnyComponent(Text(text: walkingButtonTitle, font: Font.semibold(14.0), color: foregroundColor)))
|
||||
], spacing: 0.0)
|
||||
)
|
||||
),
|
||||
contentInsets: UIEdgeInsets(),
|
||||
action: { [weak self] in
|
||||
if let item = self?.item {
|
||||
item.walkingAction()
|
||||
}
|
||||
}
|
||||
)),
|
||||
environment: {},
|
||||
containerSize: directionsSize
|
||||
)
|
||||
|
||||
var buttonOrigin = leftInset
|
||||
strongSelf.drivingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: drivingHeight))
|
||||
let drivingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: drivingButtonSize)
|
||||
if let drivingButtonView = strongSelf.drivingButton.view {
|
||||
if drivingButtonView.superview == nil {
|
||||
strongSelf.view.addSubview(drivingButtonView)
|
||||
}
|
||||
drivingButtonView.frame = drivingButtonFrame
|
||||
if drivingButtonView.layer.animation(forKey: "opacity") == nil {
|
||||
drivingButtonView.alpha = drivingButtonVisible ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
if case .ready = item.drivingTime {
|
||||
buttonOrigin += directionsWidth + directionsSpacing
|
||||
buttonOrigin += directionsSize.width + directionsSpacing
|
||||
}
|
||||
|
||||
strongSelf.transitButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: transitHeight))
|
||||
|
||||
if case .ready = item.transitTime {
|
||||
buttonOrigin += directionsWidth + directionsSpacing
|
||||
let walkingButtonFrame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: walkingButtonSize)
|
||||
if let walkingButtonView = strongSelf.walkingButton.view {
|
||||
if walkingButtonView.superview == nil {
|
||||
strongSelf.view.addSubview(walkingButtonView)
|
||||
}
|
||||
walkingButtonView.frame = walkingButtonFrame
|
||||
if walkingButtonView.layer.animation(forKey: "opacity") == nil {
|
||||
walkingButtonView.alpha = walkingButtonVisible ? 1.0 : 0.0
|
||||
}
|
||||
}
|
||||
|
||||
strongSelf.walkingButtonNode?.frame = CGRect(origin: CGPoint(x: buttonOrigin, y: subtitleFrame.maxY + 12.0), size: CGSize(width: directionsWidth, height: walkingHeight))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue