Attachment menu improvements

This commit is contained in:
Ilya Laktyushin 2022-02-26 17:48:25 +04:00
parent 5e84216346
commit ec8702ed07
77 changed files with 4533 additions and 325 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -7328,3 +7328,6 @@ Sorry for the inconvenience.";
"Attachment.DeselectedItems_0" = "%@ items deselected";
"PrivacyPhoneNumberSettings.CustomPublicLink" = "Users who have your number saved in their contacts will also see it on Telegram.\n\nThis public link opens a chat with you:\n[https://t.me/%@]()";
"Attachment.MyAlbums" = "My Albums";
"Attachment.MediaTypes" = "Media Types";

View File

@ -3,7 +3,7 @@ import Display
import SwiftSignalKit
public protocol ContactSelectionController: ViewController {
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?)?, NoError> { get }
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> { get }
var displayProgress: Bool { get set }
var dismissed: (() -> Void)? { get set }

View File

@ -8,6 +8,8 @@ import DirectionalPanGesture
import TelegramPresentationData
import MapKit
private let overflowInset: CGFloat = 70.0
final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let wrappingNode: ASDisplayNode
let clipNode: ASDisplayNode
@ -18,6 +20,7 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
var isReadyUpdated: (() -> Void)?
var updateDismissProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
var interactivelyDismissed: (() -> Void)?
var controllerRemoved: ((ViewController) -> Void)?
var updateModalProgress: ((CGFloat, ContainedViewLayoutTransition) -> Void)?
@ -49,8 +52,12 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.wrappingNode = ASDisplayNode()
self.clipNode = ASDisplayNode()
self.container = NavigationContainer(controllerRemoved: { _ in })
var controllerRemovedImpl: ((ViewController) -> Void)?
self.container = NavigationContainer(controllerRemoved: { c in
controllerRemovedImpl?(c)
})
self.container.clipsToBounds = true
self.container.overflowInset = overflowInset
super.init()
@ -72,12 +79,16 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
}
applySmoothRoundedCorners(self.container.layer)
controllerRemovedImpl = { [weak self] c in
self?.controllerRemoved?(c)
}
}
override func didLoad() {
super.didLoad()
let panRecognizer = DirectionalPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
let panRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.panGesture(_:)))
panRecognizer.delegate = self
panRecognizer.delaysTouchesBegan = false
panRecognizer.cancelsTouchesInView = true
@ -85,11 +96,23 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.wrappingNode.view.addGestureRecognizer(panRecognizer)
}
override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
if let (layout, _, _) = self.validLayout {
if case .regular = layout.metrics.widthClass {
return false
}
}
return true
}
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if gestureRecognizer is UIPanGestureRecognizer && otherGestureRecognizer is UIPanGestureRecognizer {
if let _ = otherGestureRecognizer.view?.superview as? MKMapView {
return false
}
if let _ = otherGestureRecognizer.view?.asyncdisplaykit_node as? CollectionIndexNode {
return false
}
return true
}
return false
@ -102,7 +125,11 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
return 210.0
}
if case .compact = layout.metrics.widthClass {
return max(layout.size.width, layout.size.height) * 0.2488
var factor: CGFloat = 0.2488
if layout.size.width <= 320.0 {
factor = 0.15
}
return floor(max(layout.size.width, layout.size.height) * factor)
} else {
return 210.0
}
@ -165,7 +192,6 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
self.panGestureArguments = (topInset, translation, scrollView, listNode)
if !self.isExpanded {
if currentOffset > 0.0, let scrollView = scrollView {
scrollView.panGestureRecognizer.setTranslation(CGPoint(), in: scrollView)
@ -306,15 +332,20 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let isLandscape = layout.orientation == .landscape
let edgeTopInset = isLandscape ? 0.0 : defaultTopInset
var effectiveExpanded = self.isExpanded
if case .regular = layout.metrics.widthClass {
effectiveExpanded = true
}
let topInset: CGFloat
if let (panInitialTopInset, panOffset, _, _) = self.panGestureArguments {
if self.isExpanded {
if effectiveExpanded {
topInset = min(edgeTopInset, panInitialTopInset + max(0.0, panOffset))
} else {
topInset = max(0.0, panInitialTopInset + min(0.0, panOffset))
}
} else {
topInset = self.isExpanded ? 0.0 : edgeTopInset
topInset = effectiveExpanded ? 0.0 : edgeTopInset
}
transition.updateFrame(node: self.wrappingNode, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size))
@ -359,7 +390,6 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
let effectiveStatusBarHeight: CGFloat? = nil
let overflowInset: CGFloat = 70.0
var safeInsets = layout.safeInsets
safeInsets.left += overflowInset
safeInsets.right += overflowInset
@ -379,29 +409,12 @@ final class AttachmentContainer: ASDisplayNode, UIGestureRecognizerDelegate {
clipFrame = CGRect(x: containerFrame.minX + overflowInset, y: containerFrame.minY, width: containerFrame.width - overflowInset * 2.0, height: containerFrame.height)
}
} else {
self.clipNode.clipsToBounds = true
self.clipNode.cornerRadius = 10.0
if #available(iOS 11.0, *) {
self.clipNode.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner, .layerMinXMaxYCorner, .layerMaxXMaxYCorner]
}
containerLayout = ContainerViewLayout(size: layout.size, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: nil, inputHeight: nil, inputHeightIsInteractivellyChanging: false, inVoiceOver: layout.inVoiceOver)
let verticalInset: CGFloat = 44.0
let maxSide = max(layout.size.width, layout.size.height)
let minSide = min(layout.size.width, layout.size.height)
let containerSize = CGSize(width: min(layout.size.width - 20.0, floor(maxSide / 2.0)), height: min(layout.size.height, minSide) - verticalInset * 2.0)
containerFrame = CGRect(origin: CGPoint(x: floor((layout.size.width - containerSize.width) / 2.0), y: floor((layout.size.height - containerSize.height) / 2.0)), size: containerSize)
let unscaledFrame = CGRect(origin: CGPoint(), size: containerLayout.size)
containerScale = 1.0
clipFrame = containerFrame
var inputHeight: CGFloat?
if let inputHeightValue = layout.inputHeight {
inputHeight = max(0.0, inputHeightValue - (layout.size.height - containerFrame.maxY))
}
let effectiveStatusBarHeight: CGFloat? = nil
containerLayout = ContainerViewLayout(size: containerSize, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, intrinsicInsets: UIEdgeInsets(), safeInsets: UIEdgeInsets(), additionalInsets: UIEdgeInsets(), statusBarHeight: effectiveStatusBarHeight, inputHeight: inputHeight, inputHeightIsInteractivellyChanging: layout.inputHeightIsInteractivellyChanging, inVoiceOver: layout.inVoiceOver)
containerFrame = unscaledFrame
clipFrame = unscaledFrame
}
transition.updateFrameAsPositionAndBounds(node: self.clipNode, frame: clipFrame)
transition.updateFrameAsPositionAndBounds(node: self.container, frame: CGRect(origin: CGPoint(x: containerFrame.minX, y: 0.0), size: containerFrame.size))

View File

@ -22,6 +22,8 @@ public enum AttachmentButtonType: Equatable {
public protocol AttachmentContainable: ViewController {
var requestAttachmentMenuExpansion: () -> Void { get set }
var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void { get set }
var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void { get set }
func resetForReuse()
func prepareForReuse()
@ -51,6 +53,19 @@ public protocol AttachmentMediaPickerContext {
func schedule()
}
private func generateShadowImage() -> UIImage? {
return generateImage(CGSize(width: 140.0, height: 140.0), rotatedContext: { size, context in
context.clear(CGRect(origin: CGPoint(), size: size))
context.setShadow(offset: CGSize(), blur: 60.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor)
let path = UIBezierPath(roundedRect: CGRect(x: 60.0, y: 60.0, width: 20.0, height: 20.0), cornerRadius: 11.0 - UIScreenPixel).cgPath
context.addPath(path)
context.fillPath()
})?.stretchableImage(withLeftCapWidth: 70, topCapHeight: 70)
}
public class AttachmentController: ViewController {
private let context: AccountContext
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
@ -68,11 +83,12 @@ public class AttachmentController: ViewController {
private final class Node: ASDisplayNode {
private weak var controller: AttachmentController?
private let dim: ASDisplayNode
private let shadowNode: ASImageNode
private let container: AttachmentContainer
let panel: AttachmentPanel
private var currentType: AttachmentButtonType?
private var currentController: AttachmentContainable?
private var currentControllers: [AttachmentContainable] = []
private var validLayout: ContainerViewLayout?
private var modalProgress: CGFloat = 0.0
@ -105,6 +121,8 @@ public class AttachmentController: ViewController {
}
}
private let wrapperNode: ASDisplayNode
init(controller: AttachmentController) {
self.controller = controller
@ -112,6 +130,11 @@ public class AttachmentController: ViewController {
self.dim.alpha = 0.0
self.dim.backgroundColor = UIColor(white: 0.0, alpha: 0.25)
self.shadowNode = ASImageNode()
self.wrapperNode = ASDisplayNode()
self.wrapperNode.clipsToBounds = true
self.container = AttachmentContainer()
self.container.canHaveKeyboardFocus = true
self.panel = AttachmentPanel(context: controller.context, updatedPresentationData: controller.updatedPresentationData)
@ -119,6 +142,15 @@ public class AttachmentController: ViewController {
super.init()
self.addSubnode(self.dim)
self.addSubnode(self.shadowNode)
self.addSubnode(self.wrapperNode)
self.container.controllerRemoved = { [weak self] controller in
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
strongSelf.currentControllers = strongSelf.currentControllers.filter { $0 !== controller }
strongSelf.containerLayoutUpdated(layout, transition: .immediate)
}
}
self.container.updateModalProgress = { [weak self] progress, transition in
if let strongSelf = self, let layout = strongSelf.validLayout, !strongSelf.isDismissing {
@ -218,7 +250,10 @@ public class AttachmentController: ViewController {
func switchToController(_ type: AttachmentButtonType, _ ascending: Bool) {
guard self.currentType != type else {
if let controller = self.currentController {
if self.animating {
return
}
if let controller = self.currentControllers.last {
controller.scrollToTopWithTabBar?()
controller.requestAttachmentMenuExpansion()
}
@ -235,10 +270,22 @@ public class AttachmentController: ViewController {
controller.requestAttachmentMenuExpansion = { [weak self] in
self?.container.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring))
}
let previousController = strongSelf.currentController
controller.updateNavigationStack = { [weak self] f in
if let strongSelf = self {
strongSelf.currentControllers = f(strongSelf.currentControllers)
if let layout = strongSelf.validLayout {
strongSelf.containerLayoutUpdated(layout, transition: .animated(duration: 0.4, curve: .spring))
}
}
}
controller.updateTabBarAlpha = { [weak self, weak controller] alpha, transition in
if let strongSelf = self, strongSelf.currentControllers.contains(where: { $0 === controller }) {
strongSelf.panel.updateBackgroundAlpha(alpha, transition: transition)
}
}
let previousController = strongSelf.currentControllers.last
let animateTransition = previousType != nil
strongSelf.currentController = controller
strongSelf.currentControllers = [controller]
if animateTransition, let snapshotView = strongSelf.container.container.view.snapshotView(afterScreenUpdates: false) {
snapshotView.frame = strongSelf.container.container.frame
@ -271,7 +318,16 @@ public class AttachmentController: ViewController {
})
}
private var animating = false
func animateIn() {
guard let layout = self.validLayout else {
return
}
self.animating = true
if case .regular = layout.metrics.widthClass {
self.animating = false
} else {
ContainedViewLayoutTransition.animated(duration: 0.3, curve: .linear).updateAlpha(node: self.dim, alpha: 1.0)
let targetPosition = self.container.position
@ -280,32 +336,52 @@ public class AttachmentController: ViewController {
self.container.position = startPosition
let transition = ContainedViewLayoutTransition.animated(duration: 0.4, curve: .spring)
transition.animateView(allowUserInteraction: true, {
self.animating = false
self.container.position = targetPosition
})
}
}
func animateOut(completion: @escaping () -> Void = {}) {
self.isDismissing = true
guard let layout = self.validLayout else {
return
}
self.animating = true
if case .regular = layout.metrics.widthClass {
self.layer.allowsGroupOpacity = true
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { _ in
let _ = self.container.dismiss(transition: .immediate, completion: completion)
self.animating = false
})
} else {
let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
positionTransition.updatePosition(node: self.container, position: CGPoint(x: self.container.position.x, y: self.bounds.height + self.container.bounds.height / 2.0), completion: { [weak self] _ in
let _ = self?.container.dismiss(transition: .immediate, completion: completion)
self?.animating = false
})
let alphaTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut)
alphaTransition.updateAlpha(node: self.dim, alpha: 0.0)
self.controller?.updateModalStyleOverlayTransitionFactor(0.0, transition: positionTransition)
}
}
func scrollToTop() {
self.currentController?.scrollToTop?()
self.currentControllers.last?.scrollToTop?()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
if let controller = self.controller, controller.isInteractionDisabled() {
return self.view
} else {
return super.hitTest(point, with: event)
let result = super.hitTest(point, with: event)
if result == self.wrapperNode.view {
return self.dim.view
}
return result
}
}
@ -323,13 +399,47 @@ public class AttachmentController: ViewController {
self.isCollapsed = true
}
var containerLayout = layout
let containerRect: CGRect
if case .regular = layout.metrics.widthClass {
let size = CGSize(width: 390.0, height: 600.0)
let position: CGPoint
if layout.size.width > layout.size.height {
position = CGPoint(x: 225.0, y: layout.size.height - size.height - layout.intrinsicInsets.bottom - 54.0)
} else {
position = CGPoint(x: 152.0, y: layout.size.height - size.height - layout.intrinsicInsets.bottom - 54.0)
}
containerRect = CGRect(origin: position, size: size)
containerLayout.size = containerRect.size
containerLayout.intrinsicInsets.bottom = 0.0
self.wrapperNode.cornerRadius = 10.0
if #available(iOS 13.0, *) {
self.wrapperNode.layer.cornerCurve = .continuous
}
self.shadowNode.alpha = 1.0
if self.shadowNode.image == nil {
self.shadowNode.image = generateShadowImage()
}
} else {
containerRect = CGRect(origin: CGPoint(), size: layout.size)
self.wrapperNode.cornerRadius = 0.0
self.shadowNode.alpha = 0.0
}
let isEffecitvelyCollapsedUpdated = (self.isCollapsed || self.selectionCount > 0) != (self.panel.isCollapsed || self.panel.isSelecting)
let panelHeight = self.panel.update(layout: layout, buttons: self.controller?.buttons ?? [], isCollapsed: self.isCollapsed, isSelecting: self.selectionCount > 0, transition: transition)
let panelHeight = self.panel.update(layout: containerLayout, buttons: self.controller?.buttons ?? [], isCollapsed: self.isCollapsed, isSelecting: self.selectionCount > 0, transition: transition)
var panelTransition = transition
if isEffecitvelyCollapsedUpdated {
panelTransition = .animated(duration: 0.25, curve: .easeInOut)
}
panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: layout.size.height - panelHeight), size: CGSize(width: layout.size.width, height: panelHeight)))
panelTransition.updateFrame(node: self.panel, frame: CGRect(origin: CGPoint(x: 0.0, y: containerRect.height - panelHeight), size: CGSize(width: containerRect.width, height: panelHeight)))
transition.updateFrame(node: self.shadowNode, frame: containerRect.insetBy(dx: -60.0, dy: -60.0))
transition.updateFrame(node: self.wrapperNode, frame: containerRect)
if !self.isUpdatingContainer && !self.isDismissing {
self.isUpdatingContainer = true
@ -341,17 +451,17 @@ public class AttachmentController: ViewController {
containerTransition = transition
}
let controllers = self.currentController.flatMap { [$0] } ?? []
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: layout.size))
let controllers = self.currentControllers
containerTransition.updateFrame(node: self.container, frame: CGRect(origin: CGPoint(), size: containerRect.size))
var containerInsets = layout.intrinsicInsets
var containerInsets = containerLayout.intrinsicInsets
containerInsets.bottom = panelHeight
let containerLayout = layout.withUpdatedIntrinsicInsets(containerInsets)
let containerLayout = containerLayout.withUpdatedIntrinsicInsets(containerInsets)
self.container.update(layout: containerLayout, controllers: controllers, coveredByModalTransition: 0.0, transition: self.switchingController ? .immediate : transition)
if self.container.supernode == nil, !controllers.isEmpty && self.container.isReady {
self.addSubnode(self.container)
self.wrapperNode.addSubnode(self.container)
self.container.addSubnode(self.panel)
self.animateIn()

View File

@ -410,6 +410,11 @@ final class AttachmentPanel: ASDisplayNode, UIScrollViewDelegate {
self.scrollNode.view.showsVerticalScrollIndicator = false
}
func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
transition.updateAlpha(node: self.separatorNode, alpha: alpha)
transition.updateAlpha(node: self.backgroundNode, alpha: alpha)
}
func updateCaption(_ caption: NSAttributedString) {
if !caption.string.isEmpty {
self.loadTextNodeIfNeeded()

View File

@ -385,7 +385,7 @@ public final class CallListController: TelegramBaseController {
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller, weak self] result in
controller?.dismissSearch()
if let strongSelf = self, let (contactPeers, action, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer {
if let strongSelf = self, let (contactPeers, action, _, _, _) = result, let contactPeer = contactPeers.first, case let .peer(peer, _, _) = contactPeer {
strongSelf.call(peer.id, isVideo: action == .videoCall, began: {
if let strongSelf = self {
let _ = (strongSelf.context.sharedContext.hasOngoingCall.get()

View File

@ -518,6 +518,8 @@ public final class ComposedPoll {
private class CreatePollControllerImpl: ItemListController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void = { _ in }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
}
public func createPollController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer, isQuiz: Bool? = nil, completion: @escaping (ComposedPoll) -> Void) -> AttachmentContainable {
@ -904,8 +906,8 @@ public func createPollController(context: AccountContext, updatedPresentationDat
focusItemTag = CreatePollEntryTag.solution
ensureVisibleItemTag = focusItemTag
} else {
focusItemTag = CreatePollEntryTag.text
ensureVisibleItemTag = focusItemTag
// focusItemTag = CreatePollEntryTag.text
// ensureVisibleItemTag = focusItemTag
}
let title: String
@ -927,6 +929,15 @@ public func createPollController(context: AccountContext, updatedPresentationDat
weak var currentTooltipController: TooltipController?
let controller = CreatePollControllerImpl(context: context, state: signal)
controller.navigationPresentation = .modal
// controller.visibleBottomContentOffsetChanged = { [weak controller] offset in
// switch offset {
// case let .known(value):
// let backgroundAlpha: CGFloat = min(30.0, value) / 30.0
// controller?.updateTabBarAlpha(backgroundAlpha, .immediate)
// case .unknown, .none:
// controller?.updateTabBarAlpha(1.0, .immediate)
// }
// }
presentControllerImpl = { [weak controller] c, a in
controller?.present(c, in: .window(.root), with: a)
}

View File

@ -362,6 +362,8 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
if let checkNode = strongSelf.checkNode {
transition.updateFrame(node: checkNode, frame: checkFrame)
checkNode.setSelected(isSelected, animated: true)
transition.updateAlpha(node: checkNode, alpha: strongSelf.textNode.textView.text.isEmpty ? 0.0 : 1.0)
} else {
let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(backgroundColor: item.presentationData.theme.list.itemSwitchColors.positiveColor, strokeColor: item.presentationData.theme.list.itemCheckColors.foregroundColor, borderColor: item.presentationData.theme.list.itemCheckColors.strokeColor, overlayBorder: false, hasInset: false, hasShadow: false))
checkNode.setSelected(isSelected, animated: false)
@ -372,6 +374,8 @@ class CreatePollOptionItemNode: ItemListRevealOptionsItemNode, ItemListItemNode,
strongSelf.containerNode.addSubnode(checkNode)
checkNode.frame = checkFrame
transition.animatePositionAdditive(node: checkNode, offset: CGPoint(x: -checkFrame.maxX, y: 0.0))
transition.updateAlpha(node: checkNode, alpha: strongSelf.textNode.textView.text.isEmpty ? 0.0 : 1.0)
}
} else if let checkNode = strongSelf.checkNode {
strongSelf.checkNode = nil

View File

@ -31,10 +31,11 @@ public struct Font {
case medium
case semibold
case bold
case heavy
var isBold: Bool {
switch self {
case .medium, .semibold, .bold:
case .medium, .semibold, .bold, .heavy:
return true
default:
return false
@ -51,6 +52,8 @@ public struct Font {
return .semibold
case .bold:
return .bold
case .heavy:
return .heavy
default:
return .regular
}
@ -185,6 +188,11 @@ public struct Font {
}
}
public static func heavy(_ size: CGFloat) -> UIFont {
return self.with(size: size, design: .regular, weight: .heavy, traits: [])
}
public static func light(_ size: CGFloat) -> UIFont {
if #available(iOS 8.2, *) {
return UIFont.systemFont(ofSize: size, weight: UIFont.Weight.light)

View File

@ -109,6 +109,8 @@ public final class NavigationContainer: ASDisplayNode, UIGestureRecognizerDelega
self.state.top?.value.isInFocus = isInFocus
}
public var overflowInset: CGFloat = 0.0
private var currentKeyboardLeftEdge: CGFloat = 0.0
private var additionalKeyboardLeftEdgeOffset: CGFloat = 0.0

View File

@ -61,6 +61,10 @@ public final class NavigationBarTheme {
self.badgeTextColor = badgeTextColor
}
public func withUpdatedBackgroundColor(_ color: UIColor) -> NavigationBarTheme {
return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: color, enableBackgroundBlur: false, separatorColor: self.separatorColor, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor)
}
public func withUpdatedSeparatorColor(_ color: UIColor) -> NavigationBarTheme {
return NavigationBarTheme(buttonColor: self.buttonColor, disabledButtonColor: self.disabledButtonColor, primaryTextColor: self.primaryTextColor, backgroundColor: self.backgroundColor, enableBackgroundBlur: self.enableBackgroundBlur, separatorColor: color, badgeBackgroundColor: self.badgeBackgroundColor, badgeStrokeColor: self.badgeStrokeColor, badgeTextColor: self.badgeTextColor)
}
@ -1334,6 +1338,8 @@ open class NavigationBar: ASDisplayNode {
public var intrinsicCanTransitionInline: Bool = true
public var passthroughTouches = true
public var canTransitionInline: Bool {
if let contentNode = self.contentNode, case .replacement = contentNode.mode {
return false
@ -1470,8 +1476,8 @@ open class NavigationBar: ASDisplayNode {
return nil
}
//result == self.view ||
if result == self.buttonsContainerNode.view {
if self.passthroughTouches && (result == self.view || result == self.buttonsContainerNode.view) {
return nil
}

View File

@ -34,7 +34,7 @@ final class NavigationTransitionCoordinator {
}
}
private let container: ASDisplayNode
private let container: NavigationContainer
private let transition: NavigationTransition
let isInteractive: Bool
let topNode: ASDisplayNode
@ -51,7 +51,7 @@ final class NavigationTransitionCoordinator {
private var currentCompletion: (() -> Void)?
private var didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)?
init(transition: NavigationTransition, isInteractive: Bool, container: ASDisplayNode, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) {
init(transition: NavigationTransition, isInteractive: Bool, container: NavigationContainer, topNode: ASDisplayNode, topNavigationBar: NavigationBar?, bottomNode: ASDisplayNode, bottomNavigationBar: NavigationBar?, didUpdateProgress: ((CGFloat, ContainedViewLayoutTransition, CGRect, CGRect) -> Void)? = nil) {
self.transition = transition
self.isInteractive = isInteractive
self.container = container
@ -147,7 +147,7 @@ final class NavigationTransitionCoordinator {
}
})
canInvokeCompletion = true
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, topFrame.minX), height: self.container.bounds.size.height - dimInset)))
transition.updateFrame(node: self.dimNode, frame: CGRect(origin: CGPoint(x: 0.0, y: dimInset), size: CGSize(width: max(0.0, topFrame.minX + self.container.overflowInset), height: self.container.bounds.size.height - dimInset)))
transition.updateFrame(node: self.shadowNode, frame: CGRect(origin: CGPoint(x: self.dimNode.frame.maxX - shadowWidth, y: dimInset), size: CGSize(width: shadowWidth, height: containerSize.height - dimInset)))
transition.updateAlpha(node: self.dimNode, alpha: (1.0 - position) * 0.15)
transition.updateAlpha(node: self.shadowNode, alpha: (1.0 - position) * 0.9)

View File

@ -174,6 +174,13 @@ open class ItemListController: ViewController, KeyShortcutResponder, Presentable
}
}
public var visibleBottomContentOffset: ListViewVisibleContentOffset {
if self.isNodeLoaded {
return (self.displayNode as! ItemListControllerNode).listNode.visibleBottomContentOffset()
} else {
return .unknown
}
}
public var visibleBottomContentOffsetChanged: ((ListViewVisibleContentOffset) -> Void)? {
didSet {
if self.isNodeLoaded {

View File

@ -166,10 +166,10 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
self.isAccessibilityElement = true
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.addressNode)
self.addSubnode(self.infoButton)
self.addSubnode(self.iconNode)
self.infoButton.addTarget(self, action: #selector(self.infoPressed), forControlEvents: .touchUpInside)
}
@ -301,7 +301,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
strongSelf.topStripeNode.removeFromSupernode()
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 1)
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 0)
}
if strongSelf.maskNode.supernode != nil {
strongSelf.maskNode.removeFromSupernode()
@ -377,6 +377,7 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
shimmerNode = ShimmerEffectNode()
strongSelf.placeholderNode = shimmerNode
if strongSelf.bottomStripeNode.supernode != nil {
strongSelf.insertSubnode(shimmerNode, belowSubnode: strongSelf.bottomStripeNode)
} else {
strongSelf.addSubnode(shimmerNode)
@ -403,6 +404,8 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
shapes.append(.roundedRectLine(startPoint: CGPoint(x: subtitleFrame.minX, y: subtitleFrame.minY + floor((subtitleFrame.height - lineDiameter) / 2.0)), width: subtitleLineWidth, diameter: lineDiameter))
shimmerNode.update(backgroundColor: placeholderBackgroundColor, foregroundColor: item.presentationData.theme.list.mediaPlaceholderColor, shimmeringColor: item.presentationData.theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.4), shapes: shapes, size: layout.contentSize)
strongSelf.iconNode.removeFromSupernode()
} else if let shimmerNode = strongSelf.placeholderNode {
strongSelf.placeholderNode = nil
shimmerNode.removeFromSupernode()
@ -418,16 +421,10 @@ public class ItemListVenueItemNode: ListViewItemNode, ItemListItemNode {
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.bottomStripeNode)
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: self.backgroundNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}

View File

@ -54,6 +54,8 @@ typedef enum {
@property (nonatomic, copy) void(^recognizedQRCode)(NSString *code);
@property (nonatomic, copy) void(^finishedTransitionIn)(void);
@property (nonatomic, copy) CGRect(^beginTransitionOut)(void);
@property (nonatomic, copy) void(^finishedTransitionOut)(void);
@property (nonatomic, copy) void(^customPresentOverlayController)(TGOverlayController *(^)(id<LegacyComponentsContext>));

View File

@ -26,6 +26,8 @@
TGCameraPreviewView *_previewView;
__weak PGCamera *_camera;
UIInterfaceOrientation _innerInterfaceOrientation;
}
@end
@ -233,10 +235,22 @@
{
void(^block)(void) = ^
{
_wrapperView.transform = CGAffineTransformMakeRotation(-1 * TGRotationForInterfaceOrientation(orientation));
_wrapperView.frame = self.bounds;
CGAffineTransform transform = CGAffineTransformMakeRotation(-1 * TGRotationForInterfaceOrientation(orientation));
CGFloat scale = 1.0;
if (self.frame.size.width != 0.0) {
scale = self.frame.size.height / self.frame.size.width;
}
if (_innerInterfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
transform = CGAffineTransformScale(transform, scale, scale);
} else if (_innerInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
transform = CGAffineTransformScale(transform, scale, scale);
}
_wrapperView.transform = transform;
[self layoutSubviews];
};
_innerInterfaceOrientation = orientation;
if (animated)
[UIView animateWithDuration:0.3f animations:block];
else
@ -247,12 +261,19 @@
{
[super layoutSubviews];
_wrapperView.frame = self.bounds;
_wrapperView.bounds = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);
_wrapperView.center = CGPointMake(self.bounds.size.width / 2.0, self.bounds.size.height / 2.0);
TGCameraPreviewView *previewView = _previewView;
if (previewView.superview == _wrapperView)
previewView.frame = self.bounds;
// if (_innerInterfaceOrientation == UIInterfaceOrientationLandscapeLeft) {
// _wrapperView.frame = CGRectOffset(_wrapperView.frame, 0, 100.0);
// } else if (_innerInterfaceOrientation == UIInterfaceOrientationLandscapeRight) {
// _wrapperView.frame = CGRectOffset(_wrapperView.frame, 0, -100.0);
// }
_iconView.frame = CGRectMake(self.frame.size.width - _iconView.frame.size.width - 3.0, 3.0 - TGScreenPixel, _iconView.frame.size.width, _iconView.frame.size.height);
}

View File

@ -2225,11 +2225,21 @@ static CGPoint TGCameraControllerClampPointToScreenSize(__unused id self, __unus
if (!CGRectEqualToRect(fromFrame, CGRectZero))
{
__weak TGCameraController *weakSelf = self;
POPSpringAnimation *frameAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];
frameAnimation.fromValue = [NSValue valueWithCGRect:fromFrame];
frameAnimation.toValue = [NSValue valueWithCGRect:toFrame];
frameAnimation.springSpeed = 20;
frameAnimation.springBounciness = 1;
frameAnimation.completionBlock = ^(POPAnimation *anim, BOOL finished) {
__strong TGCameraController *strongSelf = weakSelf;
if (strongSelf == nil)
return;
if (strongSelf.finishedTransitionIn != NULL) {
;strongSelf.finishedTransitionIn();
}
};
[_previewView pop_addAnimation:frameAnimation forKey:@"frame"];
POPSpringAnimation *cornersFrameAnimation = [POPSpringAnimation animationWithPropertyNamed:kPOPViewFrame];

View File

@ -927,6 +927,11 @@
grouping = false;
}
}
for (TGPhotoPaintEntity *entity in adjustments.paintingData.entities) {
if (entity.animated) {
grouping = true;
}
}
}
}
@ -942,8 +947,12 @@
NSAttributedString *caption = [editingContext captionForItem:asset];
if (editingContext.isForcedCaption && num > 0) {
if (editingContext.isForcedCaption) {
if (grouping && num > 0) {
caption = nil;
} else if (!grouping && num < selectedItems.count - 1) {
caption = nil;
}
}
switch (asset.type)

View File

@ -406,6 +406,8 @@ open class LegacyController: ViewController, PresentableController, AttachmentCo
public var disposables = DisposableSet()
open var requestAttachmentMenuExpansion: () -> Void = {}
open var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void = { _ in }
open var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public init(presentation: LegacyControllerPresentation, theme: PresentationTheme? = nil, strings: PresentationStrings? = nil, initialLayout: ContainerViewLayout? = nil) {
self.sizeClass.set(SSignal.single(UIUserInterfaceSizeClass.compact.rawValue as NSNumber))

View File

@ -61,6 +61,10 @@ private func generateExtensionImage(colors: (UInt32, UInt32)) -> UIImage? {
context.restoreGState()
context.beginPath()
let _ = try? drawSvgPath(context, path: "M6,0 L26.7573593,0 C27.5530088,-8.52837125e-16 28.3160705,0.316070521 28.8786797,0.878679656 L39.1213203,11.1213203 C39.6839295,11.6839295 40,12.4469912 40,13.2426407 L40,34 C40,37.3137085 37.3137085,40 34,40 L6,40 C2.6862915,40 4.05812251e-16,37.3137085 0,34 L0,6 C-4.05812251e-16,2.6862915 2.6862915,6.08718376e-16 6,0 ")
context.clip()
context.setFillColor(UIColor(rgb: 0xffffff, alpha: 0.2).cgColor)
context.translateBy(x: 40.0 - 14.0, y: 0.0)
let _ = try? drawSvgPath(context, path: "M-1,0 L14,0 L14,15 L14,14 C14,12.8954305 13.1045695,12 12,12 L4,12 C2.8954305,12 2,11.1045695 2,10 L2,2 C2,0.8954305 1.1045695,-2.02906125e-16 0,0 L-1,0 L-1,0 Z ")
@ -894,10 +898,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
transition.updateFrame(node: strongSelf.separatorNode, frame: CGRect(origin: CGPoint(x: leftInset + leftOffset, y: nodeLayout.contentSize.height - UIScreenPixel), size: CGSize(width: params.width - leftInset - leftOffset, height: UIScreenPixel)))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: nodeLayout.contentSize.height + UIScreenPixel))
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: nodeLayout.size.height + UIScreenPixel))
if let backgroundNode = strongSelf.backgroundNode {
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.contentSize.height))
backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -nodeLayout.insets.top), size: CGSize(width: params.width, height: nodeLayout.size.height - nodeLayout.insets.bottom))
}
switch item.style {

View File

@ -72,6 +72,8 @@ public final class LocationPickerController: ViewController, AttachmentContainab
private var interaction: LocationPickerInteraction?
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void = { _ in }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mode: LocationPickerMode, completion: @escaping (TelegramMediaMap, String?) -> Void) {
self.context = context
@ -319,6 +321,8 @@ public final class LocationPickerController: ViewController, AttachmentContainab
break
}
})
self.navigationBar?.passthroughTouches = false
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
@ -342,4 +346,8 @@ public final class LocationPickerController: ViewController, AttachmentContainab
self.interaction?.dismissSearch()
self.scrollToTop?()
}
public func prepareForReuse() {
self.updateTabBarAlpha(1.0, .animated(duration: 0.25, curve: .easeInOut))
}
}

View File

@ -74,7 +74,7 @@ enum LegacyMediaPickerGallerySource {
case selection(item: TGMediaSelectableItem)
}
func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void) {
func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?, chatLocation: ChatLocation?, presentationData: PresentationData, source: LegacyMediaPickerGallerySource, immediateThumbnail: UIImage?, selectionContext: TGMediaSelectionContext?, editingContext: TGMediaEditingContext, hasSilentPosting: Bool, hasSchedule: Bool, hasTimer: Bool, updateHiddenMedia: @escaping (String?) -> Void, initialLayout: ContainerViewLayout?, transitionHostView: @escaping () -> UIView?, transitionView: @escaping (String) -> UIView?, completed: @escaping (TGMediaSelectableItem & TGMediaEditableItem, Bool, Int32?) -> Void, presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, present: @escaping (ViewController, Any?) -> Void, finishedTransitionIn: @escaping () -> Void) {
let reminder = peer?.id == context.account.peerId
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme, initialLayout: nil)
@ -179,6 +179,8 @@ func presentLegacyMediaPickerGallery(context: AccountContext, peer: EnginePeer?,
}
controller.finishedTransitionIn = { [weak model] _, _ in
model?.interfaceView.setSelectedItemsModel(model?.selectedItemsModel)
finishedTransitionIn()
}
controller.completedTransitionOut = { [weak legacyController] in
updateHiddenMedia(nil)

View File

@ -29,7 +29,8 @@ class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
self.changeSink.putNext(changeInstance)
}
func fetchResultAssets(_ initialFetchResult: PHFetchResult<PHAsset>) -> Signal<PHFetchResult<PHAsset>?, NoError> {
func fetchAssets(_ collection: PHAssetCollection) -> Signal<PHFetchResult<PHAsset>, NoError> {
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: nil)
let fetchResult = Atomic<PHFetchResult<PHAsset>>(value: initialFetchResult)
return .single(initialFetchResult)
|> then(
@ -45,11 +46,28 @@ class MediaAssetsContext: NSObject, PHPhotoLibraryChangeObserver {
)
}
func fetchAssetsCollections(_ type: PHAssetCollectionType) -> Signal<PHFetchResult<PHAssetCollection>, NoError> {
let initialFetchResult = PHAssetCollection.fetchAssetCollections(with: type, subtype: .any, options: nil)
let fetchResult = Atomic<PHFetchResult<PHAssetCollection>>(value: initialFetchResult)
return .single(initialFetchResult)
|> then(
self.changeSink.signal()
|> mapToSignal { change in
if let updatedFetchResult = change.changeDetails(for: fetchResult.with { $0 })?.fetchResultAfterChanges {
let _ = fetchResult.modify { _ in return updatedFetchResult }
return .single(updatedFetchResult)
} else {
return .complete()
}
}
)
}
func recentAssets() -> Signal<PHFetchResult<PHAsset>?, NoError> {
let collections = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .smartAlbumUserLibrary, options: nil)
if let collection = collections.firstObject {
let initialFetchResult = PHAsset.fetchAssets(in: collection, options: nil)
return fetchResultAssets(initialFetchResult)
return fetchAssets(collection)
|> map(Optional.init)
} else {
return .single(nil)
}

View File

@ -0,0 +1,367 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import MergeLists
import Photos
private struct MediaGroupsGridAlbumEntry: Comparable, Identifiable {
let theme: PresentationTheme
let index: Int
let collection: PHAssetCollection
let firstItem: PHAsset?
let count: String
var stableId: String {
return self.collection.localIdentifier
}
static func ==(lhs: MediaGroupsGridAlbumEntry, rhs: MediaGroupsGridAlbumEntry) -> Bool {
if lhs.theme !== rhs.theme {
return false
}
if lhs.index != rhs.index {
return false
}
if lhs.collection != rhs.collection {
return false
}
if lhs.firstItem != rhs.firstItem {
return false
}
if lhs.count != rhs.count {
return false
}
return true
}
static func <(lhs: MediaGroupsGridAlbumEntry, rhs: MediaGroupsGridAlbumEntry) -> Bool {
return lhs.index < rhs.index
}
func item(action: @escaping (PHAssetCollection) -> Void) -> ListViewItem {
return MediaGroupsGridAlbumItem(theme: theme, collection: self.collection, firstItem: self.firstItem, count: self.count, action: action)
}
}
private class MediaGroupsGridAlbumItem: ListViewItem {
let theme: PresentationTheme
let collection: PHAssetCollection
let firstItem: PHAsset?
let count: String
let action: (PHAssetCollection) -> Void
public init(theme: PresentationTheme, collection: PHAssetCollection, firstItem: PHAsset?, count: String, action: @escaping (PHAssetCollection) -> Void) {
self.theme = theme
self.collection = collection
self.firstItem = firstItem
self.count = count
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MediaGroupsGridAlbumItemNode()
let (nodeLayout, apply) = node.asyncLayout()(self, params)
node.insets = nodeLayout.insets
node.contentSize = nodeLayout.contentSize
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in
apply(false)
})
})
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
assert(node() is MediaGroupsGridAlbumItemNode)
if let nodeValue = node() as? MediaGroupsGridAlbumItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { _ in
apply(animation.isAnimated)
})
}
}
}
}
}
public var selectable = true
public func selected(listView: ListView) {
self.action(self.collection)
}
}
private let textFont = Font.regular(15.0)
private final class MediaGroupsGridAlbumItemNode : ListViewItemNode {
private let containerNode: ASDisplayNode
private let imageNode: ImageNode
private let titleNode: TextNode
private let countNode: TextNode
var item: MediaGroupsGridAlbumItem?
init() {
self.containerNode = ASDisplayNode()
self.imageNode = ImageNode()
self.imageNode.clipsToBounds = true
self.imageNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 62.0, height: 62.0))
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.countNode = TextNode()
self.countNode.isUserInteractionEnabled = false
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.imageNode)
self.containerNode.addSubnode(self.titleNode)
self.containerNode.addSubnode(self.countNode)
}
override func didLoad() {
super.didLoad()
self.imageNode.cornerRadius = 10.0
if #available(iOS 13.0, *) {
self.imageNode.layer.cornerCurve = .continuous
}
self.containerNode.transform = CATransform3DMakeRotation(CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
}
func asyncLayout() -> (MediaGroupsGridAlbumItem, ListViewItemLayoutParams) -> (ListViewItemNodeLayout, (Bool) -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeCountLayout = TextNode.asyncLayout(self.countNode)
return { [weak self] item, params in
let title = NSAttributedString(string: item.collection.localizedTitle ?? "", font: textFont, textColor: item.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: title, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 170.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let count = NSAttributedString(string: item.count, font: textFont, textColor: item.theme.list.itemSecondaryTextColor)
let (countLayout, countApply) = makeCountLayout(TextNodeLayoutArguments(attributedString: count, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: 170.0, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let itemLayout = ListViewItemNodeLayout(contentSize: CGSize(width: 220.0, height: 182.0), insets: UIEdgeInsets())
return (itemLayout, { animated in
if let strongSelf = self {
strongSelf.item = item
strongSelf.containerNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 220.0, height: 220.0))
if let firstItem = item.firstItem {
let scale = min(2.0, UIScreenScale)
let targetSize = CGSize(width: 160.0 * scale, height: 160.0 * scale)
strongSelf.imageNode.setSignal(assetImage(asset: firstItem, targetSize: targetSize, exact: false))
}
strongSelf.imageNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 0.0), size: CGSize(width: 170.0, height: 170.0))
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 176.0), size: titleLayout.size)
let _ = countApply()
strongSelf.countNode.frame = CGRect(origin: CGPoint(x: 6.0, y: 196.0), size: countLayout.size)
}
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
super.animateInsertion(currentTimestamp, duration: duration, short: short)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
super.animateRemoved(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
}
override func animateAdded(_ currentTimestamp: Double, duration: Double) {
super.animateAdded(currentTimestamp, duration: duration)
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
}
}
private struct MediaGroupsAlbumGridItemNodeTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
let entries: [MediaGroupsGridAlbumEntry]
}
private func preparedTransition(action: @escaping (PHAssetCollection) -> Void, from fromEntries: [MediaGroupsGridAlbumEntry], to toEntries: [MediaGroupsGridAlbumEntry]) -> MediaGroupsAlbumGridItemNodeTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: .Down) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(action: action), directionHint: nil) }
return MediaGroupsAlbumGridItemNodeTransition(deletions: deletions, insertions: insertions, updates: updates, entries: toEntries)
}
final class MediaGroupsAlbumGridItem: ListViewItem {
let presentationData: ItemListPresentationData
let collections: [PHAssetCollection]
let action: (PHAssetCollection) -> Void
public init(presentationData: ItemListPresentationData, collections: [PHAssetCollection], action: @escaping (PHAssetCollection) -> Void) {
self.presentationData = presentationData
self.collections = collections
self.action = action
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
Queue.mainQueue().async {
let node = MediaGroupsAlbumGridItemNode()
let makeLayout = node.asyncLayout()
async {
let (nodeLayout, nodeApply) = makeLayout(self, params)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
Queue.mainQueue().async {
completion(node, nodeApply)
}
}
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MediaGroupsAlbumGridItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return false
}
}
private let titleFont = Font.bold(20.0)
private class MediaGroupsAlbumGridItemNode: ListViewItemNode {
private var item: MediaGroupsAlbumGridItem?
private var layoutParams: ListViewItemLayoutParams?
private let listNode: ListView
private var entries: [MediaGroupsGridAlbumEntry]?
private var enqueuedTransitions: [MediaGroupsAlbumGridItemNodeTransition] = []
init() {
self.listNode = ListView()
self.listNode.transform = CATransform3DMakeRotation(-CGFloat.pi / 2.0, 0.0, 0.0, 1.0)
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.listNode)
}
private func enqueueTransition(_ transition: MediaGroupsAlbumGridItemNodeTransition) {
self.enqueuedTransitions.append(transition)
if let _ = self.item {
while !self.enqueuedTransitions.isEmpty {
self.dequeueTransition()
}
}
}
private func dequeueTransition() {
guard let _ = self.item, let transition = self.enqueuedTransitions.first else {
return
}
self.enqueuedTransitions.remove(at: 0)
var options = ListViewDeleteAndInsertOptions()
options.insert(.Synchronous)
self.listNode.transaction(deleteIndices: transition.deletions, insertIndicesAndItems: transition.insertions, updateIndicesAndItems: transition.updates, options: options, scrollToItem: nil, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { _ in
})
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: MediaGroupsAlbumGridItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
return { [weak self] item, params in
let contentSize = CGSize(width: params.width, height: 220.0)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
let listInsets = UIEdgeInsets(top: 10.0, left: 0.0, bottom: 10.0, right: 0.0)
strongSelf.listNode.bounds = CGRect(x: 0.0, y: 0.0, width: contentSize.height, height: contentSize.width - params.leftInset - params.rightInset)
strongSelf.listNode.position = CGPoint(x: contentSize.width / 2.0, y: contentSize.height / 2.0)
strongSelf.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: CGSize(width: contentSize.height, height: contentSize.width), insets: listInsets, duration: 0.0, curve: .Default(duration: nil)), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
var entries: [MediaGroupsGridAlbumEntry] = []
var index: Int = 0
for collection in item.collections {
let result = PHAsset.fetchAssets(in: collection, options: nil)
if let firstItem = result.firstObject {
entries.append(MediaGroupsGridAlbumEntry(theme: item.presentationData.theme, index: index, collection: collection, firstItem: firstItem, count: presentationStringsFormattedNumber(Int32(result.count))))
index += 1
}
}
let previousEntries = strongSelf.entries ?? []
let transition = preparedTransition(action: { [weak item] collection in
item?.action(collection)
}, from: previousEntries, to: entries)
strongSelf.enqueueTransition(transition)
strongSelf.entries = entries
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
}

View File

@ -0,0 +1,329 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
import PresentationDataUtils
import AppBundle
class MediaGroupsAlbumItem: ListViewItem, ListViewItemWithHeader {
enum Icon {
case bursts
case panoramas
case screenshots
case selfPortraits
case slomoVideos
case timelapses
case videos
case animated
case depthEffect
case livePhotos
var image: UIImage? {
switch self {
case .bursts:
return UIImage(bundleImageName: "Chat/Attach Menu/Burst")
case .panoramas:
return UIImage(bundleImageName: "Chat/Attach Menu/Panorama")
case .screenshots:
return UIImage(bundleImageName: "Chat/Attach Menu/Screenshot")
case .selfPortraits:
return UIImage(bundleImageName: "Chat/Attach Menu/Selfie")
case .slomoVideos:
return UIImage(bundleImageName: "Chat/Attach Menu/SloMo")
case .timelapses:
return UIImage(bundleImageName: "Chat/Attach Menu/Timelapse")
case .videos:
return UIImage(bundleImageName: "Chat/Attach Menu/Video")
case .animated:
return UIImage(bundleImageName: "Chat/Attach Menu/Animated")
case .depthEffect:
return UIImage(bundleImageName: "Chat/Attach Menu/Portrait")
case .livePhotos:
return UIImage(bundleImageName: "Chat/Attach Menu/LivePhoto")
}
}
}
let presentationData: ItemListPresentationData
let title: String
let count: String
let icon: Icon?
let action: () -> Void
let header: ListViewItemHeader? = nil
init(presentationData: ItemListPresentationData, title: String, count: String, icon: Icon?, action: @escaping () -> Void) {
self.presentationData = presentationData
self.title = title
self.count = count
self.icon = icon
self.action = action
}
func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MediaGroupsAlbumItemNode()
let (first, last) = MediaGroupsAlbumItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (layout, apply) = node.asyncLayout()(self, params, first, last)
node.contentSize = layout.contentSize
node.insets = layout.insets
Queue.mainQueue().async {
completion(node, {
return (nil, { _ in apply() })
})
}
}
}
func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MediaGroupsAlbumItemNode {
let makeLayout = nodeValue.asyncLayout()
async {
let (first, last) = MediaGroupsAlbumItem.mergeType(item: self, previousItem: previousItem, nextItem: nextItem)
let (layout, apply) = makeLayout(self, params, first, last)
Queue.mainQueue().async {
completion(layout, { _ in
apply()
})
}
}
}
}
}
var selectable: Bool = true
public func selected(listView: ListView){
self.action()
listView.clearHighlightAnimated(true)
}
static func mergeType(item: MediaGroupsAlbumItem, previousItem: ListViewItem?, nextItem: ListViewItem?) -> (first: Bool, last: Bool) {
var first = false
var last = false
if let previousItem = previousItem, !(previousItem is MediaGroupsAlbumItem) {
first = true
}
if nextItem == nil {
last = true
}
return (first, last)
}
}
class MediaGroupsAlbumItemNode: ListViewItemNode {
private let backgroundNode: ASDisplayNode
private let topStripeNode: ASDisplayNode
private let bottomStripeNode: ASDisplayNode
private let highlightedBackgroundNode: ASDisplayNode
private let iconNode: ASImageNode
private let titleNode: TextNode
private let countNode: TextNode
private let arrowNode: ASImageNode
private let activateArea: AccessibilityAreaNode
private var item: MediaGroupsAlbumItem?
init() {
self.backgroundNode = ASDisplayNode()
self.backgroundNode.isLayerBacked = true
self.backgroundNode.backgroundColor = .white
self.topStripeNode = ASDisplayNode()
self.topStripeNode.isLayerBacked = true
self.bottomStripeNode = ASDisplayNode()
self.bottomStripeNode.isLayerBacked = true
self.titleNode = TextNode()
self.titleNode.isUserInteractionEnabled = false
self.titleNode.contentMode = .left
self.titleNode.contentsScale = UIScreen.main.scale
self.iconNode = ASImageNode()
self.iconNode.isLayerBacked = true
self.iconNode.displayWithoutProcessing = true
self.iconNode.displaysAsynchronously = false
self.countNode = TextNode()
self.countNode.isUserInteractionEnabled = false
self.countNode.contentMode = .left
self.countNode.contentsScale = UIScreen.main.scale
self.arrowNode = ASImageNode()
self.arrowNode.isLayerBacked = true
self.arrowNode.displayWithoutProcessing = true
self.arrowNode.displaysAsynchronously = false
self.highlightedBackgroundNode = ASDisplayNode()
self.highlightedBackgroundNode.isLayerBacked = true
self.activateArea = AccessibilityAreaNode()
super.init(layerBacked: false, dynamicBounce: false)
self.addSubnode(self.iconNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.countNode)
self.addSubnode(self.arrowNode)
self.addSubnode(self.activateArea)
self.activateArea.activate = { [weak self] in
self?.item?.action()
return true
}
}
func asyncLayout() -> (_ item: MediaGroupsAlbumItem, _ params: ListViewItemLayoutParams, _ first: Bool, _ last: Bool) -> (ListViewItemNodeLayout, () -> Void) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
let makeCountLayout = TextNode.asyncLayout(self.countNode)
let currentItem = self.item
return { item, params, first, last in
var updatedTheme: PresentationTheme?
if currentItem?.presentationData.theme !== item.presentationData.theme {
updatedTheme = item.presentationData.theme
}
let titleFont = Font.regular(21.0)
let countFont = Font.regular(17.0)
let leftInset: CGFloat = 60.0 + params.leftInset
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemAccentColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let (countLayout, countApply) = makeCountLayout(TextNodeLayoutArguments(attributedString: NSAttributedString(string: item.count, font: countFont, textColor: item.presentationData.theme.list.itemSecondaryTextColor), backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - 10.0 - leftInset - params.rightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentHeight: CGFloat = 48.0
let contentSize = CGSize(width: params.width, height: contentHeight)
let insets = UIEdgeInsets()
let separatorHeight = UIScreenPixel
let layout = ListViewItemNodeLayout(contentSize: contentSize, insets: insets)
return (layout, { [weak self] in
if let strongSelf = self {
strongSelf.item = item
strongSelf.activateArea.accessibilityLabel = item.title
strongSelf.activateArea.frame = CGRect(origin: CGPoint(x: params.leftInset, y: 0.0), size: CGSize(width: layout.contentSize.width - params.leftInset - params.rightInset, height: layout.contentSize.height))
if let _ = updatedTheme {
strongSelf.topStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.bottomStripeNode.backgroundColor = item.presentationData.theme.list.itemPlainSeparatorColor
strongSelf.backgroundNode.backgroundColor = item.presentationData.theme.list.plainBackgroundColor
strongSelf.highlightedBackgroundNode.backgroundColor = item.presentationData.theme.list.itemHighlightedBackgroundColor
strongSelf.iconNode.image = generateTintedImage(image: item.icon?.image, color: item.presentationData.theme.list.itemAccentColor)
strongSelf.arrowNode.image = PresentationResourcesItemList.disclosureArrowImage(item.presentationData.theme)
}
strongSelf.addSubnode(strongSelf.activateArea)
let _ = titleApply()
let _ = countApply()
let titleOffset = leftInset
let hideBottomStripe: Bool = last
if let image = strongSelf.iconNode.image {
strongSelf.iconNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 14.0, y: floorToScreenPixels((contentSize.height - image.size.height) / 2.0)), size: image.size)
}
if strongSelf.backgroundNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.backgroundNode, at: 0)
}
if strongSelf.topStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.topStripeNode, at: 1)
}
if strongSelf.bottomStripeNode.supernode == nil {
strongSelf.insertSubnode(strongSelf.bottomStripeNode, at: 2)
}
strongSelf.backgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -min(insets.top, separatorHeight)), size: CGSize(width: params.width, height: contentSize.height + min(insets.top, separatorHeight) + min(insets.bottom, separatorHeight)))
strongSelf.topStripeNode.isHidden = true
strongSelf.bottomStripeNode.isHidden = hideBottomStripe
strongSelf.bottomStripeNode.frame = CGRect(origin: CGPoint(x: leftInset, y: contentSize.height - separatorHeight), size: CGSize(width: params.width - leftInset, height: separatorHeight))
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: titleOffset, y: floorToScreenPixels((contentSize.height - titleLayout.size.height) / 2.0) + 1.0), size: titleLayout.size)
if let arrowSize = strongSelf.arrowNode.image?.size {
strongSelf.arrowNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - arrowSize.width - 12.0, y: floorToScreenPixels((contentSize.height - arrowSize.height) / 2.0)), size: arrowSize)
strongSelf.countNode.frame = CGRect(origin: CGPoint(x: params.width - params.rightInset - countLayout.size.width - arrowSize.width - 12.0 - 2.0, y: floorToScreenPixels((contentSize.height - countLayout.size.height) / 2.0) + 1.0), size: countLayout.size)
}
strongSelf.highlightedBackgroundNode.frame = CGRect(origin: CGPoint(x: 0.0, y: -UIScreenPixel), size: CGSize(width: params.width, height: contentSize.height + UIScreenPixel + UIScreenPixel))
}
})
}
}
override func setHighlighted(_ highlighted: Bool, at point: CGPoint, animated: Bool) {
super.setHighlighted(highlighted, at: point, animated: animated)
if highlighted {
self.highlightedBackgroundNode.alpha = 1.0
if self.highlightedBackgroundNode.supernode == nil {
var anchorNode: ASDisplayNode?
if self.bottomStripeNode.supernode != nil {
anchorNode = self.bottomStripeNode
} else if self.topStripeNode.supernode != nil {
anchorNode = self.topStripeNode
} else if self.backgroundNode.supernode != nil {
anchorNode = self.backgroundNode
}
if let anchorNode = anchorNode {
self.insertSubnode(self.highlightedBackgroundNode, aboveSubnode: anchorNode)
} else {
self.addSubnode(self.highlightedBackgroundNode)
}
}
} else {
if self.highlightedBackgroundNode.supernode != nil {
if animated {
self.highlightedBackgroundNode.layer.animateAlpha(from: self.highlightedBackgroundNode.alpha, to: 0.0, duration: 0.4, completion: { [weak self] completed in
if let strongSelf = self {
if completed {
strongSelf.highlightedBackgroundNode.removeFromSupernode()
}
}
})
self.highlightedBackgroundNode.alpha = 0.0
} else {
self.highlightedBackgroundNode.removeFromSupernode()
}
}
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.4)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false)
}
override public func headers() -> [ListViewItemHeader]? {
if let item = self.item {
return item.header.flatMap { [$0] }
} else {
return nil
}
}
}

View File

@ -0,0 +1,110 @@
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import TelegramPresentationData
import ItemListUI
final class MediaGroupsHeaderItem: ListViewItem {
let presentationData: ItemListPresentationData
let title: String
public init(presentationData: ItemListPresentationData, title: String) {
self.presentationData = presentationData
self.title = title
}
public func nodeConfiguredForParams(async: @escaping (@escaping () -> Void) -> Void, params: ListViewItemLayoutParams, synchronousLoads: Bool, previousItem: ListViewItem?, nextItem: ListViewItem?, completion: @escaping (ListViewItemNode, @escaping () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) -> Void) {
async {
let node = MediaGroupsHeaderItemNode()
let makeLayout = node.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(self, params)
node.contentSize = nodeLayout.contentSize
node.insets = nodeLayout.insets
completion(node, nodeApply)
}
}
public func updateNode(async: @escaping (@escaping () -> Void) -> Void, node: @escaping () -> ListViewItemNode, params: ListViewItemLayoutParams, previousItem: ListViewItem?, nextItem: ListViewItem?, animation: ListViewItemUpdateAnimation, completion: @escaping (ListViewItemNodeLayout, @escaping (ListViewItemApply) -> Void) -> Void) {
Queue.mainQueue().async {
if let nodeValue = node() as? MediaGroupsHeaderItemNode {
let layout = nodeValue.asyncLayout()
async {
let (nodeLayout, apply) = layout(self, params)
Queue.mainQueue().async {
completion(nodeLayout, { info in
apply().1(info)
})
}
}
}
}
}
public var selectable: Bool {
return false
}
}
private let titleFont = Font.bold(22.0)
private class MediaGroupsHeaderItemNode: ListViewItemNode {
private let titleNode: TextNode
private var item: MediaGroupsHeaderItem?
private var layoutParams: ListViewItemLayoutParams?
init() {
self.titleNode = TextNode()
super.init(layerBacked: false, dynamicBounce: false, rotated: false, seeThrough: false)
self.addSubnode(self.titleNode)
}
override func layoutForParams(_ params: ListViewItemLayoutParams, item: ListViewItem, previousItem: ListViewItem?, nextItem: ListViewItem?) {
if let item = self.item {
let makeLayout = self.asyncLayout()
let (nodeLayout, nodeApply) = makeLayout(item, params)
self.contentSize = nodeLayout.contentSize
self.insets = nodeLayout.insets
let _ = nodeApply()
}
}
func asyncLayout() -> (_ item: MediaGroupsHeaderItem, _ params: ListViewItemLayoutParams) -> (ListViewItemNodeLayout, () -> (Signal<Void, NoError>?, (ListViewItemApply) -> Void)) {
let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
return { [weak self] item, params in
let titleString = NSAttributedString(string: item.title, font: titleFont, textColor: item.presentationData.theme.list.itemPrimaryTextColor)
let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: params.width - params.leftInset - params.rightInset - 16.0, height: 100.0), alignment: .natural, cutout: nil, insets: UIEdgeInsets()))
let contentSize = CGSize(width: params.width, height: 45.0)
let nodeLayout = ListViewItemNodeLayout(contentSize: contentSize, insets: UIEdgeInsets())
return (nodeLayout, { [weak self] in
return (nil, { _ in
if let strongSelf = self {
strongSelf.item = item
strongSelf.layoutParams = params
let _ = titleApply()
strongSelf.titleNode.frame = CGRect(origin: CGPoint(x: params.leftInset + 17.0, y: floor((nodeLayout.contentSize.height - titleLayout.size.height) / 2.0) + 2.0), size: titleLayout.size)
}
})
})
}
}
override func animateInsertion(_ currentTimestamp: Double, duration: Double, short: Bool) {
self.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration * 0.5)
}
override func animateRemoved(_ currentTimestamp: Double, duration: Double) {
self.layer.animateAlpha(from: 1.0, to: 0.0, duration: duration * 0.5, removeOnCompletion: false)
}
}

View File

@ -1,8 +1,401 @@
//
// MediaGroupsScreen.swift
// _idx_TelegramUI_F082088E_ios_min9.0
//
// Created by Ilya Laktyushin on 22.02.2022.
//
import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext
import TelegramPresentationData
import TelegramUIPreferences
import MergeLists
import Photos
import LegacyComponents
import AttachmentUI
import ItemListUI
private enum MediaGroupsEntry: Comparable, Identifiable {
enum StableId: Hashable {
case albumsHeader
case albums
case smartAlbumsHeader
case smartAlbum(String)
}
case albumsHeader(PresentationTheme, String)
case albums(PresentationTheme, [PHAssetCollection])
case smartAlbumsHeader(PresentationTheme, String)
case smartAlbum(PresentationTheme, Int, PHAssetCollection, Int)
var stableId: StableId {
switch self {
case .albumsHeader:
return .albumsHeader
case .albums:
return .albums
case .smartAlbumsHeader:
return .smartAlbumsHeader
case let .smartAlbum(_, _, album, _):
return .smartAlbum(album.localIdentifier)
}
}
static func ==(lhs: MediaGroupsEntry, rhs: MediaGroupsEntry) -> Bool {
switch lhs {
case let .albumsHeader(lhsTheme, lhsText):
if case let .albumsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .albums(lhsTheme, lhsAssetCollections):
if case let .albums(rhsTheme, rhsAssetCollections) = rhs, lhsTheme === rhsTheme, lhsAssetCollections == rhsAssetCollections {
return true
} else {
return false
}
case let .smartAlbumsHeader(lhsTheme, lhsText):
if case let .smartAlbumsHeader(rhsTheme, rhsText) = rhs, lhsTheme === rhsTheme, lhsText == rhsText {
return true
} else {
return false
}
case let .smartAlbum(lhsTheme, lhsIndex, lhsAssetCollection, lhsCount):
if case let .smartAlbum(rhsTheme, rhsIndex, rhsAssetCollection, rhsCount) = rhs, lhsTheme === rhsTheme, lhsIndex == rhsIndex, lhsAssetCollection == rhsAssetCollection, lhsCount == rhsCount {
return true
} else {
return false
}
}
}
private var sortId: Int {
switch self {
case .albumsHeader:
return 0
case .albums:
return 1
case .smartAlbumsHeader:
return 2
case let .smartAlbum(_, index, _, _):
return 3 + index
}
}
static func <(lhs: MediaGroupsEntry, rhs: MediaGroupsEntry) -> Bool {
return lhs.sortId < rhs.sortId
}
func item(presentationData: PresentationData, openGroup: @escaping (PHAssetCollection) -> Void) -> ListViewItem {
switch self {
case let .albumsHeader(_, text), let .smartAlbumsHeader(_, text):
return MediaGroupsHeaderItem(presentationData: ItemListPresentationData(presentationData), title: text)
case let .albums(_, collections):
return MediaGroupsAlbumGridItem(presentationData: ItemListPresentationData(presentationData), collections: collections, action: { collection in
openGroup(collection)
})
case let .smartAlbum(_, _, collection, count):
let title = collection.localizedTitle ?? ""
let count = presentationStringsFormattedNumber(Int32(count), presentationData.dateTimeFormat.groupingSeparator)
var icon: MediaGroupsAlbumItem.Icon?
switch collection.assetCollectionSubtype {
case .smartAlbumAnimated:
icon = .animated
case .smartAlbumBursts:
icon = .bursts
case .smartAlbumDepthEffect:
icon = .selfPortraits
case .smartAlbumLivePhotos:
icon = .depthEffect
case .smartAlbumPanoramas:
icon = .panoramas
case .smartAlbumScreenshots:
icon = .screenshots
case .smartAlbumSelfPortraits:
icon = .selfPortraits
case .smartAlbumSlomoVideos:
icon = .slomoVideos
case .smartAlbumTimelapses:
icon = .timelapses
case .smartAlbumVideos:
icon = .videos
default:
icon = nil
}
return MediaGroupsAlbumItem(presentationData: ItemListPresentationData(presentationData), title: title, count: count, icon: icon, action: {
openGroup(collection)
})
}
}
}
private struct MediaGroupsTransition {
let deletions: [ListViewDeleteItem]
let insertions: [ListViewInsertItem]
let updates: [ListViewUpdateItem]
}
private func preparedTransition(from fromEntries: [MediaGroupsEntry], to toEntries: [MediaGroupsEntry], presentationData: PresentationData, openGroup: @escaping (PHAssetCollection) -> Void) -> MediaGroupsTransition {
let (deleteIndices, indicesAndItems, updateIndices) = mergeListsStableWithUpdates(leftList: fromEntries, rightList: toEntries)
let deletions = deleteIndices.map { ListViewDeleteItem(index: $0, directionHint: nil) }
let insertions = indicesAndItems.map { ListViewInsertItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, openGroup: openGroup), directionHint: nil) }
let updates = updateIndices.map { ListViewUpdateItem(index: $0.0, previousIndex: $0.2, item: $0.1.item(presentationData: presentationData, openGroup: openGroup), directionHint: nil) }
return MediaGroupsTransition(deletions: deletions, insertions: insertions, updates: updates)
}
public final class MediaGroupsScreen: ViewController {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let mediaAssetsContext: MediaAssetsContext
private let openGroup: (PHAssetCollection) -> Void
private class Node: ViewControllerTracingNode {
struct State {
let albums: PHFetchResult<PHAssetCollection>
let smartAlbums: PHFetchResult<PHAssetCollection>
}
private weak var controller: MediaGroupsScreen?
private var presentationData: PresentationData
private let containerNode: ASDisplayNode
private let listNode: ListView
private var nextStableId: Int = 1
private var currentEntries: [MediaGroupsEntry] = []
private var enqueuedTransactions: [MediaGroupsTransition] = []
private var state: State?
private var itemsDisposable: Disposable?
private var didSetReady = false
private let _ready = Promise<Bool>()
var ready: Promise<Bool> {
return self._ready
}
private var validLayout: (ContainerViewLayout, CGFloat)?
init(controller: MediaGroupsScreen) {
self.controller = controller
self.presentationData = controller.presentationData
self.containerNode = ASDisplayNode()
self.listNode = ListView()
super.init()
self.containerNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.listNode)
let updatedState = combineLatest(queue: Queue.mainQueue(), controller.mediaAssetsContext.fetchAssetsCollections(.album), controller.mediaAssetsContext.fetchAssetsCollections(.smartAlbum))
self.itemsDisposable = (updatedState
|> deliverOnMainQueue).start(next: { [weak self] albums, smartAlbums in
guard let strongSelf = self else {
return
}
strongSelf.updateState(State(albums: albums, smartAlbums: smartAlbums))
})
self.listNode.beganInteractiveDragging = { [weak self] _ in
self?.view.window?.endEditing(true)
}
}
deinit {
self.itemsDisposable?.dispose()
}
override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
let result = super.hitTest(point, with: event)
if result == self.view {
return nil
}
return result
}
private func updateState(_ state: State) {
self.state = state
var entries: [MediaGroupsEntry] = []
var albums: [PHAssetCollection] = []
entries.append(.albumsHeader(self.presentationData.theme, self.presentationData.strings.Attachment_MyAlbums))
state.smartAlbums.enumerateObjects { collection, _, _ in
if [.smartAlbumUserLibrary, .smartAlbumFavorites].contains(collection.assetCollectionSubtype) {
albums.append(collection)
}
}
state.albums.enumerateObjects { collection, _, _ in
albums.append(collection)
}
entries.append(.albums(self.presentationData.theme, albums))
let smartAlbumsHeaderIndex = entries.count
var addedSmartAlbum = false
state.smartAlbums.enumerateObjects { collection, index, _ in
var supportedAlbums: [PHAssetCollectionSubtype] = [
.smartAlbumBursts,
.smartAlbumPanoramas,
.smartAlbumScreenshots,
.smartAlbumSelfPortraits,
.smartAlbumSlomoVideos,
.smartAlbumTimelapses,
.smartAlbumVideos
]
if #available(iOS 11, *) {
supportedAlbums.append(.smartAlbumAnimated)
supportedAlbums.append(.smartAlbumDepthEffect)
supportedAlbums.append(.smartAlbumLivePhotos)
}
if supportedAlbums.contains(collection.assetCollectionSubtype) {
let result = PHAsset.fetchAssets(in: collection, options: nil)
if result.count > 0 {
addedSmartAlbum = true
entries.append(.smartAlbum(self.presentationData.theme, index, collection, result.count))
}
}
}
if addedSmartAlbum {
entries.insert(.smartAlbumsHeader(self.presentationData.theme, self.presentationData.strings.Attachment_MediaTypes), at: smartAlbumsHeaderIndex)
}
let previousEntries = self.currentEntries
self.currentEntries = entries
let transaction = preparedTransition(from: previousEntries, to: entries, presentationData: self.presentationData, openGroup: { [weak self] collection in
self?.view.window?.endEditing(true)
self?.controller?.openGroup(collection)
})
self.enqueueTransaction(transaction)
}
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
}
private func enqueueTransaction(_ transaction: MediaGroupsTransition) {
self.enqueuedTransactions.append(transaction)
if let _ = self.validLayout {
while !self.enqueuedTransactions.isEmpty {
self.dequeueTransaction()
}
}
}
private func dequeueTransaction() {
if self.enqueuedTransactions.isEmpty {
return
}
let transaction = self.enqueuedTransactions.removeFirst()
let options = ListViewDeleteAndInsertOptions()
self.listNode.transaction(deleteIndices: transaction.deletions, insertIndicesAndItems: transaction.insertions, updateIndicesAndItems: transaction.updates, options: options, updateSizeAndInsets: nil, updateOpaqueState: nil, completion: { [weak self] _ in
if let strongSelf = self {
if !strongSelf.didSetReady {
strongSelf.didSetReady = true
strongSelf._ready.set(.single(true))
}
}
})
}
func scrollToTop() {
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: ListViewScrollToItem(index: 0, position: .top(0.0), animated: true, curve: .Default(duration: nil), directionHint: .Up), updateSizeAndInsets: nil, stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let firstTime = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationBarHeight + 2.0), size: CGSize(width: layout.size.width, height: layout.size.height - navigationBarHeight - 2.0)))
let size = layout.size
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
self.listNode.transaction(deleteIndices: [], insertIndicesAndItems: [], updateIndicesAndItems: [], options: [.Synchronous, .LowLatency], scrollToItem: nil, updateSizeAndInsets: ListViewUpdateSizeAndInsets(size: size, insets: UIEdgeInsets(top: 7.0, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom + 50.0, right: layout.safeInsets.right), headerInsets: UIEdgeInsets(), scrollIndicatorInsets: UIEdgeInsets(), duration: duration, curve: curve), stationaryItemRange: nil, updateOpaqueState: nil, completion: { _ in })
transition.updateFrame(node: self.listNode, frame: CGRect(origin: CGPoint(), size: size))
if firstTime {
self.dequeueTransaction()
}
}
}
private var validLayout: ContainerViewLayout?
private var controllerNode: Node {
return self.displayNode as! Node
}
private let _ready = Promise<Bool>()
override public var ready: Promise<Bool> {
return self._ready
}
init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, mediaAssetsContext: MediaAssetsContext, openGroup: @escaping (PHAssetCollection) -> Void) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.mediaAssetsContext = mediaAssetsContext
self.openGroup = openGroup
super.init(navigationBarPresentationData: nil)
self.statusBar.statusBarStyle = .Ignore
self.presentationDataDisposable = ((updatedPresentationData?.signal ?? context.sharedContext.presentationData)
|> deliverOnMainQueue).start(next: { [weak self] presentationData in
if let strongSelf = self {
let previousTheme = strongSelf.presentationData.theme
let previousStrings = strongSelf.presentationData.strings
strongSelf.presentationData = presentationData
if previousTheme !== presentationData.theme || previousStrings !== presentationData.strings {
strongSelf.controllerNode.updatePresentationData(presentationData)
}
}
})
self.scrollToTop = { [weak self] in
if let strongSelf = self {
strongSelf.controllerNode.scrollToTop()
}
}
}
required init(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
self.presentationDataDisposable?.dispose()
}
override public func loadDisplayNode() {
self.displayNode = Node(controller: self)
self._ready.set(self.controllerNode.ready.get())
super.displayNodeDidLoad()
}
override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) {
super.containerLayoutUpdated(layout, transition: transition)
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
}
}

View File

@ -9,6 +9,13 @@ import SolidRoundedButtonNode
import PresentationDataUtils
final class MediaPickerPlaceholderNode: ASDisplayNode {
enum Content {
case intro
case bannedSendMedia(String)
}
private let content: Content
private var animationNode: AnimatedStickerNode
private let titleNode: ImmediateTextNode
private let textNode: ImmediateTextNode
@ -22,9 +29,22 @@ final class MediaPickerPlaceholderNode: ASDisplayNode {
var settingsPressed: () -> Void = {}
var cameraPressed: () -> Void = {}
override init() {
init(content: Content) {
self.content = content
let name: String
let playbackMode: AnimatedStickerPlaybackMode
switch content {
case .intro:
name = "Photos"
playbackMode = .loop
case .bannedSendMedia:
name = "Banned"
playbackMode = .once
}
self.animationNode = AnimatedStickerNode()
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: "Files"), width: 320, height: 320, playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode.setup(source: AnimatedStickerNodeLocalFileSource(name: name), width: 320, height: 320, playbackMode: playbackMode, mode: .direct(cachePathPrefix: nil))
self.animationNode.visibility = true
self.titleNode = ImmediateTextNode()
@ -54,8 +74,10 @@ final class MediaPickerPlaceholderNode: ASDisplayNode {
super.init()
self.addSubnode(self.animationNode)
self.addSubnode(self.titleNode)
self.addSubnode(self.textNode)
if case .intro = self.content {
self.addSubnode(self.titleNode)
self.addSubnode(self.buttonNode)
self.addSubnode(self.cameraButtonNode)
@ -83,6 +105,7 @@ final class MediaPickerPlaceholderNode: ASDisplayNode {
self?.settingsPressed()
}
}
}
@objc private func cameraButtonPressed() {
self.cameraPressed()
@ -94,20 +117,22 @@ final class MediaPickerPlaceholderNode: ASDisplayNode {
let themeUpdated = self.theme != theme
self.theme = theme
var imageSize = CGSize(width: 144.0, height: 144.0)
var insets = layout.insets(options: [])
if layout.size.width == 460.0 {
insets.top += -60.0
imageSize = CGSize(width: 112.0, height: 112.0)
} else {
insets.top += -160.0
}
let imageSpacing: CGFloat = 12.0
let textSpacing: CGFloat = 16.0
let textSpacing: CGFloat = 12.0
let buttonSpacing: CGFloat = 15.0
let cameraSpacing: CGFloat = 13.0
let imageSize = CGSize(width: 144.0, height: 144.0)
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
self.animationNode.updateLayout(size: imageSize)
if themeUpdated {
self.buttonNode.updateTheme(SolidRoundedButtonTheme(theme: theme))
self.cameraIconNode.image = generateTintedImage(image: UIImage(bundleImageName: "Chat/Attach Menu/OpenCamera"), color: theme.list.itemAccentColor)
@ -116,12 +141,23 @@ final class MediaPickerPlaceholderNode: ASDisplayNode {
let buttonWidth: CGFloat = 248.0
let buttonHeight = self.buttonNode.updateLayout(width: buttonWidth, transition: transition)
self.titleNode.attributedText = NSAttributedString(string: strings.Attachment_MediaAccessTitle, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: strings.Attachment_MediaAccessText, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
let title: String
let text: String
switch self.content {
case .intro:
title = strings.Attachment_MediaAccessTitle
text = strings.Attachment_MediaAccessText
case let .bannedSendMedia(banDescription):
title = ""
text = banDescription
}
self.titleNode.attributedText = NSAttributedString(string: title, font: Font.medium(17.0), textColor: theme.list.itemPrimaryTextColor, paragraphAlignment: .center)
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
self.cameraTextNode.attributedText = NSAttributedString(string: strings.Attachment_OpenCamera, font: Font.regular(17.0), textColor: theme.list.itemAccentColor, paragraphAlignment: .center)
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let titleSize = self.titleNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let textSize = self.textNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 40.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let cameraSize = self.cameraTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
let totalHeight = imageHeight + titleSize.height + textSpacing + textSize.height + buttonSpacing + buttonHeight + cameraSpacing + cameraSize.height
@ -129,6 +165,8 @@ final class MediaPickerPlaceholderNode: ASDisplayNode {
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
self.animationNode.updateLayout(size: imageSize)
transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - titleSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight), size: titleSize))
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: self.titleNode.frame.maxY + textSpacing), size: textSize))

View File

@ -75,12 +75,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
private let context: AccountContext
private var presentationData: PresentationData
private var presentationDataDisposable: Disposable?
private let updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)?
fileprivate var interaction: MediaPickerInteraction?
private let peer: EnginePeer?
private let chatLocation: ChatLocation?
private let bannedSendMedia: (Int32, Bool)?
private let collection: PHAssetCollection?
private let titleView: MediaPickerTitleView
private let moreButtonNode: MediaPickerMoreButtonNode
@ -91,12 +93,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
public var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?
public var presentSchedulePicker: (Bool, @escaping (Int32) -> Void) -> Void = { _, _ in }
public var presentTimerPicker: (@escaping (Int32) -> Void) -> Void = { _ in }
public var presentWebSearch: () -> Void = {}
public var presentWebSearch: (MediaGroupsScreen) -> Void = { _ in }
public var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
public var legacyCompletion: (_ signals: [Any], _ silently: Bool, _ scheduleTime: Int32?) -> Void = { _, _, _ in }
public var requestAttachmentMenuExpansion: () -> Void = {}
public var requestAttachmentMenuExpansion: () -> Void = { }
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void = { _ in }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
private class Node: ViewControllerTracingNode {
enum DisplayMode {
@ -111,14 +115,17 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
private weak var controller: MediaPickerScreen?
private var presentationData: PresentationData
private let mediaAssetsContext: MediaAssetsContext
fileprivate let mediaAssetsContext: MediaAssetsContext
private var requestedMediaAccess = false
private var requestedCameraAccess = false
private let containerNode: ASDisplayNode
private let backgroundNode: NavigationBackgroundNode
private let gridNode: GridNode
fileprivate var cameraView: TGAttachmentCameraView?
private var placeholderNode: MediaPickerPlaceholderNode?
private var manageNode: MediaPickerManageNode?
private var bannedTextNode: ImmediateTextNode?
private var selectionNode: MediaPickerSelectedListNode?
@ -137,7 +144,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
private let hiddenMediaId = Promise<String?>(nil)
private var didSetReady = false
private let _ready = Promise<Bool>(true)
private let _ready = Promise<Bool>()
var ready: Promise<Bool> {
return self._ready
}
@ -151,16 +158,20 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let mediaAssetsContext = MediaAssetsContext()
self.mediaAssetsContext = mediaAssetsContext
self.containerNode = ASDisplayNode()
self.backgroundNode = NavigationBackgroundNode(color: self.presentationData.theme.rootController.tabBar.backgroundColor)
self.backgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.gridNode = GridNode()
super.init()
self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
// self.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.addSubnode(self.backgroundNode)
self.addSubnode(self.gridNode)
self.addSubnode(self.containerNode)
self.containerNode.addSubnode(self.backgroundNode)
self.containerNode.addSubnode(self.gridNode)
let collection = controller.collection
let preloadPromise = self.preloadPromise
let updatedState = combineLatest(mediaAssetsContext.mediaAccess(), mediaAssetsContext.cameraAccess())
|> mapToSignal { mediaAccess, cameraAccess -> Signal<State, NoError> in
@ -168,6 +179,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
return .single(.assets(fetchResult: nil, preload: false, mediaAccess: mediaAccess, cameraAccess: cameraAccess))
} else if [.restricted, .denied].contains(mediaAccess) {
return .single(.noAccess(cameraAccess: cameraAccess))
} else {
if let collection = collection {
return combineLatest(mediaAssetsContext.fetchAssets(collection), preloadPromise.get())
|> map { fetchResult, preload in
return .assets(fetchResult: fetchResult, preload: preload, mediaAccess: mediaAccess, cameraAccess: cameraAccess)
}
} else {
return combineLatest(mediaAssetsContext.recentAssets(), preloadPromise.get())
|> map { fetchResult, preload in
@ -175,6 +192,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
}
}
self.itemsDisposable = (updatedState
|> deliverOnMainQueue).start(next: { [weak self] state in
@ -188,6 +206,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self?.dismissInput()
}
self.gridNode.visibleContentOffsetChanged = { [weak self] _ in
self?.updateNavigation(transition: .immediate)
}
self.hiddenMediaDisposable = (self.hiddenMediaId.get()
|> deliverOnMainQueue).start(next: { [weak self] id in
if let strongSelf = self {
@ -257,11 +279,12 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.gridNode.scrollView.alwaysBounceVertical = true
self.gridNode.scrollView.showsVerticalScrollIndicator = false
if self.controller?.collection == nil {
let cameraView = TGAttachmentCameraView(forSelfPortrait: false)!
cameraView.clipsToBounds = true
cameraView.removeCorners()
cameraView.pressed = { [weak self] in
if let strongSelf = self {
if let strongSelf = self, !strongSelf.openingMedia {
strongSelf.controller?.openCamera?(strongSelf.cameraView)
}
}
@ -269,6 +292,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
cameraView.startPreview()
self.gridNode.scrollView.addSubview(cameraView)
} else {
self.containerNode.clipsToBounds = true
}
// self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
}
@ -277,9 +303,6 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.view.window?.endEditing(true)
}
private var requestedMediaAccess = false
private var requestedCameraAccess = false
private func updateState(_ state: State) {
guard let controller = self.controller, let interaction = controller.interaction else {
return
@ -337,6 +360,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if updateLayout, let (layout, navigationBarHeight) = self.validLayout {
self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut))
}
self.updateNavigation(transition: .immediate)
}
private func updateSelectionState() {
@ -358,19 +382,22 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
func updatePresentationData(_ presentationData: PresentationData) {
self.presentationData = presentationData
self.backgroundColor = presentationData.theme.list.plainBackgroundColor
self.backgroundNode.backgroundColor = self.presentationData.theme.list.plainBackgroundColor
self.backgroundNode.updateColor(color: self.presentationData.theme.rootController.tabBar.backgroundColor, transition: .immediate)
}
private var currentDisplayMode: DisplayMode = .all
func updateMode(_ displayMode: DisplayMode) {
let updated = self.currentDisplayMode != displayMode
self.currentDisplayMode = displayMode
if case .selected = displayMode, self.selectionNode == nil, let controller = self.controller {
let selectionNode = MediaPickerSelectedListNode(context: controller.context)
selectionNode.layer.allowsGroupOpacity = true
selectionNode.alpha = 0.0
selectionNode.isUserInteractionEnabled = false
selectionNode.interaction = self.controller?.interaction
self.insertSubnode(selectionNode, aboveSubnode: self.gridNode)
self.containerNode.insertSubnode(selectionNode, aboveSubnode: self.gridNode)
self.selectionNode = selectionNode
if let (layout, navigationBarHeight) = self.validLayout {
@ -381,22 +408,42 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
self.gridNode.isUserInteractionEnabled = displayMode == .all
if let selectionNode = self.selectionNode {
transition.updateAlpha(node: selectionNode, alpha: displayMode == .selected ? 1.0 : 0.0)
selectionNode.isUserInteractionEnabled = displayMode == .selected
var completion: () -> Void = {}
if updated && displayMode == .all {
completion = {
self.updateNavigation(transition: .animated(duration: 0.1, curve: .easeInOut))
}
}
if let selectionNode = self.selectionNode {
transition.updateAlpha(node: selectionNode, alpha: displayMode == .selected ? 1.0 : 0.0, completion: { _ in
completion()
})
selectionNode.isUserInteractionEnabled = displayMode == .selected
}
if updated && displayMode == .selected {
self.updateNavigation(transition: .immediate)
}
}
var openingMedia = false
fileprivate func openMedia(fetchResult: PHFetchResult<PHAsset>, index: Int, immediateThumbnail: UIImage?) {
guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout else {
guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout, !self.openingMedia else {
return
}
Queue.mainQueue().justDispatch {
self.dismissInput()
}
var hasTimer = false
if controller.chatLocation?.peerId != controller.context.account.peerId && controller.chatLocation?.peerId.namespace == Namespaces.Peer.CloudUser {
hasTimer = true
}
self.openingMedia = true
let index = fetchResult.count - index - 1
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: true, updateHiddenMedia: { [weak self] id in
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .fetchResult(fetchResult: fetchResult, index: index), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, initialLayout: layout, transitionHostView: { [weak self] in
return self?.gridNode.view
@ -408,18 +455,26 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
}, finishedTransitionIn: {
self.openingMedia = false
})
}
fileprivate func openSelectedMedia(item: TGMediaSelectableItem, immediateThumbnail: UIImage?) {
guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout else {
guard let controller = self.controller, let interaction = controller.interaction, let (layout, _) = self.validLayout, !self.openingMedia else {
return
}
Queue.mainQueue().justDispatch {
self.dismissInput()
}
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: true, updateHiddenMedia: { [weak self] id in
var hasTimer = false
if controller.chatLocation?.peerId != controller.context.account.peerId && controller.chatLocation?.peerId.namespace == Namespaces.Peer.CloudUser {
hasTimer = true
}
self.openingMedia = true
presentLegacyMediaPickerGallery(context: controller.context, peer: controller.peer, chatLocation: controller.chatLocation, presentationData: self.presentationData, source: .selection(item: item), immediateThumbnail: immediateThumbnail, selectionContext: interaction.selectionState, editingContext: interaction.editingState, hasSilentPosting: true, hasSchedule: true, hasTimer: hasTimer, updateHiddenMedia: { [weak self] id in
self?.hiddenMediaId.set(.single(id))
}, initialLayout: layout, transitionHostView: { [weak self] in
return self?.selectionNode?.view
@ -430,11 +485,24 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
strongSelf.controller?.interaction?.sendSelected(result, silently, scheduleTime, false)
}
}, presentStickers: controller.presentStickers, presentSchedulePicker: controller.presentSchedulePicker, presentTimerPicker: controller.presentTimerPicker, getCaptionPanelView: controller.getCaptionPanelView, present: { [weak self] c, a in
self?.controller?.present(c, in: .window(.root), with: a)
self?.controller?.present(c, in: .window(.root), with: a, blockInteraction: true)
}, finishedTransitionIn: {
self.openingMedia = false
})
}
fileprivate func send(asFile: Bool = false, silently: Bool, scheduleTime: Int32?, animated: Bool) {
var hasHeic = false
let allItems = self.controller?.interaction?.selectionState?.selectedItems() ?? []
for item in allItems {
if item is TGCameraCapturedVideo {
} else if let asset = item as? TGMediaAsset, asset.uniformTypeIdentifier.contains("heic") {
hasHeic = true
break
}
}
let proceed: (Bool) -> Void = { convertToJpeg in
guard let signals = TGMediaAssetsController.resultSignals(for: self.controller?.interaction?.selectionState, editingContext: self.controller?.interaction?.editingState, intent: asFile ? TGMediaAssetsControllerSendFileIntent : TGMediaAssetsControllerSendMediaIntent, currentItem: nil, storeAssets: true, convertToJpeg: false, descriptionGenerator: legacyAssetPickerItemGenerator(), saveEditedPhotos: true) else {
return
}
@ -442,6 +510,17 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.controller?.dismiss(animated: animated)
}
if asFile && hasHeic {
self.controller?.present(standardTextAlertController(theme: AlertControllerTheme(presentationData: self.presentationData), title: nil, text: self.presentationData.strings.MediaPicker_JpegConversionText, actions: [TextAlertAction(type: .defaultAction, title: self.presentationData.strings.MediaPicker_KeepHeic, action: {
proceed(false)
}), TextAlertAction(type: .genericAction, title: self.presentationData.strings.MediaPicker_ConvertToJpeg, action: {
proceed(true)
})], actionLayout: .vertical), in: .window(.root))
} else {
proceed(false)
}
}
private func openLimitedMediaOptions() {
let presentationData = self.presentationData
let controller = ActionSheetController(presentationData: self.presentationData)
@ -504,6 +583,42 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
}
private var previousContentOffset: GridNodeVisibleContentOffset?
func updateNavigation(transition: ContainedViewLayoutTransition) {
if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 {
self.controller?.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
self.controller?.updateTabBarAlpha(1.0, transition)
} else if self.placeholderNode != nil {
self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: .immediate)
self.controller?.updateTabBarAlpha(0.0, transition)
} else {
var previousContentOffsetValue: CGFloat?
if let previousContentOffset = self.previousContentOffset, case let .known(value) = previousContentOffset {
previousContentOffsetValue = value
}
let offset = self.gridNode.visibleContentOffset()
switch offset {
case let .known(value):
let transition: ContainedViewLayoutTransition
if let previousContentOffsetValue = previousContentOffsetValue, value <= 0.0, previousContentOffsetValue > 2.0 {
transition = .animated(duration: 0.2, curve: .easeInOut)
} else {
transition = .immediate
}
self.controller?.navigationBar?.updateBackgroundAlpha(min(2.0, value) / 2.0, transition: transition)
case .unknown, .none:
self.controller?.navigationBar?.updateBackgroundAlpha(1.0, transition: .immediate)
}
}
let count = Int32(self.controller?.interaction?.selectionState?.count() ?? 0)
if count > 0 {
self.controller?.updateTabBarAlpha(1.0, transition)
}
}
func containerLayoutUpdated(_ layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
let firstTime = self.validLayout == nil
self.validLayout = (layout, navigationBarHeight)
@ -511,10 +626,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
var insets = layout.insets(options: [])
insets.top += navigationBarHeight
let overflowInset: CGFloat = 70.0
let bounds = CGRect(origin: CGPoint(), size: CGSize(width: layout.size.width, height: layout.size.height))
let innerBounds = CGRect(origin: CGPoint(x: -overflowInset, y: 0.0), size: CGSize(width: layout.size.width, height: layout.size.height))
let itemsPerRow: Int
if case .compact = layout.metrics.widthClass {
self._ready.set(.single(true))
switch layout.orientation {
case .portrait:
itemsPerRow = 3
@ -529,6 +647,9 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let itemWidth = floorToScreenPixels((width - itemSpacing * CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
var cameraRect: CGRect? = CGRect(origin: CGPoint(x: layout.safeInsets.left, y: 0.0), size: CGSize(width: itemWidth, height: itemWidth * 2.0 + 1.0))
if self.cameraView == nil {
cameraRect = nil
}
var manageHeight: CGFloat = 0.0
if case let .assets(_, _, mediaAccess, cameraAccess) = self.state {
@ -536,8 +657,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
cameraRect = nil
}
if let (untilDate, personal) = self.controller?.bannedSendMedia {
self.gridNode.alpha = 0.3
self.gridNode.isUserInteractionEnabled = false
self.gridNode.isHidden = true
let banDescription: String
if untilDate != 0 && untilDate != Int32.max {
@ -548,24 +668,21 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
banDescription = self.presentationData.strings.Conversation_DefaultRestrictedMedia
}
let bannedTextNode: ImmediateTextNode
if let current = self.bannedTextNode {
bannedTextNode = current
var placeholderTransition = transition
let placeholderNode: MediaPickerPlaceholderNode
if let current = self.placeholderNode {
placeholderNode = current
} else {
bannedTextNode = ImmediateTextNode()
bannedTextNode.maximumNumberOfLines = 0
bannedTextNode.textAlignment = .center
self.bannedTextNode = bannedTextNode
self.addSubnode(bannedTextNode)
placeholderNode = MediaPickerPlaceholderNode(content: .bannedSendMedia(banDescription))
self.containerNode.insertSubnode(placeholderNode, aboveSubnode: self.gridNode)
self.placeholderNode = placeholderNode
placeholderTransition = .immediate
}
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: false, transition: placeholderTransition)
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds)
bannedTextNode.attributedText = NSAttributedString(string: banDescription, font: Font.regular(15.0), textColor: self.presentationData.theme.list.freeTextColor, paragraphAlignment: .center)
let bannedTextSize = bannedTextNode.updateLayout(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 16.0, height: layout.size.height))
manageHeight = max(44.0, bannedTextSize.height + 20.0)
transition.updateFrame(node: bannedTextNode, frame: CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - bannedTextSize.width) / 2.0), y: insets.top + floorToScreenPixels((manageHeight - bannedTextSize.height) / 2.0) - 4.0), size: bannedTextSize))
self.updateNavigation(transition: .immediate)
} else if case .notDetermined = mediaAccess {
} else {
@ -612,11 +729,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
let cleanGridInsets = UIEdgeInsets(top: insets.top, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
let gridInsets = UIEdgeInsets(top: insets.top + manageHeight, left: layout.safeInsets.left, bottom: layout.intrinsicInsets.bottom, right: layout.safeInsets.right)
transition.updateFrame(node: self.gridNode, frame: bounds)
transition.updateFrame(node: self.gridNode, frame: innerBounds)
transition.updateFrame(node: self.backgroundNode, frame: bounds)
transition.updateFrame(node: self.backgroundNode, frame: innerBounds)
self.backgroundNode.update(size: bounds.size, transition: transition)
transition.updateFrame(node: self.containerNode, frame: CGRect(origin: CGPoint(x: overflowInset, y: 0.0), size: CGSize(width: bounds.width - overflowInset * 2.0, height: bounds.height)))
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemWidth, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemWidth), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in
guard let strongSelf = self else {
return
@ -645,7 +764,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
} else {
updateSelectionNode()
}
transition.updateFrame(node: selectionNode, frame: bounds)
transition.updateFrame(node: selectionNode, frame: innerBounds)
}
if let cameraView = self.cameraView {
@ -669,24 +788,26 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if let current = self.placeholderNode {
placeholderNode = current
} else {
placeholderNode = MediaPickerPlaceholderNode()
placeholderNode = MediaPickerPlaceholderNode(content: .intro)
placeholderNode.settingsPressed = { [weak self] in
self?.controller?.context.sharedContext.applicationBindings.openSettings()
}
placeholderNode.cameraPressed = { [weak self] in
self?.controller?.openCamera?(nil)
}
self.insertSubnode(placeholderNode, aboveSubnode: self.gridNode)
self.containerNode.insertSubnode(placeholderNode, aboveSubnode: self.gridNode)
self.placeholderNode = placeholderNode
if transition.isAnimated {
placeholderNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
}
placeholderTransition = .immediate
self.updateNavigation(transition: .immediate)
}
placeholderNode.update(layout: layout, theme: self.presentationData.theme, strings: self.presentationData.strings, hasCamera: cameraAccess == .authorized, transition: placeholderTransition)
placeholderTransition.updateFrame(node: placeholderNode, frame: bounds)
} else if let placeholderNode = self.placeholderNode {
placeholderTransition.updateFrame(node: placeholderNode, frame: innerBounds)
} else if let placeholderNode = self.placeholderNode, self.controller?.bannedSendMedia == nil {
self.placeholderNode = nil
placeholderNode.removeFromSupernode()
}
@ -716,19 +837,23 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
private let groupedPromise = ValuePromise<Bool>(true)
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?) {
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, bannedSendMedia: (Int32, Bool)?, collection: PHAssetCollection? = nil, editingContext: TGMediaEditingContext? = nil, selectionContext: TGMediaSelectionContext? = nil) {
self.context = context
self.presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
let presentationData = updatedPresentationData?.initial ?? context.sharedContext.currentPresentationData.with { $0 }
self.presentationData = presentationData
self.updatedPresentationData = updatedPresentationData
self.peer = peer
self.chatLocation = chatLocation
self.bannedSendMedia = bannedSendMedia
self.collection = collection
self.titleView = MediaPickerTitleView(theme: self.presentationData.theme, segments: [self.presentationData.strings.Attachment_AllMedia, self.presentationData.strings.Attachment_SelectedMedia(1)], selectedIndex: 0)
self.titleView.title = self.presentationData.strings.Attachment_Gallery
self.titleView.title = collection?.localizedTitle ?? presentationData.strings.Attachment_Gallery
self.moreButtonNode = MediaPickerMoreButtonNode(theme: self.presentationData.theme)
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData, hideBackground: false, hideBadge: false, hideSeparator: true))
super.init(navigationBarPresentationData: NavigationBarPresentationData(presentationData: presentationData))
self.statusBar.statusBarStyle = .Ignore
@ -753,8 +878,15 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
}
self.navigationItem.titleView = self.titleView
if collection == nil {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(title: self.presentationData.strings.Common_Cancel, style: .plain, target: self, action: #selector(self.cancelPressed))
self.navigationItem.rightBarButtonItem = UIBarButtonItem(customDisplayNode: self.moreButtonNode)
self.navigationItem.rightBarButtonItem?.action = #selector(self.rightButtonPressed)
self.navigationItem.rightBarButtonItem?.target = self
} else {
self.navigationItem.leftBarButtonItem = UIBarButtonItem(backButtonAppearanceWithTitle: self.presentationData.strings.Common_Back, target: self, action: #selector(self.backPressed))
}
self.moreButtonNode.action = { [weak self] _, gesture in
if let strongSelf = self {
@ -807,7 +939,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
if let strongSelf = self {
strongSelf.controllerNode.dismissInput()
}
}, selectionState: TGMediaSelectionContext(), editingState: TGMediaEditingContext())
}, selectionState: selectionContext ?? TGMediaSelectionContext(), editingState: editingContext ?? TGMediaEditingContext())
self.interaction?.selectionState?.grouping = true
}
@ -853,10 +985,20 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
self.controllerNode.updatePresentationData(self.presentationData)
}
@objc private func backPressed() {
self.updateNavigationStack { current in
return current.filter { $0 !== self }
}
}
@objc private func cancelPressed() {
self.dismiss()
}
@objc private func rightButtonPressed() {
self.moreButtonNode.action?(self.moreButtonNode.contextSourceNode, nil)
}
public func resetForReuse() {
if let webSearchController = self.webSearchController {
self.webSearchController = nil
@ -869,13 +1011,36 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable {
public func prepareForReuse() {
self.controllerNode.cameraView?.resumePreview()
Queue.mainQueue().after(0.2, {
self.controllerNode.updateNavigation(transition: .animated(duration: 0.15, curve: .easeInOut))
})
}
@objc private func searchOrMorePressed(node: ContextReferenceContentNode, gesture: ContextGesture?) {
switch self.moreButtonNode.iconNode.iconState {
case .search:
self.requestAttachmentMenuExpansion()
self.presentWebSearch()
self.presentWebSearch(MediaGroupsScreen(context: self.context, updatedPresentationData: self.updatedPresentationData, mediaAssetsContext: self.controllerNode.mediaAssetsContext, openGroup: { [weak self] collection in
if let strongSelf = self {
if let webSearchController = strongSelf.webSearchController {
strongSelf.webSearchController = nil
if collection.assetCollectionSubtype != .smartAlbumUserLibrary {
Queue.mainQueue().after(0.5) {
webSearchController.cancel()
}
} else {
webSearchController.cancel()
}
}
if collection.assetCollectionSubtype != .smartAlbumUserLibrary {
let mediaPicker = MediaPickerScreen(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, peer: strongSelf.peer, chatLocation: strongSelf.chatLocation, bannedSendMedia: strongSelf.bannedSendMedia, collection: collection, editingContext: strongSelf.interaction?.editingState, selectionContext: strongSelf.interaction?.selectionState)
mediaPicker._presentedInModal = true
mediaPicker.updateNavigationStack = strongSelf.updateNavigationStack
strongSelf.updateNavigationStack({ _ in return [strongSelf, mediaPicker]})
}
}
}))
case .more:
let strings = self.presentationData.strings
let selectionCount = self.selectionCount

View File

@ -652,7 +652,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
private let moved: (CGPoint) -> Void
private var initialLocation: CGPoint?
private var longTapTimer: SwiftSignalKit.Timer?
private var longPressTimer: SwiftSignalKit.Timer?
private var itemNode: MediaPickerSelectedItemNode?
@ -668,28 +667,12 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
}
deinit {
self.longTapTimer?.invalidate()
self.longPressTimer?.invalidate()
}
private func startLongTapTimer() {
self.longTapTimer?.invalidate()
let longTapTimer = SwiftSignalKit.Timer(timeout: 0.25, repeat: false, completion: { [weak self] in
self?.longTapTimerFired()
}, queue: Queue.mainQueue())
self.longTapTimer = longTapTimer
longTapTimer.start()
}
private func stopLongTapTimer() {
self.itemNode = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
}
private func startLongPressTimer() {
self.longPressTimer?.invalidate()
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.6, repeat: false, completion: { [weak self] in
let longPressTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in
self?.longPressTimerFired()
}, queue: Queue.mainQueue())
self.longPressTimer = longPressTimer
@ -706,21 +689,10 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
super.reset()
self.itemNode = nil
self.stopLongTapTimer()
self.stopLongPressTimer()
self.initialLocation = nil
}
private func longTapTimerFired() {
guard let location = self.initialLocation else {
return
}
self.longTapTimer?.invalidate()
self.longTapTimer = nil
self.willBegin(location)
}
private func longPressTimerFired() {
guard let _ = self.initialLocation else {
@ -730,13 +702,12 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
self.state = .began
self.longPressTimer?.invalidate()
self.longPressTimer = nil
self.longTapTimer?.invalidate()
self.longTapTimer = nil
if let itemNode = self.itemNode {
self.began(itemNode)
}
}
private var currentItemNode: ASDisplayNode?
override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
super.touchesBegan(touches, with: event)
@ -750,10 +721,12 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
if let location = touches.first?.location(in: self.view) {
let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location)
if allowed {
if let itemNode = itemNode {
itemNode.layer.animateScale(from: 1.0, to: 0.98, duration: 0.2, delay: 0.1)
}
self.itemNode = itemNode
self.initialLocation = location
if requiresLongPress {
self.startLongTapTimer()
self.startLongPressTimer()
} else {
self.state = .began
@ -775,7 +748,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.state = .failed
@ -795,7 +767,6 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
self.initialLocation = nil
self.stopLongTapTimer()
if self.longPressTimer != nil {
self.stopLongPressTimer()
self.state = .failed
@ -818,7 +789,8 @@ private class ReorderingGestureRecognizer: UIGestureRecognizer {
let dY = touchLocation.y - initialTapLocation.y
if dX * dX + dY * dY > 3.0 * 3.0 {
self.stopLongTapTimer()
self.itemNode?.layer.removeAllAnimations()
self.stopLongPressTimer()
self.initialLocation = nil
self.state = .failed
@ -907,8 +879,8 @@ private final class ReorderingItemNode: ASDisplayNode {
self.copyView.shadow.frame = CGRect(origin: CGPoint(x: -30.0, y: -30.0), size: CGSize(width: itemNode.bounds.size.width + 60.0, height: itemNode.bounds.size.height + 60.0))
self.copyView.shadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
self.copyView.snapshotView?.layer.animateScale(from: 1.0, to: 1.05, duration: 0.25, removeOnCompletion: false)
self.copyView.shadow.layer.animateScale(from: 1.0, to: 1.05, duration: 0.25, removeOnCompletion: false)
self.copyView.snapshotView?.layer.animateScale(from: 1.0, to: 1.05, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
self.copyView.shadow.layer.animateScale(from: 1.0, to: 1.05, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
}
func updateOffset(offset: CGPoint) {

View File

@ -1322,7 +1322,7 @@ private func addContactToExisting(context: AccountContext, parentController: Vie
(parentController.navigationController as? NavigationController)?.pushViewController(contactsController)
let _ = (contactsController.result
|> deliverOnMainQueue).start(next: { result in
if let (peers, _, _, _) = result, let peer = peers.first {
if let (peers, _, _, _, _) = result, let peer = peers.first {
let dataSignal: Signal<(Peer?, DeviceContactStableId?), NoError>
switch peer {
case let .peer(contact, _, _):

View File

@ -713,6 +713,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
public var textReturned: ((String) -> Void)?
public var clearPrefix: (() -> Void)?
public var clearTokens: (() -> Void)?
public var focusUpdated: ((Bool) -> Void)?
public var tokensUpdated: (([SearchBarToken]) -> Void)?
@ -1106,6 +1107,10 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
self.cancelButton.layer.animatePosition(from: self.cancelButton.layer.position, to: CGPoint(x: self.bounds.size.width + cancelButtonFrame.size.width / 2.0, y: targetTextBackgroundFrame.midY), duration: duration, timingFunction: timingFunction, removeOnCompletion: false)
}
public func textFieldDidBeginEditing(_ textField: UITextField) {
self.focusUpdated?(true)
}
public func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let _ = self.textField.selectedTokenIndex {
if !string.isEmpty {
@ -1137,6 +1142,7 @@ public class SearchBarNode: ASDisplayNode, UITextFieldDelegate {
}
public func textFieldDidEndEditing(_ textField: UITextField) {
self.focusUpdated?(false)
self.textField.selectedTokenIndex = nil
}

View File

@ -53,7 +53,9 @@ private class MediaHeaderItemNode: ASDisplayNode {
let titleText: String = author.flatMap(EnginePeer.init)?.displayTitle(strings: strings, displayOrder: nameDisplayOrder) ?? ""
let subtitleText: String
if let peer = peer {
if peer is TelegramGroup || peer is TelegramChannel {
if let peer = peer as? TelegramChannel, case .broadcast = peer.info {
subtitleText = strings.MusicPlayer_VoiceNote
} else if peer is TelegramGroup || peer is TelegramChannel {
subtitleText = EnginePeer(peer).displayTitle(strings: strings, displayOrder: nameDisplayOrder)
} else {
subtitleText = strings.MusicPlayer_VoiceNote

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_animated.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,173 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.629883 6.766602 cm
0.000000 0.000000 0.000000 scn
11.547852 0.000000 m
11.988281 0.000000 12.342773 0.322266 12.493164 0.623047 c
6.724609 10.581055 l
6.638672 10.731445 6.574219 10.871094 6.574219 10.989258 c
6.574219 11.107422 6.638672 11.247070 6.724609 11.397461 c
12.493164 21.355469 l
12.342773 21.656250 11.988281 21.978516 11.547852 21.978516 c
11.214844 21.978516 10.838867 21.785156 10.602539 21.387695 c
5.156250 12.160156 l
4.876953 11.708984 4.694336 11.386719 4.694336 10.989258 c
4.694336 10.602539 4.876953 10.269531 5.156250 9.818359 c
10.613281 0.590820 l
10.860352 0.182617 11.214844 0.000000 11.547852 0.000000 c
h
16.918945 0.010742 m
17.509766 0.010742 17.896484 0.354492 18.401367 1.213867 c
23.299805 9.625000 l
23.600586 10.140625 23.740234 10.559570 23.740234 10.989258 c
23.740234 11.429688 23.600586 11.837891 23.299805 12.353516 c
18.401367 20.764648 l
17.896484 21.624023 17.509766 21.967773 16.918945 21.967773 c
16.328125 21.967773 15.941406 21.624023 15.425781 20.764648 c
10.538086 12.353516 l
10.226562 11.837891 10.097656 11.429688 10.097656 10.989258 c
10.097656 10.559570 10.237305 10.140625 10.538086 9.625000 c
15.425781 1.213867 l
15.941406 0.354492 16.328125 0.010742 16.918945 0.010742 c
h
6.294922 20.356445 m
6.681641 20.356445 6.993164 20.667969 6.993164 21.054688 c
6.993164 21.452148 6.681641 21.763672 6.294922 21.763672 c
5.897461 21.763672 5.585938 21.452148 5.585938 21.054688 c
5.585938 20.667969 5.897461 20.356445 6.294922 20.356445 c
h
5.177734 18.369141 m
5.575195 18.369141 5.886719 18.680664 5.886719 19.067383 c
5.886719 19.464844 5.575195 19.776367 5.177734 19.776367 c
4.791016 19.776367 4.479492 19.464844 4.479492 19.067383 c
4.479492 18.680664 4.791016 18.369141 5.177734 18.369141 c
h
16.918945 2.255859 m
16.865234 2.255859 16.833008 2.288086 16.811523 2.341797 c
12.127930 10.516602 l
12.020508 10.688477 11.966797 10.838867 11.966797 10.989258 c
11.966797 11.139648 12.020508 11.290039 12.117188 11.461914 c
16.811523 19.647461 l
16.833008 19.690430 16.865234 19.722656 16.918945 19.722656 c
16.972656 19.722656 16.994141 19.690430 17.026367 19.647461 c
21.709961 11.461914 l
21.817383 11.290039 21.860352 11.139648 21.860352 10.989258 c
21.860352 10.838867 21.817383 10.688477 21.709961 10.516602 c
17.026367 2.341797 l
16.994141 2.288086 16.972656 2.255859 16.918945 2.255859 c
h
4.060547 16.360352 m
4.458008 16.360352 4.769531 16.682617 4.769531 17.069336 c
4.769531 17.456055 4.458008 17.778320 4.060547 17.778320 c
3.673828 17.778320 3.362305 17.456055 3.362305 17.069336 c
3.362305 16.682617 3.673828 16.360352 4.060547 16.360352 c
h
2.932617 14.383789 m
3.330078 14.383789 3.641602 14.695312 3.641602 15.092773 c
3.641602 15.479492 3.330078 15.791016 2.932617 15.791016 c
2.545898 15.791016 2.234375 15.479492 2.234375 15.092773 c
2.234375 14.695312 2.545898 14.383789 2.932617 14.383789 c
h
1.826172 12.385742 m
2.212891 12.385742 2.524414 12.697266 2.524414 13.083984 c
2.524414 13.481445 2.212891 13.792969 1.826172 13.792969 c
1.439453 13.792969 1.117188 13.481445 1.117188 13.083984 c
1.117188 12.697266 1.439453 12.385742 1.826172 12.385742 c
h
0.708984 10.398438 m
1.095703 10.398438 1.407227 10.709961 1.407227 11.096680 c
1.407227 11.494141 1.095703 11.805664 0.708984 11.805664 c
0.311523 11.805664 0.000000 11.494141 0.000000 11.096680 c
0.000000 10.709961 0.311523 10.398438 0.708984 10.398438 c
h
1.826172 8.411133 m
2.212891 8.411133 2.524414 8.722656 2.524414 9.109375 c
2.524414 9.506836 2.212891 9.818359 1.826172 9.818359 c
1.439453 9.818359 1.117188 9.506836 1.117188 9.109375 c
1.117188 8.722656 1.439453 8.411133 1.826172 8.411133 c
h
2.932617 6.402344 m
3.330078 6.402344 3.641602 6.724609 3.641602 7.111328 c
3.641602 7.498047 3.330078 7.820312 2.932617 7.820312 c
2.545898 7.820312 2.234375 7.498047 2.234375 7.111328 c
2.234375 6.724609 2.545898 6.402344 2.932617 6.402344 c
h
4.060547 4.425781 m
4.458008 4.425781 4.769531 4.748047 4.769531 5.134766 c
4.769531 5.521484 4.458008 5.843750 4.060547 5.843750 c
3.673828 5.843750 3.362305 5.521484 3.362305 5.134766 c
3.362305 4.748047 3.673828 4.425781 4.060547 4.425781 c
h
5.177734 2.438477 m
5.575195 2.438477 5.886719 2.750000 5.886719 3.147461 c
5.886719 3.534180 5.575195 3.845703 5.177734 3.845703 c
4.791016 3.845703 4.479492 3.534180 4.479492 3.147461 c
4.479492 2.750000 4.791016 2.438477 5.177734 2.438477 c
h
6.294922 0.440430 m
6.681641 0.440430 6.993164 0.751953 6.993164 1.149414 c
6.993164 1.546875 6.681641 1.858398 6.294922 1.858398 c
5.897461 1.858398 5.585938 1.546875 5.585938 1.149414 c
5.585938 0.751953 5.897461 0.440430 6.294922 0.440430 c
h
f*
n
Q
endstream
endobj
3 0 obj
4693
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000004783 00000 n
0000004806 00000 n
0000004979 00000 n
0000005053 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
5112
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_bursts.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 7.929688 5.337891 cm
0.000000 0.000000 0.000000 scn
1.106445 5.102539 m
1.299805 5.102539 1.525391 5.166992 1.740234 5.317383 c
1.740234 16.811523 l
1.740234 17.198242 1.815430 17.348633 2.148438 17.531250 c
12.106445 23.267578 l
12.149414 23.976562 11.687500 24.416992 11.085938 24.416992 c
10.806641 24.416992 10.505859 24.341797 10.183594 24.148438 c
1.170898 18.970703 l
0.118164 18.358398 0.000000 18.165039 0.000000 16.940430 c
0.000000 6.660156 l
0.000000 5.736328 0.440430 5.102539 1.106445 5.102539 c
h
5.392578 2.621094 m
5.596680 2.621094 5.811523 2.685547 6.026367 2.835938 c
6.026367 14.330078 l
6.026367 14.749023 6.090820 14.856445 6.445312 15.049805 c
16.403320 20.796875 l
16.435547 21.495117 15.995117 21.946289 15.361328 21.946289 c
15.092773 21.946289 14.791992 21.860352 14.469727 21.688477 c
5.457031 16.500000 l
4.404297 15.898438 4.275391 15.672852 4.275391 14.469727 c
4.275391 4.178711 l
4.275391 3.254883 4.748047 2.621094 5.392578 2.621094 c
h
10.000977 0.000000 m
10.376953 0.000000 10.828125 0.150391 11.365234 0.451172 c
19.787109 5.285156 l
20.775391 5.854492 21.129883 6.445312 21.129883 7.648438 c
21.129883 17.380859 l
21.129883 18.669922 20.678711 19.325195 19.873047 19.325195 c
19.507812 19.325195 19.067383 19.185547 18.562500 18.895508 c
10.119141 14.029297 l
9.109375 13.438477 8.754883 12.836914 8.754883 11.666016 c
8.754883 1.933594 l
8.754883 0.687500 9.184570 0.000000 10.000977 0.000000 c
h
10.581055 2.094727 m
10.516602 2.105469 10.505859 2.137695 10.505859 2.212891 c
10.581055 11.633789 l
10.581055 12.041992 10.699219 12.256836 11.053711 12.460938 c
19.185547 17.241211 l
19.228516 17.262695 19.260742 17.262695 19.292969 17.251953 c
19.346680 17.241211 19.368164 17.208984 19.368164 17.133789 c
19.335938 7.680664 l
19.335938 7.326172 19.217773 7.057617 18.863281 6.842773 c
10.688477 2.105469 l
10.645508 2.083984 10.613281 2.083984 10.581055 2.094727 c
h
f*
n
Q
endstream
endobj
3 0 obj
1984
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002074 00000 n
0000002097 00000 n
0000002270 00000 n
0000002344 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2403
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_cinematic.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,485 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.592773 8.925781 cm
0.000000 0.000000 0.000000 scn
4.189453 0.000000 m
15.415039 0.000000 l
18.036133 0.000000 19.604492 1.525391 19.604492 4.146484 c
19.604492 5.618164 l
23.665039 2.180664 l
24.094727 1.826172 24.567383 1.579102 25.007812 1.579102 c
25.953125 1.579102 26.576172 2.277344 26.576172 3.276367 c
26.576172 14.383789 l
26.576172 15.382812 25.953125 16.081055 25.007812 16.081055 c
24.567383 16.081055 24.094727 15.833984 23.665039 15.479492 c
19.604492 12.041992 l
19.604492 13.524414 l
19.604492 16.134766 18.036133 17.660156 15.415039 17.660156 c
4.189453 17.660156 l
1.686523 17.660156 0.000000 16.134766 0.000000 13.524414 c
0.000000 4.146484 l
0.000000 1.525391 1.568359 0.000000 4.189453 0.000000 c
h
4.490234 1.622070 m
2.728516 1.622070 1.729492 2.535156 1.729492 4.393555 c
1.729492 13.266602 l
1.729492 15.135742 2.728516 16.048828 4.490234 16.048828 c
15.114258 16.048828 l
16.865234 16.048828 17.875000 15.135742 17.875000 13.266602 c
17.875000 4.393555 l
17.875000 2.535156 16.865234 1.622070 15.114258 1.622070 c
4.490234 1.622070 l
h
24.470703 3.641602 m
19.604492 7.659180 l
19.604492 10.000977 l
24.470703 14.018555 l
24.567383 14.093750 24.631836 14.147461 24.728516 14.147461 c
24.857422 14.147461 24.911133 14.040039 24.911133 13.889648 c
24.911133 3.770508 l
24.911133 3.620117 24.857422 3.523438 24.728516 3.523438 c
24.631836 3.523438 24.567383 3.577148 24.470703 3.641602 c
h
f*
n
Q
q
q
1.000000 0.000000 -0.000000 1.000000 10.000000 11.225647 cm
0.000000 0.000000 0.000000 scn
0.450664 7.941575 m
0.693185 7.941575 0.952624 8.012091 1.149094 8.213968 c
1.343381 8.413603 1.406961 8.670843 1.406961 8.903505 c
0.406961 8.903505 l
0.406961 8.874201 0.402920 8.864758 0.404416 8.869144 c
0.406454 8.875122 0.414068 8.892520 0.432455 8.911413 c
0.451071 8.930541 0.469642 8.939795 0.478687 8.943048 c
0.485932 8.945654 0.478955 8.941575 0.450664 8.941575 c
0.450664 7.941575 l
h
1.406961 8.903505 m
1.406961 10.232964 l
0.406961 10.232964 l
0.406961 8.903505 l
1.406961 8.903505 l
h
1.406961 10.232964 m
1.406961 10.441644 1.461145 10.516622 1.485847 10.541323 c
1.510620 10.566097 1.582848 10.617392 1.780123 10.617392 c
1.780123 11.617392 l
1.414067 11.617392 1.049714 11.519404 0.778740 11.248430 c
0.507693 10.977384 0.406961 10.610147 0.406961 10.232964 c
1.406961 10.232964 l
h
1.780123 10.617392 m
3.143381 10.617392 l
3.143381 11.617392 l
1.780123 11.617392 l
1.780123 10.617392 l
h
3.143381 10.617392 m
3.381150 10.617392 3.639620 10.683693 3.838258 10.880542 c
4.037554 11.078042 4.105311 11.336237 4.105311 11.573689 c
3.105311 11.573689 l
3.105311 11.546071 3.101378 11.538866 3.103720 11.545491 c
3.106672 11.553840 3.115477 11.572130 3.134358 11.590840 c
3.153171 11.609484 3.170862 11.617524 3.177536 11.619841 c
3.182541 11.621578 3.173327 11.617392 3.143381 11.617392 c
3.143381 10.617392 l
h
4.105311 11.573689 m
4.105311 11.811355 4.037323 12.070365 3.834985 12.267233 c
3.634857 12.461953 3.376599 12.524353 3.143381 12.524353 c
3.143381 11.524353 l
3.175038 11.524353 3.185540 11.520061 3.181437 11.521447 c
3.175622 11.523413 3.157468 11.531206 3.137630 11.550508 c
3.117559 11.570037 3.107757 11.589691 3.104216 11.599611 c
3.101361 11.607611 3.105311 11.601369 3.105311 11.573689 c
4.105311 11.573689 l
h
3.143381 12.524353 m
1.768856 12.524353 l
1.768856 11.524353 l
3.143381 11.524353 l
3.143381 12.524353 l
h
1.768856 12.524353 m
1.104903 12.524353 0.514360 12.360654 0.092355 11.945700 c
-0.331199 11.529222 -0.500000 10.943424 -0.500000 10.283664 c
0.500000 10.283664 l
0.500000 10.784363 0.626947 11.068910 0.793481 11.232662 c
0.961564 11.397937 1.255449 11.524353 1.768856 11.524353 c
1.768856 12.524353 l
h
-0.500000 10.283664 m
-0.500000 8.903505 l
0.500000 8.903505 l
0.500000 10.283664 l
-0.500000 10.283664 l
h
-0.500000 8.903505 m
-0.500000 8.666346 -0.432617 8.411142 -0.240725 8.213968 c
-0.046923 8.014833 0.208894 7.941575 0.450664 7.941575 c
0.450664 8.941575 l
0.427263 8.941575 0.422817 8.945058 0.431106 8.942020 c
0.440899 8.938431 0.458558 8.929247 0.475915 8.911413 c
0.493081 8.893774 0.500330 8.877576 0.502352 8.871754 c
0.503901 8.867291 0.500000 8.876007 0.500000 8.903505 c
-0.500000 8.903505 l
h
10.573703 7.941575 m
10.816223 7.941575 11.075663 8.012091 11.272133 8.213969 c
11.466420 8.413603 11.530000 8.670844 11.530000 8.903505 c
10.530000 8.903505 l
10.530000 8.874201 10.525958 8.864758 10.527454 8.869144 c
10.529492 8.875122 10.537107 8.892520 10.555493 8.911412 c
10.574109 8.930541 10.592681 8.939795 10.601726 8.943048 c
10.608971 8.945654 10.601994 8.941575 10.573703 8.941575 c
10.573703 7.941575 l
h
11.530000 8.903505 m
11.530000 10.283664 l
10.530000 10.283664 l
10.530000 8.903505 l
11.530000 8.903505 l
h
11.530000 10.283664 m
11.530000 10.943424 11.361198 11.529222 10.937644 11.945700 c
10.515639 12.360654 9.925097 12.524353 9.261144 12.524353 c
9.261144 11.524353 l
9.774550 11.524353 10.068436 11.397937 10.236519 11.232662 c
10.403052 11.068910 10.530000 10.784363 10.530000 10.283664 c
11.530000 10.283664 l
h
9.261144 12.524353 m
7.880985 12.524353 l
7.880985 11.524353 l
9.261144 11.524353 l
9.261144 12.524353 l
h
7.880985 12.524353 m
7.648191 12.524353 7.391307 12.460656 7.192692 12.265653 c
6.992592 12.069190 6.924688 11.811304 6.924688 11.573689 c
7.924688 11.573689 l
7.924688 11.601334 7.928629 11.607865 7.925930 11.600269 c
7.922583 11.590851 7.913094 11.571540 7.893283 11.552089 c
7.873628 11.532791 7.855208 11.524535 7.848195 11.522132 c
7.842865 11.520305 7.851656 11.524353 7.880985 11.524353 c
7.880985 12.524353 l
h
6.924688 11.573689 m
6.924688 11.336283 6.992366 11.079206 7.189434 10.882138 c
7.386502 10.685069 7.643578 10.617392 7.880985 10.617392 c
7.880985 11.617392 l
7.853402 11.617392 7.845911 11.621316 7.852134 11.619125 c
7.859986 11.616362 7.877925 11.607861 7.896541 11.589245 c
7.915157 11.570628 7.923658 11.552690 7.926422 11.544838 c
7.928613 11.538614 7.924688 11.546105 7.924688 11.573689 c
6.924688 11.573689 l
h
7.880985 10.617392 m
9.244244 10.617392 l
9.244244 11.617392 l
7.880985 11.617392 l
7.880985 10.617392 l
h
9.244244 10.617392 m
9.437582 10.617392 9.513561 10.566607 9.541740 10.538825 c
9.568624 10.512321 9.623038 10.436918 9.623038 10.232964 c
10.623038 10.232964 l
10.623038 10.614872 10.516905 10.981685 10.243814 11.250929 c
9.972020 11.518894 9.608603 11.617392 9.244244 11.617392 c
9.244244 10.617392 l
h
9.623038 10.232964 m
9.623038 8.903505 l
10.623038 8.903505 l
10.623038 10.232964 l
9.623038 10.232964 l
h
9.623038 8.903505 m
9.623038 8.666346 9.690422 8.411142 9.882315 8.213968 c
10.076117 8.014833 10.331933 7.941575 10.573703 7.941575 c
10.573703 8.941575 l
10.550302 8.941575 10.545856 8.945058 10.554144 8.942020 c
10.563938 8.938431 10.581596 8.929247 10.598953 8.911413 c
10.616119 8.893775 10.623368 8.877576 10.625390 8.871755 c
10.626940 8.867291 10.623038 8.876008 10.623038 8.903505 c
9.623038 8.903505 l
h
1.768856 0.499987 m
3.143381 0.499987 l
3.143381 1.499987 l
1.768856 1.499987 l
1.768856 0.499987 l
h
3.143381 0.499987 m
3.381126 0.499987 3.638457 0.566224 3.836632 0.760829 c
4.036100 0.956702 4.105311 1.213470 4.105311 1.450650 c
3.105311 1.450650 l
3.105311 1.425433 3.101656 1.419969 3.104469 1.427811 c
3.107882 1.437328 3.117153 1.455843 3.135983 1.474335 c
3.154683 1.492697 3.172005 1.500437 3.178184 1.502572 c
3.182789 1.504164 3.173292 1.499987 3.143381 1.499987 c
3.143381 0.499987 l
h
4.105311 1.450650 m
4.105311 1.688103 4.037554 1.946297 3.838258 2.143798 c
3.639621 2.340646 3.381150 2.406948 3.143381 2.406948 c
3.143381 1.406948 l
3.173327 1.406948 3.182540 1.402761 3.177535 1.404499 c
3.170861 1.406816 3.153170 1.414856 3.134358 1.433499 c
3.115477 1.452209 3.106672 1.470500 3.103720 1.478848 c
3.101378 1.485473 3.105311 1.478268 3.105311 1.450650 c
4.105311 1.450650 l
h
3.143381 2.406948 m
1.780123 2.406948 l
1.780123 1.406948 l
3.143381 1.406948 l
3.143381 2.406948 l
h
1.780123 2.406948 m
1.582848 2.406948 1.510620 2.458242 1.485847 2.483017 c
1.461145 2.507718 1.406961 2.582696 1.406961 2.791376 c
0.406961 2.791376 l
0.406961 2.414193 0.507693 2.046957 0.778740 1.775910 c
1.049714 1.504936 1.414067 1.406948 1.780123 1.406948 c
1.780123 2.406948 l
h
1.406961 2.791376 m
1.406961 4.120834 l
0.406961 4.120834 l
0.406961 2.791376 l
1.406961 2.791376 l
h
1.406961 4.120834 m
1.406961 4.358603 1.340660 4.617074 1.143812 4.815711 c
0.946311 5.015007 0.688117 5.082765 0.450664 5.082765 c
0.450664 4.082765 l
0.478281 4.082765 0.485486 4.078832 0.478862 4.081174 c
0.470513 4.084126 0.452223 4.092931 0.433512 4.111812 c
0.414869 4.130625 0.406829 4.148315 0.404512 4.154989 c
0.402775 4.159994 0.406961 4.150780 0.406961 4.120834 c
1.406961 4.120834 l
h
0.450664 5.082765 m
0.212998 5.082765 -0.046012 5.014776 -0.242880 4.812439 c
-0.437600 4.612310 -0.500000 4.354052 -0.500000 4.120834 c
0.500000 4.120834 l
0.500000 4.152492 0.504292 4.162993 0.502905 4.158891 c
0.500940 4.153075 0.493147 4.134922 0.473846 4.115084 c
0.454316 4.095012 0.434662 4.085211 0.424742 4.081670 c
0.416742 4.078815 0.422984 4.082765 0.450664 4.082765 c
0.450664 5.082765 l
h
-0.500000 4.120834 m
-0.500000 2.746309 l
0.500000 2.746309 l
0.500000 4.120834 l
-0.500000 4.120834 l
h
-0.500000 2.746309 m
-0.500000 2.084355 -0.331527 1.497156 0.091924 1.079769 c
0.514044 0.663693 1.104907 0.499987 1.768856 0.499987 c
1.768856 1.499987 l
1.255445 1.499987 0.961880 1.626395 0.793913 1.791957 c
0.627276 1.956208 0.500000 2.242170 0.500000 2.746309 c
-0.500000 2.746309 l
h
7.880985 0.499987 m
9.261144 0.499987 l
9.261144 1.499987 l
7.880985 1.499987 l
7.880985 0.499987 l
h
9.261144 0.499987 m
9.926300 0.499987 10.516878 0.665709 10.938506 1.082307 c
11.361227 1.499982 11.530000 2.086557 11.530000 2.746309 c
10.530000 2.746309 l
10.530000 2.245602 10.403025 1.959016 10.235657 1.793645 c
10.067197 1.627195 9.773346 1.499987 9.261144 1.499987 c
9.261144 0.499987 l
h
11.530000 2.746309 m
11.530000 4.120834 l
10.530000 4.120834 l
10.530000 2.746309 l
11.530000 2.746309 l
h
11.530000 4.120834 m
11.530000 4.358603 11.463698 4.617073 11.266850 4.815711 c
11.069350 5.015007 10.811155 5.082765 10.573703 5.082765 c
10.573703 4.082765 l
10.601320 4.082765 10.608525 4.078832 10.601901 4.081174 c
10.593552 4.084126 10.575261 4.092931 10.556551 4.111812 c
10.537908 4.130625 10.529867 4.148315 10.527551 4.154989 c
10.525813 4.159994 10.530000 4.150780 10.530000 4.120834 c
11.530000 4.120834 l
h
10.573703 5.082765 m
10.336037 5.082765 10.077027 5.014776 9.880158 4.812439 c
9.685439 4.612311 9.623038 4.354053 9.623038 4.120834 c
10.623038 4.120834 l
10.623038 4.152492 10.627330 4.162993 10.625944 4.158891 c
10.623979 4.153075 10.616185 4.134922 10.596884 4.115084 c
10.577354 4.095012 10.557701 4.085210 10.547780 4.081670 c
10.539782 4.078815 10.546023 4.082765 10.573703 4.082765 c
10.573703 5.082765 l
h
9.623038 4.120834 m
9.623038 2.791376 l
10.623038 2.791376 l
10.623038 4.120834 l
9.623038 4.120834 l
h
9.623038 2.791376 m
9.623038 2.587421 9.568624 2.512020 9.541740 2.485516 c
9.513561 2.457733 9.437582 2.406948 9.244244 2.406948 c
9.244244 1.406948 l
9.608603 1.406948 9.972020 1.505445 10.243814 1.773412 c
10.516905 2.042655 10.623038 2.409468 10.623038 2.791376 c
9.623038 2.791376 l
h
9.244244 2.406948 m
7.880985 2.406948 l
7.880985 1.406948 l
9.244244 1.406948 l
9.244244 2.406948 l
h
7.880985 2.406948 m
7.643578 2.406948 7.386502 2.339270 7.189434 2.142201 c
6.992366 1.945133 6.924688 1.688057 6.924688 1.450650 c
7.924688 1.450650 l
7.924688 1.478234 7.928613 1.485724 7.926422 1.479502 c
7.923658 1.471650 7.915157 1.453712 7.896542 1.435096 c
7.877926 1.416480 7.859987 1.407979 7.852135 1.405214 c
7.845911 1.403024 7.853402 1.406948 7.880985 1.406948 c
7.880985 2.406948 l
h
6.924688 1.450650 m
6.924688 1.213532 6.993807 0.957870 7.191052 0.762418 c
7.387669 0.567588 7.643617 0.499987 7.880985 0.499987 c
7.880985 1.499987 l
7.853435 1.499987 7.845659 1.503901 7.851480 1.501862 c
7.858835 1.499284 7.876413 1.491087 7.894923 1.472746 c
7.913496 1.454342 7.922459 1.436171 7.925679 1.427151 c
7.928335 1.419713 7.924688 1.425466 7.924688 1.450650 c
6.924688 1.450650 l
h
f
n
Q
q
1.000000 0.000000 -0.000000 1.000000 10.000000 11.225647 cm
0.000000 0.000000 0.000000 scn
0.450664 8.441575 m
0.749229 8.441575 0.906961 8.610574 0.906961 8.903505 c
0.906961 10.232964 l
0.906961 10.818827 1.216793 11.117392 1.780123 11.117392 c
3.143381 11.117392 l
3.441946 11.117392 3.605311 11.280758 3.605311 11.573689 c
3.605311 11.866621 3.441946 12.024353 3.143381 12.024353 c
1.768856 12.024353 l
0.591496 12.024353 0.000000 11.444123 0.000000 10.283664 c
0.000000 8.903505 l
0.000000 8.610574 0.163366 8.441575 0.450664 8.441575 c
h
10.573703 8.441575 m
10.872268 8.441575 11.030000 8.610574 11.030000 8.903505 c
11.030000 10.283664 l
11.030000 11.444123 10.438503 12.024353 9.261144 12.024353 c
7.880985 12.024353 l
7.588054 12.024353 7.424688 11.866621 7.424688 11.573689 c
7.424688 11.280758 7.588054 11.117392 7.880985 11.117392 c
9.244244 11.117392 l
9.801940 11.117392 10.123038 10.818827 10.123038 10.232964 c
10.123038 8.903505 l
10.123038 8.610574 10.286405 8.441575 10.573703 8.441575 c
h
1.768856 0.999987 m
3.143381 0.999987 l
3.441946 0.999987 3.605311 1.163352 3.605311 1.450650 c
3.605311 1.743582 3.441946 1.906948 3.143381 1.906948 c
1.780123 1.906948 l
1.216793 1.906948 0.906961 2.205513 0.906961 2.791376 c
0.906961 4.120834 l
0.906961 4.419399 0.743595 4.582765 0.450664 4.582765 c
0.157732 4.582765 0.000000 4.419399 0.000000 4.120834 c
0.000000 2.746309 l
0.000000 1.580216 0.591496 0.999987 1.768856 0.999987 c
h
7.880985 0.999987 m
9.261144 0.999987 l
10.438503 0.999987 11.030000 1.585850 11.030000 2.746309 c
11.030000 4.120834 l
11.030000 4.419399 10.866634 4.582765 10.573703 4.582765 c
10.280771 4.582765 10.123038 4.419399 10.123038 4.120834 c
10.123038 2.791376 l
10.123038 2.205513 9.801940 1.906948 9.244244 1.906948 c
7.880985 1.906948 l
7.588054 1.906948 7.424688 1.743582 7.424688 1.450650 c
7.424688 1.163352 7.588054 0.999987 7.880985 0.999987 c
h
f*
n
Q
Q
endstream
endobj
3 0 obj
14103
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000014193 00000 n
0000014217 00000 n
0000014390 00000 n
0000014464 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
14523
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "favefilled_24.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,75 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 3.500000 3.753906 cm
0.000000 0.000000 0.000000 scn
8.500000 0.000054 m
8.718812 0.000054 9.030198 0.159956 9.282673 0.319857 c
13.987128 3.349560 17.000000 6.901045 17.000000 10.503025 c
17.000000 13.591639 14.870791 15.746094 12.186138 15.746094 c
10.511386 15.746094 9.257425 14.820352 8.500000 13.431738 c
7.759406 14.811935 6.497030 15.746094 4.822277 15.746094 c
2.137624 15.746094 0.000000 13.591639 0.000000 10.503025 c
0.000000 6.901045 3.012871 3.349560 7.717327 0.319857 c
7.978218 0.159956 8.289604 0.000054 8.500000 0.000054 c
h
f
n
Q
endstream
endobj
3 0 obj
615
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000705 00000 n
0000000727 00000 n
0000000900 00000 n
0000000974 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1033
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_live.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,305 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.065430 5.799805 cm
0.000000 0.000000 0.000000 scn
11.934570 22.751953 m
12.246094 22.751953 12.482422 22.999023 12.482422 23.299805 c
12.482422 23.600586 12.246094 23.869141 11.934570 23.869141 c
11.644531 23.869141 11.375977 23.600586 11.375977 23.299805 c
11.375977 22.999023 11.644531 22.751953 11.934570 22.751953 c
h
13.921875 22.580078 m
14.211914 22.580078 14.480469 22.837891 14.480469 23.127930 c
14.480469 23.428711 14.211914 23.697266 13.921875 23.697266 c
13.599609 23.697266 13.363281 23.428711 13.363281 23.127930 c
13.363281 22.837891 13.599609 22.580078 13.921875 22.580078 c
h
9.947266 22.580078 m
10.258789 22.580078 10.495117 22.837891 10.495117 23.127930 c
10.495117 23.428711 10.258789 23.697266 9.947266 23.697266 c
9.657227 23.697266 9.388672 23.428711 9.388672 23.127930 c
9.388672 22.837891 9.657227 22.580078 9.947266 22.580078 c
h
15.833984 22.075195 m
16.113281 22.075195 16.381836 22.322266 16.381836 22.612305 c
16.381836 22.913086 16.113281 23.181641 15.833984 23.181641 c
15.511719 23.181641 15.275391 22.913086 15.275391 22.612305 c
15.275391 22.322266 15.511719 22.075195 15.833984 22.075195 c
h
8.035156 22.075195 m
8.346680 22.075195 8.583008 22.322266 8.583008 22.612305 c
8.583008 22.913086 8.346680 23.181641 8.035156 23.181641 c
7.745117 23.181641 7.487305 22.913086 7.487305 22.612305 c
7.487305 22.322266 7.745117 22.075195 8.035156 22.075195 c
h
17.627930 21.226562 m
17.917969 21.226562 18.175781 21.473633 18.175781 21.774414 c
18.175781 22.075195 17.917969 22.333008 17.627930 22.333008 c
17.316406 22.333008 17.069336 22.075195 17.069336 21.774414 c
17.069336 21.473633 17.316406 21.226562 17.627930 21.226562 c
h
6.241211 21.226562 m
6.563477 21.226562 6.799805 21.473633 6.799805 21.774414 c
6.799805 22.075195 6.563477 22.333008 6.241211 22.333008 c
5.940430 22.333008 5.682617 22.075195 5.682617 21.774414 c
5.682617 21.473633 5.940430 21.226562 6.241211 21.226562 c
h
19.250000 20.087891 m
19.550781 20.087891 19.808594 20.345703 19.808594 20.625000 c
19.808594 20.936523 19.550781 21.194336 19.250000 21.194336 c
18.949219 21.194336 18.712891 20.936523 18.712891 20.625000 c
18.712891 20.345703 18.949219 20.087891 19.250000 20.087891 c
h
4.608398 20.087891 m
4.919922 20.087891 5.166992 20.345703 5.166992 20.625000 c
5.166992 20.936523 4.919922 21.194336 4.608398 21.194336 c
4.318359 21.194336 4.049805 20.936523 4.049805 20.625000 c
4.049805 20.345703 4.318359 20.087891 4.608398 20.087891 c
h
11.945312 3.254883 m
16.736328 3.254883 20.614258 7.122070 20.614258 11.934570 c
20.614258 16.704102 16.714844 20.603516 11.945312 20.603516 c
7.122070 20.603516 3.265625 16.736328 3.265625 11.934570 c
3.265625 7.100586 7.111328 3.254883 11.945312 3.254883 c
h
20.657227 18.691406 m
20.958008 18.691406 21.215820 18.938477 21.215820 19.228516 c
21.215820 19.540039 20.958008 19.797852 20.657227 19.797852 c
20.345703 19.797852 20.098633 19.540039 20.098633 19.228516 c
20.098633 18.938477 20.345703 18.691406 20.657227 18.691406 c
h
3.201172 18.691406 m
3.512695 18.691406 3.759766 18.938477 3.759766 19.228516 c
3.759766 19.540039 3.512695 19.797852 3.201172 19.797852 c
2.911133 19.797852 2.653320 19.540039 2.653320 19.228516 c
2.653320 18.938477 2.911133 18.691406 3.201172 18.691406 c
h
11.945312 4.307617 m
7.691406 4.307617 4.318359 7.680664 4.318359 11.934570 c
4.318359 16.156250 7.712891 19.550781 11.945312 19.550781 c
16.145508 19.550781 19.561523 16.134766 19.561523 11.934570 c
19.561523 7.702148 16.166992 4.307617 11.945312 4.307617 c
h
21.795898 17.058594 m
22.085938 17.058594 22.343750 17.305664 22.343750 17.606445 c
22.343750 17.896484 22.085938 18.165039 21.795898 18.165039 c
21.484375 18.165039 21.248047 17.896484 21.248047 17.606445 c
21.248047 17.305664 21.484375 17.058594 21.795898 17.058594 c
h
2.073242 17.058594 m
2.374023 17.058594 2.631836 17.305664 2.631836 17.606445 c
2.631836 17.896484 2.374023 18.165039 2.073242 18.165039 c
1.772461 18.165039 1.514648 17.896484 1.514648 17.606445 c
1.514648 17.305664 1.772461 17.058594 2.073242 17.058594 c
h
22.633789 15.253906 m
22.923828 15.253906 23.181641 15.500977 23.181641 15.801758 c
23.181641 16.102539 22.923828 16.371094 22.633789 16.371094 c
22.311523 16.371094 22.075195 16.102539 22.075195 15.801758 c
22.075195 15.500977 22.311523 15.253906 22.633789 15.253906 c
h
1.235352 15.253906 m
1.557617 15.253906 1.793945 15.500977 1.793945 15.801758 c
1.793945 16.102539 1.557617 16.371094 1.235352 16.371094 c
0.934570 16.371094 0.676758 16.102539 0.676758 15.801758 c
0.676758 15.500977 0.934570 15.253906 1.235352 15.253906 c
h
11.945312 7.498047 m
14.383789 7.498047 16.349609 9.463867 16.349609 11.913086 c
16.349609 14.351562 14.383789 16.317383 11.945312 16.317383 c
9.496094 16.317383 7.530273 14.351562 7.530273 11.913086 c
7.530273 9.453125 9.485352 7.498047 11.945312 7.498047 c
h
23.138672 13.341797 m
23.428711 13.341797 23.697266 13.599609 23.697266 13.900391 c
23.697266 14.190430 23.428711 14.458984 23.138672 14.458984 c
22.816406 14.458984 22.580078 14.190430 22.580078 13.900391 c
22.580078 13.599609 22.816406 13.341797 23.138672 13.341797 c
h
0.719727 13.341797 m
1.041992 13.341797 1.278320 13.599609 1.278320 13.900391 c
1.278320 14.190430 1.041992 14.458984 0.719727 14.458984 c
0.429688 14.458984 0.171875 14.190430 0.171875 13.900391 c
0.171875 13.599609 0.429688 13.341797 0.719727 13.341797 c
h
11.945312 9.517578 m
10.613281 9.517578 9.549805 10.581055 9.549805 11.913086 c
9.549805 13.223633 10.624023 14.297852 11.945312 14.297852 c
13.255859 14.297852 14.330078 13.212891 14.330078 11.913086 c
14.330078 10.591797 13.266602 9.517578 11.945312 9.517578 c
h
0.558594 11.375977 m
0.870117 11.375977 1.106445 11.633789 1.106445 11.913086 c
1.106445 12.224609 0.870117 12.482422 0.558594 12.482422 c
0.257812 12.482422 0.000000 12.224609 0.000000 11.913086 c
0.000000 11.633789 0.257812 11.375977 0.558594 11.375977 c
h
23.310547 11.375977 m
23.600586 11.375977 23.869141 11.633789 23.869141 11.913086 c
23.869141 12.224609 23.600586 12.482422 23.310547 12.482422 c
22.999023 12.482422 22.751953 12.224609 22.751953 11.913086 c
22.751953 11.633789 22.999023 11.375977 23.310547 11.375977 c
h
0.719727 9.399414 m
1.041992 9.399414 1.278320 9.667969 1.278320 9.968750 c
1.278320 10.258789 1.041992 10.516602 0.719727 10.516602 c
0.429688 10.516602 0.171875 10.258789 0.171875 9.968750 c
0.171875 9.667969 0.429688 9.399414 0.719727 9.399414 c
h
23.138672 9.399414 m
23.428711 9.399414 23.697266 9.667969 23.697266 9.968750 c
23.697266 10.258789 23.428711 10.516602 23.138672 10.516602 c
22.816406 10.516602 22.580078 10.258789 22.580078 9.968750 c
22.580078 9.667969 22.816406 9.399414 23.138672 9.399414 c
h
1.235352 7.498047 m
1.557617 7.498047 1.793945 7.755859 1.793945 8.067383 c
1.793945 8.346680 1.557617 8.615234 1.235352 8.615234 c
0.934570 8.615234 0.676758 8.346680 0.676758 8.067383 c
0.676758 7.755859 0.934570 7.498047 1.235352 7.498047 c
h
22.633789 7.498047 m
22.923828 7.498047 23.181641 7.755859 23.181641 8.067383 c
23.181641 8.346680 22.923828 8.615234 22.633789 8.615234 c
22.311523 8.615234 22.075195 8.346680 22.075195 8.067383 c
22.075195 7.755859 22.311523 7.498047 22.633789 7.498047 c
h
2.073242 5.693359 m
2.374023 5.693359 2.631836 5.961914 2.631836 6.262695 c
2.631836 6.563477 2.374023 6.810547 2.073242 6.810547 c
1.772461 6.810547 1.514648 6.563477 1.514648 6.262695 c
1.514648 5.961914 1.772461 5.693359 2.073242 5.693359 c
h
21.795898 5.693359 m
22.085938 5.693359 22.343750 5.961914 22.343750 6.262695 c
22.343750 6.563477 22.085938 6.810547 21.795898 6.810547 c
21.484375 6.810547 21.248047 6.563477 21.248047 6.262695 c
21.248047 5.961914 21.484375 5.693359 21.795898 5.693359 c
h
3.201172 4.071289 m
3.512695 4.071289 3.759766 4.329102 3.759766 4.629883 c
3.759766 4.930664 3.512695 5.177734 3.201172 5.177734 c
2.911133 5.177734 2.653320 4.930664 2.653320 4.629883 c
2.653320 4.329102 2.911133 4.071289 3.201172 4.071289 c
h
20.657227 4.071289 m
20.958008 4.071289 21.215820 4.329102 21.215820 4.629883 c
21.215820 4.930664 20.958008 5.177734 20.657227 5.177734 c
20.345703 5.177734 20.098633 4.930664 20.098633 4.629883 c
20.098633 4.329102 20.345703 4.071289 20.657227 4.071289 c
h
4.608398 2.664062 m
4.919922 2.664062 5.166992 2.921875 5.166992 3.233398 c
5.166992 3.523438 4.919922 3.770508 4.608398 3.770508 c
4.318359 3.770508 4.049805 3.523438 4.049805 3.233398 c
4.049805 2.921875 4.318359 2.664062 4.608398 2.664062 c
h
19.250000 2.664062 m
19.550781 2.664062 19.808594 2.921875 19.808594 3.233398 c
19.808594 3.523438 19.550781 3.770508 19.250000 3.770508 c
18.949219 3.770508 18.712891 3.523438 18.712891 3.233398 c
18.712891 2.921875 18.949219 2.664062 19.250000 2.664062 c
h
6.241211 1.525391 m
6.563477 1.525391 6.799805 1.793945 6.799805 2.094727 c
6.799805 2.384766 6.563477 2.642578 6.241211 2.642578 c
5.940430 2.642578 5.682617 2.384766 5.682617 2.094727 c
5.682617 1.793945 5.940430 1.525391 6.241211 1.525391 c
h
17.627930 1.525391 m
17.917969 1.525391 18.175781 1.793945 18.175781 2.094727 c
18.175781 2.384766 17.917969 2.642578 17.627930 2.642578 c
17.316406 2.642578 17.069336 2.384766 17.069336 2.094727 c
17.069336 1.793945 17.316406 1.525391 17.627930 1.525391 c
h
8.035156 0.676758 m
8.346680 0.676758 8.583008 0.945312 8.583008 1.246094 c
8.583008 1.546875 8.346680 1.793945 8.035156 1.793945 c
7.745117 1.793945 7.487305 1.546875 7.487305 1.246094 c
7.487305 0.945312 7.745117 0.676758 8.035156 0.676758 c
h
15.833984 0.676758 m
16.113281 0.676758 16.381836 0.945312 16.381836 1.246094 c
16.381836 1.546875 16.113281 1.793945 15.833984 1.793945 c
15.511719 1.793945 15.275391 1.546875 15.275391 1.246094 c
15.275391 0.945312 15.511719 0.676758 15.833984 0.676758 c
h
9.947266 0.171875 m
10.258789 0.171875 10.495117 0.440430 10.495117 0.741211 c
10.495117 1.031250 10.258789 1.278320 9.947266 1.278320 c
9.657227 1.278320 9.388672 1.031250 9.388672 0.741211 c
9.388672 0.440430 9.657227 0.171875 9.947266 0.171875 c
h
13.921875 0.171875 m
14.211914 0.171875 14.480469 0.440430 14.480469 0.741211 c
14.480469 1.031250 14.211914 1.278320 13.921875 1.278320 c
13.599609 1.278320 13.363281 1.031250 13.363281 0.741211 c
13.363281 0.440430 13.599609 0.171875 13.921875 0.171875 c
h
11.934570 0.000000 m
12.246094 0.000000 12.482422 0.257812 12.482422 0.569336 c
12.482422 0.848633 12.246094 1.106445 11.934570 1.106445 c
11.644531 1.106445 11.375977 0.848633 11.375977 0.569336 c
11.375977 0.257812 11.644531 0.000000 11.934570 0.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
10544
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000010634 00000 n
0000010658 00000 n
0000010831 00000 n
0000010905 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
10964
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_pano.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,89 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 4.889648 8.539062 cm
0.000000 0.000000 0.000000 scn
1.804688 0.000000 m
3.791992 0.000000 6.821289 2.363281 13.610352 2.363281 c
20.388672 2.363281 23.439453 0.010742 25.416016 0.010742 c
26.597656 0.010742 27.220703 0.730469 27.220703 1.922852 c
27.220703 16.478516 l
27.220703 17.681641 26.597656 18.401367 25.416016 18.401367 c
23.439453 18.401367 20.388672 16.038086 13.610352 16.038086 c
6.842773 16.038086 3.791992 18.401367 1.804688 18.401367 c
0.623047 18.401367 0.000000 17.681641 0.000000 16.489258 c
0.000000 1.912109 l
0.000000 0.719727 0.623047 0.000000 1.804688 0.000000 c
h
2.030273 1.783203 m
1.836914 1.783203 1.729492 1.890625 1.729492 2.094727 c
1.729492 16.306641 l
1.729492 16.510742 1.836914 16.618164 2.030273 16.618164 c
3.083008 16.618164 6.778320 14.405273 13.610352 14.405273 c
20.442383 14.405273 24.374023 16.628906 25.201172 16.628906 c
25.383789 16.628906 25.491211 16.521484 25.491211 16.306641 c
25.491211 2.094727 l
25.491211 1.890625 25.383789 1.783203 25.201172 1.783203 c
24.148438 1.783203 20.442383 3.996094 13.610352 3.996094 c
6.756836 3.996094 2.857422 1.783203 2.030273 1.783203 c
h
f*
n
Q
endstream
endobj
3 0 obj
1200
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001290 00000 n
0000001313 00000 n
0000001486 00000 n
0000001560 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1619
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_portrait.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,100 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 7.150391 5.654785 cm
0.000000 0.000000 0.000000 scn
1.503906 5.322754 m
9.958008 0.531738 l
10.559570 0.187988 11.128906 0.187988 11.741211 0.531738 c
20.184570 5.322754 l
21.172852 5.881348 21.688477 6.450684 21.688477 7.986816 c
21.688477 16.494629 l
21.688477 17.611816 21.280273 18.310059 20.377930 18.825684 c
12.772461 23.144043 l
11.472656 23.895996 10.215820 23.895996 8.916016 23.144043 c
1.321289 18.825684 l
0.408203 18.310059 0.000000 17.611816 0.000000 16.494629 c
0.000000 7.986816 l
0.000000 6.450684 0.526367 5.881348 1.503906 5.322754 c
h
10.849609 12.874512 m
2.556641 17.568848 l
9.635742 21.607910 l
10.462891 22.080566 11.225586 22.091309 12.063477 21.607910 c
19.142578 17.568848 l
10.849609 12.874512 l
h
2.470703 6.740723 m
1.847656 7.084473 1.632812 7.449707 1.632812 8.040527 c
1.632812 16.150879 l
10.000977 11.370605 l
10.000977 2.454590 l
2.470703 6.740723 l
h
19.228516 6.740723 m
11.687500 2.454590 l
11.687500 11.370605 l
20.055664 16.150879 l
20.055664 8.040527 l
20.055664 7.449707 19.840820 7.084473 19.228516 6.740723 c
h
f*
n
Q
endstream
endobj
3 0 obj
1132
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001222 00000 n
0000001245 00000 n
0000001418 00000 n
0000001492 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1551
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_screenrec.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,83 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 7.042969 6.798828 cm
0.000000 0.000000 0.000000 scn
10.957031 0.000000 m
16.951172 0.000000 21.914062 4.973633 21.914062 10.957031 c
21.914062 16.951172 16.940430 21.914062 10.946289 21.914062 c
4.962891 21.914062 0.000000 16.951172 0.000000 10.957031 c
0.000000 4.973633 4.973633 0.000000 10.957031 0.000000 c
h
10.957031 1.826172 m
5.886719 1.826172 1.836914 5.886719 1.836914 10.957031 c
1.836914 16.027344 5.875977 20.087891 10.946289 20.087891 c
16.016602 20.087891 20.077148 16.027344 20.087891 10.957031 c
20.098633 5.886719 16.027344 1.826172 10.957031 1.826172 c
h
10.967773 6.756836 m
13.288086 6.756836 15.178711 8.647461 15.178711 10.978516 c
15.178711 13.298828 13.288086 15.178711 10.967773 15.178711 c
8.636719 15.178711 6.746094 13.298828 6.746094 10.978516 c
6.746094 8.647461 8.636719 6.756836 10.967773 6.756836 c
h
f*
n
Q
endstream
endobj
3 0 obj
909
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000000999 00000 n
0000001021 00000 n
0000001194 00000 n
0000001268 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1327
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_screen.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,113 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 7.983398 7.228516 cm
0.000000 0.000000 0.000000 scn
0.859375 14.190430 m
1.428711 14.190430 1.729492 14.512695 1.729492 15.071289 c
1.729492 17.606445 l
1.729492 18.723633 2.320312 19.292969 3.394531 19.292969 c
5.994141 19.292969 l
6.563477 19.292969 6.875000 19.604492 6.875000 20.163086 c
6.875000 20.721680 6.563477 21.022461 5.994141 21.022461 c
3.373047 21.022461 l
1.127930 21.022461 0.000000 19.916016 0.000000 17.703125 c
0.000000 15.071289 l
0.000000 14.512695 0.311523 14.190430 0.859375 14.190430 c
h
20.163086 14.190430 m
20.732422 14.190430 21.033203 14.512695 21.033203 15.071289 c
21.033203 17.703125 l
21.033203 19.916016 19.905273 21.022461 17.660156 21.022461 c
15.028320 21.022461 l
14.469727 21.022461 14.158203 20.721680 14.158203 20.163086 c
14.158203 19.604492 14.469727 19.292969 15.028320 19.292969 c
17.627930 19.292969 l
18.691406 19.292969 19.303711 18.723633 19.303711 17.606445 c
19.303711 15.071289 l
19.303711 14.512695 19.615234 14.190430 20.163086 14.190430 c
h
3.373047 0.000000 m
5.994141 0.000000 l
6.563477 0.000000 6.875000 0.311523 6.875000 0.859375 c
6.875000 1.417969 6.563477 1.729492 5.994141 1.729492 c
3.394531 1.729492 l
2.320312 1.729492 1.729492 2.298828 1.729492 3.416016 c
1.729492 5.951172 l
1.729492 6.520508 1.417969 6.832031 0.859375 6.832031 c
0.300781 6.832031 0.000000 6.520508 0.000000 5.951172 c
0.000000 3.330078 l
0.000000 1.106445 1.127930 0.000000 3.373047 0.000000 c
h
15.028320 0.000000 m
17.660156 0.000000 l
19.905273 0.000000 21.033203 1.117188 21.033203 3.330078 c
21.033203 5.951172 l
21.033203 6.520508 20.721680 6.832031 20.163086 6.832031 c
19.604492 6.832031 19.303711 6.520508 19.303711 5.951172 c
19.303711 3.416016 l
19.303711 2.298828 18.691406 1.729492 17.627930 1.729492 c
15.028320 1.729492 l
14.469727 1.729492 14.158203 1.417969 14.158203 0.859375 c
14.158203 0.311523 14.469727 0.000000 15.028320 0.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
1970
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000002060 00000 n
0000002083 00000 n
0000002256 00000 n
0000002330 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
2389
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_selfie.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,92 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 8.106445 7.851562 cm
0.000000 0.000000 0.000000 scn
3.373047 0.000000 m
16.403320 0.000000 l
18.659180 0.000000 19.776367 1.117188 19.776367 3.330078 c
19.776367 16.446289 l
19.776367 18.659180 18.659180 19.776367 16.403320 19.776367 c
3.373047 19.776367 l
1.127930 19.776367 0.000000 18.669922 0.000000 16.446289 c
0.000000 3.330078 l
0.000000 1.106445 1.127930 0.000000 3.373047 0.000000 c
h
1.729492 3.416016 m
1.729492 16.360352 l
1.729492 17.477539 2.320312 18.046875 3.394531 18.046875 c
16.381836 18.046875 l
17.445312 18.046875 18.046875 17.477539 18.046875 16.360352 c
18.046875 3.416016 l
18.046875 2.578125 17.703125 2.051758 17.080078 1.836914 c
16.156250 4.468750 13.320312 6.305664 9.904297 6.305664 c
6.477539 6.305664 3.641602 4.458008 2.707031 1.826172 c
2.073242 2.041016 1.729492 2.567383 1.729492 3.416016 c
h
9.893555 8.099609 m
11.945312 8.078125 13.567383 9.829102 13.567383 12.127930 c
13.567383 14.287109 11.945312 16.070312 9.893555 16.070312 c
7.841797 16.070312 6.208984 14.287109 6.219727 12.127930 c
6.230469 9.829102 7.841797 8.121094 9.893555 8.099609 c
h
f*
n
Q
endstream
endobj
3 0 obj
1162
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001252 00000 n
0000001275 00000 n
0000001448 00000 n
0000001522 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1581
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_slowmo.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,161 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.382812 5.627930 cm
0.000000 0.000000 0.000000 scn
12.106445 24.223633 m
11.676758 24.223633 11.343750 23.879883 11.343750 23.450195 c
11.343750 19.636719 l
11.343750 19.207031 11.676758 18.863281 12.106445 18.863281 c
12.546875 18.863281 12.879883 19.207031 12.879883 19.636719 c
12.879883 23.450195 l
12.879883 23.879883 12.546875 24.223633 12.106445 24.223633 c
h
6.058594 22.612305 m
5.671875 22.386719 5.564453 21.924805 5.779297 21.548828 c
7.680664 18.240234 l
7.906250 17.864258 8.357422 17.735352 8.733398 17.960938 c
9.120117 18.175781 9.227539 18.637695 9.023438 19.002930 c
7.111328 22.322266 l
6.885742 22.687500 6.434570 22.816406 6.058594 22.612305 c
h
18.175781 22.601562 m
17.789062 22.816406 17.337891 22.687500 17.112305 22.322266 c
15.210938 19.002930 l
14.996094 18.637695 15.103516 18.175781 15.490234 17.950195 c
15.866211 17.746094 16.317383 17.864258 16.542969 18.240234 c
18.455078 21.548828 l
18.659180 21.924805 18.551758 22.386719 18.175781 22.601562 c
h
1.632812 18.175781 m
1.407227 17.789062 1.536133 17.337891 1.912109 17.112305 c
5.220703 15.200195 l
5.596680 14.996094 6.047852 15.103516 6.273438 15.479492 c
6.477539 15.866211 6.359375 16.317383 5.983398 16.542969 c
2.674805 18.455078 l
2.298828 18.659180 1.836914 18.551758 1.632812 18.175781 c
h
22.612305 18.165039 m
22.386719 18.551758 21.924805 18.659180 21.548828 18.455078 c
18.240234 16.542969 l
17.864258 16.317383 17.735352 15.866211 17.971680 15.479492 c
18.175781 15.114258 18.637695 14.996094 19.002930 15.200195 c
22.322266 17.112305 l
22.687500 17.337891 22.816406 17.789062 22.612305 18.165039 c
h
24.223633 12.117188 m
24.223633 12.546875 23.890625 12.879883 23.450195 12.879883 c
19.636719 12.879883 l
19.207031 12.879883 18.863281 12.546875 18.863281 12.117188 c
18.863281 11.676758 19.207031 11.343750 19.636719 11.343750 c
23.450195 11.343750 l
23.890625 11.343750 24.223633 11.676758 24.223633 12.117188 c
h
0.000000 12.117188 m
0.000000 11.676758 0.343750 11.343750 0.773438 11.343750 c
4.586914 11.343750 l
5.027344 11.343750 5.360352 11.676758 5.360352 12.117188 c
5.360352 12.546875 5.027344 12.879883 4.586914 12.879883 c
0.773438 12.879883 l
0.343750 12.879883 0.000000 12.546875 0.000000 12.117188 c
h
22.601562 6.058594 m
22.827148 6.434570 22.687500 6.885742 22.322266 7.111328 c
19.002930 9.023438 l
18.637695 9.227539 18.175781 9.120117 17.960938 8.744141 c
17.746094 8.357422 17.864258 7.906250 18.240234 7.680664 c
21.548828 5.768555 l
21.924805 5.564453 22.386719 5.671875 22.601562 6.058594 c
h
1.632812 6.058594 m
1.836914 5.671875 2.298828 5.564453 2.674805 5.768555 c
5.983398 7.680664 l
6.359375 7.906250 6.488281 8.357422 6.273438 8.733398 c
6.047852 9.120117 5.596680 9.227539 5.220703 9.023438 c
1.912109 7.111328 l
1.536133 6.885742 1.407227 6.434570 1.632812 6.058594 c
h
18.165039 1.622070 m
18.551758 1.836914 18.659180 2.298828 18.455078 2.674805 c
16.542969 5.983398 l
16.328125 6.359375 15.866211 6.488281 15.490234 6.273438 c
15.103516 6.047852 14.996094 5.596680 15.210938 5.220703 c
17.112305 1.912109 l
17.337891 1.536133 17.789062 1.407227 18.165039 1.622070 c
h
6.058594 1.622070 m
6.434570 1.407227 6.885742 1.536133 7.111328 1.912109 c
9.023438 5.220703 l
9.227539 5.596680 9.120117 6.047852 8.744141 6.262695 c
8.357422 6.477539 7.906250 6.359375 7.680664 5.983398 c
5.779297 2.674805 l
5.564453 2.298828 5.671875 1.836914 6.058594 1.622070 c
h
12.106445 0.000000 m
12.546875 0.000000 12.879883 0.343750 12.879883 0.773438 c
12.879883 4.586914 l
12.879883 5.016602 12.546875 5.360352 12.106445 5.360352 c
11.676758 5.360352 11.343750 5.016602 11.343750 4.586914 c
11.343750 0.773438 l
11.343750 0.343750 11.676758 0.000000 12.106445 0.000000 c
h
f*
n
Q
endstream
endobj
3 0 obj
3768
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003858 00000 n
0000003881 00000 n
0000004054 00000 n
0000004128 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
4187
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_timelapse.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,257 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 6.382812 5.627930 cm
0.000000 0.000000 0.000000 scn
12.106445 24.223633 m
11.751953 24.223633 11.472656 23.933594 11.472656 23.579102 c
11.472656 19.593750 l
11.472656 19.228516 11.751953 18.949219 12.106445 18.949219 c
12.471680 18.949219 12.750977 19.228516 12.750977 19.593750 c
12.750977 23.579102 l
12.750977 23.933594 12.471680 24.223633 12.106445 24.223633 c
h
8.937500 23.933594 m
8.583008 23.858398 8.389648 23.546875 8.497070 23.138672 c
8.851562 21.785156 l
8.948242 21.452148 9.281250 21.269531 9.678711 21.387695 c
10.054688 21.505859 10.205078 21.806641 10.097656 22.150391 c
9.721680 23.450195 l
9.614258 23.836914 9.302734 23.998047 8.937500 23.933594 c
h
15.286133 23.901367 m
14.942383 24.019531 14.609375 23.847656 14.501953 23.450195 c
14.136719 22.128906 l
14.040039 21.785156 14.233398 21.430664 14.577148 21.344727 c
14.920898 21.258789 15.264648 21.441406 15.361328 21.795898 c
15.715820 23.117188 l
15.812500 23.482422 15.629883 23.804688 15.286133 23.901367 c
h
6.058594 22.612305 m
5.747070 22.429688 5.650391 22.042969 5.833008 21.731445 c
7.820312 18.272461 l
8.002930 17.950195 8.389648 17.853516 8.690430 18.036133 c
9.001953 18.208008 9.109375 18.594727 8.926758 18.906250 c
6.928711 22.375977 l
6.746094 22.676758 6.370117 22.773438 6.058594 22.612305 c
h
18.175781 22.601562 m
17.864258 22.773438 17.477539 22.676758 17.294922 22.375977 c
15.307617 18.906250 l
15.114258 18.594727 15.221680 18.208008 15.533203 18.036133 c
15.833984 17.853516 16.231445 17.950195 16.403320 18.272461 c
18.401367 21.731445 l
18.573242 22.042969 18.476562 22.418945 18.175781 22.601562 c
h
3.458984 20.753906 m
3.201172 20.506836 3.233398 20.098633 3.469727 19.851562 c
4.425781 18.884766 l
4.694336 18.616211 5.059570 18.605469 5.338867 18.874023 c
5.585938 19.121094 5.596680 19.507812 5.328125 19.787109 c
4.372070 20.743164 l
4.092773 21.022461 3.727539 21.011719 3.458984 20.753906 c
h
20.764648 20.743164 m
20.506836 21.000977 20.098633 20.979492 19.862305 20.743164 c
18.884766 19.776367 l
18.616211 19.497070 18.616211 19.131836 18.884766 18.874023 c
19.142578 18.605469 19.529297 18.605469 19.776367 18.863281 c
20.732422 19.830078 l
21.000977 20.109375 21.011719 20.485352 20.764648 20.743164 c
h
1.632812 18.175781 m
1.450195 17.864258 1.546875 17.477539 1.858398 17.294922 c
5.317383 15.296875 l
5.639648 15.114258 6.015625 15.221680 6.198242 15.522461 c
6.370117 15.833984 6.273438 16.220703 5.951172 16.403320 c
2.492188 18.401367 l
2.180664 18.573242 1.804688 18.476562 1.632812 18.175781 c
h
22.612305 18.165039 m
22.429688 18.476562 22.042969 18.573242 21.731445 18.401367 c
18.272461 16.403320 l
17.950195 16.220703 17.853516 15.833984 18.036133 15.522461 c
18.208008 15.221680 18.594727 15.114258 18.906250 15.296875 c
22.375977 17.294922 l
22.676758 17.477539 22.773438 17.853516 22.612305 18.165039 c
h
23.922852 15.264648 m
23.836914 15.619141 23.471680 15.823242 23.149414 15.726562 c
21.806641 15.361328 l
21.462891 15.264648 21.258789 14.931641 21.376953 14.534180 c
21.462891 14.254883 21.763672 14.029297 22.150391 14.125977 c
23.450195 14.491211 l
23.826172 14.587891 24.008789 14.931641 23.922852 15.264648 c
h
0.290039 15.275391 m
0.214844 14.931641 0.429688 14.587891 0.751953 14.491211 c
2.073242 14.115234 l
2.438477 14.029297 2.782227 14.222656 2.857422 14.577148 c
2.932617 14.920898 2.771484 15.286133 2.416992 15.372070 c
1.095703 15.705078 l
0.708984 15.801758 0.365234 15.608398 0.290039 15.275391 c
h
24.223633 12.117188 m
24.223633 12.471680 23.944336 12.750977 23.579102 12.750977 c
19.593750 12.750977 l
19.228516 12.750977 18.949219 12.471680 18.949219 12.117188 c
18.949219 11.751953 19.228516 11.472656 19.593750 11.472656 c
23.579102 11.472656 l
23.944336 11.472656 24.223633 11.751953 24.223633 12.117188 c
h
0.000000 12.117188 m
0.000000 11.751953 0.290039 11.472656 0.644531 11.472656 c
4.629883 11.472656 l
5.005859 11.472656 5.274414 11.751953 5.274414 12.117188 c
5.274414 12.471680 5.005859 12.750977 4.629883 12.750977 c
0.644531 12.750977 l
0.290039 12.750977 0.000000 12.471680 0.000000 12.117188 c
h
23.922852 8.916016 m
24.019531 9.281250 23.858398 9.614258 23.482422 9.721680 c
22.150391 10.097656 l
21.785156 10.194336 21.452148 9.990234 21.366211 9.646484 c
21.280273 9.291992 21.473633 8.926758 21.806641 8.840820 c
23.127930 8.497070 l
23.503906 8.400391 23.826172 8.593750 23.922852 8.916016 c
h
0.300781 8.948242 m
0.386719 8.604492 0.751953 8.421875 1.084961 8.497070 c
2.416992 8.830078 l
2.782227 8.926758 2.964844 9.259766 2.868164 9.614258 c
2.771484 9.958008 2.427734 10.162109 2.073242 10.076172 c
0.784180 9.721680 l
0.386719 9.614258 0.214844 9.281250 0.300781 8.948242 c
h
22.601562 6.058594 m
22.773438 6.359375 22.676758 6.746094 22.375977 6.928711 c
18.906250 8.926758 l
18.594727 9.109375 18.208008 9.001953 18.036133 8.701172 c
17.853516 8.389648 17.950195 8.002930 18.272461 7.820312 c
21.731445 5.822266 l
22.042969 5.650391 22.418945 5.747070 22.601562 6.058594 c
h
1.622070 6.058594 m
1.793945 5.747070 2.180664 5.650391 2.492188 5.822266 c
5.951172 7.820312 l
6.273438 8.002930 6.370117 8.389648 6.198242 8.690430 c
6.015625 9.001953 5.639648 9.109375 5.317383 8.926758 c
1.858398 6.928711 l
1.546875 6.746094 1.450195 6.370117 1.622070 6.058594 c
h
6.058594 1.622070 m
6.359375 1.450195 6.756836 1.546875 6.928711 1.858398 c
8.926758 5.317383 l
9.109375 5.628906 9.001953 6.015625 8.701172 6.198242 c
8.389648 6.370117 8.002930 6.273438 7.820312 5.951172 c
5.833008 2.492188 l
5.650391 2.180664 5.747070 1.804688 6.058594 1.622070 c
h
18.165039 1.622070 m
18.476562 1.793945 18.573242 2.180664 18.401367 2.492188 c
16.403320 5.951172 l
16.231445 6.273438 15.833984 6.370117 15.533203 6.198242 c
15.221680 6.015625 15.114258 5.628906 15.307617 5.317383 c
17.294922 1.858398 l
17.477539 1.546875 17.853516 1.450195 18.165039 1.622070 c
h
20.753906 3.458984 m
21.022461 3.727539 21.000977 4.135742 20.775391 4.372070 c
19.787109 5.338867 l
19.529297 5.585938 19.142578 5.618164 18.863281 5.338867 c
18.616211 5.070312 18.637695 4.705078 18.884766 4.447266 c
19.851562 3.480469 l
20.120117 3.211914 20.496094 3.201172 20.753906 3.458984 c
h
3.469727 3.469727 m
3.738281 3.211914 4.125000 3.211914 4.361328 3.448242 c
5.338867 4.415039 l
5.607422 4.694336 5.618164 5.059570 5.349609 5.317383 c
5.102539 5.575195 4.694336 5.585938 4.447266 5.328125 c
3.491211 4.361328 l
3.222656 4.082031 3.211914 3.727539 3.469727 3.469727 c
h
12.106445 0.000000 m
12.471680 0.000000 12.750977 0.290039 12.750977 0.644531 c
12.750977 4.629883 l
12.750977 4.995117 12.471680 5.274414 12.106445 5.274414 c
11.751953 5.274414 11.472656 4.995117 11.472656 4.629883 c
11.472656 0.644531 l
11.472656 0.290039 11.751953 0.000000 12.106445 0.000000 c
h
15.286133 0.290039 m
15.694336 0.386719 15.812500 0.741211 15.726562 1.074219 c
15.382812 2.427734 l
15.286133 2.771484 14.920898 2.964844 14.577148 2.868164 c
14.201172 2.760742 14.040039 2.406250 14.136719 2.083984 c
14.523438 0.773438 l
14.630859 0.418945 14.899414 0.214844 15.286133 0.290039 c
h
8.883789 0.300781 m
9.281250 0.171875 9.635742 0.408203 9.721680 0.741211 c
10.086914 2.062500 l
10.183594 2.406250 9.990234 2.760742 9.646484 2.846680 c
9.302734 2.932617 8.958984 2.750000 8.862305 2.395508 c
8.507812 1.074219 l
8.411133 0.708984 8.529297 0.408203 8.883789 0.300781 c
h
f*
n
Q
endstream
endobj
3 0 obj
7389
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000007479 00000 n
0000007502 00000 n
0000007675 00000 n
0000007749 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
7808
%%EOF

View File

@ -0,0 +1,12 @@
{
"images" : [
{
"filename" : "attach_video.pdf",
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,103 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 5.592773 8.925781 cm
0.000000 0.000000 0.000000 scn
4.189453 0.000000 m
15.415039 0.000000 l
18.036133 0.000000 19.604492 1.525391 19.604492 4.146484 c
19.604492 5.618164 l
23.665039 2.180664 l
24.094727 1.826172 24.567383 1.579102 25.007812 1.579102 c
25.953125 1.579102 26.576172 2.277344 26.576172 3.276367 c
26.576172 14.383789 l
26.576172 15.382812 25.953125 16.081055 25.007812 16.081055 c
24.567383 16.081055 24.094727 15.833984 23.665039 15.479492 c
19.604492 12.041992 l
19.604492 13.524414 l
19.604492 16.134766 18.036133 17.660156 15.415039 17.660156 c
4.189453 17.660156 l
1.686523 17.660156 0.000000 16.134766 0.000000 13.524414 c
0.000000 4.146484 l
0.000000 1.525391 1.568359 0.000000 4.189453 0.000000 c
h
4.490234 1.622070 m
2.728516 1.622070 1.729492 2.535156 1.729492 4.393555 c
1.729492 13.266602 l
1.729492 15.135742 2.728516 16.048828 4.490234 16.048828 c
15.114258 16.048828 l
16.865234 16.048828 17.875000 15.135742 17.875000 13.266602 c
17.875000 4.393555 l
17.875000 2.535156 16.865234 1.622070 15.114258 1.622070 c
4.490234 1.622070 l
h
24.470703 3.641602 m
19.604492 7.659180 l
19.604492 10.000977 l
24.470703 14.018555 l
24.567383 14.093750 24.631836 14.147461 24.728516 14.147461 c
24.857422 14.147461 24.911133 14.040039 24.911133 13.889648 c
24.911133 3.770508 l
24.911133 3.620117 24.857422 3.523438 24.728516 3.523438 c
24.631836 3.523438 24.567383 3.577148 24.470703 3.641602 c
h
f*
n
Q
endstream
endobj
3 0 obj
1489
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 36.000000 36.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000001579 00000 n
0000001602 00000 n
0000001775 00000 n
0000001849 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
1908
%%EOF

View File

@ -1,16 +1,8 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
"filename" : "target_24.pdf",
"idiom" : "universal"
}
],
"info" : {

View File

@ -0,0 +1,131 @@
%PDF-1.7
1 0 obj
<< >>
endobj
2 0 obj
<< /Length 3 0 R >>
stream
/DeviceRGB CS
/DeviceRGB cs
q
1.000000 0.000000 -0.000000 1.000000 1.868011 1.601807 cm
0.000000 0.000000 0.000000 scn
9.467012 16.798109 m
9.467012 16.430840 9.764743 16.133110 10.132012 16.133110 c
10.499282 16.133110 10.797012 16.430840 10.797012 16.798109 c
10.797012 17.703321 l
14.317344 17.386963 17.120886 14.583434 17.437263 11.063108 c
16.532019 11.063108 l
16.164749 11.063108 15.867019 10.765378 15.867019 10.398108 c
15.867019 10.030839 16.164749 9.733109 16.532019 9.733109 c
17.437273 9.733109 l
17.120937 6.212737 14.317376 3.409161 10.797012 3.092800 c
10.797012 3.998110 l
10.797012 4.365379 10.499282 4.663109 10.132012 4.663109 c
9.764743 4.663109 9.467012 4.365379 9.467012 3.998110 c
9.467012 3.092798 l
5.946643 3.409155 3.143077 6.212733 2.826741 9.733109 c
3.732019 9.733109 l
4.099288 9.733109 4.397019 10.030839 4.397019 10.398108 c
4.397019 10.765378 4.099288 11.063108 3.732019 11.063108 c
2.826750 11.063108 l
3.143129 14.583438 5.946676 17.386969 9.467012 17.703321 c
9.467012 16.798109 l
h
10.797012 19.037922 m
10.797012 19.998110 l
10.797012 20.365379 10.499282 20.663109 10.132012 20.663109 c
9.764743 20.663109 9.467012 20.365379 9.467012 19.998110 c
9.467012 19.037922 l
5.211674 18.714972 1.815125 15.318439 1.492149 11.063108 c
0.532019 11.063108 l
0.164750 11.063108 -0.132981 10.765378 -0.132981 10.398108 c
-0.132981 10.030839 0.164750 9.733109 0.532019 9.733109 c
1.492141 9.733109 l
1.815073 5.477732 5.211642 2.081148 9.467012 1.758196 c
9.467012 0.798109 l
9.467012 0.430840 9.764743 0.133108 10.132012 0.133108 c
10.499282 0.133108 10.797012 0.430840 10.797012 0.798109 c
10.797012 1.758198 l
15.052378 2.081156 18.448942 5.477736 18.771873 9.733109 c
19.732019 9.733109 l
20.099289 9.733109 20.397018 10.030839 20.397018 10.398108 c
20.397018 10.765378 20.099289 11.063108 19.732019 11.063108 c
18.771868 11.063108 l
18.448893 15.318436 15.052345 18.714966 10.797012 19.037922 c
h
10.132013 14.132962 m
9.173799 14.132962 8.397013 13.356176 8.397013 12.397963 c
8.397013 11.439748 9.173799 10.662962 10.132013 10.662962 c
11.090227 10.662962 11.867013 11.439748 11.867013 12.397963 c
11.867013 13.356176 11.090227 14.132962 10.132013 14.132962 c
h
7.067013 12.397963 m
7.067013 14.090715 8.439260 15.462962 10.132013 15.462962 c
11.824766 15.462962 13.197013 14.090715 13.197013 12.397963 c
13.197013 10.705210 11.824766 9.332962 10.132013 9.332962 c
8.439260 9.332962 7.067013 10.705210 7.067013 12.397963 c
h
6.310586 5.946628 m
6.984863 6.676261 8.128437 7.332962 10.132026 7.332962 c
12.135614 7.332962 13.279188 6.676261 13.953466 5.946628 c
14.202730 5.676899 14.623459 5.660309 14.893188 5.909575 c
15.162916 6.158839 15.179506 6.579567 14.930241 6.849297 c
13.975744 7.882154 12.462423 8.662962 10.132026 8.662962 c
7.801628 8.662962 6.288308 7.882154 5.333811 6.849297 c
5.084546 6.579567 5.101135 6.158839 5.370864 5.909575 c
5.640593 5.660309 6.061321 5.676899 6.310586 5.946628 c
h
f*
n
Q
endstream
endobj
3 0 obj
2949
endobj
4 0 obj
<< /Annots []
/Type /Page
/MediaBox [ 0.000000 0.000000 24.000000 24.000000 ]
/Resources 1 0 R
/Contents 2 0 R
/Parent 5 0 R
>>
endobj
5 0 obj
<< /Kids [ 4 0 R ]
/Count 1
/Type /Pages
>>
endobj
6 0 obj
<< /Pages 5 0 R
/Type /Catalog
>>
endobj
xref
0 7
0000000000 65535 f
0000000010 00000 n
0000000034 00000 n
0000003039 00000 n
0000003062 00000 n
0000003235 00000 n
0000003309 00000 n
trailer
<< /ID [ (some) (id) ]
/Root 6 0 R
/Size 7
>>
startxref
3368
%%EOF

View File

@ -136,7 +136,10 @@ private enum AttachmentFileEntry: ItemListNodeEntry {
}
}
private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?) -> [AttachmentFileEntry] {
private func attachmentFileControllerEntries(presentationData: PresentationData, recentDocuments: [Message]?, empty: Bool) -> [AttachmentFileEntry] {
guard !empty else {
return []
}
var entries: [AttachmentFileEntry] = []
entries.append(.selectFromGallery(presentationData.theme, presentationData.strings.Attachment_SelectFromGallery))
entries.append(.selectFromFiles(presentationData.theme, presentationData.strings.Attachment_SelectFromFiles))
@ -152,7 +155,7 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
}
} else {
entries.append(.recentHeader(presentationData.theme, presentationData.strings.Attachment_RecentlySentFiles.uppercased()))
for i in 0 ..< 8 {
for i in 0 ..< 11 {
entries.append(.file(Int32(i), presentationData.theme, nil))
}
}
@ -162,19 +165,25 @@ private func attachmentFileControllerEntries(presentationData: PresentationData,
private class AttachmentFileControllerImpl: ItemListController, AttachmentContainable {
public var requestAttachmentMenuExpansion: () -> Void = {}
public var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void = { _ in }
public var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
var prepareForReuseImpl: () -> Void = {}
var resetForReuseImpl: () -> Void = {}
public func resetForReuse() {
self.prepareForReuseImpl()
self.resetForReuseImpl()
self.scrollToTop?()
}
public func prepareForReuse() {
self.visibleBottomContentOffsetChanged?(self.visibleBottomContentOffset)
}
}
private struct AttachmentFileControllerState: Equatable {
var searching: Bool
}
public func attachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentContainable {
public func attachmentFileController(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, bannedSendMedia: (Int32, Bool)?, presentGallery: @escaping () -> Void, presentFiles: @escaping () -> Void, send: @escaping (AnyMediaReference) -> Void) -> AttachmentContainable {
let actionsDisposable = DisposableSet()
let statePromise = ValuePromise(AttachmentFileControllerState(searching: false), ignoreRepeated: true)
@ -236,9 +245,8 @@ public func attachmentFileController(context: AccountContext, updatedPresentatio
}
var rightNavigationButton: ItemListNavigationButton?
if let recentDocuments = recentDocuments, recentDocuments.count > 10 {
if bannedSendMedia == nil && (recentDocuments == nil || (recentDocuments?.count ?? 0) > 10) {
rightNavigationButton = ItemListNavigationButton(content: .icon(.search), style: .regular, enabled: true, action: {
expandImpl?()
updateState { state in
var updatedState = state
updatedState.searching = true
@ -252,13 +260,25 @@ public func attachmentFileController(context: AccountContext, updatedPresentatio
}), rightNavigationButton: rightNavigationButton, backNavigationButton: ItemListBackButton(title: presentationData.strings.Common_Back), animateChanges: true)
var emptyItem: AttachmentFileEmptyStateItem?
if let recentDocuments = recentDocuments, recentDocuments.isEmpty {
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings)
if let (untilDate, personal) = bannedSendMedia {
let banDescription: String
if untilDate != 0 && untilDate != Int32.max {
banDescription = presentationData.strings.Conversation_RestrictedMediaTimed(stringForFullDate(timestamp: untilDate, strings: presentationData.strings, dateTimeFormat: presentationData.dateTimeFormat)).string
} else if personal {
banDescription = presentationData.strings.Conversation_RestrictedMedia
} else {
banDescription = presentationData.strings.Conversation_DefaultRestrictedMedia
}
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .bannedSendMedia(banDescription))
} else if let recentDocuments = recentDocuments, recentDocuments.isEmpty {
emptyItem = AttachmentFileEmptyStateItem(context: context, theme: presentationData.theme, strings: presentationData.strings, content: .intro)
}
var searchItem: ItemListControllerSearch?
if state.searching {
searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, cancel: {
searchItem = AttachmentFileSearchItem(context: context, presentationData: presentationData, focus: {
expandImpl?()
}, cancel: {
updateState { state in
var updatedState = state
updatedState.searching = false
@ -271,7 +291,7 @@ public func attachmentFileController(context: AccountContext, updatedPresentatio
})
}
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
let listState = ItemListNodeState(presentationData: ItemListPresentationData(presentationData), entries: attachmentFileControllerEntries(presentationData: presentationData, recentDocuments: recentDocuments, empty: bannedSendMedia != nil), style: .blocks, emptyStateItem: emptyItem, searchItem: searchItem, crossfadeState: crossfade, animateChanges: animateChanges)
return (controllerState, (listState, arguments))
} |> afterDisposed {
@ -279,7 +299,16 @@ public func attachmentFileController(context: AccountContext, updatedPresentatio
}
let controller = AttachmentFileControllerImpl(context: context, state: signal)
controller.prepareForReuseImpl = {
controller.visibleBottomContentOffsetChanged = { [weak controller] offset in
switch offset {
case let .known(value):
let backgroundAlpha: CGFloat = min(30.0, value) / 30.0
controller?.updateTabBarAlpha(backgroundAlpha, .immediate)
case .unknown, .none:
controller?.updateTabBarAlpha(1.0, .immediate)
}
}
controller.resetForReuseImpl = {
updateState { state in
var updatedState = state
updatedState.searching = false

View File

@ -10,19 +10,26 @@ import TelegramAnimatedStickerNode
import AccountContext
final class AttachmentFileEmptyStateItem: ItemListControllerEmptyStateItem {
enum Content: Equatable {
case intro
case bannedSendMedia(String)
}
let context: AccountContext
let theme: PresentationTheme
let strings: PresentationStrings
let content: Content
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings) {
init(context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, content: Content) {
self.context = context
self.theme = theme
self.strings = strings
self.content = content
}
func isEqual(to: ItemListControllerEmptyStateItem) -> Bool {
if let item = to as? AttachmentFileEmptyStateItem {
return self.theme === item.theme && self.strings === item.strings
return self.theme === item.theme && self.strings === item.strings && self.content == item.content
} else {
return false
}
@ -75,21 +82,31 @@ final class AttachmentFileEmptyStateItemNode: ItemListControllerEmptyStateItemNo
}
private func updateThemeAndStrings(theme: PresentationTheme, strings: PresentationStrings) {
self.textNode.attributedText = NSAttributedString(string: strings.Attachment_FilesIntro, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
let text: String
switch self.item.content {
case .intro:
text = strings.Attachment_FilesIntro
case let .bannedSendMedia(banDescription):
text = banDescription
}
self.textNode.attributedText = NSAttributedString(string: text, font: Font.regular(15.0), textColor: theme.list.freeTextColor, paragraphAlignment: .center)
}
override func updateLayout(layout: ContainerViewLayout, navigationBarHeight: CGFloat, transition: ContainedViewLayoutTransition) {
self.validLayout = (layout, navigationBarHeight)
var insets = layout.insets(options: [])
insets.top += navigationBarHeight - 92.0
insets.top += navigationBarHeight
let imageSpacing: CGFloat = 12.0
let imageSize = CGSize(width: 144.0, height: 144.0)
let imageHeight = layout.size.width < layout.size.height ? imageSize.height + imageSpacing : 0.0
self.animationNode.frame = CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: -10.0), size: imageSize)
self.animationNode.updateLayout(size: imageSize)
if !imageHeight.isZero {
if case .intro = self.item.content {
insets.top -= 92.0
} else {
insets.top -= 160.0
}
}
let textSize = self.textNode.measure(CGSize(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 70.0, height: max(1.0, layout.size.height - insets.top - insets.bottom)))
@ -98,6 +115,8 @@ final class AttachmentFileEmptyStateItemNode: ItemListControllerEmptyStateItemNo
transition.updateAlpha(node: self.animationNode, alpha: imageHeight > 0.0 ? 1.0 : 0.0)
transition.updateFrame(node: self.animationNode, frame: CGRect(origin: CGPoint(x: floor((layout.size.width - imageSize.width) / 2.0), y: topOffset), size: imageSize))
self.animationNode.updateLayout(size: imageSize)
transition.updateFrame(node: self.textNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - textSize.width - layout.safeInsets.left - layout.safeInsets.right) / 2.0), y: topOffset + imageHeight), size: textSize))
}
}

View File

@ -24,6 +24,7 @@ private final class AttachmentFileSearchNavigationContentNode: NavigationBarCont
private var theme: PresentationTheme
private let strings: PresentationStrings
private let focus: () -> Void
private let cancel: () -> Void
private let searchBar: SearchBarNode
@ -34,10 +35,11 @@ private final class AttachmentFileSearchNavigationContentNode: NavigationBarCont
self.searchBar.activity = activity
}
}
init(theme: PresentationTheme, strings: PresentationStrings, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) {
init(theme: PresentationTheme, strings: PresentationStrings, focus: @escaping () -> Void, cancel: @escaping () -> Void, updateActivity: @escaping(@escaping(Bool)->Void) -> Void) {
self.theme = theme
self.strings = strings
self.focus = focus
self.cancel = cancel
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: false)
@ -55,6 +57,12 @@ private final class AttachmentFileSearchNavigationContentNode: NavigationBarCont
self?.queryUpdated?(query)
}
self.searchBar.focusUpdated = { [weak self] focus in
if focus {
self?.focus()
}
}
updateActivity({ [weak self] value in
self?.activity = value
})
@ -99,6 +107,7 @@ private final class AttachmentFileSearchNavigationContentNode: NavigationBarCont
final class AttachmentFileSearchItem: ItemListControllerSearch {
let context: AccountContext
let presentationData: PresentationData
let focus: () -> Void
let cancel: () -> Void
let send: (Message) -> Void
let dismissInput: () -> Void
@ -107,9 +116,10 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
private var activity: ValuePromise<Bool> = ValuePromise(ignoreRepeated: false)
private let activityDisposable = MetaDisposable()
init(context: AccountContext, presentationData: PresentationData, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
init(context: AccountContext, presentationData: PresentationData, focus: @escaping () -> Void, cancel: @escaping () -> Void, send: @escaping (Message) -> Void, dismissInput: @escaping () -> Void) {
self.context = context
self.presentationData = presentationData
self.focus = focus
self.cancel = cancel
self.send = send
self.dismissInput = dismissInput
@ -145,7 +155,7 @@ final class AttachmentFileSearchItem: ItemListControllerSearch {
current.updateTheme(presentationData.theme)
return current
} else {
return AttachmentFileSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, cancel: self.cancel, updateActivity: { [weak self] value in
return AttachmentFileSearchNavigationContentNode(theme: presentationData.theme, strings: presentationData.strings, focus: self.focus, cancel: self.cancel, updateActivity: { [weak self] value in
self?.updateActivity = value
})
}

View File

@ -10267,13 +10267,9 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let storeEditedPhotos = settings.storeEditedPhotos
let inputText = strongSelf.presentationInterfaceState.interfaceState.effectiveInputState.inputText
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, editingMedia: false, saveCapturedPhotos: storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
presentedLegacyCamera(context: strongSelf.context, peer: peer, chatLocation: strongSelf.chatLocation, cameraView: cameraView, menuController: nil, parentController: strongSelf, attachmentController: self?.attachmentController, editingMedia: false, saveCapturedPhotos: storeEditedPhotos, mediaGrouping: true, initialCaption: inputText.string, hasSchedule: strongSelf.presentationInterfaceState.subject != .scheduledMessages && peer.id.namespace != Namespaces.Peer.SecretChat, photoOnly: photoOnly, sendMessagesWithSignals: { [weak self] signals, silentPosting, scheduleTime in
if let strongSelf = self {
// if editMediaOptions != nil {
// strongSelf.editMessageMediaWithLegacySignals(signals!)
// } else {
strongSelf.enqueueMediaMessages(signals: signals, silentPosting: silentPosting, scheduleTime: scheduleTime > 0 ? scheduleTime : nil)
// }
if !inputText.string.isEmpty {
strongSelf.clearInputText()
}
@ -10316,6 +10312,8 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
return self?.getCaptionPanelView()
}, dismissedWithResult: { [weak self] in
self?.attachmentController?.dismiss(animated: false, completion: nil)
}, finishedTransitionIn: { [weak self] in
self?.attachmentController?.scrollToTop?()
})
})
}
@ -10348,7 +10346,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
var availableTabs: [AttachmentButtonType] = [.gallery, .file, .location, .contact]
if canSendPolls {
availableTabs.append(.poll)
availableTabs.insert(.poll, at: availableTabs.count - 1)
}
let inputText = self.presentationInterfaceState.interfaceState.effectiveInputState.inputText
@ -10367,12 +10365,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
strongSelf.controllerNavigationDisposable.set(nil)
let existingController = currentMediaController.with { $0 }
if let controller = existingController {
controller.prepareForReuse()
completion(controller, nil)
controller.prepareForReuse()
return
}
strongSelf.presentMediaPicker(bannedSendMedia: bannedSendMedia, present: { controller, mediaPickerContext in
let _ = currentMediaController.swap(controller)
mediaPickerContext?.setCaption(inputText)
completion(controller, mediaPickerContext)
}, updateMediaPickerContext: { [weak attachmentController] mediaPickerContext in
attachmentController?.mediaPickerContext = mediaPickerContext
@ -10387,9 +10386,10 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let existingController = currentFilesController.with { $0 }
if let controller = existingController {
completion(controller, nil)
controller.prepareForReuse()
return
}
let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, presentGallery: { [weak self, weak attachmentController] in
let controller = attachmentFileController(context: strongSelf.context, updatedPresentationData: strongSelf.updatedPresentationData, bannedSendMedia: bannedSendMedia, presentGallery: { [weak self, weak attachmentController] in
attachmentController?.dismiss(animated: true)
self?.presentFileGallery()
}, presentFiles: { [weak self, weak attachmentController] in
@ -10415,6 +10415,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
let existingController = currentLocationController.with { $0 }
if let controller = existingController {
completion(controller, nil)
controller.prepareForReuse()
return
}
let selfPeerId: PeerId
@ -10463,9 +10464,21 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
completion(contactsController, contactsController.mediaPickerContext)
strongSelf.controllerNavigationDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self, let (peers, _, silent, scheduleTime) = peers {
if let strongSelf = self, let (peers, _, silent, scheduleTime, text) = peers {
var textEnqueueMessage: EnqueueMessage?
if let text = text, text.length > 0 {
var attributes: [MessageAttribute] = []
let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text))
if !entities.isEmpty {
attributes.append(TextEntitiesMessageAttribute(entities: entities))
}
textEnqueueMessage = .message(text: text.string, attributes: attributes, mediaReference: nil, replyToMessageId: nil, localGroupingKey: nil, correlationId: nil)
}
if peers.count > 1 {
var enqueueMessages: [EnqueueMessage] = []
if let textEnqueueMessage = textEnqueueMessage {
enqueueMessages.append(textEnqueueMessage)
}
for peer in peers {
var media: TelegramMediaContact?
switch peer {
@ -10555,8 +10568,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.sendMessages(strongSelf.transformEnqueueMessages([message], silentPosting: silent, scheduleTime: scheduleTime))
var enqueueMessages: [EnqueueMessage] = []
if let textEnqueueMessage = textEnqueueMessage {
enqueueMessages.append(textEnqueueMessage)
}
enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil))
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
} else {
let contactController = strongSelf.context.sharedContext.makeDeviceContactInfoController(context: strongSelf.context, subject: .filter(peer: peerAndContactData.0, contactId: nil, contactData: contactData, completion: { peer, contactData in
guard let strongSelf = self, !contactData.basicData.phoneNumbers.isEmpty else {
@ -10573,8 +10591,13 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
})
}
}, nil)
let message = EnqueueMessage.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil)
strongSelf.sendMessages(strongSelf.transformEnqueueMessages([message], silentPosting: silent, scheduleTime: scheduleTime))
var enqueueMessages: [EnqueueMessage] = []
if let textEnqueueMessage = textEnqueueMessage {
enqueueMessages.append(textEnqueueMessage)
}
enqueueMessages.append(.message(text: "", attributes: [], mediaReference: .standalone(media: media), replyToMessageId: replyMessageId, localGroupingKey: nil, correlationId: nil))
strongSelf.sendMessages(strongSelf.transformEnqueueMessages(enqueueMessages, silentPosting: silent, scheduleTime: scheduleTime))
}
}), completed: nil, cancelled: nil)
strongSelf.effectiveNavigationController?.pushViewController(contactController)
@ -11007,10 +11030,18 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
controller.openCamera = { [weak self] cameraView in
self?.openCamera(cameraView: cameraView)
}
controller.presentWebSearch = { [weak self, weak controller] in
controller.presentWebSearch = { [weak self, weak controller] mediaGroups in
self?.presentWebSearch(editingMessage: false, attachment: true, present: { [weak controller] c, a in
controller?.present(c, in: .current)
if let webSearchController = c as? WebSearchController {
webSearchController.searchingUpdated = { [weak mediaGroups] searching in
if let mediaGroups = mediaGroups, mediaGroups.isNodeLoaded {
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
transition.updateAlpha(node: mediaGroups.displayNode, alpha: searching ? 0.0 : 1.0)
mediaGroups.displayNode.isUserInteractionEnabled = !searching
}
}
webSearchController.present(mediaGroups, in: .current)
webSearchController.dismissed = {
updateMediaPickerContext(mediaPickerContext)
}
@ -11292,7 +11323,7 @@ public final class ChatControllerImpl: TelegramBaseController, ChatController, G
self.effectiveNavigationController?.pushViewController(contactsController)
self.controllerNavigationDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak self] peers in
if let strongSelf = self, let (peers, _, _, _) = peers {
if let strongSelf = self, let (peers, _, _, _, _) = peers {
if peers.count > 1 {
var enqueueMessages: [EnqueueMessage] = []
for peer in peers {

View File

@ -868,6 +868,20 @@ class ChatMessageInstantVideoItemNode: ChatMessageItemView, UIGestureRecognizerD
}
}
let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
if let forwardBackgroundNode = strongSelf.forwardBackgroundNode {
transition.updateAlpha(node: forwardBackgroundNode, alpha: isPlaying ? 0.0 : 1.0)
}
if let replyBackgroundNode = strongSelf.replyBackgroundNode {
transition.updateAlpha(node: replyBackgroundNode, alpha: isPlaying ? 0.0 : 1.0)
}
if let forwardInfoNode = strongSelf.forwardInfoNode {
transition.updateAlpha(node: forwardInfoNode, alpha: isPlaying ? 0.0 : 1.0)
}
if let replyInfoNode = strongSelf.replyInfoNode {
transition.updateAlpha(node: replyInfoNode, alpha: isPlaying ? 0.0 : 1.0)
}
if let (_, f) = strongSelf.awaitingAppliedReaction {
strongSelf.awaitingAppliedReaction = nil

View File

@ -157,7 +157,7 @@ public class ComposeControllerImpl: ViewController, ComposeController {
strongSelf.createActionDisposable.set((controller.result
|> take(1)
|> deliverOnMainQueue).start(next: { [weak controller] result in
if let strongSelf = self, let (contactPeers, _, _, _) = result, case let .peer(peer, _, _) = contactPeers.first {
if let strongSelf = self, let (contactPeers, _, _, _, _) = result, case let .peer(peer, _, _) = contactPeers.first {
controller?.dismissSearch()
controller?.displayNavigationActivity = true
strongSelf.createActionDisposable.set((strongSelf.context.engine.peers.createSecretChat(peerId: peer.id) |> deliverOnMainQueue).start(next: { peerId in

View File

@ -43,8 +43,10 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
return self._ready
}
private let _result = Promise<([ContactListPeer], ContactListAction, Bool, Int32?)?>()
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?)?, NoError> {
fileprivate var caption: NSAttributedString?
private let _result = Promise<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?>()
var result: Signal<([ContactListPeer], ContactListAction, Bool, Int32?, NSAttributedString?)?, NoError> {
return self._result.get()
}
@ -74,6 +76,8 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
}
var requestAttachmentMenuExpansion: () -> Void = {}
var updateNavigationStack: (@escaping ([AttachmentContainable]) -> [AttachmentContainable]) -> Void = { _ in }
var updateTabBarAlpha: (CGFloat, ContainedViewLayoutTransition) -> Void = { _, _ in }
init(_ params: ContactSelectionControllerParams) {
self.context = params.context
@ -199,6 +203,10 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
}
}
self.contactsNode.cancelSearch = { [weak self] in
self?.deactivateSearch()
}
self.contactsNode.dismiss = { [weak self] in
self?.presentingViewController?.dismiss(animated: true, completion: nil)
}
@ -220,7 +228,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.contactsNode.requestMultipleAction = { [weak self] silent, scheduleTime in
if let strongSelf = self {
let selectedPeers = strongSelf.contactsNode.contactListNode.selectedPeers
strongSelf._result.set(.single((selectedPeers, .generic, silent, scheduleTime)))
strongSelf._result.set(.single((selectedPeers, .generic, silent, scheduleTime, strongSelf.caption)))
if strongSelf.autoDismiss {
strongSelf.dismiss()
}
@ -307,6 +315,8 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
if let searchContentNode = self.searchContentNode as? NavigationBarSearchContentNode {
self.contactsNode.deactivateSearch(placeholderNode: searchContentNode.placeholderNode)
}
} else if let searchContentNode = self.searchContentNode as? ContactsSearchNavigationContentNode {
searchContentNode.cancel()
}
}
@ -315,7 +325,7 @@ class ContactSelectionControllerImpl: ViewController, ContactSelectionController
self.confirmationDisposable.set((self.confirmation(peer) |> deliverOnMainQueue).start(next: { [weak self] value in
if let strongSelf = self {
if value {
strongSelf._result.set(.single(([peer], action, false, nil)))
strongSelf._result.set(.single(([peer], action, false, nil, nil)))
if strongSelf.autoDismiss {
strongSelf.dismiss()
}
@ -377,6 +387,10 @@ final class ContactsSearchNavigationContentNode: NavigationBarContentNode {
self.searchBar.deactivate(clear: false)
}
func cancel() {
self.searchBar.cancel?()
}
func updateActivity(_ activity: Bool) {
self.searchBar.activity = activity
}
@ -410,7 +424,7 @@ final class ContactsPickerContext: AttachmentMediaPickerContext {
}
func setCaption(_ caption: NSAttributedString) {
self.controller?.caption = caption
}
func send(silently: Bool, mode: AttachmentMediaPickerSendMode) {

View File

@ -38,6 +38,7 @@ final class ContactSelectionControllerNode: ASDisplayNode {
var requestOpenPeerFromSearch: ((ContactListPeer) -> Void)?
var requestMultipleAction: ((_ silent: Bool, _ scheduleTime: Int32?) -> Void)?
var dismiss: (() -> Void)?
var cancelSearch: (() -> Void)?
var presentationData: PresentationData {
didSet {
@ -216,6 +217,9 @@ final class ContactSelectionControllerNode: ASDisplayNode {
}
}
}, contextAction: nil)
searchContainerNode.cancel = { [weak self] in
self?.cancelSearch?()
}
self.insertSubnode(searchContainerNode, belowSubnode: navigationBar)
self.searchContainerNode = searchContainerNode

View File

@ -10,7 +10,7 @@ import ShareController
import LegacyUI
import LegacyMediaPickerUI
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: String, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}) {
func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: ChatLocation, cameraView: TGAttachmentCameraView?, menuController: TGMenuSheetController?, parentController: ViewController, attachmentController: ViewController? = nil, editingMedia: Bool, saveCapturedPhotos: Bool, mediaGrouping: Bool, initialCaption: String, hasSchedule: Bool, photoOnly: Bool, sendMessagesWithSignals: @escaping ([Any]?, Bool, Int32) -> Void, recognizedQRCode: @escaping (String) -> Void = { _ in }, presentSchedulePicker: @escaping (Bool, @escaping (Int32) -> Void) -> Void, presentTimerPicker: @escaping (@escaping (Int32) -> Void) -> Void, presentStickers: @escaping (@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?, getCaptionPanelView: @escaping () -> TGCaptionPanelView?, dismissedWithResult: @escaping () -> Void = {}, finishedTransitionIn: @escaping () -> Void = {}) {
let presentationData = context.sharedContext.currentPresentationData.with { $0 }
let legacyController = LegacyController(presentation: .custom, theme: presentationData.theme)
legacyController.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .portrait, compactSize: .portrait)
@ -94,7 +94,9 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
let screenSize = parentController.view.bounds.size
var startFrame = CGRect(x: 0, y: screenSize.height, width: screenSize.width, height: screenSize.height)
if let cameraView = cameraView {
if let menuController = menuController {
if let attachmentController = attachmentController {
startFrame = attachmentController.view.convert(cameraView.previewView()!.frame, from: cameraView)
} else if let menuController = menuController {
startFrame = menuController.view.convert(cameraView.previewView()!.frame, from: cameraView)
} else {
startFrame = parentController.view.convert(cameraView.previewView()!.frame, from: cameraView)
@ -109,7 +111,9 @@ func presentedLegacyCamera(context: AccountContext, peer: Peer, chatLocation: Ch
controller.view.disablesInteractiveTransitionGestureRecognizer = true
}
}
controller.finishedTransitionIn = {
finishedTransitionIn()
}
controller.beginTransitionOut = { [weak controller, weak cameraView] in
if let controller = controller, let cameraView = cameraView {
cameraView.willAttachPreviewView()

View File

@ -8059,7 +8059,7 @@ func presentAddMembers(context: AccountContext, updatedPresentationData: (initia
if let contactsController = contactsController as? ContactSelectionController {
selectAddMemberDisposable.set((contactsController.result
|> deliverOnMainQueue).start(next: { [weak contactsController] result in
guard let (peers, _, _, _) = result, let memberPeer = peers.first else {
guard let (peers, _, _, _, _) = result, let memberPeer = peers.first else {
return
}

View File

@ -164,6 +164,8 @@ public final class WebSearchController: ViewController {
public var dismissed: () -> Void = { }
public var searchingUpdated: (Bool) -> Void = { _ in }
public init(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal<PresentationData, NoError>)? = nil, peer: EnginePeer?, chatLocation: ChatLocation?, configuration: SearchBotsConfiguration, mode: WebSearchControllerMode) {
self.context = context
self.mode = mode
@ -237,6 +239,7 @@ public final class WebSearchController: ViewController {
navigationContentNode.setQueryUpdated { [weak self] query in
if let strongSelf = self, strongSelf.isNodeLoaded {
strongSelf.updateSearchQuery(query)
strongSelf.searchingUpdated(!query.isEmpty)
}
}
navigationContentNode.cancel = { [weak self] in
@ -354,7 +357,7 @@ public final class WebSearchController: ViewController {
if case let .media(attachmentValue, _) = self.mode, attachmentValue {
attachment = true
}
self.displayNode = WebSearchControllerNode(context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode, attachment: attachment)
self.displayNode = WebSearchControllerNode(controller: self, context: self.context, presentationData: self.interfaceState.presentationData, controllerInteraction: self.controllerInteraction!, peer: self.peer, chatLocation: self.chatLocation, mode: self.mode.mode, attachment: attachment)
self.controllerNode.requestUpdateInterfaceState = { [weak self] animated, f in
if let strongSelf = self {
strongSelf.updateInterfaceState(f)
@ -374,6 +377,8 @@ public final class WebSearchController: ViewController {
self._ready.set(.single(true))
self.displayNodeDidLoad()
self.controllerNode.updateBackgroundAlpha(0.0, transition: .immediate)
}
private func updateInterfaceState(animated: Bool = true, _ f: (WebSearchInterfaceState) -> WebSearchInterfaceState) {
@ -542,7 +547,8 @@ public final class WebSearchController: ViewController {
self.validLayout = layout
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: self.navigationLayout(layout: layout).navigationFrame.maxY, transition: transition)
let navigationBarHeight = self.navigationLayout(layout: layout).navigationFrame.maxY
self.controllerNode.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: transition)
}
}

View File

@ -64,8 +64,20 @@ private func preparedTransition(from fromEntries: [WebSearchEntry], to toEntries
return WebSearchTransition(deleteItems: deleteIndices, insertItems: insertions, updateItems: updates, entryCount: toEntries.count, hasMore: hasMore)
}
private func gridNodeLayoutForContainerLayout(size: CGSize, insets: UIEdgeInsets) -> GridNodeLayoutType {
let side = floorToScreenPixels((size.width - insets.left - insets.right - 2.0) / 3.0)
private func gridNodeLayoutForContainerLayout(_ layout: ContainerViewLayout) -> GridNodeLayoutType {
let itemsPerRow: Int
if case .compact = layout.metrics.widthClass {
switch layout.orientation {
case .portrait:
itemsPerRow = 3
case .landscape:
itemsPerRow = 5
}
} else {
itemsPerRow = 3
}
let side = floorToScreenPixels((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - CGFloat(itemsPerRow - 1)) / CGFloat(itemsPerRow))
return .fixed(itemSize: CGSize(width: side, height: side), fillWidth: true, lineSpacing: 1.0, itemSpacing: 1.0)
}
@ -116,6 +128,7 @@ private func preparedWebSearchRecentTransition(from fromEntries: [WebSearchRecen
}
class WebSearchControllerNode: ASDisplayNode {
private weak var controller: WebSearchController?
private let context: AccountContext
private let peer: EnginePeer?
private let chatLocation: ChatLocation?
@ -129,6 +142,7 @@ class WebSearchControllerNode: ASDisplayNode {
private var webSearchInterfaceState: WebSearchInterfaceState
private let webSearchInterfaceStatePromise: ValuePromise<WebSearchInterfaceState>
private let segmentedContainerNode: ASDisplayNode
private let segmentedBackgroundNode: ASDisplayNode
private let segmentedSeparatorNode: ASDisplayNode
private let segmentedControlNode: SegmentedControlNode
@ -173,7 +187,8 @@ class WebSearchControllerNode: ASDisplayNode {
var presentStickers: ((@escaping (TelegramMediaFile, Bool, UIView, CGRect) -> Void) -> TGPhotoPaintStickersScreen?)?
var getCaptionPanelView: () -> TGCaptionPanelView? = { return nil }
init(context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) {
init(controller: WebSearchController, context: AccountContext, presentationData: PresentationData, controllerInteraction: WebSearchControllerInteraction, peer: EnginePeer?, chatLocation: ChatLocation?, mode: WebSearchMode, attachment: Bool) {
self.controller = controller
self.context = context
self.theme = presentationData.theme
self.strings = presentationData.strings
@ -187,6 +202,9 @@ class WebSearchControllerNode: ASDisplayNode {
self.webSearchInterfaceState = WebSearchInterfaceState(presentationData: context.sharedContext.currentPresentationData.with { $0 })
self.webSearchInterfaceStatePromise = ValuePromise(self.webSearchInterfaceState, ignoreRepeated: true)
self.segmentedContainerNode = ASDisplayNode()
self.segmentedContainerNode.clipsToBounds = true
self.segmentedBackgroundNode = ASDisplayNode()
self.segmentedSeparatorNode = ASDisplayNode()
@ -225,10 +243,11 @@ class WebSearchControllerNode: ASDisplayNode {
if !attachment {
self.addSubnode(self.recentQueriesNode)
}
self.addSubnode(self.segmentedBackgroundNode)
self.addSubnode(self.segmentedSeparatorNode)
self.addSubnode(self.segmentedContainerNode)
self.segmentedContainerNode.addSubnode(self.segmentedBackgroundNode)
self.segmentedContainerNode.addSubnode(self.segmentedSeparatorNode)
if case .media = mode {
self.addSubnode(self.segmentedControlNode)
self.segmentedContainerNode.addSubnode(self.segmentedControlNode)
}
if !attachment {
self.addSubnode(self.toolbarBackgroundNode)
@ -339,6 +358,11 @@ class WebSearchControllerNode: ASDisplayNode {
self.applyPresentationData(themeUpdated: themeUpdated)
}
func updateBackgroundAlpha(_ alpha: CGFloat, transition: ContainedViewLayoutTransition) {
self.controller?.navigationBar?.updateBackgroundAlpha(0.0, transition: transition)
transition.updateAlpha(node: self.segmentedBackgroundNode, alpha: alpha)
}
func applyPresentationData(themeUpdated: Bool = true) {
self.cancelButton.setTitle(self.strings.Common_Cancel, with: Font.regular(17.0), with: self.theme.rootController.navigationBar.accentTextColor, for: .normal)
@ -349,6 +373,7 @@ class WebSearchControllerNode: ASDisplayNode {
}
if themeUpdated {
self.gridNode.backgroundColor = self.theme.list.plainBackgroundColor
self.segmentedBackgroundNode.backgroundColor = self.theme.rootController.navigationBar.opaqueBackgroundColor
self.segmentedSeparatorNode.backgroundColor = self.theme.rootController.navigationBar.separatorColor
self.segmentedControlNode.updateTheme(SegmentedControlTheme(theme: self.theme))
@ -393,14 +418,19 @@ class WebSearchControllerNode: ASDisplayNode {
var insets = layout.insets(options: [.input])
insets.top += navigationBarHeight
let hasQuery = !(self.webSearchInterfaceState.state?.query ?? "").isEmpty
let segmentedHeight: CGFloat = self.segmentedControlNode.supernode != nil ? 44.0 : 5.0
let panelY: CGFloat = insets.top - UIScreenPixel - 4.0
transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight)))
transition.updateFrame(node: self.segmentedSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY + segmentedHeight), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
transition.updateSublayerTransformOffset(layer: self.segmentedContainerNode.layer, offset: CGPoint(x: 0.0, y: !hasQuery ? -44.0 : 0.0), completion: nil)
transition.updateFrame(node: self.segmentedContainerNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelY), size: CGSize(width: layout.size.width, height: segmentedHeight)))
transition.updateFrame(node: self.segmentedBackgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: layout.size.width, height: segmentedHeight)))
transition.updateFrame(node: self.segmentedSeparatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: segmentedHeight - UIScreenPixel), size: CGSize(width: layout.size.width, height: UIScreenPixel)))
let controlSize = self.segmentedControlNode.updateLayout(.stretchToFill(width: layout.size.width - layout.safeInsets.left - layout.safeInsets.right - 10.0 * 2.0), transition: transition)
transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - controlSize.width) / 2.0), y: panelY + 5.0), size: controlSize))
transition.updateFrame(node: self.segmentedControlNode, frame: CGRect(origin: CGPoint(x: layout.safeInsets.left + floor((layout.size.width - layout.safeInsets.left - layout.safeInsets.right - controlSize.width) / 2.0), y: 5.0), size: controlSize))
insets.top -= 4.0
@ -466,7 +496,7 @@ class WebSearchControllerNode: ASDisplayNode {
insets.top += segmentedHeight
insets.bottom += toolbarHeight
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(size: layout.size, insets: insets)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in })
self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: layout.size, insets: insets, preloadSize: 400.0, type: gridNodeLayoutForContainerLayout(layout)), transition: .immediate), itemTransition: .immediate, stationaryItems: .none,updateFirstIndexInSectionOffset: nil), completion: { _ in })
let (duration, curve) = listViewAnimationDurationAndCurve(transition: transition)
@ -541,7 +571,7 @@ class WebSearchControllerNode: ASDisplayNode {
if self.recentQueriesNode.supernode != nil {
self.insertSubnode(gridNode, belowSubnode: self.recentQueriesNode)
} else {
self.addSubnode(gridNode)
self.insertSubnode(gridNode, aboveSubnode: previousNode)
}
self.gridNode = gridNode
self.currentEntries = nil

View File

@ -22,7 +22,7 @@ final class WebSearchNavigationContentNode: NavigationBarContentNode {
self.theme = theme
self.strings = strings
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern)
self.searchBar = SearchBarNode(theme: SearchBarNodeTheme(theme: theme, hasSeparator: false), strings: strings, fieldStyle: .modern, displayBackground: !attachment)
self.searchBar.hasCancelButton = attachment
self.searchBar.placeholderString = NSAttributedString(string: attachment ? strings.Attachment_SearchWeb : strings.Common_Search, font: searchBarFont, textColor: theme.rootController.navigationSearchBar.inputPlaceholderTextColor)
@ -30,14 +30,9 @@ final class WebSearchNavigationContentNode: NavigationBarContentNode {
self.addSubnode(self.searchBar)
self.searchBar.textReturned = { [weak self] query in
self?.queryUpdated?(query)
}
self.searchBar.textUpdated = { [weak self] query, _ in
if query.isEmpty {
self?.queryUpdated?(query)
}
}
self.searchBar.cancel = { [weak self] in
self?.cancel?()
}