diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 0820fe1a67..37c5051f5e 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -774,8 +774,25 @@ public struct StoryCameraTransitionOut { } } +public struct StoryCameraTransitionInCoordinator { + public let animateIn: () -> Void + public let updateTransitionProgress: (CGFloat) -> Void + public let completeWithTransitionProgressAndVelocity: (CGFloat, CGFloat) -> Void + + public init( + animateIn: @escaping () -> Void, + updateTransitionProgress: @escaping (CGFloat) -> Void, + completeWithTransitionProgressAndVelocity: @escaping (CGFloat, CGFloat) -> Void + ) { + self.animateIn = animateIn + self.updateTransitionProgress = updateTransitionProgress + self.completeWithTransitionProgressAndVelocity = completeWithTransitionProgressAndVelocity + } +} + public protocol TelegramRootControllerInterface: NavigationController { - func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) + @discardableResult + func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? } public protocol SharedAccountContext: AnyObject { @@ -874,7 +891,7 @@ public protocol SharedAccountContext: AnyObject { func makeStickerPackScreen(context: AccountContext, updatedPresentationData: (initial: PresentationData, signal: Signal)?, mainStickerPack: StickerPackReference, stickerPacks: [StickerPackReference], loadedStickerPacks: [LoadedStickerPack], parentNavigationController: NavigationController?, sendSticker: ((FileMediaReference, UIView, CGRect) -> Bool)?) -> ViewController - func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController + func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 2a2daea5b1..0a469b2820 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -46,9 +46,10 @@ private final class CameraContext { let ciContext = CIContext() var ciImage = CIImage(cvImageBuffer: pixelBuffer) ciImage = ciImage.transformed(by: CGAffineTransform(scaleX: 0.33, y: 0.33)) + ciImage = ciImage.clampedToExtent() if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) { let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) - CameraSimplePreviewView.saveLastState(uiImage) + CameraSimplePreviewView.saveLastStateImage(uiImage) } } } diff --git a/submodules/Camera/Sources/CameraPreviewView.swift b/submodules/Camera/Sources/CameraPreviewView.swift index 974f6668c4..3ed8925847 100644 --- a/submodules/Camera/Sources/CameraPreviewView.swift +++ b/submodules/Camera/Sources/CameraPreviewView.swift @@ -19,7 +19,7 @@ public class CameraSimplePreviewView: UIView { } } - static func saveLastState(_ image: UIImage) { + static func saveLastStateImage(_ image: UIImage) { let imagePath = NSTemporaryDirectory() + "cameraImage.jpg" if let blurredImage = blurredImage(image, radius: 60.0), let data = blurredImage.jpegData(compressionQuality: 0.85) { try? data.write(to: URL(fileURLWithPath: imagePath)) diff --git a/submodules/Camera/Sources/VideoRecorder.swift b/submodules/Camera/Sources/VideoRecorder.swift index e67f6c8773..234957074c 100644 --- a/submodules/Camera/Sources/VideoRecorder.swift +++ b/submodules/Camera/Sources/VideoRecorder.swift @@ -93,7 +93,7 @@ final class VideoRecorder { self.isRecording = true - assetWriter.startWriting() + //assetWriter.startWriting() } } @@ -132,17 +132,15 @@ final class VideoRecorder { } } - private var skippedCount = 0 func appendVideo(sampleBuffer: CMSampleBuffer) { - if self.skippedCount < 2 { - self.skippedCount += 1 - return - } self.queue.async { guard let assetWriter = self.assetWriter, let videoInput = self.videoInput, (self.isRecording || self.isStopping) && !self.finishedWriting else { return } let timestamp = sampleBuffer.presentationTimestamp + if let startTimestamp = self.captureStartTimestamp, timestamp.seconds < startTimestamp { + return + } switch assetWriter.status { case .unknown: diff --git a/submodules/ChatListUI/Sources/ChatListController.swift b/submodules/ChatListUI/Sources/ChatListController.swift index 1d11364e8d..4b0b2b3faa 100644 --- a/submodules/ChatListUI/Sources/ChatListController.swift +++ b/submodules/ChatListUI/Sources/ChatListController.swift @@ -2439,17 +2439,6 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } } - var cameraTransitionIn: StoryCameraTransitionIn? - if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { - if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { - cameraTransitionIn = StoryCameraTransitionIn( - sourceView: transitionView, - sourceRect: transitionView.bounds, - sourceCornerRadius: transitionView.bounds.height * 0.5 - ) - } - } - var initialFocusedId: AnyHashable? if let peer { initialFocusedId = AnyHashable(peer.id) @@ -2457,11 +2446,11 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController if initialFocusedId == AnyHashable(self.context.account.peerId), let firstItem = initialContent.first, firstItem.id == initialFocusedId && firstItem.items.isEmpty { if let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface { - rootController.openStoryCamera(transitionIn: cameraTransitionIn, transitionOut: { [weak self] _ in + let coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in guard let self else { return nil } - if let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { return StoryCameraTransitionOut( destinationView: transitionView, @@ -2472,6 +2461,7 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController } return nil }) + coordinator?.animateIn() } return @@ -4862,6 +4852,43 @@ public class ChatListControllerImpl: TelegramBaseController, ChatListController self.view.addSubview(ConfettiView(frame: self.view.bounds)) } } + + private var storyCameraTransitionInCoordinator: StoryCameraTransitionInCoordinator? + func storyCameraPanGestureChanged(transitionFraction: CGFloat) { + guard let rootController = self.context.sharedContext.mainWindow?.viewController as? TelegramRootControllerInterface else { + return + } + + let coordinator: StoryCameraTransitionInCoordinator? + if let current = self.storyCameraTransitionInCoordinator { + coordinator = current + } else { + coordinator = rootController.openStoryCamera(transitionIn: nil, transitionOut: { [weak self] finished in + guard let self else { + return nil + } + if finished, let componentView = self.headerContentView.view as? ChatListHeaderComponent.View { + if let transitionView = componentView.storyPeerListView()?.transitionViewForItem(peerId: self.context.account.peerId) { + return StoryCameraTransitionOut( + destinationView: transitionView, + destinationRect: transitionView.bounds, + destinationCornerRadius: transitionView.bounds.height * 0.5 + ) + } + } + return nil + }) + self.storyCameraTransitionInCoordinator = coordinator + } + coordinator?.updateTransitionProgress(transitionFraction) + } + + func storyCameraPanGestureEnded(transitionFraction: CGFloat, velocity: CGFloat) { + if let coordinator = self.storyCameraTransitionInCoordinator { + coordinator.completeWithTransitionProgressAndVelocity(transitionFraction, velocity) + self.storyCameraTransitionInCoordinator = nil + } + } } private final class ChatListTabBarContextExtractedContentSource: ContextExtractedContentSource { diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index be1c1b7053..ae54b3ca1f 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1081,8 +1081,11 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele } if selectedIndex <= 0 && translation.x > 0.0 { - let overscroll = translation.x - transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width + //let overscroll = translation.x + //transitionFraction = rubberBandingOffset(offset: overscroll, bandingStart: 0.0) / layout.size.width + transitionFraction = 0.0 + + self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width) } if selectedIndex >= maxFilterIndex && translation.x < 0.0 { @@ -1159,6 +1162,8 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele self.update(layout: layout, navigationBarHeight: navigationBarHeight, visualNavigationHeight: visualNavigationHeight, originalNavigationHeight: originalNavigationHeight, cleanNavigationBarHeight: cleanNavigationBarHeight, insets: insets, isReorderingFilters: isReorderingFilters, isEditing: isEditing, inlineNavigationLocation: inlineNavigationLocation, inlineNavigationTransitionFraction: inlineNavigationTransitionFraction, transition: .immediate) } } + + self.controller?.storyCameraPanGestureEnded(transitionFraction: translation.x / layout.size.width, velocity: velocity.x) } default: break diff --git a/submodules/ContactListUI/BUILD b/submodules/ContactListUI/BUILD index 7d77adb63e..54e01e9ff0 100644 --- a/submodules/ContactListUI/BUILD +++ b/submodules/ContactListUI/BUILD @@ -36,6 +36,7 @@ swift_library( "//submodules/AnimatedStickerNode:AnimatedStickerNode", "//submodules/TelegramAnimatedStickerNode:TelegramAnimatedStickerNode", "//submodules/QrCodeUI:QrCodeUI", + "//submodules/LocalizedPeerData:LocalizedPeerData" ], visibility = [ "//visibility:public", diff --git a/submodules/ContactListUI/Sources/ContactListNode.swift b/submodules/ContactListUI/Sources/ContactListNode.swift index 2f5960fe20..7bfa65ab5b 100644 --- a/submodules/ContactListUI/Sources/ContactListNode.swift +++ b/submodules/ContactListUI/Sources/ContactListNode.swift @@ -22,6 +22,7 @@ import TelegramPermissionsUI import AppBundle import ContextUI import PhoneNumberFormat +import LocalizedPeerData private let dropDownIcon = { () -> UIImage in UIGraphicsBeginImageContextWithOptions(CGSize(width: 12.0, height: 12.0), false, 0.0) @@ -369,72 +370,6 @@ private enum ContactListNodeEntry: Comparable, Identifiable { } } -private extension EnginePeer.IndexName { - func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult { - switch self { - case let .title(lhsTitle, _): - let rhsString: String - switch other { - case let .title(title, _): - rhsString = title - case let .personName(first, last, _, _): - switch ordering { - case .firstLast: - if first.isEmpty { - rhsString = last - } else { - rhsString = first + last - } - case .lastFirst: - if last.isEmpty { - rhsString = first - } else { - rhsString = last + first - } - } - } - return lhsTitle.caseInsensitiveCompare(rhsString) - case let .personName(lhsFirst, lhsLast, _, _): - let lhsString: String - switch ordering { - case .firstLast: - if lhsFirst.isEmpty { - lhsString = lhsLast - } else { - lhsString = lhsFirst + lhsLast - } - case .lastFirst: - if lhsLast.isEmpty { - lhsString = lhsFirst - } else { - lhsString = lhsLast + lhsFirst - } - } - let rhsString: String - switch other { - case let .title(title, _): - rhsString = title - case let .personName(first, last, _, _): - switch ordering { - case .firstLast: - if first.isEmpty { - rhsString = last - } else { - rhsString = first + last - } - case .lastFirst: - if last.isEmpty { - rhsString = first - } else { - rhsString = last + first - } - } - } - return lhsString.caseInsensitiveCompare(rhsString) - } - } -} - private func contactListNodeEntries(accountPeer: EnginePeer?, peers: [ContactListPeer], presences: [EnginePeer.Id: EnginePeer.Presence], presentation: ContactListPresentation, selectionState: ContactListNodeGroupSelectionState?, theme: PresentationTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, sortOrder: PresentationPersonNameOrder, displayOrder: PresentationPersonNameOrder, disabledPeerIds: Set, authorizationStatus: AccessType, warningSuppressed: (Bool, Bool), displaySortOptions: Bool, displayCallIcons: Bool) -> [ContactListNodeEntry] { var entries: [ContactListNodeEntry] = [] diff --git a/submodules/Display/Source/ContainableController.swift b/submodules/Display/Source/ContainableController.swift index 22da56873a..5b9b269f10 100644 --- a/submodules/Display/Source/ContainableController.swift +++ b/submodules/Display/Source/ContainableController.swift @@ -21,7 +21,6 @@ public protocol ContainableController: AnyObject { func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) func updateToInterfaceOrientation(_ orientation: UIInterfaceOrientation) - func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) func preferredContentSizeForLayout(_ layout: ContainerViewLayout) -> CGSize? func viewWillAppear(_ animated: Bool) diff --git a/submodules/Display/Source/ContainedViewLayoutTransition.swift b/submodules/Display/Source/ContainedViewLayoutTransition.swift index 6aaea8af80..14efefd113 100644 --- a/submodules/Display/Source/ContainedViewLayoutTransition.swift +++ b/submodules/Display/Source/ContainedViewLayoutTransition.swift @@ -1106,9 +1106,13 @@ public extension ContainedViewLayoutTransition { } func updateTransform(node: ASDisplayNode, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { + self.updateTransform(layer: node.layer, transform: transform, beginWithCurrentState: beginWithCurrentState, delay: delay, completion: completion) + } + + func updateTransform(layer: CALayer, transform: CGAffineTransform, beginWithCurrentState: Bool = false, delay: Double = 0.0, completion: ((Bool) -> Void)? = nil) { let transform = CATransform3DMakeAffineTransform(transform) - if CATransform3DEqualToTransform(node.layer.transform, transform) { + if CATransform3DEqualToTransform(layer.transform, transform) { if let completion = completion { completion(true) } @@ -1117,19 +1121,19 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: - node.layer.transform = transform + layer.transform = transform if let completion = completion { completion(true) } case let .animated(duration, curve): let previousTransform: CATransform3D - if beginWithCurrentState, let presentation = node.layer.presentation() { + if beginWithCurrentState, let presentation = layer.presentation() { previousTransform = presentation.transform } else { - previousTransform = node.layer.transform + previousTransform = layer.transform } - node.layer.transform = transform - node.layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in + layer.transform = transform + layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: transform), keyPath: "transform", timingFunction: curve.timingFunction, duration: duration, mediaTimingFunction: curve.mediaTimingFunction, completion: { value in completion?(value) }) } @@ -1262,13 +1266,8 @@ public extension ContainedViewLayoutTransition { } } - func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { - if !node.isNodeLoaded { - node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) - completion?(true) - return - } - let t = node.layer.sublayerTransform + func updateSublayerTransformScaleAndOffset(layer: CALayer, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + let t = layer.sublayerTransform let currentScale = sqrt((t.m11 * t.m11) + (t.m12 * t.m12) + (t.m13 * t.m13)) let currentOffset = CGPoint(x: t.m41 / currentScale, y: t.m42 / currentScale) if abs(currentScale - scale) <= CGFloat.ulpOfOne && abs(currentOffset.x - offset.x) <= CGFloat.ulpOfOne && abs(currentOffset.y - offset.y) <= CGFloat.ulpOfOne { @@ -1282,21 +1281,21 @@ public extension ContainedViewLayoutTransition { switch self { case .immediate: - node.layer.removeAnimation(forKey: "sublayerTransform") - node.layer.sublayerTransform = transform + layer.removeAnimation(forKey: "sublayerTransform") + layer.sublayerTransform = transform if let completion = completion { completion(true) } case let .animated(duration, curve): let initialTransform: CATransform3D - if beginWithCurrentState, node.isNodeLoaded { - initialTransform = node.layer.presentation()?.sublayerTransform ?? t + if beginWithCurrentState { + initialTransform = layer.presentation()?.sublayerTransform ?? t } else { initialTransform = t } - node.layer.sublayerTransform = transform - node.layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: node.layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { + layer.sublayerTransform = transform + layer.animate(from: NSValue(caTransform3D: initialTransform), to: NSValue(caTransform3D: layer.sublayerTransform), keyPath: "sublayerTransform", timingFunction: curve.timingFunction, duration: duration, delay: 0.0, mediaTimingFunction: curve.mediaTimingFunction, removeOnCompletion: true, additive: false, completion: { result in if let completion = completion { completion(result) @@ -1305,6 +1304,15 @@ public extension ContainedViewLayoutTransition { } } + func updateSublayerTransformScaleAndOffset(node: ASDisplayNode, scale: CGFloat, offset: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { + if !node.isNodeLoaded { + node.subnodeTransform = CATransform3DMakeScale(scale, scale, 1.0) + completion?(true) + return + } + return updateSublayerTransformScaleAndOffset(layer: node.layer, scale: scale, offset: offset, beginWithCurrentState: beginWithCurrentState, completion: completion) + } + func updateSublayerTransformScale(node: ASDisplayNode, scale: CGPoint, beginWithCurrentState: Bool = false, completion: ((Bool) -> Void)? = nil) { if !node.isNodeLoaded { node.subnodeTransform = CATransform3DMakeScale(scale.x, scale.y, 1.0) diff --git a/submodules/Display/Source/DeviceMetrics.swift b/submodules/Display/Source/DeviceMetrics.swift index c2eda7b160..3e58e08d6e 100644 --- a/submodules/Display/Source/DeviceMetrics.swift +++ b/submodules/Display/Source/DeviceMetrics.swift @@ -185,13 +185,15 @@ public enum DeviceMetrics: CaseIterable, Equatable { case .iPhoneX, .iPhoneXSMax: return 39.0 case .iPhoneXr: - return 41.0 + UIScreenPixel + return 41.5 case .iPhone12Mini: return 44.0 - case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14Pro, .iPhone14ProZoomed: + case .iPhone12, .iPhone13, .iPhone13Pro, .iPhone14ProZoomed: return 47.0 + UIScreenPixel - case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMax, .iPhone14ProMaxZoomed: + case .iPhone12ProMax, .iPhone13ProMax, .iPhone14ProMaxZoomed: return 53.0 + UIScreenPixel + case .iPhone14Pro, .iPhone14ProMax: + return 55.0 case let .unknown(_, _, onScreenNavigationHeight): if let _ = onScreenNavigationHeight { return 39.0 diff --git a/submodules/Display/Source/GenerateImage.swift b/submodules/Display/Source/GenerateImage.swift index 114b2929c7..35d1cee10a 100644 --- a/submodules/Display/Source/GenerateImage.swift +++ b/submodules/Display/Source/GenerateImage.swift @@ -566,7 +566,7 @@ public class DrawingContext { f(self.context) } - public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil) { + public init?(size: CGSize, scale: CGFloat = 0.0, opaque: Bool = false, clear: Bool = false, bytesPerRow: Int? = nil, colorSpace: CGColorSpace? = nil) { if size.width <= 0.0 || size.height <= 0.0 { return nil } @@ -601,7 +601,7 @@ public class DrawingContext { height: Int(self.scaledSize.height), bitsPerComponent: DeviceGraphicsContextSettings.shared.bitsPerComponent, bytesPerRow: self.bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, + space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: self.bitmapInfo.rawValue, releaseCallback: nil, releaseInfo: nil @@ -616,7 +616,7 @@ public class DrawingContext { } } - public func generateImage() -> UIImage? { + public func generateImage(colorSpace: CGColorSpace? = nil) -> UIImage? { if self.scaledSize.width.isZero || self.scaledSize.height.isZero { return nil } @@ -633,7 +633,7 @@ public class DrawingContext { bitsPerComponent: self.context.bitsPerComponent, bitsPerPixel: self.context.bitsPerPixel, bytesPerRow: self.context.bytesPerRow, - space: DeviceGraphicsContextSettings.shared.colorSpace, + space: colorSpace ?? DeviceGraphicsContextSettings.shared.colorSpace, bitmapInfo: self.context.bitmapInfo, provider: dataProvider, decode: nil, diff --git a/submodules/Display/Source/Navigation/NavigationController.swift b/submodules/Display/Source/Navigation/NavigationController.swift index da9591bf79..6ad2308f96 100644 --- a/submodules/Display/Source/Navigation/NavigationController.swift +++ b/submodules/Display/Source/Navigation/NavigationController.swift @@ -1274,9 +1274,6 @@ open class NavigationController: UINavigationController, ContainableController, self.filterController(controller, animated: false) } - public func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { - } - private func scrollToTop(_ subject: NavigationSplitContainerScrollToTop) { if let _ = self.inCallStatusBar { self.inCallNavigate?() @@ -1765,4 +1762,11 @@ open class NavigationController: UINavigationController, ContainableController, private func notifyAccessibilityScreenChanged() { UIAccessibility.post(notification: UIAccessibility.Notification.screenChanged, argument: nil) } + + public func updateRootContainerTransitionOffset(_ offset: CGFloat, transition: ContainedViewLayoutTransition) { + guard let rootContainer = self.rootContainer, case let .flat(container) = rootContainer else { + return + } + transition.updateTransform(node: container, transform: CGAffineTransformMakeTranslation(offset, 0.0)) + } } diff --git a/submodules/Display/Source/ViewController.swift b/submodules/Display/Source/ViewController.swift index 1444cb8bcf..b7745a3820 100644 --- a/submodules/Display/Source/ViewController.swift +++ b/submodules/Display/Source/ViewController.swift @@ -166,10 +166,12 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { public private(set) var modalStyleOverlayTransitionFactor: CGFloat = 0.0 public var modalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)? + public var customModalStyleOverlayTransitionFactorUpdated: ((ContainedViewLayoutTransition) -> Void)? public func updateModalStyleOverlayTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { if self.modalStyleOverlayTransitionFactor != value { self.modalStyleOverlayTransitionFactor = value self.modalStyleOverlayTransitionFactorUpdated?(transition) + self.customModalStyleOverlayTransitionFactorUpdated?(transition) } } @@ -452,10 +454,6 @@ public protocol CustomViewControllerNavigationDataSummary: AnyObject { } } - open func updateModalTransition(_ value: CGFloat, transition: ContainedViewLayoutTransition) { - - } - open func navigationStackConfigurationUpdated(next: [ViewController]) { } diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 6e59843601..6f346a965b 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -51,7 +51,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { private let size: CGSize weak var drawingView: DrawingView? - weak var selectionContainerView: DrawingSelectionContainerView? + public weak var selectionContainerView: DrawingSelectionContainerView? private var tapGestureRecognizer: UITapGestureRecognizer! private(set) var selectedEntityView: DrawingEntityView? @@ -220,7 +220,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return CGSize(width: width, height: width) } - func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { + public func prepareNewEntity(_ entity: DrawingEntity, setup: Bool = true, relativeTo: DrawingEntity? = nil) { let center = self.startPosition(relativeTo: relativeTo) let rotation = self.getEntityInitialRotation() let zoomScale = 1.0 / (self.drawingView?.zoomScale ?? 1.0) @@ -485,7 +485,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { } } - func selectEntity(_ entity: DrawingEntity?) { + public func selectEntity(_ entity: DrawingEntity?) { if entity?.isMedia == true { return } @@ -596,7 +596,7 @@ public class DrawingEntityView: UIView { let entity: DrawingEntity var isTracking = false - weak var selectionView: DrawingEntitySelectionView? + public weak var selectionView: DrawingEntitySelectionView? weak var containerView: DrawingEntitiesView? var onSnapToXAxis: (Bool) -> Void = { _ in } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index 15384d14c5..b78e163148 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -22,6 +22,7 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia didSet { if let previewView = self.previewView { previewView.isUserInteractionEnabled = false + previewView.layer.allowsEdgeAntialiasing = true self.addSubview(previewView) } } diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index d791ee91e2..712a2b5077 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -2310,8 +2310,13 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private var _selectionContainerView: DrawingSelectionContainerView? var selectionContainerView: DrawingSelectionContainerView { - if self._selectionContainerView == nil { - self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) + if self._selectionContainerView == nil, let controller = self.controller { + if let externalSelectionContainerView = controller.externalSelectionContainerView { + self._selectionContainerView = externalSelectionContainerView + } else { + self._selectionContainerView = DrawingSelectionContainerView(frame: .zero) + } + } return self._selectionContainerView! } @@ -3013,6 +3018,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U private let isAvatar: Bool private let externalDrawingView: DrawingView? private let externalEntitiesView: DrawingEntitiesView? + private let externalSelectionContainerView: DrawingSelectionContainerView? private let existingStickerPickerInputData: Promise? public var requestDismiss: () -> Void = {} @@ -3020,7 +3026,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U public var getCurrentImage: () -> UIImage? = { return nil } public var updateVideoPlayback: (Bool) -> Void = { _ in } - public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, existingStickerPickerInputData: Promise? = nil) { + public init(context: AccountContext, sourceHint: SourceHint? = nil, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, drawingView: DrawingView?, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?, selectionContainerView: DrawingSelectionContainerView?, existingStickerPickerInputData: Promise? = nil) { self.context = context self.sourceHint = sourceHint self.size = size @@ -3041,6 +3047,12 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U self.externalEntitiesView = nil } + if let selectionContainerView = selectionContainerView { + self.externalSelectionContainerView = selectionContainerView + } else { + self.externalSelectionContainerView = nil + } + super.init(navigationBarPresentationData: nil) self.statusBar.statusBarStyle = .Hide @@ -3081,13 +3093,7 @@ public class DrawingScreen: ViewController, TGPhotoDrawingInterfaceController, U return nil } - let drawingImage = generateImage(self.drawingView.imageSize, contextGenerator: { size, context in - let bounds = CGRect(origin: .zero, size: size) - context.clear(bounds) - if let cgImage = self.drawingView.drawingImage?.cgImage { - context.draw(cgImage, in: bounds) - } - }, opaque: false, scale: 1.0) + let drawingImage = self.drawingView.drawingImage let _ = self.entitiesView.entitiesData let codableEntities = self.entitiesView.entities.filter { !($0 is DrawingMediaEntity) }.compactMap({ CodableDrawingEntity(entity: $0) }) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index 47ca066839..a35bd6e844 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -201,7 +201,7 @@ private final class StickerSelectionComponent: Component { } } -class StickerPickerScreen: ViewController { +public class StickerPickerScreen: ViewController { final class Node: ViewControllerTracingNode, UIScrollViewDelegate, UIGestureRecognizerDelegate { private var presentationData: PresentationData private weak var controller: StickerPickerScreen? @@ -999,9 +999,9 @@ class StickerPickerScreen: ViewController { public var pushController: (ViewController) -> Void = { _ in } public var presentController: (ViewController) -> Void = { _ in } - var completion: (TelegramMediaFile?) -> Void = { _ in } + public var completion: (TelegramMediaFile?) -> Void = { _ in } - init(context: AccountContext, inputData: Signal) { + public init(context: AccountContext, inputData: Signal) { self.context = context self.theme = defaultDarkColorPresentationTheme self.inputData = inputData @@ -1015,14 +1015,14 @@ class StickerPickerScreen: ViewController { fatalError("init(coder:) has not been implemented") } - override func loadDisplayNode() { + public override func loadDisplayNode() { self.displayNode = Node(context: self.context, controller: self, theme: self.theme) self.displayNodeDidLoad() self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) } - override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { self.view.endEditing(true) if flag { self.node.animateOut(completion: { @@ -1035,19 +1035,19 @@ class StickerPickerScreen: ViewController { } } - override func viewDidAppear(_ animated: Bool) { + public override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) self.node.updateIsVisible(isVisible: true) } - override func viewDidDisappear(_ animated: Bool) { + public override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) self.node.updateIsVisible(isVisible: false) } - override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { + public override func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { self.currentLayout = layout super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index e1b3520201..6a475beccb 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -579,7 +579,7 @@ public final class LegacyPaintStickersContext: NSObject, TGPhotoPaintStickersCon let interfaceController: TGPhotoDrawingInterfaceController init(context: AccountContext, size: CGSize, originalSize: CGSize, isVideo: Bool, isAvatar: Bool, entitiesView: (UIView & TGPhotoDrawingEntitiesView)?) { - let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView) + let interfaceController = DrawingScreen(context: context, size: size, originalSize: originalSize, isVideo: isVideo, isAvatar: isAvatar, drawingView: nil, entitiesView: entitiesView, selectionContainerView: nil) self.interfaceController = interfaceController self.drawingView = interfaceController.drawingView self.drawingEntitiesView = interfaceController.entitiesView diff --git a/submodules/LocalizedPeerData/Sources/PeerTitle.swift b/submodules/LocalizedPeerData/Sources/PeerTitle.swift index 0986fe7de3..b30e6cabcd 100644 --- a/submodules/LocalizedPeerData/Sources/PeerTitle.swift +++ b/submodules/LocalizedPeerData/Sources/PeerTitle.swift @@ -59,3 +59,69 @@ public extension EnginePeer { } } } + +public extension EnginePeer.IndexName { + func isLessThan(other: EnginePeer.IndexName, ordering: PresentationPersonNameOrder) -> ComparisonResult { + switch self { + case let .title(lhsTitle, _): + let rhsString: String + switch other { + case let .title(title, _): + rhsString = title + case let .personName(first, last, _, _): + switch ordering { + case .firstLast: + if first.isEmpty { + rhsString = last + } else { + rhsString = first + last + } + case .lastFirst: + if last.isEmpty { + rhsString = first + } else { + rhsString = last + first + } + } + } + return lhsTitle.caseInsensitiveCompare(rhsString) + case let .personName(lhsFirst, lhsLast, _, _): + let lhsString: String + switch ordering { + case .firstLast: + if lhsFirst.isEmpty { + lhsString = lhsLast + } else { + lhsString = lhsFirst + lhsLast + } + case .lastFirst: + if lhsLast.isEmpty { + lhsString = lhsFirst + } else { + lhsString = lhsLast + lhsFirst + } + } + let rhsString: String + switch other { + case let .title(title, _): + rhsString = title + case let .personName(first, last, _, _): + switch ordering { + case .firstLast: + if first.isEmpty { + rhsString = last + } else { + rhsString = first + last + } + case .lastFirst: + if last.isEmpty { + rhsString = first + } else { + rhsString = last + first + } + } + } + return lhsString.caseInsensitiveCompare(rhsString) + } + } +} diff --git a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift index acd681407a..4f5ec48c76 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerGridItem.swift @@ -29,30 +29,32 @@ final class MediaPickerGridItem: GridItem { let theme: PresentationTheme let selectable: Bool let enableAnimations: Bool + let stories: Bool let section: GridSection? = nil - init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + init(content: MediaPickerGridItemContent, interaction: MediaPickerInteraction, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.content = content self.interaction = interaction self.theme = theme self.selectable = selectable self.enableAnimations = enableAnimations + self.stories = stories } func node(layout: GridNodeLayout, synchronousLoad: Bool) -> GridItemNode { switch self.content { case let .asset(fetchResult, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node case let .media(media, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node case let .draft(draft, index): let node = MediaPickerGridItemNode() - node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) return node } } @@ -64,11 +66,11 @@ final class MediaPickerGridItem: GridItem { } switch self.content { case let .asset(fetchResult, index): - node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, fetchResult: fetchResult, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) case let .media(media, index): - node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, media: media, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) case let .draft(draft, index): - node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations) + node.setup(interaction: self.interaction, draft: draft, index: index, theme: self.theme, selectable: self.selectable, enableAnimations: self.enableAnimations, stories: self.stories) } } } @@ -151,7 +153,11 @@ final class MediaPickerGridItemNode: GridItemNode { } var identifier: String { - return self.selectableItem?.uniqueIdentifier ?? "" + if let (draft, _) = self.currentDraftState { + return draft.path + } else { + return self.selectableItem?.uniqueIdentifier ?? "" + } } var selectableItem: TGMediaSelectableItem? { @@ -235,7 +241,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.imageNodeTap(_:)))) } - func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + func setup(interaction: MediaPickerInteraction, draft: MediaEditorDraft, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -262,7 +268,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } - func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + func setup(interaction: MediaPickerInteraction, media: MediaPickerScreen.Subject.Media, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -279,7 +285,7 @@ final class MediaPickerGridItemNode: GridItemNode { self.updateHiddenMedia() } - func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool) { + func setup(interaction: MediaPickerInteraction, fetchResult: PHFetchResult, index: Int, theme: PresentationTheme, selectable: Bool, enableAnimations: Bool, stories: Bool) { self.interaction = interaction self.theme = theme self.selectable = selectable @@ -315,7 +321,12 @@ final class MediaPickerGridItemNode: GridItemNode { } let scale = min(2.0, UIScreenScale) - let targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale) + let targetSize: CGSize + if stories { + targetSize = CGSize(width: 128.0 * UIScreenScale, height: 128.0 * UIScreenScale) + } else { + targetSize = CGSize(width: 128.0 * scale, height: 128.0 * scale) + } let assetImageSignal = assetImage(fetchResult: fetchResult, index: index, targetSize: targetSize, exact: false, deliveryMode: .fastFormat, synchronous: true) |> then( @@ -456,10 +467,18 @@ final class MediaPickerGridItemNode: GridItemNode { } } - func transitionView() -> UIView { - let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! - view.frame = self.convert(self.bounds, to: nil) - return view + func transitionView(snapshot: Bool) -> UIView { + if snapshot { + let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)! + view.frame = self.convert(self.bounds, to: nil) + return view + } else { + return self.imageNode.view + } + } + + func transitionImage() -> UIImage? { + return self.imageNode.image } @objc func imageNodeTap(_ recognizer: UITapGestureRecognizer) { diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index 2ccd53bbd1..54206cdf45 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -53,13 +53,14 @@ private struct MediaPickerGridEntry: Comparable, Identifiable { let stableId: Int let content: MediaPickerGridItemContent let selectable: Bool + let stories: Bool static func <(lhs: MediaPickerGridEntry, rhs: MediaPickerGridEntry) -> Bool { return lhs.stableId < rhs.stableId } func item(context: AccountContext, interaction: MediaPickerInteraction, theme: PresentationTheme) -> MediaPickerGridItem { - return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency) + return MediaPickerGridItem(content: self.content, interaction: interaction, theme: theme, selectable: self.selectable, enableAnimations: context.sharedContext.energyUsageSettings.fullTranslucency, stories: self.stories) } } @@ -583,9 +584,13 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var updateLayout = false + var stories = false var selectable = true if case let .assets(_, mode) = controller.subject, mode != .default { selectable = false + if mode == .story { + stories = true + } } switch state { @@ -606,7 +611,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var draftIndex = 0 for draft in drafts { - entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable)) + entries.append(MediaPickerGridEntry(stableId: stableId, content: .draft(draft, draftIndex), selectable: selectable, stories: stories)) stableId += 1 draftIndex += 1 } @@ -618,7 +623,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } else { index = totalCount - i - 1 } - entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable)) + entries.append(MediaPickerGridEntry(stableId: stableId, content: .asset(fetchResult, index), selectable: selectable, stories: stories)) stableId += 1 } @@ -647,7 +652,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { case let .media(media): let count = media.count for i in 0 ..< count { - entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true)) + entries.append(MediaPickerGridEntry(stableId: stableId, content: .media(media[i], i), selectable: true, stories: stories)) stableId += 1 } } @@ -976,7 +981,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } - private func transitionView(for identifier: String, hideSource: Bool = false) -> UIView? { + fileprivate func transitionView(for identifier: String, snapshot: Bool = true, hideSource: Bool = false) -> UIView? { if let selectionNode = self.selectionNode, selectionNode.alpha > 0.0 { return selectionNode.transitionView(for: identifier, hideSource: hideSource) } else { @@ -986,7 +991,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { transitionNode = itemNode } } - let transitionView = transitionNode?.transitionView() + let transitionView = transitionNode?.transitionView(snapshot: snapshot) if hideSource { transitionNode?.isHidden = true } @@ -994,6 +999,16 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + fileprivate func transitionImage(for identifier: String) -> UIImage? { + var transitionNode: MediaPickerGridItemNode? + self.gridNode.forEachItemNode { itemNode in + if let itemNode = itemNode as? MediaPickerGridItemNode, itemNode.identifier == identifier { + transitionNode = itemNode + } + } + return transitionNode?.transitionImage() + } + private func enqueueTransaction(_ transaction: MediaPickerGridTransaction) { self.enqueuedTransactions.append(transaction) @@ -1189,7 +1204,7 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { var itemHeight = itemWidth if case let .assets(_, mode) = controller.subject, case .story = mode { - itemHeight = 180.0 + itemHeight = round(itemWidth / 9.0 * 16.0) } self.gridNode.transaction(GridNodeTransaction(deleteItems: [], insertItems: [], updateItems: [], scrollToItem: nil, updateLayout: GridNodeUpdateLayout(layout: GridNodeLayout(size: bounds.size, insets: gridInsets, scrollIndicatorInsets: nil, preloadSize: itemHeight * 3.0, type: .fixed(itemSize: CGSize(width: itemWidth, height: itemHeight), fillWidth: true, lineSpacing: itemSpacing, itemSpacing: itemSpacing), cutout: cameraRect), transition: transition), itemTransition: .immediate, stationaryItems: .none, updateFirstIndexInSectionOffset: nil, updateOpaqueState: nil, synchronousLoads: false), completion: { [weak self] _ in @@ -1905,6 +1920,14 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { } } + fileprivate func transitionView(for identifier: String, snapshot: Bool, hideSource: Bool = false) -> UIView? { + return self.controllerNode.transitionView(for: identifier, snapshot: snapshot, hideSource: hideSource) + } + + fileprivate func transitionImage(for identifier: String) -> UIImage? { + return self.controllerNode.transitionImage(for: identifier) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) @@ -2112,7 +2135,8 @@ public func wallpaperMediaPickerController( public func storyMediaPickerController( context: AccountContext, - completion: @escaping (Any) -> Void = { _ in } + completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void = { _, _, _, _, _ in }, + dismissed: @escaping () -> Void ) -> ViewController { let presentationData = context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkColorPresentationTheme) let updatedPresentationData: (PresentationData, Signal) = (presentationData, .single(presentationData)) @@ -2121,9 +2145,37 @@ public func storyMediaPickerController( }) controller.requestController = { _, present in let mediaPickerController = MediaPickerScreen(context: context, updatedPresentationData: updatedPresentationData, peer: nil, threadTitle: nil, chatLocation: nil, bannedSendPhotos: nil, bannedSendVideos: nil, subject: .assets(nil, .story), mainButtonState: nil, mainButtonAction: nil) - mediaPickerController.customSelection = completion + mediaPickerController.customSelection = { [weak mediaPickerController] result in + guard let controller = mediaPickerController else { + return + } + if let result = result as? MediaEditorDraft { + if let transitionView = controller.transitionView(for: result.path, snapshot: false) { + let transitionOut: () -> (UIView, CGRect)? = { + if let transitionView = controller.transitionView(for: result.path, snapshot: false) { + return (transitionView, transitionView.bounds) + } + return nil + } + completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.path), transitionOut) + } + } else if let result = result as? PHAsset { + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + let transitionOut: () -> (UIView, CGRect)? = { + if let transitionView = controller.transitionView(for: result.localIdentifier, snapshot: false) { + return (transitionView, transitionView.bounds) + } + return nil + } + completion(result, transitionView, transitionView.bounds, controller.transitionImage(for: result.localIdentifier), transitionOut) + } + } + } present(mediaPickerController, mediaPickerController.mediaPickerContext) } + controller.willDismiss = { + dismissed() + } controller.navigationPresentation = .flatModal controller.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) return controller diff --git a/submodules/StickerResources/Sources/StickerResources.swift b/submodules/StickerResources/Sources/StickerResources.swift index 815840466b..97735c297f 100644 --- a/submodules/StickerResources/Sources/StickerResources.swift +++ b/submodules/StickerResources/Sources/StickerResources.swift @@ -328,8 +328,8 @@ public func chatMessageLegacySticker(account: Account, userLocation: MediaResour } } -public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { - return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad) +public func chatMessageSticker(account: Account, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { + return chatMessageSticker(postbox: account.postbox, userLocation: userLocation, file: file, small: small, fetched: fetched, onlyFullSize: onlyFullSize, thumbnail: thumbnail, synchronousLoad: synchronousLoad, colorSpace: colorSpace) } public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaResource, animated: Bool = false, synchronousLoad: Bool = false, nilIfEmpty: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { @@ -385,7 +385,7 @@ public func chatMessageStickerPackThumbnail(postbox: Postbox, resource: MediaRes } } -public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { +public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUserLocation, file: TelegramMediaFile, small: Bool, fetched: Bool = false, onlyFullSize: Bool = false, thumbnail: Bool = false, synchronousLoad: Bool = false, colorSpace: CGColorSpace? = nil) -> Signal<(TransformImageArguments) -> DrawingContext?, NoError> { let signal: Signal, NoError> if thumbnail { signal = chatMessageStickerThumbnailData(postbox: postbox, userLocation: userLocation, file: file, synchronousLoad: synchronousLoad) @@ -408,7 +408,7 @@ public func chatMessageSticker(postbox: Postbox, userLocation: MediaResourceUser return nil } - guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil) else { + guard let context = DrawingContext(size: arguments.drawingSize, scale: arguments.scale ?? 0.0, clear: arguments.emptyColor == nil, colorSpace: colorSpace) else { return nil } diff --git a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift index 5421591c6d..bae9426009 100644 --- a/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift +++ b/submodules/TelegramCore/Sources/ApiUtils/TelegramUser.swift @@ -64,6 +64,9 @@ extension TelegramUser { if (flags & (1 << 28)) != 0 { userFlags.insert(.isPremium) } + if (flags2 & (1 << 2)) != 0 { + userFlags.insert(.isCloseFriend) + } var botInfo: BotUserInfo? if (flags & (1 << 14)) != 0 { @@ -96,7 +99,7 @@ extension TelegramUser { static func merge(_ lhs: TelegramUser?, rhs: Api.User) -> TelegramUser? { switch rhs { - case let .user(flags, _, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _): + case let .user(flags, flags2, _, rhsAccessHash, _, _, _, _, photo, _, _, restrictionReason, botInlinePlaceholder, _, emojiStatus, _): let isMin = (flags & (1 << 20)) != 0 if !isMin { return TelegramUser(user: rhs) @@ -129,6 +132,9 @@ extension TelegramUser { if (flags & (1 << 28)) != 0 { userFlags.insert(.isPremium) } + if (flags2 & (1 << 2)) != 0 { + userFlags.insert(.isCloseFriend) + } var botInfo: BotUserInfo? if (flags & (1 << 14)) != 0 { diff --git a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift index 2830b1724e..f7815f68d2 100644 --- a/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift +++ b/submodules/TelegramCore/Sources/SyncCore/SyncCore_TelegramUser.swift @@ -16,6 +16,7 @@ public struct UserInfoFlags: OptionSet { public static let isScam = UserInfoFlags(rawValue: (1 << 2)) public static let isFake = UserInfoFlags(rawValue: (1 << 3)) public static let isPremium = UserInfoFlags(rawValue: (1 << 4)) + public static let isCloseFriend = UserInfoFlags(rawValue: (1 << 5)) } public struct BotUserInfoFlags: OptionSet { @@ -351,4 +352,8 @@ public final class TelegramUser: Peer, Equatable { public func withUpdatedEmojiStatus(_ emojiStatus: PeerEmojiStatus?) -> TelegramUser { return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames) } + + public func withUpdatedFlags(_ flags: UserInfoFlags) -> TelegramUser { + return TelegramUser(id: self.id, accessHash: self.accessHash, firstName: self.firstName, lastName: self.lastName, username: self.username, phone: self.phone, photo: self.photo, botInfo: self.botInfo, restrictionInfo: self.restrictionInfo, flags: self.flags, emojiStatus: emojiStatus, usernames: self.usernames) + } } diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift new file mode 100644 index 0000000000..37dee8cde2 --- /dev/null +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/CloseFriends.swift @@ -0,0 +1,36 @@ +import Foundation +import Postbox +import SwiftSignalKit +import TelegramApi +import MtProtoKit + +public func _internal_updateCloseFriends(account: Account, peerIds: [EnginePeer.Id]) -> Signal { + let ids: [Int64] = peerIds.map { $0.id._internalGetInt64Value() } + return account.network.request(Api.functions.contacts.editCloseFriends(id: ids)) + |> retryRequest + |> mapToSignal { result -> Signal in + return account.postbox.transaction { transaction in + let contactPeerIds = transaction.getContactPeerIds() + var updatedPeers: [Peer] = [] + for peerId in contactPeerIds { + if let peer = transaction.getPeer(peerId) as? TelegramUser { + if peerIds.contains(peerId) { + var updatedFlags = peer.flags + updatedFlags.insert(.isCloseFriend) + let updatedPeer = peer.withUpdatedFlags(updatedFlags) + updatedPeers.append(updatedPeer) + } else if peer.flags.contains(.isCloseFriend) { + var updatedFlags = peer.flags + updatedFlags.remove(.isCloseFriend) + let updatedPeer = peer.withUpdatedFlags(updatedFlags) + updatedPeers.append(updatedPeer) + } + } + } + updatePeers(transaction: transaction, peers: updatedPeers, update: { _, updated in + return updated + }) + } + } + |> ignoreValues +} diff --git a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift index 9f56c06f76..0291b7675d 100644 --- a/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift +++ b/submodules/TelegramCore/Sources/TelegramEngine/Privacy/TelegramEnginePrivacy.swift @@ -44,5 +44,9 @@ public extension TelegramEngine { public func updateSelectiveAccountPrivacySettings(type: UpdateSelectiveAccountPrivacySettingsType, settings: SelectivePrivacySettings) -> Signal { return _internal_updateSelectiveAccountPrivacySettings(account: self.account, type: type, settings: settings) } + + public func updateCloseFriends(peerIds: [EnginePeer.Id]) -> Signal { + return _internal_updateCloseFriends(account: self.account, peerIds: peerIds) + } } } diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 75c4febdd0..acf39becc8 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -70,6 +70,7 @@ swift_library( "//submodules/Components/MultilineTextComponent", "//submodules/Components/BlurredBackgroundComponent", "//submodules/Components/LottieAnimationComponent:LottieAnimationComponent", + "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/MediaEditor", ], diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift index a408ca9f43..867f42e71e 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift @@ -3,14 +3,14 @@ import UIKit import ComponentFlow final class CameraButton: Component { - let content: AnyComponent + let content: AnyComponentWithIdentity let minSize: CGSize? let tag: AnyObject? let isEnabled: Bool let action: () -> Void init( - content: AnyComponent, + content: AnyComponentWithIdentity, minSize: CGSize? = nil, tag: AnyObject? = nil, isEnabled: Bool = true, @@ -50,7 +50,7 @@ final class CameraButton: Component { } final class View: UIButton, ComponentTaggedView { - private let contentView: ComponentHostView + private var contentView: ComponentHostView private var component: CameraButton? private var currentIsHighlighted: Bool = false { @@ -123,9 +123,17 @@ final class CameraButton: Component { } func update(component: CameraButton, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + if let currentId = self.component?.content.id, currentId != component.content.id { + self.contentView.removeFromSuperview() + + self.contentView = ComponentHostView() + self.contentView.isUserInteractionEnabled = false + self.contentView.layer.allowsGroupOpacity = true + self.addSubview(self.contentView) + } let contentSize = self.contentView.update( transition: transition, - component: component.content, + component: component.content.component, environment: {}, containerSize: availableSize ) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 3d78dda0bd..306e91861c 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -17,6 +17,7 @@ import Photos import LottieAnimationComponent import TooltipUI import MediaEditor +import BundleIconComponent let videoRedColor = UIColor(rgb: 0xff3b30) @@ -33,29 +34,31 @@ private struct CameraState { } let mode: CameraMode let flashMode: Camera.FlashMode + let flashModeDidChange: Bool let recording: Recording let duration: Double func updatedMode(_ mode: CameraMode) -> CameraState { - return CameraState(mode: mode, flashMode: self.flashMode, recording: self.recording, duration: self.duration) + return CameraState(mode: mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: self.duration) } func updatedFlashMode(_ flashMode: Camera.FlashMode) -> CameraState { - return CameraState(mode: self.mode, flashMode: flashMode, recording: self.recording, duration: self.duration) + return CameraState(mode: self.mode, flashMode: flashMode, flashModeDidChange: self.flashMode != flashMode, recording: self.recording, duration: self.duration) } func updatedRecording(_ recording: Recording) -> CameraState { - return CameraState(mode: self.mode, flashMode: self.flashMode, recording: recording, duration: self.duration) + return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: recording, duration: self.duration) } func updatedDuration(_ duration: Double) -> CameraState { - return CameraState(mode: self.mode, flashMode: self.flashMode, recording: self.recording, duration: duration) + return CameraState(mode: self.mode, flashMode: self.flashMode, flashModeDidChange: self.flashModeDidChange, recording: self.recording, duration: duration) } } enum CameraScreenTransition { case animateIn case animateOut + case finishedAnimateIn } private let cancelButtonTag = GenericComponentViewTag() @@ -71,7 +74,7 @@ private final class CameraScreenComponent: CombinedComponent { let context: AccountContext let camera: Camera let changeMode: ActionSlot - let isDismissing: Bool + let hasAppeared: Bool let present: (ViewController) -> Void let push: (ViewController) -> Void let completion: ActionSlot> @@ -80,7 +83,7 @@ private final class CameraScreenComponent: CombinedComponent { context: AccountContext, camera: Camera, changeMode: ActionSlot, - isDismissing: Bool, + hasAppeared: Bool, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: ActionSlot> @@ -88,7 +91,7 @@ private final class CameraScreenComponent: CombinedComponent { self.context = context self.camera = camera self.changeMode = changeMode - self.isDismissing = isDismissing + self.hasAppeared = hasAppeared self.present = present self.push = push self.completion = completion @@ -98,7 +101,7 @@ private final class CameraScreenComponent: CombinedComponent { if lhs.context !== rhs.context { return false } - if lhs.isDismissing != rhs.isDismissing { + if lhs.hasAppeared != rhs.hasAppeared { return false } return true @@ -108,7 +111,6 @@ private final class CameraScreenComponent: CombinedComponent { enum ImageKey: Hashable { case cancel case flip - case flash } private var cachedImages: [ImageKey: UIImage] = [:] func image(_ key: ImageKey) -> UIImage { @@ -121,8 +123,6 @@ private final class CameraScreenComponent: CombinedComponent { image = UIImage(bundleImageName: "Camera/CloseIcon")! case .flip: image = UIImage(bundleImageName: "Camera/FlipIcon")! - case .flash: - image = UIImage(bundleImageName: "Camera/FlashIcon")! } cachedImages[key] = image return image @@ -141,7 +141,7 @@ private final class CameraScreenComponent: CombinedComponent { fileprivate var lastGalleryAsset: PHAsset? private var lastGalleryAssetsDisposable: Disposable? - var cameraState = CameraState(mode: .photo, flashMode: .off, recording: .none, duration: 0.0) + var cameraState = CameraState(mode: .photo, flashMode: .off, flashModeDidChange: false, recording: .none, duration: 0.0) var swipeHint: CaptureControlsComponent.SwipeHint = .none init(context: AccountContext, camera: Camera, present: @escaping (ViewController) -> Void, completion: ActionSlot>) { @@ -188,6 +188,9 @@ private final class CameraScreenComponent: CombinedComponent { } func updateSwipeHint(_ hint: CaptureControlsComponent.SwipeHint) { + guard hint != self.swipeHint else { + return + } self.swipeHint = hint self.updated(transition: .easeInOut(duration: 0.2)) } @@ -274,10 +277,15 @@ private final class CameraScreenComponent: CombinedComponent { if case .none = state.cameraState.recording { let cancelButton = cancelButton.update( component: CameraButton( - content: AnyComponent(Image( - image: state.image(.cancel), - size: CGSize(width: 40.0, height: 40.0) - )), + content: AnyComponentWithIdentity( + id: "cancel", + component: AnyComponent( + Image( + image: state.image(.cancel), + size: CGSize(width: 40.0, height: 40.0) + ) + ) + ), action: { guard let controller = controller() as? CameraScreen else { return @@ -294,32 +302,50 @@ private final class CameraScreenComponent: CombinedComponent { .disappear(.default(scale: true)) .cornerRadius(20.0) ) - - let flashIconName: String - switch state.cameraState.flashMode { - case .off: - flashIconName = "flash_off" - case .on: - flashIconName = "flash_on" - case .auto: - flashIconName = "flash_auto" - @unknown default: - flashIconName = "flash_off" - } - - let flashButton = flashButton.update( - component: CameraButton( - content: AnyComponent( + + let flashContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + let flashIconName: String + switch state.cameraState.flashMode { + case .off: + flashIconName = "flash_off" + case .on: + flashIconName = "flash_on" + case .auto: + flashIconName = "flash_auto" + @unknown default: + flashIconName = "flash_off" + } + + flashContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: flashIconName, - mode: .animating(loop: false), + mode: !state.cameraState.flashModeDidChange ? .still(position: .end) : .animating(loop: false), range: nil ), colors: [:], size: CGSize(width: 40.0, height: 40.0) ) - ), + ) + ) + } else { + flashContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Camera/FlashOffIcon", + tintColor: nil + ) + ) + ) + } + + let flashButton = flashButton.update( + component: CameraButton( + content: flashContentComponent, action: { [weak state] in guard let state else { return @@ -514,7 +540,7 @@ private final class CameraScreenComponent: CombinedComponent { } } - if case .none = state.cameraState.recording, !component.isDismissing { + if case .none = state.cameraState.recording { let modeControl = modeControl.update( component: ModeComponent( availableModes: [.photo, .video], @@ -638,13 +664,16 @@ public class CameraScreen: ViewController { private let context: AccountContext private let updateState: ActionSlot - private let backgroundEffectView: UIVisualEffectView - private let backgroundDimView: UIView + fileprivate let backgroundView: UIView + fileprivate let containerView: UIView fileprivate let componentHost: ComponentView private let previewContainerView: UIView fileprivate let previewView: CameraPreviewView? fileprivate let simplePreviewView: CameraSimplePreviewView? fileprivate let previewBlurView: BlurView + private var previewSnapshotView: UIView? + fileprivate let transitionDimView: UIView + fileprivate let transitionCornersView: UIImageView fileprivate let camera: Camera private var presentationData: PresentationData @@ -665,7 +694,7 @@ public class CameraScreen: ViewController { } } - private var previewBlurPromise = ValuePromise(false) + fileprivate var previewBlurPromise = ValuePromise(false) init(controller: CameraScreen) { self.controller = controller @@ -674,9 +703,11 @@ public class CameraScreen: ViewController { self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } - self.backgroundEffectView = UIVisualEffectView(effect: nil) - self.backgroundDimView = UIView() - self.backgroundDimView.backgroundColor = UIColor(rgb: 0x000000) + self.backgroundView = UIView() + self.backgroundView.backgroundColor = UIColor(rgb: 0x000000) + + self.containerView = UIView() + self.containerView.clipsToBounds = true self.componentHost = ComponentView() @@ -710,17 +741,25 @@ public class CameraScreen: ViewController { #endif } } + + self.transitionDimView = UIView() + self.transitionDimView.backgroundColor = UIColor(rgb: 0x000000) + self.transitionDimView.isUserInteractionEnabled = false + + self.transitionCornersView = UIImageView() super.init() self.backgroundColor = .clear - self.view.addSubview(self.backgroundEffectView) - self.view.addSubview(self.backgroundDimView) + self.view.addSubview(self.backgroundView) + self.view.addSubview(self.containerView) - self.view.addSubview(self.previewContainerView) + self.containerView.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.effectivePreviewView) self.previewContainerView.addSubview(self.previewBlurView) + self.containerView.addSubview(self.transitionDimView) + self.view.addSubview(self.transitionCornersView) self.changingPositionDisposable = combineLatest( queue: Queue.mainQueue(), @@ -733,7 +772,9 @@ public class CameraScreen: ViewController { self.previewBlurView.effect = UIBlurEffect(style: .dark) }) } else if forceBlur { - self.previewBlurView.effect = UIBlurEffect(style: .dark) + UIView.animate(withDuration: 0.4) { + self.previewBlurView.effect = UIBlurEffect(style: .dark) + } } else { UIView.animate(withDuration: 0.4) { self.previewBlurView.effect = nil @@ -776,7 +817,8 @@ public class CameraScreen: ViewController { self.camera.stopCapture() } } - } + }, + nil ) } } @@ -814,96 +856,43 @@ public class CameraScreen: ViewController { } } - private var panTranslation: CGFloat? private var previewInitialPosition: CGPoint? private var controlsInitialPosition: CGPoint? @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let controller = self.controller else { + return + } + let translation = gestureRecognizer.translation(in: gestureRecognizer.view) switch gestureRecognizer.state { case .began: - self.panTranslation = nil self.previewInitialPosition = self.previewContainerView.center self.controlsInitialPosition = self.componentHost.view?.center case .changed: - let translation = gestureRecognizer.translation(in: gestureRecognizer.view) if !"".isEmpty { } else { - if abs(translation.x) > 50.0 && abs(translation.y) < 50.0, self.panTranslation == nil { - self.changeMode.invoke(translation.x > 0.0 ? .photo : .video) - gestureRecognizer.isEnabled = false - gestureRecognizer.isEnabled = true - } else if translation.y > 10.0 { - let isFirstPanChange = self.panTranslation == nil - self.panTranslation = translation.y - if let previewInitialPosition = self.previewInitialPosition { - self.previewContainerView.center = CGPoint(x: previewInitialPosition.x, y: previewInitialPosition.y + translation.y) - } - if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view { - view.center = CGPoint(x: controlsInitialPosition.x, y: controlsInitialPosition.y + translation.y) - } - - if self.backgroundEffectView.isHidden { - self.backgroundEffectView.isHidden = false - - UIView.animate(withDuration: 0.25, animations: { - self.backgroundEffectView.effect = nil - self.backgroundDimView.alpha = 0.0 - }) - } - - if isFirstPanChange { - if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2)) - } - } + if translation.x < -10.0 { + let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + controller.updateTransitionProgress(transitionFraction, transition: .immediate) } else if translation.y < -10.0 { - self.controller?.presentGallery() + controller.presentGallery() gestureRecognizer.isEnabled = false gestureRecognizer.isEnabled = true } } case .ended: let velocity = gestureRecognizer.velocity(in: self.view) - if velocity.y > 1000.0 { - self.controller?.requestDismiss(animated: true) - } else if let panTranslation = self.panTranslation, abs(panTranslation) > 300.0 { - self.controller?.requestDismiss(animated: true) - } else { - let transition = Transition(animation: .curve(duration: 0.3, curve: .spring)) - if let previewInitialPosition = self.previewInitialPosition { - transition.setPosition(view: self.previewContainerView, position: previewInitialPosition) - } - if let controlsInitialPosition = self.controlsInitialPosition, let view = self.componentHost.view { - transition.setPosition(view: view, position: controlsInitialPosition) - } - if !self.backgroundEffectView.isHidden { - UIView.animate(withDuration: 0.25, animations: { - self.backgroundEffectView.effect = UIBlurEffect(style: .dark) - self.backgroundDimView.alpha = 1.0 - }, completion: { _ in - self.backgroundEffectView.isHidden = true - }) - } - } - if let _ = self.panTranslation { - self.panTranslation = nil - if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: .easeInOut(duration: 0.2)) - } - } + let transitionFraction = 1.0 - abs(translation.x) / self.frame.width + controller.completeWithTransitionProgress(transitionFraction, velocity: abs(velocity.x), dismissing: true) default: break } } func animateIn() { - self.backgroundDimView.alpha = 0.0 + self.backgroundView.alpha = 0.0 UIView.animate(withDuration: 0.4, animations: { - self.backgroundEffectView.effect = UIBlurEffect(style: .dark) - self.backgroundDimView.alpha = 1.0 - - }, completion: { _ in - self.backgroundEffectView.isHidden = true + self.backgroundView.alpha = 1.0 }) if let transitionIn = self.controller?.transitionIn, let sourceView = transitionIn.sourceView { @@ -926,23 +915,13 @@ public class CameraScreen: ViewController { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) } } - - if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { - view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) - view.layer.shadowRadius = 4.0 - view.layer.shadowColor = UIColor.black.cgColor - view.layer.shadowOpacity = 0.2 - } } func animateOut(completion: @escaping () -> Void) { self.camera.stopCapture(invalidate: true) - - self.backgroundEffectView.isHidden = false - + UIView.animate(withDuration: 0.25, animations: { - self.backgroundEffectView.effect = nil - self.backgroundDimView.alpha = 0.0 + self.backgroundView.alpha = 0.0 }) if let transitionOut = self.controller?.transitionOut(false), let destinationView = transitionOut.destinationView { @@ -994,13 +973,15 @@ public class CameraScreen: ViewController { } } - func commitTransitionToEditor() { - self.previewContainerView.alpha = 0.0 + func pauseCameraCapture() { + self.simplePreviewView?.isEnabled = false + Queue.mainQueue().after(0.3) { + self.previewBlurPromise.set(true) + } + self.camera.stopCapture() } - private var previewSnapshotView: UIView? - func animateInFromEditor() { - self.previewContainerView.alpha = 1.0 + func resumeCameraCapture() { if let snapshot = self.simplePreviewView?.snapshotView(afterScreenUpdates: false) { self.simplePreviewView?.addSubview(snapshot) self.previewSnapshotView = snapshot @@ -1023,26 +1004,46 @@ public class CameraScreen: ViewController { self.previewBlurPromise.set(false) } } + } + + func animateInFromEditor(toGallery: Bool) { + if !toGallery { + self.resumeCameraCapture() + + let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) + if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { + view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + transition.setAlpha(view: view, alpha: 1.0) + } + if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { + view.animateInFromEditor(transition: transition) + } + if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View { + view.animateInFromEditor(transition: transition) + } + } + } + + func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + guard let layout = self.validLayout else { + return + } - let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) - if let view = self.componentHost.findTaggedView(tag: cancelButtonTag) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - transition.setAlpha(view: view, alpha: 1.0) - } - if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - transition.setAlpha(view: view, alpha: 1.0) - } - if let view = self.componentHost.findTaggedView(tag: zoomControlTag) { - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) - transition.setAlpha(view: view, alpha: 1.0) - } - if let view = self.componentHost.findTaggedView(tag: captureControlsTag) as? CaptureControlsComponent.View { - view.animateInFromEditor(transition: transition) - } - if let view = self.componentHost.findTaggedView(tag: modeControlTag) as? ModeComponent.View { - view.animateInFromEditor(transition: transition) - } + let progress = 1.0 - value + let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width + let maxOffset = -56.0 + + let scale = 1.0 * progress + (1.0 - progress) * maxScale + let offset = (1.0 - progress) * maxOffset + transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true) } func presentDraftTooltip() { @@ -1067,8 +1068,22 @@ public class CameraScreen: ViewController { } return result } + + func requestUpdateLayout(hasAppeared: Bool, transition: Transition) { + if let layout = self.validLayout { + self.containerLayoutUpdated(layout: layout, forceUpdate: true, hasAppeared: hasAppeared, transition: transition) + + if let view = self.componentHost.findTaggedView(tag: flashButtonTag) { + view.layer.shadowOffset = CGSize(width: 0.0, height: 0.0) + view.layer.shadowRadius = 4.0 + view.layer.shadowColor = UIColor.black.cgColor + view.layer.shadowOpacity = 0.2 + } + } + } - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + fileprivate var hasAppeared = false + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { guard let _ = self.controller else { return } @@ -1103,8 +1118,9 @@ public class CameraScreen: ViewController { var transition = transition if isFirstTime { transition = transition.withUserData(CameraScreenTransition.animateIn) - } else if animateOut { - transition = transition.withUserData(CameraScreenTransition.animateOut) + } else if hasAppeared && !self.hasAppeared { + self.hasAppeared = hasAppeared + transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) } let componentSize = self.componentHost.update( @@ -1114,7 +1130,7 @@ public class CameraScreen: ViewController { context: self.context, camera: self.camera, changeMode: self.changeMode, - isDismissing: self.panTranslation != nil, + hasAppeared: self.hasAppeared, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, @@ -1127,34 +1143,48 @@ public class CameraScreen: ViewController { environment: { environment }, - forceUpdate: forceUpdate || animateOut, + forceUpdate: forceUpdate, containerSize: layout.size ) if let componentView = self.componentHost.view { if componentView.superview == nil { - self.view.insertSubview(componentView, at: 3) + self.containerView.insertSubview(componentView, belowSubview: transitionDimView) componentView.clipsToBounds = true } - - if self.panTranslation == nil { - let componentFrame = CGRect(origin: .zero, size: componentSize) - transition.setFrame(view: componentView, frame: componentFrame) - } + + let componentFrame = CGRect(origin: .zero, size: componentSize) + transition.setFrame(view: componentView, frame: componentFrame) } - transition.setFrame(view: self.backgroundDimView, frame: CGRect(origin: .zero, size: layout.size)) - transition.setFrame(view: self.backgroundEffectView, frame: CGRect(origin: .zero, size: layout.size)) + transition.setPosition(view: self.backgroundView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) + transition.setBounds(view: self.backgroundView, bounds: CGRect(origin: .zero, size: layout.size)) - if self.panTranslation == nil { - let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) - transition.setFrame(view: self.previewContainerView, frame: previewFrame) - transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) - transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) - } + transition.setPosition(view: self.containerView, position: CGPoint(x: layout.size.width / 2.0, y: layout.size.height / 2.0)) + transition.setBounds(view: self.containerView, bounds: CGRect(origin: .zero, size: layout.size)) - if isFirstTime { - self.animateIn() + transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size)) + + + let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) + transition.setFrame(view: self.previewContainerView, frame: previewFrame) + transition.setFrame(view: self.effectivePreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.previewBlurView, frame: CGRect(origin: .zero, size: previewFrame.size)) + + let screenCornerRadius = layout.deviceMetrics.screenCornerRadius + if screenCornerRadius > 0.0, self.transitionCornersView.image == nil { + self.transitionCornersView.image = generateImage(CGSize(width: screenCornerRadius, height: screenCornerRadius * 3.0), rotatedContext: { size, context in + context.setFillColor(UIColor.black.cgColor) + context.fill(CGRect(origin: .zero, size: size)) + context.setBlendMode(.clear) + + let path = UIBezierPath(roundedRect: CGRect(origin: .zero, size: CGSize(width: size.width * 2.0, height: size.height)), cornerRadius: size.width) + context.addPath(path.cgPath) + context.fillPath() + })?.stretchableImage(withLeftCapWidth: 0, topCapHeight: Int(screenCornerRadius)) } + + transition.setPosition(view: self.transitionCornersView, position: CGPoint(x: layout.size.width + screenCornerRadius / 2.0, y: layout.size.height / 2.0)) + transition.setBounds(view: self.transitionCornersView, bounds: CGRect(origin: .zero, size: CGSize(width: screenCornerRadius, height: layout.size.height))) } } @@ -1167,15 +1197,36 @@ public class CameraScreen: ViewController { fileprivate let holder: CameraHolder? fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool) -> TransitionOut? - fileprivate let completion: (Signal) -> Void + public final class ResultTransition { + public weak var sourceView: UIView? + public let sourceRect: CGRect + public let sourceImage: UIImage? + public let transitionOut: () -> (UIView, CGRect)? + + public init( + sourceView: UIView, + sourceRect: CGRect, + sourceImage: UIImage?, + transitionOut: @escaping () -> (UIView, CGRect)? + ) { + self.sourceView = sourceView + self.sourceRect = sourceRect + self.sourceImage = sourceImage + self.transitionOut = transitionOut + } + } + fileprivate let completion: (Signal, ResultTransition?) -> Void + + private var audioSessionDisposable: Disposable? + public init( context: AccountContext, mode: Mode, holder: CameraHolder? = nil, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, - completion: @escaping (Signal) -> Void + completion: @escaping (Signal, ResultTransition?) -> Void ) { self.context = context self.mode = mode @@ -1186,15 +1237,21 @@ public class CameraScreen: ViewController { super.init(navigationBarPresentationData: nil) - self.statusBar.statusBarStyle = .White + self.statusBar.statusBarStyle = .Ignore self.supportedOrientations = ViewControllerSupportedOrientations(regularSize: .all, compactSize: .portrait) self.navigationPresentation = .flatModal + + self.requestAudioSession() } required public init(coder: NSCoder) { preconditionFailure() } + + deinit { + self.audioSessionDisposable?.dispose() + } override public func loadDisplayNode() { self.displayNode = Node(controller: self) @@ -1202,31 +1259,61 @@ public class CameraScreen: ViewController { super.displayNodeDidLoad() } - public func returnFromEditor() { - self.node.animateInFromEditor() + private func requestAudioSession() { + self.audioSessionDisposable = self.context.sharedContext.mediaManager.audioSession.push(audioSessionType: .recordWithOthers, activate: { _ in }, deactivate: { _ in + return .single(Void()) + }) } - public func commitTransitionToEditor() { - self.node.commitTransitionToEditor() + private weak var galleryController: ViewController? + public func returnFromEditor() { + self.node.animateInFromEditor(toGallery: self.galleryController != nil) } func presentGallery() { - var dismissGalleryControllerImpl: (() -> Void)? - let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result in - dismissGalleryControllerImpl?() + var didStopCameraCapture = false + let stopCameraCapture = { [weak self] in + guard !didStopCameraCapture, let self else { + return + } + didStopCameraCapture = true + + self.node.pauseCameraCapture() + } + + let controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut in if let self { - self.node.animateOutToEditor() + stopCameraCapture() + + let resultTransition = ResultTransition( + sourceView: transitionView, + sourceRect: transitionRect, + sourceImage: transitionImage, + transitionOut: transitionOut + ) if let asset = result as? PHAsset { - self.completion(.single(.asset(asset))) + self.completion(.single(.asset(asset)), resultTransition) } else if let draft = result as? MediaEditorDraft { - self.completion(.single(.draft(draft))) + self.completion(.single(.draft(draft)), resultTransition) } } + }, dismissed: { [weak self] in + if let self { + self.node.resumeCameraCapture() + } }) - dismissGalleryControllerImpl = { [weak controller] in - controller?.dismiss(animated: true) + controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in + if let self, let controller { + let transitionFactor = controller.modalStyleOverlayTransitionFactor + if transitionFactor > 0.1 { + stopCameraCapture() + } + self.node.updateModalTransitionFactor(transitionFactor, transition: transition) + } } - push(controller) + + self.galleryController = controller + self.push(controller) } public func presentDraftTooltip() { @@ -1234,21 +1321,82 @@ public class CameraScreen: ViewController { } private var isDismissed = false - fileprivate func requestDismiss(animated: Bool) { + fileprivate func requestDismiss(animated: Bool, interactive: Bool = false) { guard !self.isDismissed else { return } + self.node.camera.stopCapture(invalidate: true) self.isDismissed = true - self.statusBar.statusBarStyle = .Ignore if animated { - self.node.animateOut(completion: { - self.dismiss(animated: false) + if !interactive { + if let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate) + } + } + self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in + self?.dismiss(animated: false) }) } else { self.dismiss(animated: false) } } + private var isTransitioning = false + public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { + self.isTransitioning = true + let offsetX = (1.0 - transitionFraction) * self.node.frame.width * -1.0 + transition.updateTransform(layer: self.node.backgroundView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) + transition.updateTransform(layer: self.node.containerView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) + let scale = max(0.8, min(1.0, 0.8 + 0.2 * transitionFraction)) + transition.updateSublayerTransformScaleAndOffset(layer: self.node.containerView.layer, scale: scale, offset: CGPoint(x: -offsetX * 1.0 / scale * 0.5, y: 0.0), completion: { _ in + completion() + }) + + let dimAlpha = 0.6 * (1.0 - transitionFraction) + transition.updateAlpha(layer: self.node.transitionDimView.layer, alpha: dimAlpha) + transition.updateTransform(layer: self.node.transitionCornersView.layer, transform: CGAffineTransform(translationX: offsetX, y: 0.0)) + + self.statusBar.updateStatusBarStyle(transitionFraction > 0.45 ? .White : .Ignore, animated: true) + + if let navigationController = self.navigationController as? NavigationController { + let offsetX = transitionFraction * self.node.frame.width + navigationController.updateRootContainerTransitionOffset(offsetX, transition: transition) + } + } + + public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { + self.isTransitioning = false + if dismissing { + if transitionFraction < 0.7 || velocity > 1000.0 { + self.requestDismiss(animated: true, interactive: true) + } else { + self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in + if let self, let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + } + }) + } + } else { + if transitionFraction > 0.33 || velocity > 1000.0 { + self.updateTransitionProgress(1.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in + if let self, let navigationController = self.navigationController as? NavigationController { + navigationController.updateRootContainerTransitionOffset(0.0, transition: .immediate) + self.node.requestUpdateLayout(hasAppeared: true, transition: .immediate) + } + }) + } else { + self.requestDismiss(animated: true, interactive: true) + } + } + } + + public override func dismiss(animated flag: Bool, completion: (() -> Void)? = nil) { + if !flag { + self.galleryController?.dismiss(animated: false) + } + super.dismiss(animated: flag, completion: completion) + } + override public func containerLayoutUpdated(_ layout: ContainerViewLayout, transition: ContainedViewLayoutTransition) { super.containerLayoutUpdated(layout, transition: transition) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index f8d3623a28..a6bb919642 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -108,7 +108,7 @@ private final class ShutterButtonContentComponent: Component { } self.blobView.updatePrimaryOffset(bandedOffset, transition: transition) } else { - self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.15)) + self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.2)) } } } @@ -411,11 +411,11 @@ final class CaptureControlsComponent: Component { if let galleryButton = self.galleryButtonView.view { blobOffset = galleryButton.center.x - self.frame.width / 2.0 } - self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.5))) + self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.35))) } else { self.hapticFeedback.impact(.light) self.component?.shutterReleased() - self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.3))) + self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.25))) } default: break @@ -450,7 +450,7 @@ final class CaptureControlsComponent: Component { self.component?.zoomUpdated(1.0) } - if location.x < self.frame.width / 2.0 - 20.0 { + if location.x < self.frame.width / 2.0 - 30.0 { if location.x < self.frame.width / 2.0 - 60.0 { self.component?.swipeHintUpdated(.releaseLock) if location.x < 75.0 { @@ -464,7 +464,7 @@ final class CaptureControlsComponent: Component { blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0) isBanding = true } - } else if location.x > self.frame.width / 2.0 + 20.0 { + } else if location.x > self.frame.width / 2.0 + 30.0 { self.component?.swipeHintUpdated(.flip) if location.x > self.frame.width / 2.0 + 60.0 { self.panBlobState = .transientToFlip @@ -488,8 +488,8 @@ final class CaptureControlsComponent: Component { } var transition: Transition = .immediate if let wasBanding = self.wasBanding, wasBanding != isBanding { - self.hapticFeedback.impact(.light) - transition = .spring(duration: 0.3) + //self.hapticFeedback.impact(.light) + transition = .spring(duration: 0.35) } self.wasBanding = isBanding self.shutterUpdateOffset.invoke((blobOffset, transition)) @@ -574,11 +574,14 @@ final class CaptureControlsComponent: Component { transition: .immediate, component: AnyComponent( CameraButton( - content: AnyComponent( - Image( - image: state.cachedAssetImage?.1, - size: CGSize(width: 50.0, height: 50.0), - contentMode: .scaleAspectFill + content: AnyComponentWithIdentity( + id: "gallery", + component: AnyComponent( + Image( + image: state.cachedAssetImage?.1, + size: CGSize(width: 50.0, height: 50.0), + contentMode: .scaleAspectFill + ) ) ), tag: component.galleryButtonTag, @@ -632,8 +635,11 @@ final class CaptureControlsComponent: Component { transition: .immediate, component: AnyComponent( CameraButton( - content: AnyComponent( - FlipButtonContentComponent(action: flipAnimationAction) + content: AnyComponentWithIdentity( + id: "flip", + component: AnyComponent( + FlipButtonContentComponent(action: flipAnimationAction) + ) ), minSize: CGSize(width: 44.0, height: 44.0), action: { diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift new file mode 100644 index 0000000000..975831c251 --- /dev/null +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/FocusCrosshairsView.swift @@ -0,0 +1,22 @@ +import Foundation +import UIKit + +final class FocusCrosshairsView: UIView { + private let indicatorView: UIImageView + + override init(frame: CGRect) { + self.indicatorView = UIImageView() + + super.init(frame: frame) + + self.addSubview(self.indicatorView) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func update(pointOfInterest: CGPoint) { + + } +} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index ccc76a634b..7230dff4cd 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -41,15 +41,15 @@ private final class AnimatableProperty { } func update(value: T, transition: Transition = .immediate) { + let currentTimestamp = CACurrentMediaTime() if case .none = transition.animation { if let animation = self.animation, case let .curve(duration, curve) = animation.animation { self.value = value - let elapsed = duration - (CACurrentMediaTime() - animation.startTimestamp) - if elapsed < 0.1 { - self.presentationValue = value - self.animation = nil + let elapsed = duration - (currentTimestamp - animation.startTimestamp) + if let presentationValue = self.presentationValue as? CGFloat, let newValue = value as? CGFloat, abs(presentationValue - newValue) > 0.56 { + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed * 0.8, curve: curve), startTimestamp: currentTimestamp) } else { - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: CACurrentMediaTime()) + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: .curve(duration: elapsed, curve: curve), startTimestamp: currentTimestamp) } } else { self.value = value @@ -58,7 +58,7 @@ private final class AnimatableProperty { } } else { self.value = value - self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: CACurrentMediaTime()) + self.animation = PropertyAnimation(fromValue: self.presentationValue, toValue: value, animation: transition.animation, startTimestamp: currentTimestamp) } } @@ -73,13 +73,18 @@ private final class AnimatableProperty { case .easeInOut: t = listViewAnimationCurveEaseInOut(t) case .spring: - t = listViewAnimationCurveSystem(t) + t = listViewAnimationCurveEaseInOut(t) + //t = listViewAnimationCurveSystem(t) case let .custom(x1, y1, x2, y2): t = bezierPoint(CGFloat(x1), CGFloat(y1), CGFloat(x2), CGFloat(y2), t) } self.presentationValue = animation.valueAt(t) as! T - return timeFromStart <= duration + if timeFromStart <= duration { + return true + } + self.animation = nil + return false } } @@ -204,26 +209,21 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { super.init(frame: CGRect(), device: device) - self.delegate = self - self.isOpaque = false self.backgroundColor = .clear - self.framebufferOnly = true self.colorPixelFormat = .bgra8Unorm + self.framebufferOnly = true + self.presentsWithTransaction = true + self.isPaused = true + self.delegate = self self.displayLink = SharedDisplayLinkDriver.shared.add { [weak self] in self?.tick() } self.displayLink?.isPaused = true - - self.isPaused = true } - public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { - self.viewportDimensions = size - } - required public init(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } @@ -232,6 +232,10 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { self.displayLink?.invalidate() } + public func mtkView(_ view: MTKView, drawableSizeWillChange size: CGSize) { + self.viewportDimensions = size + } + func updateState(_ state: BlobState, transition: Transition = .immediate) { guard self.state != state else { return @@ -253,6 +257,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { } let mappedOffset = offset / self.frame.height * 2.0 self.primaryOffset.update(value: mappedOffset, transition: transition) + self.tick() } @@ -262,6 +267,7 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { } let mappedOffset = offset / self.frame.height * 2.0 self.secondaryOffset.update(value: mappedOffset, transition: transition) + self.tick() } @@ -288,47 +294,51 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { private func tick() { self.updateAnimations() - self.draw() + autoreleasepool { + self.draw(in: self) + } } - - override public func draw(_ rect: CGRect) { - self.redraw(drawable: self.currentDrawable!) + + override func layoutSubviews() { + super.layoutSubviews() + + self.tick() } - - private func redraw(drawable: MTLDrawable) { + + func draw(in view: MTKView) { guard let commandBuffer = self.commandQueue.makeCommandBuffer() else { return } - - let renderPassDescriptor = self.currentRenderPassDescriptor! + + guard let renderPassDescriptor = self.currentRenderPassDescriptor else { + return + } renderPassDescriptor.colorAttachments[0].loadAction = .clear renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0.0) - + guard let renderEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return } - + let viewportDimensions = self.viewportDimensions renderEncoder.setViewport(MTLViewport(originX: 0.0, originY: 0.0, width: viewportDimensions.width, height: viewportDimensions.height, znear: -1.0, zfar: 1.0)) - renderEncoder.setRenderPipelineState(self.drawPassthroughPipelineState) - + let w = Float(1) let h = Float(1) - var vertices: [Float] = [ - w, -h, + w, -h, -w, -h, -w, h, - w, -h, + w, -h, -w, h, - w, h + w, h ] renderEncoder.setVertexBytes(&vertices, length: 4 * vertices.count, index: 0) - + var resolution = simd_uint2(UInt32(viewportDimensions.width), UInt32(viewportDimensions.height)) renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) - + var primaryParameters = simd_float4( Float(self.primarySize.presentationValue), Float(self.primaryOffset.presentationValue), @@ -343,23 +353,15 @@ final class ShutterBlobView: MTKView, MTKViewDelegate { Float(self.secondaryRedness.presentationValue) ) renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout.size, index: 2) - renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1) - renderEncoder.endEncoding() - drawable.present() - //commandBuffer.present(drawable) + + if let currentDrawable = self.currentDrawable { + commandBuffer.present(currentDrawable) + } commandBuffer.commit() - commandBuffer.waitUntilScheduled() - } - - override func layoutSubviews() { - super.layoutSubviews() - - self.tick() - } - - func draw(in view: MTKView) { + //commandBuffer.waitUntilScheduled() + //self.currentDrawable?.present() } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift index bca4ee384c..a1499a7dc7 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ZoomComponent.swift @@ -120,15 +120,6 @@ final class ZoomComponent: Component { let spacing: CGFloat = 3.0 let buttonSize = CGSize(width: 37.0, height: 37.0) let size: CGSize = CGSize(width: buttonSize.width * CGFloat(component.availableValues.count) + spacing * CGFloat(component.availableValues.count - 1) + sideInset * 2.0, height: 43.0) - - if let screenTransition = transition.userData(CameraScreenTransition.self) { - switch screenTransition { - case .animateIn: - self.animateIn() - case .animateOut: - self.animateOut() - } - } var i = 0 var itemFrame = CGRect(origin: CGPoint(x: sideInset, y: 3.0), size: buttonSize) diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal index 64c16902ae..e8827432bb 100644 --- a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorEnhance.metal @@ -78,7 +78,7 @@ fragment half4 enhanceColorLookupFragmentShader(RasterizerData in [[stage_in]], constexpr sampler lutSampler(min_filter::linear, mag_filter::linear, address::clamp_to_zero); float2 sourceCoord = in.texCoord; - half4 color = sourceTexture.sample(colorSampler,sourceCoord); + half4 color = sourceTexture.sample(colorSampler, sourceCoord); half3 hslColor = rgbToHsl(color.rgb); float txf = sourceCoord.x * tileGridSize.x - 0.5; diff --git a/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal new file mode 100644 index 0000000000..ac60ab77da --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/MetalResources/EditorVideo.metal @@ -0,0 +1,42 @@ +#include +#include "EditorCommon.h" +#include "EditorUtils.h" + +using namespace metal; + +static inline float4 BT709_decode(const float Y, const float Cb, const float Cr) { + float Yn = Y; + + float Cbn = (Cb - (128.0f/255.0f)); + float Crn = (Cr - (128.0f/255.0f)); + + float3 YCbCr = float3(Yn, Cbn, Crn); + + const float3x3 kColorConversion709 = float3x3(float3(1.0, 1.0, 1.0), + float3(0.0f, -0.1873, 1.8556), + float3(1.5748, -0.4681, 0.0)); + + float3 rgb = kColorConversion709 * YCbCr; + + rgb = saturate(rgb); + + return float4(rgb.r, rgb.g, rgb.b, 1.0f); +} + + +fragment float4 bt709ToRGBFragmentShader(RasterizerData in [[stage_in]], + texture2d inYTexture [[texture(0)]], + texture2d inUVTexture [[texture(1)]] + ) +{ + constexpr sampler textureSampler (mag_filter::nearest, min_filter::nearest); + + float Y = float(inYTexture.sample(textureSampler, in.texCoord).r); + half2 uvSamples = inUVTexture.sample(textureSampler, in.texCoord).rg; + + float Cb = float(uvSamples[0]); + float Cr = float(uvSamples[1]); + + float4 pixel = BT709_decode(Y, Cb, Cr); + return pixel; +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift index d10be35fe2..4224ff1f07 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/AdjustmentsRenderPass.swift @@ -18,6 +18,45 @@ struct MediaEditorAdjustments { var warmth: simd_float1 var grain: simd_float1 var vignette: simd_float1 + + var hasValues: Bool { + let epsilon: simd_float1 = 0.005 + + if abs(self.shadows) > epsilon { + return true + } + if abs(self.highlights) > epsilon { + return true + } + if abs(self.contrast) > epsilon { + return true + } + if abs(self.fade) > epsilon { + return true + } + if abs(self.saturation) > epsilon { + return true + } + if abs(self.shadowsTintIntensity) > epsilon { + return true + } + if abs(self.highlightsTintIntensity) > epsilon { + return true + } + if abs(self.exposure) > epsilon { + return true + } + if abs(self.warmth) > epsilon { + return true + } + if abs(self.grain) > epsilon { + return true + } + if abs(self.vignette) > epsilon { + return true + } + return false + } } final class AdjustmentsRenderPass: DefaultRenderPass { @@ -50,10 +89,14 @@ final class AdjustmentsRenderPass: DefaultRenderPass { return "adjustmentsFragmentShader" } - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + guard self.adjustments.hasValues else { + return input + } + self.setupVerticesBuffer(device: device) + + let width = input.width + let height = input.height if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { self.adjustments.dimensions = simd_float2(Float(width), Float(height)) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift index 030fff5640..eda463bb1d 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/BlurRenderPass.swift @@ -29,11 +29,11 @@ private final class BlurGaussianPass: RenderPass { } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { return nil } - func process(input: MTLTexture, intensity: Float, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, intensity: Float, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { let radius = round(4.0 + intensity * 26.0) if self.blur?.sigma != radius { self.blur = MPSImageGaussianBlur(device: device, sigma: radius) @@ -67,10 +67,8 @@ private final class BlurLinearPass: DefaultRenderPass { return "blurLinearFragmentShader" } - func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = output @@ -83,7 +81,7 @@ private final class BlurLinearPass: DefaultRenderPass { renderCommandEncoder.setViewport(MTLViewport( originX: 0, originY: 0, - width: Double(width), height: Double(height), + width: Double(input.width), height: Double(input.height), znear: -1.0, zfar: 1.0) ) @@ -105,10 +103,8 @@ private final class BlurRadialPass: DefaultRenderPass { return "blurRadialFragmentShader" } - func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + func process(input: MTLTexture, blurredTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = output @@ -121,7 +117,7 @@ private final class BlurRadialPass: DefaultRenderPass { renderCommandEncoder.setViewport(MTLViewport( originX: 0, originY: 0, - width: Double(width), height: Double(height), + width: Double(input.width), height: Double(input.height), znear: -1.0, zfar: 1.0) ) @@ -145,11 +141,9 @@ private final class BlurPortraitPass: DefaultRenderPass { return "blurPortraitFragmentShader" } - func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, blurredTexture: MTLTexture, maskTexture: MTLTexture, values: MediaEditorBlur, output: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) - let renderPassDescriptor = MTLRenderPassDescriptor() renderPassDescriptor.colorAttachments[0].texture = output renderPassDescriptor.colorAttachments[0].loadAction = .dontCare @@ -161,7 +155,7 @@ private final class BlurPortraitPass: DefaultRenderPass { renderCommandEncoder.setViewport(MTLViewport( originX: 0, originY: 0, - width: Double(width), height: Double(height), + width: Double(input.width), height: Double(input.height), znear: -1.0, zfar: 1.0) ) @@ -208,17 +202,18 @@ final class BlurRenderPass: RenderPass { self.portraitPass.setup(device: device, library: library) } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.process(input: input, maskTexture: self.maskTexture, rotation: rotation, device: device, commandBuffer: commandBuffer) + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.process(input: input, maskTexture: self.maskTexture, device: device, commandBuffer: commandBuffer) } - func process(input: MTLTexture, maskTexture: MTLTexture?, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, maskTexture: MTLTexture?, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { guard self.intensity > 0.005 && self.mode != .off else { return input } - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) - + let width = input.width + let height = input.height + if self.cachedTexture == nil { self.value.aspectRatio = Float(height) / Float(width) @@ -235,18 +230,18 @@ final class BlurRenderPass: RenderPass { self.cachedTexture = texture } - guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, rotation: rotation, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else { + guard let blurredTexture = self.blurPass.process(input: input, intensity: self.intensity, device: device, commandBuffer: commandBuffer), let output = self.cachedTexture else { return input } switch self.mode { case .linear: - return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + return self.linearPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer) case .radial: - return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + return self.radialPass.process(input: input, blurredTexture: blurredTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer) case .portrait: if let maskTexture { - return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, rotation: rotation, device: device, commandBuffer: commandBuffer) + return self.portraitPass.process(input: input, blurredTexture: blurredTexture, maskTexture: maskTexture, values: self.value, output: output, device: device, commandBuffer: commandBuffer) } else { return input } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift index 5a5f85f3ec..400369f49e 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/EnhanceRenderPass.swift @@ -21,20 +21,12 @@ private final class EnhanceLightnessPass: DefaultRenderPass { return .r8Unorm } - func process(input: MTLTexture, size: TextureSize, scale: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, size: TextureSize, scale: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let width: Int - let height: Int - switch rotation { - case .rotate90Degrees, .rotate270Degrees: - width = size.height - height = size.width - default: - width = size.width - height = size.height - } - + let width = size.width + let height = size.height + if self.cachedTexture == nil { let textureDescriptor = MTLTextureDescriptor() textureDescriptor.textureType = .type2D @@ -130,7 +122,7 @@ private final class EnhanceLUTGeneratorPass: RenderPass { } } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { return nil } @@ -204,10 +196,11 @@ private final class EnhanceLookupPass: DefaultRenderPass { return "enhanceColorLookupFragmentShader" } - func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, lookupTexture: MTLTexture, value: simd_float1, gridSize: simd_float2, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + let width = input.width + let height = input.height if self.cachedTexture == nil { let textureDescriptor = MTLTextureDescriptor() @@ -269,7 +262,7 @@ final class EnhanceRenderPass: RenderPass { self.lookupPass.setup(device: device, library: library) } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { guard self.value > 0.005 else { return input } @@ -279,12 +272,12 @@ final class EnhanceRenderPass: RenderPass { let lightnessSize = TextureSize(width: input.width + dX, height: input.height + dY) let lightnessScale = simd_float2(Float(input.width + dX) / Float(input.width), Float(input.height + dY) / Float(input.height)) - let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, rotation: rotation, device: device, commandBuffer: commandBuffer) + let lightness = self.lightnessPass.process(input: input, size: lightnessSize, scale: lightnessScale, device: device, commandBuffer: commandBuffer) let lookupTexture = self.lutGeneratorPass.process(input: lightness!, gridSize: self.tileGridSize, clipLimit: self.clipLimit, device: device, commandBuffer: commandBuffer) let gridSize = simd_float2(Float(self.tileGridSize.width), Float(self.tileGridSize.height)) - let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, rotation: rotation, device: device, commandBuffer: commandBuffer) + let output = self.lookupPass.process(input: input, lookupTexture: lookupTexture!, value: self.value, gridSize: gridSize, device: device, commandBuffer: commandBuffer) return output } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift index 1402227669..db830bd4d2 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/HistogramCalculationPass.swift @@ -8,6 +8,7 @@ final class HistogramCalculationPass: DefaultRenderPass { fileprivate var histogramBuffer: MTLBuffer? fileprivate var calculation: MPSImageHistogram? + var isEnabled = false var updated: ((Data) -> Void)? override var fragmentShaderFunctionName: String { @@ -40,58 +41,61 @@ final class HistogramCalculationPass: DefaultRenderPass { super.setup(device: device, library: library) } - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) - - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) - - if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { - let textureDescriptor = MTLTextureDescriptor() - textureDescriptor.textureType = .type2D - textureDescriptor.width = width - textureDescriptor.height = height - textureDescriptor.pixelFormat = .r8Unorm - textureDescriptor.storageMode = .shared - textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] - guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + if self.isEnabled { + self.setupVerticesBuffer(device: device) + + let width = input.width + let height = input.height + + if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = width + textureDescriptor.height = height + textureDescriptor.pixelFormat = .r8Unorm + textureDescriptor.storageMode = .shared + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + guard let texture = device.makeTexture(descriptor: textureDescriptor) else { + return input + } + self.cachedTexture = texture + texture.label = "lumaTexture" + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { return input } - self.cachedTexture = texture - texture.label = "lumaTexture" - } - - let renderPassDescriptor = MTLRenderPassDescriptor() - renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! - renderPassDescriptor.colorAttachments[0].loadAction = .dontCare - renderPassDescriptor.colorAttachments[0].storeAction = .store - renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) - guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { - return input - } - - renderCommandEncoder.setViewport(MTLViewport( - originX: 0, originY: 0, - width: Double(width), height: Double(height), - znear: -1.0, zfar: 1.0) - ) - - renderCommandEncoder.setFragmentTexture(input, index: 0) - - var texCoordScales = simd_float2(x: 1.0, y: 1.0) - renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) - - self.encodeDefaultCommands(using: renderCommandEncoder) - - renderCommandEncoder.endEncoding() - - if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation { - calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0) - let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm) - calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength) + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(width), height: Double(height), + znear: -1.0, zfar: 1.0) + ) - let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length) - self.updated?(histogramData) + renderCommandEncoder.setFragmentTexture(input, index: 0) + + var texCoordScales = simd_float2(x: 1.0, y: 1.0) + renderCommandEncoder.setFragmentBytes(&texCoordScales, length: MemoryLayout.stride, index: 0) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + if let histogramBuffer = self.histogramBuffer, let calculation = self.calculation { + calculation.encode(to: commandBuffer, sourceTexture: input, histogram: histogramBuffer, histogramOffset: 0) + + let lumaHistogramBufferLength = calculation.histogramSize(forSourceFormat: .r8Unorm) + calculation.encode(to: commandBuffer, sourceTexture: self.cachedTexture!, histogram: histogramBuffer, histogramOffset: histogramBuffer.length - lumaHistogramBufferLength) + + let histogramData = Data(bytes: histogramBuffer.contents(), count: histogramBuffer.length) + self.updated?(histogramData) + } } return input diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift index ce7b5a7fba..8017410b88 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/ImageTextureSource.swift @@ -3,45 +3,57 @@ import AVFoundation import Metal import MetalKit import Display +import Accelerate + +func loadTexture(image: UIImage, device: MTLDevice) -> MTLTexture? { + func dataForImage(_ image: UIImage) -> UnsafeMutablePointer { + let imageRef = image.cgImage + let width = Int(image.size.width) + let height = Int(image.size.height) + let colorSpace = CGColorSpaceCreateDeviceRGB() + + let rawData = UnsafeMutablePointer.allocate(capacity: width * height * 4) + let bytePerPixel = 4 + let bytesPerRow = bytePerPixel * Int(width) + let bitsPerComponent = 8 + let bitmapInfo = CGBitmapInfo.byteOrder32Little.rawValue + CGImageAlphaInfo.premultipliedFirst.rawValue + let context = CGContext.init(data: rawData, width: width, height: height, bitsPerComponent: bitsPerComponent, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: bitmapInfo) + context?.draw(imageRef!, in: CGRect(x: 0, y: 0, width: width, height: height)) + + return rawData + } + + let width = Int(image.size.width * image.scale) + let height = Int(image.size.height * image.scale) + let bytePerPixel = 4 + let bytesPerRow = bytePerPixel * width + + var texture : MTLTexture? + let region = MTLRegionMake2D(0, 0, Int(width), Int(height)) + let textureDescriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .bgra8Unorm, width: width, height: height, mipmapped: false) + texture = device.makeTexture(descriptor: textureDescriptor) + + let data = dataForImage(image) + texture?.replace(region: region, mipmapLevel: 0, withBytes: data, bytesPerRow: bytesPerRow) + + return texture +} final class ImageTextureSource: TextureSource { weak var output: TextureConsumer? - var textureLoader: MTKTextureLoader? var texture: MTLTexture? init(image: UIImage, renderTarget: RenderTarget) { - guard let device = renderTarget.mtlDevice, var cgImage = image.cgImage else { - return + if let device = renderTarget.mtlDevice { + self.texture = loadTexture(image: image, device: device) } - let textureLoader = MTKTextureLoader(device: device) - self.textureLoader = textureLoader - - if let bitsPerPixel = image.cgImage?.bitsPerPixel, bitsPerPixel > 32 { - let updatedImage = generateImage(image.size, contextGenerator: { size, context in - context.setFillColor(UIColor.black.cgColor) - context.fill(CGRect(origin: .zero, size: size)) - context.draw(cgImage, in: CGRect(origin: .zero, size: size)) - }, opaque: false) - cgImage = updatedImage?.cgImage ?? cgImage - } - - self.texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) - } - - func start() { - - } - - func pause() { - } func connect(to consumer: TextureConsumer) { self.output = consumer - if let texture = self.texture { - self.output?.consumeTexture(texture, rotation: .rotate0Degrees) + self.output?.consumeTexture(texture) } } } @@ -59,3 +71,17 @@ func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer, textureCache: CVMetalTe return nil } + +func getTextureImage(device: MTLDevice, texture: MTLTexture) -> UIImage? { + let colorSpace = CGColorSpaceCreateDeviceRGB() + let context = CIContext(mtlDevice: device, options: [:]) + guard var ciImage = CIImage(mtlTexture: texture, options: [.colorSpace: colorSpace]) else { + return nil + } + let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height) + ciImage = ciImage.transformed(by: transform) + guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else { + return nil + } + return UIImage(cgImage: cgImage) +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index d74d2a80b8..1cdddad3ec 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -76,7 +76,15 @@ public final class MediaEditor { public var histogram: Signal { return self.histogramPromise.get() } - + public var isHistogramEnabled: Bool { + get { + return self.histogramCalculationPass.isEnabled + } + set { + self.histogramCalculationPass.isEnabled = newValue + } + } + private var textureCache: CVMetalTextureCache! public var hasPortraitMask: Bool { @@ -90,7 +98,7 @@ public final class MediaEditor { public var resultImage: UIImage? { return self.renderer.finalRenderedImage() } - + private let playerPromise = Promise() private var playerPlaybackState: (Double, Double, Bool) = (0.0, 0.0, false) { didSet { @@ -250,6 +258,7 @@ public final class MediaEditor { } } + private var volumeFade: SwiftSignalKit.Timer? private func setupSource() { guard let renderTarget = self.previewView else { return @@ -367,14 +376,19 @@ public final class MediaEditor { |> deliverOnMainQueue).start(next: { [weak self] sourceAndColors in if let self { let (source, image, player, topColor, bottomColor) = sourceAndColors + self.renderer.onNextRender = { [weak self] in + self?.previewView?.removeTransitionImage() + } self.renderer.textureSource = source self.player = player self.playerPromise.set(.single(player)) self.gradientColorsValue = (topColor, bottomColor) self.setGradientColors([topColor, bottomColor]) - self.maybeGeneratePersonSegmentation(image) - + if player == nil { + self.maybeGeneratePersonSegmentation(image) + } + if let player { self.timeObserver = player.addPeriodicTimeObserver(forInterval: CMTimeMake(value: 1, timescale: 10), queue: DispatchQueue.main) { [weak self] time in guard let self, let duration = player.currentItem?.duration.seconds else { @@ -394,6 +408,7 @@ public final class MediaEditor { } }) self.player?.play() + self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) } } }) @@ -431,16 +446,45 @@ public final class MediaEditor { self.values = self.values.withUpdatedVideoIsMuted(videoIsMuted) } - public func seek(_ position: Double, andPlay: Bool) { - if !andPlay { + private var targetTimePosition: (CMTime, Bool)? + private var updatingTimePosition = false + public func seek(_ position: Double, andPlay play: Bool) { + if !play { self.player?.pause() } - self.player?.seek(to: CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in }) - if andPlay { + let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)) + if self.targetTimePosition?.0 != targetPosition { + self.targetTimePosition = (targetPosition, play) + if !self.updatingTimePosition { + self.updateVideoTimePosition() + } + } + if play { self.player?.play() } } + public func stop() { + self.player?.pause() + } + + private func updateVideoTimePosition() { + guard let (targetPosition, _) = self.targetTimePosition else { + return + } + self.updatingTimePosition = true + self.player?.seek(to: targetPosition, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { [weak self] _ in + if let self { + if let (currentTargetPosition, _) = self.targetTimePosition, currentTargetPosition == targetPosition { + self.updatingTimePosition = false + self.targetTimePosition = nil + } else { + self.updateVideoTimePosition() + } + } + }) + } + public func setVideoTrimStart(_ trimStart: Double) { let trimEnd = self.values.videoTrimRange?.upperBound ?? self.playerPlaybackState.0 let trimRange = trimStart ..< trimEnd @@ -583,15 +627,23 @@ final class MediaEditorRenderChain { } case .shadowsTint: if let value = value as? TintValue { - let (red, green, blue, _) = value.color.components - self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) - self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity + if value.color != .clear { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.shadowsTintIntensity = value.intensity + } else { + self.adjustmentsPass.adjustments.shadowsTintIntensity = 0.0 + } } case .highlightsTint: if let value = value as? TintValue { - let (red, green, blue, _) = value.color.components - self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) - self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity + if value.color != .clear { + let (red, green, blue, _) = value.color.components + self.adjustmentsPass.adjustments.shadowsTintColor = simd_float3(Float(red), Float(green), Float(blue)) + self.adjustmentsPass.adjustments.highlightsTintIntensity = value.intensity + } else { + self.adjustmentsPass.adjustments.highlightsTintIntensity = 0.0 + } } case .blur: if let value = value as? BlurValue { @@ -626,3 +678,11 @@ final class MediaEditorRenderChain { } } } + +public func debugSaveImage(_ image: UIImage, name: String) { + let path = NSTemporaryDirectory() + "debug_\(name)_\(Int64.random(in: .min ... .max)).png" + print(path) + if let data = image.pngData() { + try? data.write(to: URL(fileURLWithPath: path)) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift index b73c5a99c3..ccc6a9e539 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposer.swift @@ -12,8 +12,26 @@ import TelegramAnimatedStickerNode import YuvConversion import StickerResources +func mediaEditorGenerateGradientImage(size: CGSize, colors: [UIColor]) -> UIImage? { + UIGraphicsBeginImageContextWithOptions(size, false, 1.0) + if let context = UIGraphicsGetCurrentContext() { + let gradientColors = colors.map { $0.cgColor } as CFArray + let colorSpace = CGColorSpaceCreateDeviceRGB() + + var locations: [CGFloat] = [0.0, 1.0] + let gradient = CGGradient(colorsSpace: colorSpace, colors: gradientColors, locations: &locations)! + context.drawLinearGradient(gradient, start: CGPoint(x: 0.0, y: 0.0), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) + } + + let image = UIGraphicsGetImageFromCurrentImageContext()! + UIGraphicsEndImageContext() + + return image +} + final class MediaEditorComposer { let device: MTLDevice? + private let colorSpace: CGColorSpace private let values: MediaEditorValues private let dimensions: CGSize @@ -34,27 +52,29 @@ final class MediaEditorComposer { self.dimensions = dimensions self.outputDimensions = outputDimensions + let colorSpace = CGColorSpaceCreateDeviceRGB() + self.colorSpace = colorSpace + self.renderer.addRenderChain(self.renderChain) self.renderer.addRenderPass(ComposerRenderPass()) - if let gradientColors = values.gradientColors { - let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])! - self.gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) { + self.gradientImage = CIImage(image: image, options: [.colorSpace: self.colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { self.gradientImage = CIImage(color: .black) } - if let drawing = values.drawing, let drawingImage = CIImage(image: drawing) { + if let drawing = values.drawing, let drawingImage = CIImage(image: drawing, options: [.colorSpace: self.colorSpace]) { self.drawingImage = drawingImage.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { self.drawingImage = nil } - self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0) } + self.entities = values.entities.map { $0.entity } .compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) } self.device = MTLCreateSystemDefaultDevice() if let device = self.device { - self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : NSNull()]) + self.ciContext = CIContext(mtlDevice: device, options: [.workingColorSpace : self.colorSpace]) CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &self.textureCache) } else { @@ -82,10 +102,10 @@ final class MediaEditorComposer { texture = CVMetalTextureGetTexture(textureRef!) } if let texture { - self.renderer.consumeTexture(texture, rotation: .rotate90Degrees) + self.renderer.consumeTexture(texture) self.renderer.renderFrame() - if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) { + if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) var pixelBuffer: CVPixelBuffer? @@ -116,13 +136,12 @@ final class MediaEditorComposer { completion(nil, time) return } - if self.filteredImage == nil, let device = self.device, let cgImage = inputImage.cgImage { - let textureLoader = MTKTextureLoader(device: device) - if let texture = try? textureLoader.newTexture(cgImage: cgImage, options: [.SRGB : false]) { - self.renderer.consumeTexture(texture, rotation: .rotate0Degrees) + if self.filteredImage == nil, let device = self.device { + if let texture = loadTexture(image: inputImage, device: device) { + self.renderer.consumeTexture(texture) self.renderer.renderFrame() - if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture) { + if let finalTexture = self.renderer.finalTexture, var ciImage = CIImage(mtlTexture: finalTexture, options: [.colorSpace: self.colorSpace]) { ciImage = ciImage.transformed(by: CGAffineTransformMakeScale(1.0, -1.0).translatedBy(x: 0.0, y: -ciImage.extent.height)) self.filteredImage = ciImage } @@ -137,7 +156,7 @@ final class MediaEditorComposer { makeEditorImageFrameComposition(inputImage: image, gradientImage: self.gradientImage, drawingImage: self.drawingImage, dimensions: self.dimensions, values: self.values, entities: self.entities, time: time, completion: { compositedImage in if var compositedImage { let scale = self.outputDimensions.width / self.dimensions.width - compositedImage = compositedImage.transformed(by: CGAffineTransform(scaleX: scale, y: scale)) + compositedImage = compositedImage.samplingLinear().transformed(by: CGAffineTransform(scaleX: scale, y: scale)) self.ciContext?.render(compositedImage, to: pixelBuffer) completion(pixelBuffer, time) @@ -157,21 +176,21 @@ final class MediaEditorComposer { } public func makeEditorImageComposition(account: Account, inputImage: UIImage, dimensions: CGSize, values: MediaEditorValues, time: CMTime, completion: @escaping (UIImage?) -> Void) { - let inputImage = CIImage(image: inputImage)! + let colorSpace = CGColorSpaceCreateDeviceRGB() + let inputImage = CIImage(image: inputImage, options: [.colorSpace: colorSpace])! let gradientImage: CIImage var drawingImage: CIImage? - if let gradientColors = values.gradientColors { - let image = generateGradientImage(size: dimensions, scale: 1.0, colors: gradientColors, locations: [0.0, 1.0])! - gradientImage = CIImage(image: image)!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) + if let gradientColors = values.gradientColors, let image = mediaEditorGenerateGradientImage(size: dimensions, colors: gradientColors) { + gradientImage = CIImage(image: image, options: [.colorSpace: colorSpace])!.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } else { gradientImage = CIImage(color: .black) } - if let drawing = values.drawing, let image = CIImage(image: drawing) { + if let drawing = values.drawing, let image = CIImage(image: drawing, options: [.colorSpace: colorSpace]) { drawingImage = image.transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) } - let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0) } + let entities: [MediaEditorComposerEntity] = values.entities.map { $0.entity }.compactMap { composerEntityForDrawingEntity(account: account, entity: $0, colorSpace: colorSpace) } makeEditorImageFrameComposition(inputImage: inputImage, gradientImage: gradientImage, drawingImage: drawingImage, dimensions: dimensions, values: values, entities: entities, time: time, completion: { ciImage in if let ciImage { let context = CIContext(options: [.workingColorSpace : NSNull()]) @@ -190,7 +209,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: var resultImage = CIImage(color: .black).cropped(to: CGRect(origin: .zero, size: dimensions)).transformed(by: CGAffineTransform(translationX: -dimensions.width / 2.0, y: -dimensions.height / 2.0)) resultImage = gradientImage.composited(over: resultImage) - var mediaImage = inputImage.transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) + var mediaImage = inputImage.samplingLinear().transformed(by: CGAffineTransform(translationX: -inputImage.extent.midX, y: -inputImage.extent.midY)) var initialScale: CGFloat if mediaImage.extent.height > mediaImage.extent.width { @@ -206,7 +225,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: resultImage = mediaImage.composited(over: resultImage) if let drawingImage { - resultImage = drawingImage.composited(over: resultImage) + resultImage = drawingImage.samplingLinear().composited(over: resultImage) } let frameRate: Float = 30.0 @@ -235,7 +254,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: } let index = i entity.image(for: time, frameRate: frameRate, completion: { image in - if var image = image { + if var image = image?.samplingLinear() { let resetTransform = CGAffineTransform(translationX: -image.extent.width / 2.0, y: -image.extent.height / 2.0) image = image.transformed(by: resetTransform) @@ -266,7 +285,7 @@ private func makeEditorImageFrameComposition(inputImage: CIImage, gradientImage: maybeFinalize() } -private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity) -> MediaEditorComposerEntity? { +private func composerEntityForDrawingEntity(account: Account, entity: DrawingEntity, colorSpace: CGColorSpace) -> MediaEditorComposerEntity? { if let entity = entity as? DrawingStickerEntity { let content: MediaEditorComposerStickerEntity.Content switch entity.content { @@ -275,8 +294,8 @@ private func composerEntityForDrawingEntity(account: Account, entity: DrawingEnt case let .image(image): content = .image(image) } - return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored) - } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage) { + return MediaEditorComposerStickerEntity(account: account, content: content, position: entity.position, scale: entity.scale, rotation: entity.rotation, baseSize: entity.baseSize, mirrored: entity.mirrored, colorSpace: colorSpace) + } else if let renderImage = entity.renderImage, let image = CIImage(image: renderImage, options: [.colorSpace: colorSpace]) { if let entity = entity as? DrawingBubbleEntity { return MediaEditorComposerStaticEntity(image: image, position: entity.position, scale: 1.0, rotation: entity.rotation, baseSize: entity.size, mirrored: false) } else if let entity = entity as? DrawingSimpleShapeEntity { @@ -331,6 +350,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let rotation: CGFloat let baseSize: CGSize? let mirrored: Bool + let colorSpace: CGColorSpace var isAnimated: Bool var source: AnimatedStickerNodeSource? @@ -349,13 +369,14 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { var imagePixelBuffer: CVPixelBuffer? let imagePromise = Promise() - init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool) { + init(account: Account, content: Content, position: CGPoint, scale: CGFloat, rotation: CGFloat, baseSize: CGSize, mirrored: Bool, colorSpace: CGColorSpace) { self.content = content self.position = position self.scale = scale self.rotation = rotation self.baseSize = baseSize self.mirrored = mirrored + self.colorSpace = colorSpace switch content { case let .file(file): @@ -373,7 +394,6 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let queue = strongSelf.queue let frameSource = QueueLocalObject(queue: queue, generate: { return AnimatedStickerDirectFrameSource(queue: queue, data: data, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, useMetalCache: false, fitzModifier: nil)! - //return AnimatedStickerCachedFrameSource(queue: queue, data: data, complete: complete, notifyUpdated: {})! }) frameSource.syncWith { frameSource in strongSelf.frameCount = frameSource.frameCount @@ -391,13 +411,13 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } } else { self.isAnimated = false - self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false) + self.disposables.add((chatMessageSticker(account: account, userLocation: .other, file: file, small: false, fetched: true, onlyFullSize: true, thumbnail: false, synchronousLoad: false, colorSpace: self.colorSpace) |> deliverOn(self.queue)).start(next: { [weak self] generator in - if let strongSelf = self { + if let self { let context = generator(TransformImageArguments(corners: ImageCorners(), imageSize: baseSize, boundingSize: baseSize, intrinsicInsets: UIEdgeInsets())) - let image = context?.generateImage() - if let image = image { - strongSelf.imagePromise.set(.single(image)) + let image = context?.generateImage(colorSpace: self.colorSpace) + if let image { + self.imagePromise.set(.single(image)) } } })) @@ -488,7 +508,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } if let imagePixelBuffer { - let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, tintColor: tintColor) + let image = render(width: frame.width, height: frame.height, bytesPerRow: frame.bytesPerRow, data: frame.data, type: frame.type, pixelBuffer: imagePixelBuffer, colorSpace: strongSelf.colorSpace, tintColor: tintColor) strongSelf.image = image } completion(strongSelf.image) @@ -508,9 +528,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { let _ = (self.imagePromise.get() |> take(1) |> deliverOn(self.queue)).start(next: { [weak self] image in - if let strongSelf = self { - strongSelf.image = CIImage(image: image) - completion(strongSelf.image) + if let self { + self.image = CIImage(image: image, options: [.colorSpace: self.colorSpace]) + completion(self.image) } }) } @@ -528,7 +548,7 @@ protocol MediaEditorComposerEntity { func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) } -private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, tintColor: UIColor?) -> CIImage? { +private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: AnimationRendererFrameType, pixelBuffer: CVPixelBuffer, colorSpace: CGColorSpace, tintColor: UIColor?) -> CIImage? { //let calculatedBytesPerRow = (4 * Int(width) + 31) & (~31) //assert(bytesPerRow == calculatedBytesPerRow) @@ -557,16 +577,17 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: CVPixelBufferUnlockBaseAddress(pixelBuffer, CVPixelBufferLockFlags(rawValue: 0)) - return CIImage(cvPixelBuffer: pixelBuffer) + return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: colorSpace]) } final class ComposerRenderPass: DefaultRenderPass { fileprivate var cachedTexture: MTLTexture? - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) - let (width, height) = textureDimensionsForRotation(texture: input, rotation: rotation) + let width = input.width + let height = input.height if self.cachedTexture == nil || self.cachedTexture?.width != width || self.cachedTexture?.height != height { let textureDescriptor = MTLTextureDescriptor() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift index adf68aefbe..c3c2de83ae 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorPreviewView.swift @@ -67,4 +67,33 @@ public final class MediaEditorPreviewView: MTKView, MTKViewDelegate, RenderTarge } self.renderer?.renderFrame() } + + private var transitionView: UIImageView? + public func setTransitionImage(_ image: UIImage) { + self.transitionView?.removeFromSuperview() + + let transitionView = UIImageView(image: image) + transitionView.frame = self.bounds + self.addSubview(transitionView) + + self.transitionView = transitionView + } + + public func removeTransitionImage() { + if let transitionView = self.transitionView { +// transitionView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak transitionView] _ in +// +// }) + transitionView.removeFromSuperview() + self.transitionView = nil + } + } + + public override func layoutSubviews() { + super.layoutSubviews() + + if let transitionView = self.transitionView { + transitionView.frame = self.bounds + } + } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 4cb0e5c7fe..cd06a3af9f 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -6,7 +6,8 @@ import Photos import SwiftSignalKit protocol TextureConsumer: AnyObject { - func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) + func consumeTexture(_ texture: MTLTexture) + func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) } final class RenderingContext { @@ -24,7 +25,7 @@ final class RenderingContext { protocol RenderPass: AnyObject { func setup(device: MTLDevice, library: MTLLibrary) - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? } protocol TextureSource { @@ -51,7 +52,9 @@ final class MediaEditorRenderer: TextureConsumer { var semaphore = DispatchSemaphore(value: 3) private var renderPasses: [RenderPass] = [] - private var outputRenderPass = OutputRenderPass() + + private let videoInputPass = VideoInputPass() + private let outputRenderPass = OutputRenderPass() private weak var renderTarget: RenderTarget? { didSet { self.outputRenderPass.renderTarget = self.renderTarget @@ -59,10 +62,14 @@ final class MediaEditorRenderer: TextureConsumer { } private var device: MTLDevice? - private var commandQueue: MTLCommandQueue? - private var currentTexture: MTLTexture? - private var currentRotation: TextureRotation = .rotate0Degrees private var library: MTLLibrary? + private var commandQueue: MTLCommandQueue? + private var textureCache: CVMetalTextureCache? + + private var currentTexture: MTLTexture? + private var currentPixelBuffer: (CVPixelBuffer, TextureRotation)? + + public var onNextRender: (() -> Void)? var finalTexture: MTLTexture? @@ -94,6 +101,8 @@ final class MediaEditorRenderer: TextureConsumer { return } + CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) + let mainBundle = Bundle(for: MediaEditorRenderer.self) guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else { return @@ -102,15 +111,16 @@ final class MediaEditorRenderer: TextureConsumer { return } - guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { return } - self.library = defaultLibrary + self.library = library self.commandQueue = device.makeCommandQueue() self.commandQueue?.label = "Media Editor Command Queue" - self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) } - self.outputRenderPass.setup(device: device, library: defaultLibrary) + self.videoInputPass.setup(device: device, library: library) + self.renderPasses.forEach { $0.setup(device: device, library: library) } + self.outputRenderPass.setup(device: device, library: library) } func setupForComposer(composer: MediaEditorComposer) { @@ -118,6 +128,7 @@ final class MediaEditorRenderer: TextureConsumer { return } self.device = device + CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) let mainBundle = Bundle(for: MediaEditorRenderer.self) guard let path = mainBundle.path(forResource: "MediaEditorBundle", ofType: "bundle") else { @@ -127,17 +138,17 @@ final class MediaEditorRenderer: TextureConsumer { return } - guard let defaultLibrary = try? device.makeDefaultLibrary(bundle: bundle) else { + guard let library = try? device.makeDefaultLibrary(bundle: bundle) else { return } - self.library = defaultLibrary + self.library = library self.commandQueue = device.makeCommandQueue() self.commandQueue?.label = "Media Editor Command Queue" - self.renderPasses.forEach { $0.setup(device: device, library: defaultLibrary) } + self.videoInputPass.setup(device: device, library: library) + self.renderPasses.forEach { $0.setup(device: device, library: library) } } - private var currentCommandBuffer: MTLCommandBuffer? func renderFrame() { let device: MTLDevice? if let renderTarget = self.renderTarget { @@ -149,7 +160,7 @@ final class MediaEditorRenderer: TextureConsumer { } guard let device = device, let commandQueue = self.commandQueue, - var texture = self.currentTexture else { + let textureCache = self.textureCache else { return } @@ -157,22 +168,36 @@ final class MediaEditorRenderer: TextureConsumer { return } - var rotation: TextureRotation = self.currentRotation + var texture: MTLTexture + if let currentTexture = self.currentTexture { + texture = currentTexture + } else if let (currentPixelBuffer, textureRotation) = self.currentPixelBuffer, let videoTexture = self.videoInputPass.processPixelBuffer(currentPixelBuffer, rotation: textureRotation, textureCache: textureCache, device: device, commandBuffer: commandBuffer) { + texture = videoTexture + } else { + return + } + for renderPass in self.renderPasses { - if let nextTexture = renderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) { - if nextTexture !== texture { - rotation = .rotate0Degrees - } + if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) { texture = nextTexture } } if self.renderTarget != nil { - let _ = self.outputRenderPass.process(input: texture, rotation: rotation, device: device, commandBuffer: commandBuffer) + let _ = self.outputRenderPass.process(input: texture, device: device, commandBuffer: commandBuffer) } self.finalTexture = texture commandBuffer.addCompletedHandler { [weak self] _ in - self?.semaphore.signal() + if let self { + self.semaphore.signal() + + if let onNextRender = self.onNextRender { + self.onNextRender = nil + Queue.mainQueue().async { + onNextRender() + } + } + } } if let _ = self.renderTarget { @@ -184,18 +209,17 @@ final class MediaEditorRenderer: TextureConsumer { } } - func commit() { - if let commandBuffer = self.currentCommandBuffer { - commandBuffer.commit() - self.currentCommandBuffer = nil - } - } - - func consumeTexture(_ texture: MTLTexture, rotation: TextureRotation) { + func consumeTexture(_ texture: MTLTexture) { self.semaphore.wait() self.currentTexture = texture - self.currentRotation = rotation + self.renderTarget?.scheduleFrame() + } + + func consumeVideoPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation) { + self.semaphore.wait() + + self.currentPixelBuffer = (pixelBuffer, rotation) self.renderTarget?.scheduleFrame() } @@ -209,27 +233,10 @@ final class MediaEditorRenderer: TextureConsumer { } func finalRenderedImage() -> UIImage? { - if let finalTexture = self.finalTexture { - return getTextureImage(finalTexture) + if let finalTexture = self.finalTexture, let device = self.renderTarget?.mtlDevice { + return getTextureImage(device: device, texture: finalTexture) } else { return nil } } - - private func getTextureImage(_ texture: MTLTexture) -> UIImage? { - guard let device = self.renderTarget?.mtlDevice else { - return nil - } - let options = [CIImageOption.colorSpace: CGColorSpaceCreateDeviceRGB()] - let context = CIContext(mtlDevice: device) - guard var ciImage = CIImage(mtlTexture: texture, options: options) else { - return nil - } - let transform = CGAffineTransform(1.0, 0.0, 0.0, -1.0, 0.0, ciImage.extent.height) - ciImage = ciImage.transformed(by: transform) - guard let cgImage = context.createCGImage(ciImage, from: CGRect(origin: .zero, size: CGSize(width: ciImage.extent.width, height: ciImage.extent.height))) else { - return nil - } - return UIImage(cgImage: cgImage) - } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift new file mode 100644 index 0000000000..a93192fc58 --- /dev/null +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorUtils.swift @@ -0,0 +1,41 @@ +import Foundation +import AVFoundation +import SwiftSignalKit + +extension AVPlayer { + func fadeVolume(from: Float, to: Float, duration: Float, completion: (() -> Void)? = nil) -> SwiftSignalKit.Timer? { + self.volume = from + guard from != to else { return nil } + + let interval: Float = 0.1 + let range = to - from + let step = (range * interval) / duration + + func reachedTarget() -> Bool { + guard self.volume >= 0, self.volume <= 1 else { + self.volume = to + return true + } + + if to > from { + return self.volume >= to + } + return self.volume <= to + } + + var invalidateImpl: (() -> Void)? + let timer = SwiftSignalKit.Timer(timeout: Double(interval), repeat: true, completion: { [weak self] in + if let self, !reachedTarget() { + self.volume += step + } else { + invalidateImpl?() + completion?() + } + }, queue: Queue.mainQueue()) + invalidateImpl = { [weak timer] in + timer?.invalidate() + } + timer.start() + return timer + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift index 7301dabbd2..111c9cae2a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/RenderPass.swift @@ -102,7 +102,7 @@ class DefaultRenderPass: RenderPass { } } - func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation) { + func setupVerticesBuffer(device: MTLDevice, rotation: TextureRotation = .rotate0Degrees) { if self.verticesBuffer == nil || rotation != self.textureRotation { self.textureRotation = rotation let vertices = verticesDataForRotation(rotation) @@ -113,8 +113,8 @@ class DefaultRenderPass: RenderPass { } } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { - self.setupVerticesBuffer(device: device, rotation: rotation) + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device) return nil } @@ -131,11 +131,11 @@ class DefaultRenderPass: RenderPass { final class OutputRenderPass: DefaultRenderPass { weak var renderTarget: RenderTarget? - override func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + override func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { guard let renderTarget = self.renderTarget, let renderPassDescriptor = renderTarget.renderPassDescriptor else { return nil } - self.setupVerticesBuffer(device: device, rotation: rotation) + self.setupVerticesBuffer(device: device) let drawableSize = renderTarget.drawableSize diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift index 07e0e8e651..9730c80d8a 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/SharpenRenderPass.swift @@ -11,7 +11,7 @@ final class SharpenRenderPass: RenderPass { } - func process(input: MTLTexture, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func process(input: MTLTexture, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { return input } } diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift index e83c5520fb..33c009a189 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/VideoTextureSource.swift @@ -13,22 +13,19 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD private var displayLink: CADisplayLink? + private let device: MTLDevice? private var textureRotation: TextureRotation = .rotate0Degrees - + private var forceUpdate: Bool = false weak var output: TextureConsumer? - var textureCache: CVMetalTextureCache! var queue: DispatchQueue! var started: Bool = false init(player: AVPlayer, renderTarget: RenderTarget) { self.player = player - - if let device = renderTarget.mtlDevice, CVMetalTextureCacheCreate(nil, nil, device, nil, &self.textureCache) != kCVReturnSuccess { - print("error") - } - + self.device = renderTarget.mtlDevice! + self.queue = DispatchQueue( label: "VideoTextureSource Queue", qos: .userInteractive, @@ -47,7 +44,8 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD } deinit { - print() + self.playerItemObservation?.invalidate() + self.playerItemStatusObservation?.invalidate() } private func updatePlayerItem(_ playerItem: AVPlayerItem?) { @@ -63,7 +61,7 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD self.playerItemStatusObservation = nil self.playerItem = playerItem - self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial,.new], changeHandler: { [weak self] item, change in + self.playerItemStatusObservation = self.playerItem?.observe(\.status, options: [.initial, .new], changeHandler: { [weak self] item, change in guard let strongSelf = self else { return } @@ -91,21 +89,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD } else if t.b == -1.0 && t.c == 1.0 { self.textureRotation = .rotate270Degrees } else if t.a == -1.0 && t.d == 1.0 { -// if (mirrored != NULL) { -// *mirrored = true; -// } self.textureRotation = .rotate270Degrees } else if t.a == 1.0 && t.d == -1.0 { -// if (mirrored != NULL) { -// *mirrored = true; -// } self.textureRotation = .rotate180Degrees } else { -// if (t.c == 1) { -// if (mirrored != NULL) { -// *mirrored = true; -// } -// } self.textureRotation = .rotate90Degrees } } @@ -115,7 +102,20 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD return } - let output = AVPlayerItemVideoOutput(pixelBufferAttributes: [kCVPixelBufferPixelFormatTypeKey as NSString as String: kCVPixelFormatType_32BGRA]) + let colorProperties: [String: Any] = [ + AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, + AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, + AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2 + ] + + let outputSettings: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + kCVPixelBufferMetalCompatibilityKey as String: true, + AVVideoColorPropertiesKey: colorProperties + ] + + let output = AVPlayerItemVideoOutput(outputSettings: outputSettings) + output.suppressesPlayerRendering = true output.setDelegate(self, queue: self.queue) playerItem.add(output) self.playerItemOutput = output @@ -174,12 +174,10 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD var presentationTime: CMTime = .zero if let pixelBuffer = output.copyPixelBuffer(forItemTime: requestTime, itemTimeForDisplay: &presentationTime) { - if let texture = self.pixelBufferToMTLTexture(pixelBuffer: pixelBuffer) { - self.output?.consumeTexture(texture, rotation: self.textureRotation) - } + self.output?.consumeVideoPixelBuffer(pixelBuffer, rotation: self.textureRotation) } } - + func setNeedsUpdate() { self.displayLink?.isPaused = false self.forceUpdate = true @@ -196,19 +194,88 @@ final class VideoTextureSource: NSObject, TextureSource, AVPlayerItemOutputPullD self.output = consumer } - private func pixelBufferToMTLTexture(pixelBuffer: CVPixelBuffer) -> MTLTexture? { - let width = CVPixelBufferGetWidth(pixelBuffer) - let height = CVPixelBufferGetHeight(pixelBuffer) - let format: MTLPixelFormat = .bgra8Unorm - var textureRef : CVMetalTexture? - let status = CVMetalTextureCacheCreateTextureFromImage(nil, self.textureCache, pixelBuffer, nil, format, width, height, 0, &textureRef) - if status == kCVReturnSuccess { - return CVMetalTextureGetTexture(textureRef!) - } - return nil - } - public func outputMediaDataWillChange(_ sender: AVPlayerItemOutput) { self.displayLink?.isPaused = false } } + +final class VideoInputPass: DefaultRenderPass { + private var cachedTexture: MTLTexture? + + override var fragmentShaderFunctionName: String { + return "bt709ToRGBFragmentShader" + } + + func processPixelBuffer(_ pixelBuffer: CVPixelBuffer, rotation: TextureRotation, textureCache: CVMetalTextureCache, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + func textureFromPixelBuffer(_ pixelBuffer: CVPixelBuffer, pixelFormat: MTLPixelFormat, width: Int, height: Int, plane: Int) -> MTLTexture? { + var textureRef : CVMetalTexture? + let status = CVMetalTextureCacheCreateTextureFromImage(nil, textureCache, pixelBuffer, nil, pixelFormat, width, height, plane, &textureRef) + if status == kCVReturnSuccess, let textureRef { + return CVMetalTextureGetTexture(textureRef) + } + return nil + } + + let width = CVPixelBufferGetWidth(pixelBuffer) + let height = CVPixelBufferGetHeight(pixelBuffer) + guard let inputYTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .r8Unorm, width: width, height: height, plane: 0), + let inputCbCrTexture = textureFromPixelBuffer(pixelBuffer, pixelFormat: .rg8Unorm, width: width >> 1, height: height >> 1, plane: 1) else { + return nil + } + return self.process(yTexture: inputYTexture, cbcrTexture: inputCbCrTexture, width: width, height: height, rotation: rotation, device: device, commandBuffer: commandBuffer) + } + + func process(yTexture: MTLTexture, cbcrTexture: MTLTexture, width: Int, height: Int, rotation: TextureRotation, device: MTLDevice, commandBuffer: MTLCommandBuffer) -> MTLTexture? { + self.setupVerticesBuffer(device: device, rotation: rotation) + + func textureDimensionsForRotation(width: Int, height: Int, rotation: TextureRotation) -> (width: Int, height: Int) { + switch rotation { + case .rotate90Degrees, .rotate270Degrees: + return (height, width) + default: + return (width, height) + } + } + + let (outputWidth, outputHeight) = textureDimensionsForRotation(width: width, height: height, rotation: rotation) +// let outputSize = CGSize(width: outputWidth, height: outputHeight).fitted(CGSize(width: 1920.0, height: 1920.0)) +// outputWidth = Int(outputSize.width) +// outputHeight = Int(outputSize.height) + if self.cachedTexture == nil { + let textureDescriptor = MTLTextureDescriptor() + textureDescriptor.textureType = .type2D + textureDescriptor.width = outputWidth + textureDescriptor.height = outputHeight + textureDescriptor.pixelFormat = .bgra8Unorm + textureDescriptor.storageMode = .private + textureDescriptor.usage = [.shaderRead, .shaderWrite, .renderTarget] + if let texture = device.makeTexture(descriptor: textureDescriptor) { + self.cachedTexture = texture + } + } + + let renderPassDescriptor = MTLRenderPassDescriptor() + renderPassDescriptor.colorAttachments[0].texture = self.cachedTexture! + renderPassDescriptor.colorAttachments[0].loadAction = .dontCare + renderPassDescriptor.colorAttachments[0].storeAction = .store + renderPassDescriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.0) + guard let renderCommandEncoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDescriptor) else { + return nil + } + + renderCommandEncoder.setViewport(MTLViewport( + originX: 0, originY: 0, + width: Double(outputWidth), height: Double(outputHeight), + znear: -1.0, zfar: 1.0) + ) + + renderCommandEncoder.setFragmentTexture(yTexture, index: 0) + renderCommandEncoder.setFragmentTexture(cbcrTexture, index: 1) + + self.encodeDefaultCommands(using: renderCommandEncoder) + + renderCommandEncoder.endEncoding() + + return self.cachedTexture + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 5b487994d5..f36fbafd5b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -21,6 +21,7 @@ import BlurredBackgroundComponent import AvatarNode import ShareWithPeersScreen import PresentationDataUtils +import ContextUI enum DrawingScreenType { case drawing @@ -37,23 +38,20 @@ final class MediaEditorScreenComponent: Component { let context: AccountContext let mediaEditor: MediaEditor? - let privacy: EngineStoryPrivacy - let timeout: Bool + let privacy: MediaEditorResultPrivacy let openDrawing: (DrawingScreenType) -> Void let openTools: () -> Void init( context: AccountContext, mediaEditor: MediaEditor?, - privacy: EngineStoryPrivacy, - timeout: Bool, + privacy: MediaEditorResultPrivacy, openDrawing: @escaping (DrawingScreenType) -> Void, openTools: @escaping () -> Void ) { self.context = context self.mediaEditor = mediaEditor self.privacy = privacy - self.timeout = timeout self.openDrawing = openDrawing self.openTools = openTools } @@ -65,9 +63,6 @@ final class MediaEditorScreenComponent: Component { if lhs.privacy != rhs.privacy { return false } - if lhs.timeout != rhs.timeout { - return false - } return true } @@ -186,7 +181,11 @@ final class MediaEditorScreenComponent: Component { fatalError("init(coder:) has not been implemented") } - func animateInFromCamera() { + enum TransitionAnimationSource { + case camera + case gallery + } + func animateIn(from source: TransitionAnimationSource) { if let view = self.cancelButton.view { view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) @@ -217,7 +216,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.inputPanel.view { view.layer.animatePosition(from: CGPoint(x: 0.0, y: 44.0), to: .zero, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, additive: true) view.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) - view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2) + view.layer.animateScale(from: 0.6, to: 1.0, duration: 0.2) } if let view = self.saveButton.view { @@ -236,7 +235,7 @@ final class MediaEditorScreenComponent: Component { } } - func animateOutToCamera() { + func animateOut(to source: TransitionAnimationSource) { let transition = Transition(animation: .curve(duration: 0.2, curve: .easeInOut)) if let view = self.cancelButton.view { transition.setAlpha(view: view, alpha: 0.0) @@ -334,7 +333,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.privacyButton.view { transition.setAlpha(view: view, alpha: 0.0) - transition.setScale(view: view, scale: 0.1) + view.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2) } if let view = self.scrubber.view { @@ -386,7 +385,7 @@ final class MediaEditorScreenComponent: Component { if let view = self.privacyButton.view { transition.setAlpha(view: view, alpha: 1.0) - transition.setScale(view: view, scale: 1.0) + view.layer.animateScale(from: 0.0, to: 1.0, duration: 0.2) } if let view = self.scrubber.view { @@ -637,6 +636,17 @@ final class MediaEditorScreenComponent: Component { } + let timeoutValue: Int32 + let timeoutSelected: Bool + switch component.privacy { + case let .story(_, archive): + timeoutValue = 24 + timeoutSelected = archive + case let .message(_, timeout): + timeoutValue = timeout ?? 1 + timeoutSelected = timeout != nil + } + self.inputPanel.parentState = state let inputPanelSize = self.inputPanel.update( transition: transition, @@ -665,16 +675,25 @@ final class MediaEditorScreenComponent: Component { discardMediaRecordingPreview: nil, attachmentAction: nil, reactionAction: nil, - timeoutAction: { view in - + timeoutAction: { [weak self] view in + guard let self, let controller = self.environment?.controller() as? MediaEditorScreen else { + return + } + switch controller.state.privacy { + case let .story(privacy, archive): + controller.state.privacy = .story(privacy: privacy, archive: !archive) + controller.node.presentStoryArchiveTooltip(sourceView: view) + case .message: + controller.presentTimeoutSetup(sourceView: view) + } }, audioRecorder: nil, videoRecordingStatus: nil, isRecordingLocked: false, recordedAudioPreview: nil, wasRecordingDismissed: false, - timeoutValue: 24, - timeoutSelected: component.timeout, + timeoutValue: timeoutValue, + timeoutSelected: timeoutSelected, displayGradient: false,//component.inputHeight != 0.0, bottomInset: 0.0 //component.inputHeight != 0.0 ? 0.0 : bottomContentInset )), @@ -697,18 +716,26 @@ final class MediaEditorScreenComponent: Component { } let privacyText: String - switch component.privacy.base { - case .everyone: - privacyText = "Everyone" - case .closeFriends: - privacyText = "Close Friends" - case .contacts: - privacyText = "Contacts" - case .nobody: - privacyText = "Selected Contacts" + switch component.privacy { + case let .story(privacy, _): + switch privacy.base { + case .everyone: + privacyText = "Everyone" + case .closeFriends: + privacyText = "Close Friends" + case .contacts: + privacyText = "Contacts" + case .nobody: + privacyText = "Selected Contacts" + } + case let .message(peerIds, _): + if peerIds.count == 1 { + privacyText = "User Test" + } else { + privacyText = "\(peerIds.count) Recipients" + } } - let privacyButtonSize = self.privacyButton.update( transition: transition, component: AnyComponent(Button( @@ -845,21 +872,31 @@ final class MediaEditorScreenComponent: Component { private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) +public enum MediaEditorResultPrivacy: Equatable { + case story(privacy: EngineStoryPrivacy, archive: Bool) + case message(peers: [EnginePeer.Id], timeout: Int32?) +} + public final class MediaEditorScreen: ViewController { - public final class TransitionIn { - public weak var sourceView: UIView? - public let sourceRect: CGRect - public let sourceCornerRadius: CGFloat - - public init( - sourceView: UIView, - sourceRect: CGRect, - sourceCornerRadius: CGFloat - ) { - self.sourceView = sourceView - self.sourceRect = sourceRect - self.sourceCornerRadius = sourceCornerRadius + public enum TransitionIn { + public final class GalleryTransitionIn { + public weak var sourceView: UIView? + public let sourceRect: CGRect + public let sourceImage: UIImage? + + public init( + sourceView: UIView, + sourceRect: CGRect, + sourceImage: UIImage? + ) { + self.sourceView = sourceView + self.sourceRect = sourceRect + self.sourceImage = sourceImage + } } + + case camera + case gallery(GalleryTransitionIn) } public final class TransitionOut { @@ -878,6 +915,16 @@ public final class MediaEditorScreen: ViewController { } } + struct State { + var privacy: MediaEditorResultPrivacy = .story(privacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), archive: true) + } + + var state = State() { + didSet { + self.node.requestUpdate() + } + } + fileprivate final class Node: ViewControllerTracingNode, UIGestureRecognizerDelegate { private weak var controller: MediaEditorScreen? private let context: AccountContext @@ -885,8 +932,6 @@ public final class MediaEditorScreen: ViewController { fileprivate var subject: MediaEditorScreen.Subject? private var subjectDisposable: Disposable? - fileprivate var storyPrivacy: EngineStoryPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) - fileprivate var timeout: Bool = true private let backgroundDimView: UIView fileprivate let componentHost: ComponentView @@ -898,6 +943,7 @@ public final class MediaEditorScreen: ViewController { fileprivate let entitiesContainerView: UIView fileprivate let entitiesView: DrawingEntitiesView + fileprivate let selectionContainerView: DrawingSelectionContainerView fileprivate let drawingView: DrawingView fileprivate let previewView: MediaEditorPreviewView fileprivate var mediaEditor: MediaEditor? @@ -910,7 +956,7 @@ public final class MediaEditorScreen: ViewController { init(controller: MediaEditorScreen) { self.controller = controller self.context = controller.context - + self.presentationData = self.context.sharedContext.currentPresentationData.with { $0 } self.backgroundDimView = UIView() @@ -938,16 +984,20 @@ public final class MediaEditorScreen: ViewController { self.drawingView = DrawingView(size: storyDimensions) self.drawingView.isUserInteractionEnabled = false + self.selectionContainerView = DrawingSelectionContainerView(frame: .zero) + self.entitiesView.selectionContainerView = self.selectionContainerView + super.init() self.backgroundColor = .clear - //self.view.addSubview(self.backgroundDimView) + self.view.addSubview(self.backgroundDimView) self.view.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.gradientView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) self.previewContainerView.addSubview(self.drawingView) + self.previewContainerView.addSubview(self.selectionContainerView) self.subjectDisposable = ( controller.subject @@ -1035,7 +1085,7 @@ public final class MediaEditorScreen: ViewController { let mediaEntity = DrawingMediaEntity(content: subject.mediaContent, size: fittedSize) mediaEntity.position = CGPoint(x: storyDimensions.width / 2.0, y: storyDimensions.height / 2.0) if fittedSize.height > fittedSize.width { - mediaEntity.scale = storyDimensions.height / fittedSize.height + mediaEntity.scale = storyDimensions.height / fittedSize.height } else { mediaEntity.scale = storyDimensions.width / fittedSize.width } @@ -1082,10 +1132,10 @@ public final class MediaEditorScreen: ViewController { self.previewContainerView.layer.allowsGroupOpacity = true self.previewContainerView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in self.previewContainerView.layer.allowsGroupOpacity = false - self.controller?.onReady() + self.backgroundDimView.alpha = 1.0 }) } else { - self.controller?.onReady() + self.backgroundDimView.alpha = 1.0 } } } @@ -1131,18 +1181,47 @@ public final class MediaEditorScreen: ViewController { } func animateIn() { - if let sourceHint = self.controller?.sourceHint { - switch sourceHint { + if let transitionIn = self.controller?.transitionIn { + switch transitionIn { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateInFromCamera() + view.animateIn(from: .camera) + } + case let .gallery(transitionIn): + if let transitionImage = transitionIn.sourceImage { + self.previewContainerView.alpha = 1.0 + self.previewView.setTransitionImage(transitionImage) + } + if let sourceView = transitionIn.sourceView { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateIn(from: .gallery) + } + + let sourceLocalFrame = sourceView.convert(transitionIn.sourceRect, to: self.view) + let sourceScale = sourceLocalFrame.width / self.previewContainerView.frame.width + let sourceAspectRatio = sourceLocalFrame.height / sourceLocalFrame.width + + let duration: Double = 0.5 + + self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.previewContainerView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.previewContainerView.layer.animateBounds(from: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * sourceAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * sourceAspectRatio)), to: self.previewContainerView.bounds, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + + self.backgroundDimView.alpha = 1.0 + self.backgroundDimView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) + + if let componentView = self.componentHost.view { + componentView.layer.animatePosition(from: sourceLocalFrame.center, to: componentView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + componentView.layer.animateScale(from: sourceScale, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + componentView.layer.animateAlpha(from: 0.0, to: 1.0, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + } } } } -// Queue.mainQueue().after(0.5) { -// self.presentPrivacyTooltip() -// } + // Queue.mainQueue().after(0.5) { + // self.presentPrivacyTooltip() + // } } func animateOut(finished: Bool, completion: @escaping () -> Void) { @@ -1151,18 +1230,68 @@ public final class MediaEditorScreen: ViewController { } controller.statusBar.statusBarStyle = .Ignore - if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView { - let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) - - let targetScale = destinationLocalFrame.width / self.previewContainerView.frame.width - self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + self.backgroundDimView.alpha = 0.0 + self.backgroundDimView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25) + + if finished, case .message = controller.state.privacy { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOut(to: .camera) + } + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in completion() }) - self.previewContainerView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + } else if let transitionOut = controller.transitionOut(finished), let destinationView = transitionOut.destinationView { + if !finished, let view = self.componentHost.view as? MediaEditorScreenComponent.View { + if let transitionIn = controller.transitionIn, case let .gallery(galleryTransitionIn) = transitionIn, let sourceImage = galleryTransitionIn.sourceImage { + let transitionOutView = UIImageView(image: sourceImage) + var initialScale: CGFloat + if sourceImage.size.height > sourceImage.size.width { + initialScale = self.previewContainerView.bounds.height / sourceImage.size.height + } else { + initialScale = self.previewContainerView.bounds.width / sourceImage.size.width + } + transitionOutView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) + transitionOutView.transform = CGAffineTransformMakeScale(initialScale, initialScale) + transitionOutView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3) + self.previewContainerView.addSubview(transitionOutView) + } + view.animateOut(to: .gallery) + } + let destinationLocalFrame = destinationView.convert(transitionOut.destinationRect, to: self.view) + let destinatinoScale = destinationLocalFrame.width / self.previewContainerView.frame.width + let destinationAspectRatio = destinationLocalFrame.height / destinationLocalFrame.width + + var destinationSnapshotView: UIView? + if let destinationNode = destinationView.asyncdisplaykit_node, destinationNode is AvatarNode, let snapshotView = destinationView.snapshotView(afterScreenUpdates: false) { + destinationView.isHidden = true + + let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width + snapshotView.center = CGPoint(x: self.previewContainerView.bounds.width / 2.0, y: self.previewContainerView.bounds.height / 2.0) + snapshotView.transform = CGAffineTransform(scaleX: snapshotScale, y: snapshotScale) + + self.previewContainerView.addSubview(snapshotView) + destinationSnapshotView = snapshotView + } + + self.previewContainerView.layer.animatePosition(from: self.previewContainerView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { _ in + destinationView.isHidden = false + destinationSnapshotView?.removeFromSuperview() + completion() + }) + self.previewContainerView.layer.animateScale(from: 1.0, to: destinatinoScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + self.previewContainerView.layer.animateBounds(from: self.previewContainerView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (self.previewContainerView.bounds.height - self.previewContainerView.bounds.width * destinationAspectRatio) / 2.0), size: CGSize(width: self.previewContainerView.bounds.width, height: self.previewContainerView.bounds.width * destinationAspectRatio)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + + let targetCornerRadius: CGFloat + if transitionOut.destinationCornerRadius > 0.0 { + targetCornerRadius = self.previewContainerView.bounds.width + } else { + targetCornerRadius = 0.0 + } + self.previewContainerView.layer.animate( from: self.previewContainerView.layer.cornerRadius as NSNumber, - to: self.previewContainerView.bounds.width / 2.0 as NSNumber, + to: targetCornerRadius / 2.0 as NSNumber, keyPath: "cornerRadius", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.4, @@ -1172,7 +1301,7 @@ public final class MediaEditorScreen: ViewController { if let componentView = self.componentHost.view { componentView.clipsToBounds = true componentView.layer.animatePosition(from: componentView.center, to: destinationLocalFrame.center, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) - componentView.layer.animateScale(from: 1.0, to: targetScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) + componentView.layer.animateScale(from: 1.0, to: destinatinoScale, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateBounds(from: componentView.bounds, to: CGRect(origin: CGPoint(x: 0.0, y: (componentView.bounds.height - componentView.bounds.width) / 2.0), size: CGSize(width: componentView.bounds.width, height: componentView.bounds.width)), duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.4, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) componentView.layer.animate( @@ -1184,17 +1313,14 @@ public final class MediaEditorScreen: ViewController { removeOnCompletion: false ) } - } else if let sourceHint = controller.sourceHint { - switch sourceHint { - case .camera: - if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateOutToCamera() - } - let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) - transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in - completion() - }) + } else if let transitionIn = controller.transitionIn, case .camera = transitionIn { + if let view = self.componentHost.view as? MediaEditorScreenComponent.View { + view.animateOut(to: .camera) } + let transition = Transition(animation: .curve(duration: 0.25, curve: .easeInOut)) + transition.setAlpha(view: self.previewContainerView, alpha: 0.0, completion: { _ in + completion() + }) } else { completion() } @@ -1211,7 +1337,7 @@ public final class MediaEditorScreen: ViewController { view.animateInFromTool() } } - + func presentPrivacyTooltip() { guard let sourceView = self.componentHost.findTaggedView(tag: privacyButtonTag) else { return @@ -1221,10 +1347,10 @@ public final class MediaEditorScreen: ViewController { let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.maxY + 3.0), size: CGSize()) - let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: "You can set who can view this story", location: .point(location, .top), displayDuration: .manual, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) - self.controller?.present(controller, in: .current) + self.controller?.present(tooltipController, in: .current) } func presentSaveTooltip() { @@ -1243,11 +1369,40 @@ public final class MediaEditorScreen: ViewController { } else { text = "Image saved to Photos" } - - let controller = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .top), displayDuration: .default, inset: 16.0, shouldDismissOnTouch: { _ in return .ignore }) - self.controller?.present(controller, in: .current) + self.controller?.present(tooltipController, in: .current) + } + + private weak var storyArchiveTooltip: ViewController? + func presentStoryArchiveTooltip(sourceView: UIView) { + guard let controller = self.controller, case let .story(_, archive) = controller.state.privacy else { + return + } + + if let storyArchiveTooltip = self.storyArchiveTooltip { + storyArchiveTooltip.dismiss(animated: true) + self.storyArchiveTooltip = nil + } + + let parentFrame = self.view.convert(self.bounds, to: nil) + let absoluteFrame = sourceView.convert(sourceView.bounds, to: nil).offsetBy(dx: -parentFrame.minX, dy: 0.0) + let location = CGRect(origin: CGPoint(x: absoluteFrame.midX, y: absoluteFrame.minY - 3.0), size: CGSize()) + + let text: String + if archive { + text = "Story will be kept on your page." + } else { + text = "Story will disappear in 24 hours." + } + + let tooltipController = TooltipScreen(account: self.context.account, sharedContext: self.context.sharedContext, text: text, location: .point(location, .bottom), displayDuration: .default, inset: 7.0, shouldDismissOnTouch: { _ in + return .ignore + }) + self.storyArchiveTooltip = tooltipController + self.controller?.present(tooltipController, in: .current) } override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { @@ -1266,9 +1421,24 @@ public final class MediaEditorScreen: ViewController { } } + private func insertDrawingEntity(_ entity: DrawingEntity) { + self.entitiesView.prepareNewEntity(entity) + self.entitiesView.add(entity) + self.entitiesView.selectEntity(entity) + + if let entityView = entitiesView.getView(for: entity.uuid) { + entityView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2) + entityView.layer.animateScale(from: 0.1, to: entity.scale, duration: 0.2) + + if let selectionView = entityView.selectionView { + selectionView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2, delay: 0.2) + } + } + } + private var drawingScreen: DrawingScreen? func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { - guard let _ = self.controller else { + guard let controller = self.controller else { return } let isFirstTime = self.validLayout == nil @@ -1305,19 +1475,31 @@ public final class MediaEditorScreen: ViewController { MediaEditorScreenComponent( context: self.context, mediaEditor: self.mediaEditor, - privacy: self.storyPrivacy, - timeout: self.timeout, + privacy: controller.state.privacy, openDrawing: { [weak self] mode in if let self { - let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, existingStickerPickerInputData: self.stickerPickerInputData) + switch mode { + case .sticker: + let controller = StickerPickerScreen(context: self.context, inputData: self.stickerPickerInputData.get()) + controller.completion = { [weak self] file in + if let self, let file { + let stickerEntity = DrawingStickerEntity(content: .file(file)) + self.insertDrawingEntity(stickerEntity) + } + } + self.controller?.present(controller, in: .current) + return + case .text: + break + default: + break + } + + let controller = DrawingScreen(context: self.context, sourceHint: .storyEditor, size: self.previewContainerView.frame.size, originalSize: storyDimensions, isVideo: false, isAvatar: false, drawingView: self.drawingView, entitiesView: self.entitiesView, selectionContainerView: self.selectionContainerView, existingStickerPickerInputData: self.stickerPickerInputData) self.drawingScreen = controller self.drawingView.isUserInteractionEnabled = true - - let selectionContainerView = controller.selectionContainerView - selectionContainerView.frame = self.previewContainerView.bounds - self.previewContainerView.addSubview(selectionContainerView) - - controller.requestDismiss = { [weak controller, weak self, weak selectionContainerView] in + + controller.requestDismiss = { [weak controller, weak self] in self?.drawingScreen = nil controller?.animateOut({ controller?.dismiss() @@ -1325,9 +1507,9 @@ public final class MediaEditorScreen: ViewController { self?.drawingView.isUserInteractionEnabled = false self?.animateInFromTool() - selectionContainerView?.removeFromSuperview() + self?.entitiesView.selectEntity(nil) } - controller.requestApply = { [weak controller, weak self, weak selectionContainerView] in + controller.requestApply = { [weak controller, weak self] in self?.drawingScreen = nil controller?.animateOut({ controller?.dismiss() @@ -1341,7 +1523,7 @@ public final class MediaEditorScreen: ViewController { self?.mediaEditor?.setDrawingAndEntities(data: nil, image: nil, entities: []) } - selectionContainerView?.removeFromSuperview() + self?.entitiesView.selectEntity(nil) } self.controller?.present(controller, in: .current) @@ -1407,6 +1589,8 @@ public final class MediaEditorScreen: ViewController { transition.setFrame(view: self.gradientView, frame: CGRect(origin: .zero, size: previewFrame.size)) transition.setFrame(view: self.drawingView, frame: CGRect(origin: .zero, size: previewFrame.size)) + transition.setFrame(view: self.selectionContainerView, frame: CGRect(origin: .zero, size: previewFrame.size)) + if isFirstTime { self.animateIn() } @@ -1475,22 +1659,16 @@ public final class MediaEditorScreen: ViewController { fileprivate let subject: Signal fileprivate let transitionIn: TransitionIn? fileprivate let transitionOut: (Bool) -> TransitionOut? - - public enum SourceHint { - case camera - } - public var sourceHint: SourceHint? - + public var cancelled: (Bool) -> Void = { _ in } - public var completion: (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void = { _, _, _ in } - public var onReady: () -> Void = {} + public var completion: (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void = { _, _, _ in } public init( context: AccountContext, subject: Signal, transitionIn: TransitionIn?, transitionOut: @escaping (Bool) -> TransitionOut?, - completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, EngineStoryPrivacy) -> Void + completion: @escaping (MediaEditorScreen.Result, @escaping () -> Void, MediaEditorResultPrivacy) -> Void ) { self.context = context self.subject = subject @@ -1518,22 +1696,175 @@ public final class MediaEditorScreen: ViewController { } func presentPrivacySettings() { - let stateContext = ShareWithPeersScreen.StateContext(context: self.context) + if case .message(_, _) = self.state.privacy { + self.presentSendAsMessage() + } else { + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .stories) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + + let initialPrivacy: EngineStoryPrivacy + if case let .story(privacy, _) = self.state.privacy { + initialPrivacy = privacy + } else { + initialPrivacy = EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []) + } + + self.push( + ShareWithPeersScreen( + context: self.context, + initialPrivacy: initialPrivacy, + stateContext: stateContext, + completion: { [weak self] privacy in + guard let self else { + return + } + self.state.privacy = .story(privacy: privacy, archive: true) + }, + editCategory: { [weak self] privacy in + guard let self else { + return + } + self.presentEditCategory(privacy: privacy, completion: { [weak self] privacy in + guard let self else { + return + } + self.state.privacy = .story(privacy: privacy, archive: true) + self.presentPrivacySettings() + }) + }, + secondaryAction: { [weak self] in + guard let self else { + return + } + self.presentSendAsMessage() + } + ) + ) + }) + } + } + + private func presentEditCategory(privacy: EngineStoryPrivacy, completion: @escaping (EngineStoryPrivacy) -> Void) { + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .contacts(privacy.base)) let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in guard let self else { return } - self.push(ShareWithPeersScreen(context: self.context, initialPrivacy: self.node.storyPrivacy, stateContext: stateContext, completion: { [weak self] privacy in - guard let self else { - return - } - self.node.storyPrivacy = privacy - self.node.requestUpdate() - })) + self.push( + ShareWithPeersScreen( + context: self.context, + initialPrivacy: privacy, + stateContext: stateContext, + completion: { [weak self] result in + guard let self else { + return + } + if case .closeFriends = privacy.base { + let _ = self.context.engine.privacy.updateCloseFriends(peerIds: result.additionallyIncludePeers).start() + } + completion(result) + }, + editCategory: { _ in }, + secondaryAction: { [weak self] in + guard let self else { + return + } + self.presentSendAsMessage() + } + ) + ) }) } + private func presentSendAsMessage() { + var initialPeerIds = Set() + if case let .message(peers, _) = self.state.privacy { + initialPeerIds = Set(peers) + } + let stateContext = ShareWithPeersScreen.StateContext(context: self.context, subject: .chats, initialPeerIds: initialPeerIds) + let _ = (stateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + + self.push( + ShareWithPeersScreen( + context: self.context, + initialPrivacy: EngineStoryPrivacy(base: .everyone, additionallyIncludePeers: []), + stateContext: stateContext, + completion: { [weak self] privacy in + guard let self else { + return + } + self.state.privacy = .message(peers: privacy.additionallyIncludePeers, timeout: nil) + }, + editCategory: { _ in }, + secondaryAction: {} + ) + ) + }) + } + + func presentTimeoutSetup(sourceView: UIView) { + var items: [ContextMenuItem] = [] + + let updateTimeout: (Int32?) -> Void = { [weak self] timeout in + guard let self else { + return + } + if case let .message(peers, _) = self.state.privacy { + self.state.privacy = .message(peers: peers, timeout: timeout) + } + } + + let emptyAction: ((ContextMenuActionItem.Action) -> Void)? = nil + items.append(.action(ContextMenuActionItem(text: "Choose how long the media will be kept after opening.", textLayout: .multiline, textFont: .small, icon: { _ in return nil }, action: emptyAction))) + + items.append(.action(ContextMenuActionItem(text: "Until First View", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(1) + }))) + items.append(.action(ContextMenuActionItem(text: "3 Seconds", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(3) + }))) + items.append(.action(ContextMenuActionItem(text: "10 Seconds", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(10) + }))) + items.append(.action(ContextMenuActionItem(text: "1 Minute", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(60) + }))) + items.append(.action(ContextMenuActionItem(text: "Keep Always", icon: { _ in + return nil + }, action: { _, a in + a(.default) + + updateTimeout(nil) + }))) + + let presentationData = self.context.sharedContext.currentPresentationData.with({ $0 }).withUpdated(theme: defaultDarkPresentationTheme) + let contextController = ContextController(account: self.context.account, presentationData: presentationData, source: .reference(HeaderContextReferenceContentSource(controller: self, sourceView: sourceView)), items: .single(ContextController.Items(content: .list(items))), gesture: nil) + self.present(contextController, in: .window(.root)) + } + func maybePresentDiscardAlert() { if let subject = self.node.subject, case .asset = subject { self.requestDismiss(saveDraft: false, animated: true) @@ -1606,6 +1937,10 @@ public final class MediaEditorScreen: ViewController { } } + if let mediaEditor = self.node.mediaEditor { + mediaEditor.stop() + } + self.cancelled(saveDraft) self.node.animateOut(finished: false, completion: { [weak self] in @@ -1617,6 +1952,8 @@ public final class MediaEditorScreen: ViewController { guard let mediaEditor = self.node.mediaEditor, let subject = self.node.subject else { return } + + mediaEditor.stop() if mediaEditor.resultIsVideo { let videoResult: Result.VideoResult @@ -1664,7 +2001,7 @@ public final class MediaEditorScreen: ViewController { self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() }) - }, self.node.storyPrivacy) + }, self.state.privacy) if case let .draft(draft) = subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) @@ -1677,7 +2014,7 @@ public final class MediaEditorScreen: ViewController { self?.node.animateOut(finished: true, completion: { [weak self] in self?.dismiss() }) - }, self.node.storyPrivacy) + }, self.state.privacy) if case let .draft(draft) = subject { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) } @@ -1866,3 +2203,20 @@ final class PrivacyButtonComponent: CombinedComponent { } } } + +private final class HeaderContextReferenceContentSource: ContextReferenceContentSource { + private let controller: ViewController + private let sourceView: UIView + var keepInPlace: Bool { + return true + } + + init(controller: ViewController, sourceView: UIView) { + self.controller = controller + self.sourceView = sourceView + } + + func transitionInfo() -> ContextControllerReferenceViewInfo? { + return ContextControllerReferenceViewInfo(referenceView: self.sourceView, contentAreaInScreenSpace: UIScreen.main.bounds, actionsPosition: .top) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index cd5bc4560c..a072eb531b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -520,6 +520,7 @@ private final class MediaToolsScreenComponent: Component { }) } + var needsHistogram = false let screenSize: CGSize let optionsSize: CGSize let optionsTransition: Transition = sectionChanged ? .immediate : transition @@ -711,6 +712,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) ) case .curves: + needsHistogram = true let internalState: CurvesInternalState if let current = self.curvesState { internalState = current @@ -755,6 +757,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) ) } + component.mediaEditor.isHistogramEnabled = needsHistogram let optionsFrame = CGRect(origin: .zero, size: optionsSize) if let optionsView = self.toolOptions.view { diff --git a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift index 5ff550726c..9fdbe2e22a 100644 --- a/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift +++ b/submodules/TelegramUI/Components/MessageInputPanelComponent/Sources/MessageInputPanelComponent.swift @@ -239,7 +239,7 @@ public final class MessageInputPanelComponent: Component { } func update(component: MessageInputPanelComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { - var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 7.0) + var insets = UIEdgeInsets(top: 14.0, left: 7.0, bottom: 6.0, right: 41.0) if let _ = component.attachmentAction { insets.left = 41.0 @@ -321,8 +321,8 @@ public final class MessageInputPanelComponent: Component { environment: {}, containerSize: availableTextFieldSize ) - if self.textFieldExternalState.isEditing { - insets.right = 41.0 + if !self.textFieldExternalState.isEditing && component.setMediaRecordingActive == nil { + insets.right = insets.left } let fieldFrame = CGRect(origin: CGPoint(x: insets.left, y: insets.top), size: CGSize(width: availableSize.width - insets.left - insets.right, height: textFieldSize.height)) @@ -707,7 +707,7 @@ public final class MessageInputPanelComponent: Component { if timeoutButtonView.superview == nil { self.addSubview(timeoutButtonView) } - let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.minY + 1.0 + floor((fieldFrame.height - timeoutButtonSize.height) * 0.5)), size: timeoutButtonSize) + let timeoutIconFrame = CGRect(origin: CGPoint(x: fieldIconNextX - timeoutButtonSize.width, y: fieldFrame.maxY - 3.0 - timeoutButtonSize.height), size: timeoutButtonSize) transition.setPosition(view: timeoutButtonView, position: timeoutIconFrame.center) transition.setBounds(view: timeoutButtonView, bounds: CGRect(origin: CGPoint(), size: timeoutIconFrame.size)) diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD index 5bcc1de71f..af39190090 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/BUILD @@ -29,9 +29,11 @@ swift_library( "//submodules/TelegramUI/Components/PlainButtonComponent", "//submodules/TelegramUI/Components/AnimatedCounterComponent", "//submodules/TelegramUI/Components/TokenListTextField", + "//submodules/Components/BundleIconComponent", "//submodules/AvatarNode", "//submodules/CheckNode", "//submodules/PeerPresenceStatusManager", + "//submodules/LocalizedPeerData" ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift index 1788bc76f8..8c38a9b484 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/CategoryListItemComponent.swift @@ -10,6 +10,7 @@ import MultilineTextComponent import AvatarNode import TelegramPresentationData import CheckNode +import BundleIconComponent final class CategoryListItemComponent: Component { enum SelectionState: Equatable { @@ -27,6 +28,7 @@ final class CategoryListItemComponent: Component { let selectionState: SelectionState let hasNext: Bool let action: () -> Void + let secondaryAction: () -> Void init( context: AccountContext, @@ -38,7 +40,8 @@ final class CategoryListItemComponent: Component { subtitle: String?, selectionState: SelectionState, hasNext: Bool, - action: @escaping () -> Void + action: @escaping () -> Void, + secondaryAction: @escaping () -> Void ) { self.context = context self.theme = theme @@ -50,6 +53,7 @@ final class CategoryListItemComponent: Component { self.selectionState = selectionState self.hasNext = hasNext self.action = action + self.secondaryAction = secondaryAction } static func ==(lhs: CategoryListItemComponent, rhs: CategoryListItemComponent) -> Bool { @@ -88,6 +92,7 @@ final class CategoryListItemComponent: Component { private let title = ComponentView() private let label = ComponentView() + private let labelArrow = ComponentView() private let separatorLayer: SimpleLayer private let iconView: UIImageView @@ -120,7 +125,11 @@ final class CategoryListItemComponent: Component { guard let component = self.component else { return } - component.action() + if case .editing(true, _) = component.selectionState { + component.secondaryAction() + } else { + component.action() + } } func update(component: CategoryListItemComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { @@ -223,7 +232,12 @@ final class CategoryListItemComponent: Component { transition.setFrame(view: self.iconView, frame: avatarFrame) - let labelData: (String, Bool) = ("", false) + let labelData: (String, Bool, Bool) + if let subtitle = component.subtitle { + labelData = (subtitle, true, true) + } else { + labelData = ("", false, false) + } let labelSize = self.label.update( transition: .immediate, @@ -234,6 +248,13 @@ final class CategoryListItemComponent: Component { containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) ) + let labelArrowSize = self.labelArrow.update( + transition: .immediate, + component: AnyComponent(BundleIconComponent(name: "Contact List/SubtitleArrow", tintColor: component.theme.list.itemAccentColor)), + environment: {}, + containerSize: CGSize(width: availableSize.width - leftInset - rightInset, height: 100.0) + ) + let previousTitleFrame = self.title.view?.frame var previousTitleContents: UIView? if hasSelectionUpdated && !"".isEmpty { @@ -284,6 +305,13 @@ final class CategoryListItemComponent: Component { } transition.setFrame(view: labelView, frame: CGRect(origin: CGPoint(x: titleFrame.minX, y: titleFrame.maxY + titleSpacing), size: labelSize)) } + if let labelArrowView = self.labelArrow.view, !labelData.0.isEmpty { + if labelArrowView.superview == nil { + labelArrowView.isUserInteractionEnabled = false + self.containerButton.addSubview(labelArrowView) + } + transition.setFrame(view: labelArrowView, frame: CGRect(origin: CGPoint(x: titleFrame.minX + labelSize.width + 5.0, y: titleFrame.maxY + titleSpacing + floorToScreenPixels(labelSize.height / 2.0 - labelArrowSize.height / 2.0)), size: labelArrowSize)) + } if themeUpdated { self.separatorLayer.backgroundColor = component.theme.list.itemPlainSeparatorColor.cgColor diff --git a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift index bf886c3f3c..ee97a5f4fe 100644 --- a/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift +++ b/submodules/TelegramUI/Components/ShareWithPeersScreen/Sources/ShareWithPeersScreen.swift @@ -17,6 +17,7 @@ import PlainButtonComponent import AnimatedCounterComponent import TokenListTextField import AvatarNode +import LocalizedPeerData final class ShareWithPeersScreenComponent: Component { typealias EnvironmentType = ViewControllerComponentContainer.Environment @@ -26,19 +27,25 @@ final class ShareWithPeersScreenComponent: Component { let initialPrivacy: EngineStoryPrivacy let categoryItems: [CategoryItem] let completion: (EngineStoryPrivacy) -> Void + let editCategory: (EngineStoryPrivacy) -> Void + let secondaryAction: () -> Void init( context: AccountContext, stateContext: ShareWithPeersScreen.StateContext, initialPrivacy: EngineStoryPrivacy, categoryItems: [CategoryItem], - completion: @escaping (EngineStoryPrivacy) -> Void + completion: @escaping (EngineStoryPrivacy) -> Void, + editCategory: @escaping (EngineStoryPrivacy) -> Void, + secondaryAction: @escaping () -> Void ) { self.context = context self.stateContext = stateContext self.initialPrivacy = initialPrivacy self.categoryItems = categoryItems self.completion = completion + self.editCategory = editCategory + self.secondaryAction = secondaryAction } static func ==(lhs: ShareWithPeersScreenComponent, rhs: ShareWithPeersScreenComponent) -> Bool { @@ -204,6 +211,7 @@ final class ShareWithPeersScreenComponent: Component { private let navigationBackgroundView: BlurredBackgroundView private let navigationTitle = ComponentView() private let navigationLeftButton = ComponentView() + private let navigationRightButton = ComponentView() private let navigationSeparatorLayer: SimpleLayer private let navigationTextFieldState = TokenListTextField.ExternalState() private let navigationTextField = ComponentView() @@ -440,7 +448,11 @@ final class ShareWithPeersScreenComponent: Component { if section.id == 0 { sectionTitle = "WHO CAN VIEW FOR 24 HOURS" } else { - sectionTitle = "CONTACTS" + if case .chats = component.stateContext.subject { + sectionTitle = "CHATS" + } else { + sectionTitle = "CONTACTS" + } } let _ = sectionHeader.update( @@ -521,6 +533,26 @@ final class ShareWithPeersScreenComponent: Component { } } self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + }, + secondaryAction: { [weak self] in + guard let self, let environment = self.environment, let controller = environment.controller() else { + return + } + let base: EngineStoryPrivacy.Base? + switch categoryId { + case .everyone: + base = nil + case .contacts: + base = .contacts + case .closeFriends: + base = .closeFriends + case .selectedContacts: + base = .nobody + } + if let base { + component.editCategory(EngineStoryPrivacy(base: base, additionallyIncludePeers: self.selectedPeers)) + controller.dismiss() + } } )), environment: {}, @@ -700,6 +732,8 @@ final class ShareWithPeersScreenComponent: Component { var applyState = false self.defaultStateValue = component.stateContext.stateValue + self.selectedPeers = Array(component.stateContext.initialPeerIds) + self.stateDisposable = (component.stateContext.state |> deliverOnMainQueue).start(next: { [weak self] stateValue in guard let self else { @@ -738,97 +772,85 @@ final class ShareWithPeersScreenComponent: Component { self.bottomSeparatorLayer.backgroundColor = environment.theme.rootController.navigationBar.separatorColor.cgColor } - var tokens: [TokenListTextField.Token] = [] - for categoryId in self.selectedCategories.sorted(by: { $0.rawValue < $1.rawValue }) { - let categoryTitle: String - var categoryImage: UIImage? - switch categoryId { - case .everyone: - categoryTitle = "Everyone" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Channel"), color: .white), iconScale: 0.6, cornerRadius: 6.0, circleCorners: true, color: .blue) - case .contacts: - categoryTitle = "Contacts" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Tabs/IconContacts"), color: .white), iconScale: 0.6 * 0.9, cornerRadius: 6.0, circleCorners: true, color: .yellow) - case .closeFriends: - categoryTitle = "Close Friends" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Call/StarHighlighted"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .green) - case .selectedContacts: - categoryTitle = "Selected Contacts" - categoryImage = generateAvatarImage(size: CGSize(width: 22.0, height: 22.0), icon: generateTintedImage(image: UIImage(bundleImageName: "Chat List/Filters/Group"), color: .white), iconScale: 0.6 * 1.0, cornerRadius: 6.0, circleCorners: true, color: .purple) - } - tokens.append(TokenListTextField.Token( - id: AnyHashable(categoryId), - title: categoryTitle, - fixedPosition: categoryId.rawValue, - content: .category(categoryImage) - )) - } - for peerId in self.selectedPeers { - guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { - continue - } - tokens.append(TokenListTextField.Token( - id: AnyHashable(peerId), - title: peer.compactDisplayTitle, - fixedPosition: nil, - content: .peer(peer) - )) - } - - self.navigationTextField.parentState = state - let navigationTextFieldSize = self.navigationTextField.update( - transition: transition, - component: AnyComponent(TokenListTextField( - externalState: self.navigationTextFieldState, - context: component.context, - theme: environment.theme, - placeholder: "Search Contacts", - tokens: tokens, - sideInset: sideInset, - deleteToken: { [weak self] tokenId in - guard let self else { - return - } - if let categoryId = tokenId.base as? CategoryId { - self.selectedCategories.remove(categoryId) - } else if let peerId = tokenId.base as? EnginePeer.Id { - self.selectedPeers.removeAll(where: { $0 == peerId }) - } - if self.selectedCategories.isEmpty { - self.selectedCategories.insert(.everyone) - } - self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + let navigationTextFieldSize: CGSize + if case .stories = component.stateContext.subject { + navigationTextFieldSize = .zero + } else { + var tokens: [TokenListTextField.Token] = [] + for peerId in self.selectedPeers { + guard let stateValue = self.defaultStateValue, let peer = stateValue.peers.first(where: { $0.id == peerId }) else { + continue } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width, height: 1000.0) - ) - - if !self.navigationTextFieldState.text.isEmpty { - if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) { - } else { - self.searchStateDisposable?.dispose() - let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text)) - var applyState = false - self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in - guard let self else { - return - } - self.searchStateContext = searchStateContext - if applyState { - self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) - } - }) - applyState = true + tokens.append(TokenListTextField.Token( + id: AnyHashable(peerId), + title: peer.compactDisplayTitle, + fixedPosition: nil, + content: .peer(peer) + )) } - } else if let _ = self.searchStateContext { - self.searchStateContext = nil - self.searchStateDisposable?.dispose() - self.searchStateDisposable = nil - contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true)) + let placeholder: String + switch component.stateContext.subject { + case .chats: + placeholder = "Search Chats" + default: + placeholder = "Search Contacts" + } + self.navigationTextField.parentState = state + navigationTextFieldSize = self.navigationTextField.update( + transition: transition, + component: AnyComponent(TokenListTextField( + externalState: self.navigationTextFieldState, + context: component.context, + theme: environment.theme, + placeholder: placeholder, + tokens: tokens, + sideInset: sideInset, + deleteToken: { [weak self] tokenId in + guard let self else { + return + } + if let categoryId = tokenId.base as? CategoryId { + self.selectedCategories.remove(categoryId) + } else if let peerId = tokenId.base as? EnginePeer.Id { + self.selectedPeers.removeAll(where: { $0 == peerId }) + } + if self.selectedCategories.isEmpty { + self.selectedCategories.insert(.everyone) + } + self.state?.updated(transition: Transition(animation: .curve(duration: 0.35, curve: .spring))) + } + )), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: 1000.0) + ) + + if !self.navigationTextFieldState.text.isEmpty { + if let searchStateContext = self.searchStateContext, searchStateContext.subject == .search(self.navigationTextFieldState.text) { + } else { + self.searchStateDisposable?.dispose() + let searchStateContext = ShareWithPeersScreen.StateContext(context: component.context, subject: .search(self.navigationTextFieldState.text)) + var applyState = false + self.searchStateDisposable = (searchStateContext.ready |> filter { $0 } |> take(1) |> deliverOnMainQueue).start(next: { [weak self] _ in + guard let self else { + return + } + self.searchStateContext = searchStateContext + if applyState { + self.state?.updated(transition: Transition(animation: .none).withUserData(AnimationHint(contentReloaded: true))) + } + }) + applyState = true + } + } else if let _ = self.searchStateContext { + self.searchStateContext = nil + self.searchStateDisposable?.dispose() + self.searchStateDisposable = nil + + contentTransition = contentTransition.withUserData(AnimationHint(contentReloaded: true)) + } } - + transition.setFrame(view: self.dimView, frame: CGRect(origin: CGPoint(), size: availableSize)) let categoryItemSize = self.categoryTemplateItem.update( @@ -843,7 +865,8 @@ final class ShareWithPeersScreenComponent: Component { subtitle: nil, selectionState: .editing(isSelected: false, isTinted: false), hasNext: true, - action: {} + action: {}, + secondaryAction: {} )), environment: {}, containerSize: CGSize(width: availableSize.width, height: 1000.0) @@ -870,54 +893,121 @@ final class ShareWithPeersScreenComponent: Component { var sections: [ItemLayout.Section] = [] if let stateValue = self.effectiveStateValue { - if self.searchStateContext == nil { + if case .stories = component.stateContext.subject { sections.append(ItemLayout.Section( id: 0, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0), + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), itemHeight: categoryItemSize.height, itemCount: component.categoryItems.count )) + } else { + sections.append(ItemLayout.Section( + id: 1, + insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 0.0, right: 0.0), + itemHeight: peerItemSize.height, + itemCount: stateValue.peers.count + )) } - sections.append(ItemLayout.Section( - id: 1, - insets: UIEdgeInsets(top: 28.0, left: 0.0, bottom: 00, right: 0.0), - itemHeight: peerItemSize.height, - itemCount: stateValue.peers.count - )) } let containerInset: CGFloat = environment.statusBarHeight + 10.0 var navigationHeight: CGFloat = 56.0 - let navigationSideInset: CGFloat = 16.0 - let navigationLeftButtonSize = self.navigationLeftButton.update( + var navigationButtonsWidth: CGFloat = 0.0 + + if case .stories = component.stateContext.subject { + } else { + let navigationLeftButtonSize = self.navigationLeftButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + action: { [weak self] in + guard let self, let environment = self.environment, let controller = environment.controller() else { + return + } + controller.dismiss() + } + ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), + environment: {}, + containerSize: CGSize(width: availableSize.width, height: navigationHeight) + ) + let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) + if let navigationLeftButtonView = self.navigationLeftButton.view { + if navigationLeftButtonView.superview == nil { + self.navigationContainerView.addSubview(navigationLeftButtonView) + } + transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + } + navigationButtonsWidth += navigationLeftButtonSize.width + navigationSideInset + } + + let navigationRightButtonSize = self.navigationRightButton.update( transition: transition, component: AnyComponent(Button( - content: AnyComponent(Text(text: "Cancel", font: Font.regular(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), + content: AnyComponent(Text(text: "Done", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.accentTextColor)), action: { [weak self] in - guard let self, let environment = self.environment, let controller = environment.controller() else { + guard let self, let component = self.component, let controller = self.environment?.controller() else { return } + + let base: EngineStoryPrivacy.Base + if self.selectedCategories.contains(.everyone) { + base = .everyone + } else if self.selectedCategories.contains(.closeFriends) { + base = .closeFriends + } else if self.selectedCategories.contains(.contacts) { + base = .contacts + } else if self.selectedCategories.contains(.selectedContacts) { + base = .nobody + } else { + base = .nobody + } + + component.completion(EngineStoryPrivacy( + base: base, + additionallyIncludePeers: self.selectedPeers + )) controller.dismiss() } ).minSize(CGSize(width: navigationHeight, height: navigationHeight))), environment: {}, containerSize: CGSize(width: availableSize.width, height: navigationHeight) ) - let navigationLeftButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: floor((navigationHeight - navigationLeftButtonSize.height) * 0.5)), size: navigationLeftButtonSize) - if let navigationLeftButtonView = self.navigationLeftButton.view { - if navigationLeftButtonView.superview == nil { - self.navigationContainerView.addSubview(navigationLeftButtonView) + let navigationRightButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - navigationSideInset - navigationRightButtonSize.width, y: floor((navigationHeight - navigationRightButtonSize.height) * 0.5)), size: navigationRightButtonSize) + if let navigationRightButtonView = self.navigationRightButton.view { + if navigationRightButtonView.superview == nil { + self.navigationContainerView.addSubview(navigationRightButtonView) } - transition.setFrame(view: navigationLeftButtonView, frame: navigationLeftButtonFrame) + transition.setFrame(view: navigationRightButtonView, frame: navigationRightButtonFrame) } + navigationButtonsWidth += navigationRightButtonSize.width + navigationSideInset + let title: String + switch component.stateContext.subject { + case .stories: + title = "Share Story" + case .chats: + title = "Send as a Message" + case let .contacts(category): + switch category { + case .closeFriends: + title = "Close Friends" + case .contacts: + title = "Excluded People" + case .nobody: + title = "Selected Contacts" + case .everyone: + title = "" + } + case .search: + title = "" + } let navigationTitleSize = self.navigationTitle.update( transition: .immediate, - component: AnyComponent(Text(text: "Share Story", font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)), + component: AnyComponent(Text(text: title, font: Font.semibold(17.0), color: environment.theme.rootController.navigationBar.primaryTextColor)), environment: {}, - containerSize: CGSize(width: availableSize.width - navigationSideInset - navigationLeftButtonFrame.maxX, height: navigationHeight) + containerSize: CGSize(width: availableSize.width - navigationButtonsWidth, height: navigationHeight) ) let navigationTitleFrame = CGRect(origin: CGPoint(x: floor((availableSize.width - navigationTitleSize.width) * 0.5), y: floor((navigationHeight - navigationTitleSize.height) * 0.5)), size: navigationTitleSize) if let navigationTitleView = self.navigationTitle.view { @@ -943,7 +1033,11 @@ final class ShareWithPeersScreenComponent: Component { if environment.inputHeight != 0.0 || !self.navigationTextFieldState.text.isEmpty { topInset = 0.0 } else { - topInset = max(0.0, availableSize.height - containerInset - 600.0) + if case .stories = component.stateContext.subject { + topInset = max(0.0, availableSize.height - containerInset - 410.0) + } else { + topInset = max(0.0, availableSize.height - containerInset - 600.0) + } } self.navigationBackgroundView.update(size: CGSize(width: availableSize.width, height: navigationHeight), cornerRadius: 10.0, maskedCorners: [.layerMinXMinYCorner, .layerMaxXMinYCorner], transition: transition.containedViewLayoutTransition) @@ -951,84 +1045,47 @@ final class ShareWithPeersScreenComponent: Component { transition.setFrame(layer: self.navigationSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: navigationHeight), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - var actionButtonTitle: String = "Post Story" - if self.selectedCategories.contains(.everyone) { - actionButtonTitle = "Post Story" - } else if self.selectedCategories.contains(.closeFriends) { - actionButtonTitle = "Send to Close Friends" - } else if self.selectedCategories.contains(.contacts) { - actionButtonTitle = "Send to Contacts" - } else if self.selectedCategories.contains(.selectedContacts) { - actionButtonTitle = "Send to Selected Contacts" - } - - let actionButtonSize = self.actionButton.update( - transition: transition, - component: AnyComponent(ButtonComponent( - background: ButtonComponent.Background( - color: environment.theme.list.itemCheckColors.fillColor, - foreground: environment.theme.list.itemCheckColors.foregroundColor, - pressedColor: environment.theme.list.itemCheckColors.fillColor.withMultipliedAlpha(0.9) - ), - content: AnyComponentWithIdentity( - id: actionButtonTitle, - component: AnyComponent(ButtonTextContentComponent( - text: actionButtonTitle, - badge: 0, - textColor: environment.theme.list.itemCheckColors.foregroundColor, - badgeBackground: environment.theme.list.itemCheckColors.foregroundColor, - badgeForeground: environment.theme.list.itemCheckColors.fillColor - )) - ), - isEnabled: true, - displaysProgress: false, - action: { [weak self] in - guard let self, let component = self.component, let controller = self.environment?.controller() else { - return - } - - let base: EngineStoryPrivacy.Base - if self.selectedCategories.contains(.everyone) { - base = .everyone - } else if self.selectedCategories.contains(.closeFriends) { - base = .closeFriends - } else if self.selectedCategories.contains(.contacts) { - base = .contacts - } else if self.selectedCategories.contains(.selectedContacts) { - base = .nobody - } else { - base = .nobody - } - - component.completion(EngineStoryPrivacy( - base: base, - additionallyIncludePeers: self.selectedPeers - )) - controller.dismiss() - } - )), - environment: {}, - containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 50.0) - ) - var bottomPanelHeight: CGFloat = 0.0 - if environment.inputHeight != 0.0 { - bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height - } else { - bottomPanelHeight += 10.0 + environment.safeInsets.bottom + actionButtonSize.height - } - let actionButtonFrame = CGRect(origin: CGPoint(x: navigationSideInset, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) - if let actionButtonView = self.actionButton.view { - if actionButtonView.superview == nil { - self.addSubview(actionButtonView) + if case .stories = component.stateContext.subject { + let actionButtonSize = self.actionButton.update( + transition: transition, + component: AnyComponent(Button( + content: AnyComponent(Text( + text: "Send as a Message", + font: Font.regular(17.0), + color: environment.theme.list.itemAccentColor + )), + action: { [weak self] in + guard let self, let component = self.component, let controller = self.environment?.controller() else { + return + } + + component.secondaryAction() + controller.dismiss() + } + ).minSize(CGSize(width: 200.0, height: 44.0))), + environment: {}, + containerSize: CGSize(width: availableSize.width - navigationSideInset * 2.0, height: 44.0) + ) + + if environment.inputHeight != 0.0 { + bottomPanelHeight += environment.inputHeight + 8.0 + actionButtonSize.height + } else { + bottomPanelHeight += environment.safeInsets.bottom + actionButtonSize.height } - transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + let actionButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - actionButtonSize.width) / 2.0, y: availableSize.height - bottomPanelHeight), size: actionButtonSize) + if let actionButtonView = self.actionButton.view { + if actionButtonView.superview == nil { + self.addSubview(actionButtonView) + } + transition.setFrame(view: actionButtonView, frame: actionButtonFrame) + } + + transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0))) + self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) + transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) } - transition.setFrame(view: self.bottomBackgroundView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0), size: CGSize(width: availableSize.width, height: bottomPanelHeight + 8.0))) - self.bottomBackgroundView.update(size: self.bottomBackgroundView.bounds.size, transition: transition.containedViewLayoutTransition) - transition.setFrame(layer: self.bottomSeparatorLayer, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - bottomPanelHeight - 8.0 - UIScreenPixel), size: CGSize(width: availableSize.width, height: UIScreenPixel))) - let itemLayout = ItemLayout(containerSize: availableSize, containerInset: containerInset, bottomInset: bottomPanelHeight + environment.safeInsets.bottom, topInset: topInset, sideInset: sideInset, navigationHeight: navigationHeight, sections: sections) let previousItemLayout = self.itemLayout self.itemLayout = itemLayout @@ -1099,13 +1156,16 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public final class StateContext { public enum Subject: Equatable { - case contacts + case stories + case chats + case contacts(EngineStoryPrivacy.Base) case search(String) } fileprivate var stateValue: State? public let subject: Subject + public private(set) var initialPeerIds: Set = Set() private var stateDisposable: Disposable? private let stateSubject = Promise() @@ -1119,12 +1179,51 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { public init( context: AccountContext, - subject: Subject = .contacts + subject: Subject = .chats, + initialPeerIds: Set = Set() ) { self.subject = subject + self.initialPeerIds = initialPeerIds switch subject { - case .contacts: + case .stories: + let state = State(peers: [], presences: [:]) + self.stateValue = state + self.stateSubject.set(.single(state)) + self.readySubject.set(true) + case .chats: + self.stateDisposable = (context.engine.messages.chatList(group: .root, count: 200) + |> deliverOnMainQueue).start(next: { [weak self] chatList in + guard let self else { + return + } + + var selectedPeers: [EnginePeer] = [] + for item in chatList.items.reversed() { + if self.initialPeerIds.contains(item.renderedPeer.peerId), let peer = item.renderedPeer.peer { + selectedPeers.append(peer) + } + } + + var presences: [EnginePeer.Id: EnginePeer.Presence] = [:] + for item in chatList.items { + presences[item.renderedPeer.peerId] = item.presence + } + + var peers: [EnginePeer] = [] + peers = chatList.items.filter { !self.initialPeerIds.contains($0.renderedPeer.peerId) }.reversed().compactMap { $0.renderedPeer.peer } + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, + presences: presences + ) + self.stateValue = state + self.stateSubject.set(.single(state)) + + self.readySubject.set(true) + }) + case let .contacts(base): self.stateDisposable = (context.engine.data.subscribe( TelegramEngine.EngineData.Item.Contacts.List(includePresences: true) ) @@ -1133,23 +1232,40 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { return } - let state = State( - peers: contactList.peers.sorted(by: { lhs, rhs in - let lhsPresence = contactList.presences[lhs.id] - let rhsPresence = contactList.presences[rhs.id] - - if let lhsPresence, let rhsPresence { - return lhsPresence.status > rhsPresence.status - } else if lhsPresence != nil { - return true - } else if rhsPresence != nil { - return false - } else { - return lhs.id < rhs.id + var selectedPeers: [EnginePeer] = [] + if case .closeFriends = base { + for peer in contactList.peers { + if case let .user(user) = peer, user.flags.contains(.isCloseFriend) { + selectedPeers.append(peer) } - }), + } + selectedPeers = selectedPeers.sorted(by: { lhs, rhs in + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) + if result == .orderedSame { + return lhs.id < rhs.id + } else { + return result == .orderedAscending + } + }) + self.initialPeerIds = Set(selectedPeers.map { $0.id }) + } + + var peers: [EnginePeer] = [] + peers = contactList.peers.filter { !self.initialPeerIds.contains($0.id) }.sorted(by: { lhs, rhs in + let result = lhs.indexName.isLessThan(other: rhs.indexName, ordering: .firstLast) + if result == .orderedSame { + return lhs.id < rhs.id + } else { + return result == .orderedAscending + } + }) + peers.insert(contentsOf: selectedPeers, at: 0) + + let state = State( + peers: peers, presences: contactList.presences ) + self.stateValue = state self.stateSubject.set(.single(state)) @@ -1183,45 +1299,49 @@ public class ShareWithPeersScreen: ViewControllerComponentContainer { private var isDismissed: Bool = false - public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void) { + public init(context: AccountContext, initialPrivacy: EngineStoryPrivacy, stateContext: StateContext, completion: @escaping (EngineStoryPrivacy) -> Void, editCategory: @escaping (EngineStoryPrivacy) -> Void, secondaryAction: @escaping () -> Void) { self.context = context var categoryItems: [ShareWithPeersScreenComponent.CategoryItem] = [] - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .everyone, - title: "Everyone", - icon: "Chat List/Filters/Channel", - iconColor: .blue, - actionTitle: nil - )) - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .contacts, - title: "Contacts", - icon: "Chat List/Tabs/IconContacts", - iconColor: .yellow, - actionTitle: nil - )) - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .closeFriends, - title: "Close Friends", - icon: "Call/StarHighlighted", - iconColor: .green, - actionTitle: nil - )) - categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( - id: .selectedContacts, - title: "Selected Contacts", - icon: "Chat List/Filters/Group", - iconColor: .purple, - actionTitle: nil - )) + if case .stories = stateContext.subject { + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .everyone, + title: "Everyone", + icon: "Chat List/Filters/Channel", + iconColor: .blue, + actionTitle: nil + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .contacts, + title: "Contacts", + icon: "Chat List/Tabs/IconContacts", + iconColor: .yellow, + actionTitle: "exclude people" + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .closeFriends, + title: "Close Friends", + icon: "Call/StarHighlighted", + iconColor: .green, + actionTitle: "edit list" + )) + categoryItems.append(ShareWithPeersScreenComponent.CategoryItem( + id: .selectedContacts, + title: "Selected Contacts", + icon: "Chat List/Filters/Group", + iconColor: .violet, + actionTitle: "edit list" + )) + } super.init(context: context, component: ShareWithPeersScreenComponent( context: context, stateContext: stateContext, initialPrivacy: initialPrivacy, categoryItems: categoryItems, - completion: completion + completion: completion, + editCategory: editCategory, + secondaryAction: secondaryAction ), navigationBarAppearance: .none, theme: .dark) self.statusBar.statusBarStyle = .Ignore diff --git a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift index 230bbdf369..a191096c7c 100644 --- a/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift +++ b/submodules/TelegramUI/Components/TextFieldComponent/Sources/TextFieldComponent.swift @@ -134,7 +134,7 @@ public final class TextFieldComponent: Component { self.layoutManager.ensureLayout(for: self.textContainer) let boundingRect = self.layoutManager.boundingRect(forGlyphRange: NSRange(location: 0, length: self.textStorage.length), in: self.textContainer) - let size = CGSize(width: availableSize.width, height: min(100.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom)) + let size = CGSize(width: availableSize.width, height: min(200.0, ceil(boundingRect.height) + self.textView.textContainerInset.top + self.textView.textContainerInset.bottom)) let refreshScrolling = self.textView.bounds.size != size self.textView.frame = CGRect(origin: CGPoint(), size: size) diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json new file mode 100644 index 0000000000..ac6650008b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "off shadow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf new file mode 100644 index 0000000000..297a8ab85d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf @@ -0,0 +1,1366 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 187.691406 139.141357 cm +1.000000 1.000000 1.000000 scn +112.318077 230.817093 m +114.451881 236.621597 106.507004 239.951080 102.709404 235.070602 c +70.894165 194.178711 37.289165 151.643723 1.607707 108.848343 c +-1.328853 105.327103 -0.157611 101.418564 4.426768 101.506927 c +20.839167 101.823715 57.499165 100.883713 57.172985 100.331924 c +57.264164 100.648712 34.854565 40.328918 24.311525 11.312057 c +22.424946 6.120438 29.099884 2.524002 31.687706 5.647614 c +67.369164 48.713715 102.736664 92.306213 134.258621 130.929871 c +138.408722 136.015289 136.225098 141.255768 130.969559 141.267990 c +115.661659 141.303711 79.236656 141.303711 79.280838 141.150482 c +79.236656 141.303711 103.559158 206.986221 112.318077 230.817093 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 134.211060 cm +1.000000 1.000000 1.000000 scn +112.318077 235.747391 m +108.788986 237.044724 l +108.788910 237.044525 l +112.318077 235.747391 l +h +102.709404 240.000900 m +99.741928 242.309937 l +99.741806 242.309784 l +102.709404 240.000900 l +h +1.607707 113.778641 m +4.495335 111.370483 l +4.495601 111.370804 l +1.607707 113.778641 l +h +4.426768 106.437225 m +4.499225 102.677933 l +4.499327 102.677933 l +4.426768 106.437225 l +h +57.172985 105.262222 m +53.559685 106.302261 l +53.036144 104.483368 53.946251 102.558853 55.684322 101.809479 c +57.422398 101.060104 59.446564 101.719513 60.409737 103.348846 c +57.172985 105.262222 l +h +24.311525 16.242355 m +27.845428 14.958176 l +27.845484 14.958313 l +24.311525 16.242355 l +h +31.687706 10.577911 m +28.792362 12.976776 l +28.792278 12.976685 l +31.687706 10.577911 l +h +134.258621 135.860168 m +137.171616 133.482788 l +137.171692 133.482880 l +134.258621 135.860168 l +h +130.969559 146.198288 m +130.960785 142.438293 l +130.960815 142.438293 l +130.969559 146.198288 l +h +75.668030 145.039062 m +76.243362 143.043762 78.327271 141.892639 80.322571 142.467987 c +82.317871 143.043304 83.468979 145.127228 82.893646 147.122528 c +75.668030 145.039062 l +h +115.847176 234.450058 m +116.701851 236.775009 116.619583 239.131058 115.641846 241.171753 c +114.692940 243.152252 113.039856 244.550491 111.229118 245.332703 c +107.622391 246.890778 102.796585 246.235626 99.741928 242.309937 c +105.676872 237.691849 l +106.419823 238.646637 107.465248 238.766983 108.246910 238.429321 c +108.630371 238.263672 108.800774 238.046219 108.860069 237.922440 c +108.890541 237.858856 109.001205 237.622040 108.788986 237.044724 c +115.847176 234.450058 l +h +99.741806 242.309784 m +67.937111 201.431458 34.361767 158.934479 -1.280186 116.186478 c +4.495601 111.370804 l +40.216564 154.213562 73.851219 196.786575 105.676994 237.692001 c +99.741806 242.309784 l +h +-1.279920 116.186798 m +-3.258407 113.814377 -4.442426 110.586319 -3.346803 107.497406 c +-2.147628 104.116547 1.122458 102.612839 4.499225 102.677933 c +4.354311 110.196533 l +3.731298 110.184525 3.509113 110.314011 3.518988 110.308304 c +3.525570 110.304504 3.570336 110.276199 3.624385 110.211884 c +3.678738 110.147202 3.717865 110.075287 3.740574 110.011261 c +3.787205 109.879791 3.727518 109.904022 3.795444 110.156403 c +3.863918 110.410828 4.051676 110.838486 4.495335 111.370483 c +-1.279920 116.186798 l +h +4.499327 102.677933 m +12.622606 102.834717 25.825699 102.680542 36.987919 102.416489 c +42.562939 102.284607 47.599640 102.125992 51.211418 101.966797 c +53.024452 101.886887 54.443344 101.808319 55.382313 101.735413 c +55.867409 101.697754 56.148544 101.667206 56.272243 101.649170 c +56.369865 101.634933 56.239033 101.647369 56.024120 101.711349 c +55.987492 101.722260 55.526577 101.847763 55.024200 102.194839 c +54.785992 102.359406 54.143162 102.837708 53.738750 103.746353 c +53.220856 104.909973 53.351353 106.186203 53.936230 107.175613 c +60.409737 103.348846 l +60.999710 104.346878 61.130787 105.631775 60.609016 106.804108 c +60.200726 107.721466 59.549557 108.208527 59.298603 108.381912 c +58.770741 108.746582 58.266712 108.889893 58.169785 108.918747 c +57.834274 109.018631 57.514095 109.067612 57.357121 109.090500 c +56.971634 109.146698 56.482571 109.192627 55.964401 109.232849 c +54.896851 109.315735 53.376972 109.398651 51.542557 109.479507 c +47.859436 109.641846 42.766521 109.801895 37.165764 109.934387 c +25.976433 110.199081 12.643328 110.356522 4.354208 110.196533 c +4.499327 102.677933 l +h +60.786282 104.222198 m +60.809654 104.307068 60.897423 104.745392 60.929855 105.103455 c +58.742435 108.680466 54.195972 107.559952 53.865387 107.051224 c +53.814571 106.953873 53.747608 106.813766 53.728859 106.771698 c +53.718666 106.748245 53.702507 106.710175 53.696033 106.694595 c +53.676624 106.647614 53.659908 106.604416 53.655502 106.593063 c +53.640240 106.553726 53.620911 106.502960 53.600471 106.449005 c +53.557949 106.336761 53.495045 106.169464 53.413906 105.952957 c +53.251022 105.518311 53.008900 104.869934 52.696381 104.031525 c +52.071083 102.354004 51.161480 99.908997 50.034843 96.876465 c +47.781475 90.811142 44.658810 82.392624 41.203846 73.057480 c +34.294983 54.390076 26.053322 32.046478 20.777569 17.526382 c +27.845484 14.958313 l +33.112766 29.455093 41.347427 51.779816 48.256329 70.447327 c +51.710243 79.779648 54.831825 88.195236 57.084080 94.257553 c +58.210258 97.288849 59.118813 99.731049 59.742771 101.404984 c +60.054882 102.242294 60.295158 102.885712 60.455673 103.314026 c +60.536228 103.528992 60.595272 103.685959 60.632847 103.785156 c +60.652458 103.836929 60.662716 103.863693 60.666313 103.872971 c +60.671341 103.885925 60.661373 103.859802 60.646000 103.822601 c +60.640533 103.809448 60.625214 103.773361 60.615692 103.751434 c +60.597614 103.710892 60.531147 103.571854 60.480659 103.475113 c +60.150402 102.966995 55.604095 101.846634 53.416664 105.423340 c +53.449085 105.781082 53.536671 106.218628 53.559685 106.302261 c +60.786282 104.222198 l +h +20.777622 17.526535 m +19.123594 12.974869 21.347857 9.066910 24.252548 7.152466 c +26.852655 5.438782 31.562777 4.533432 34.583130 8.179138 c +28.792278 12.976685 l +28.944626 13.160568 29.138533 13.175171 29.096033 13.174240 c +28.995413 13.172043 28.716915 13.216492 28.390881 13.431366 c +28.077776 13.637741 27.886385 13.893036 27.802916 14.105087 c +27.740448 14.263763 27.684525 14.515381 27.845428 14.958176 c +20.777622 17.526535 l +h +34.583050 8.179047 m +70.276260 51.259323 105.658882 94.870407 137.171616 133.482788 c +131.345612 138.237579 l +99.814445 99.602615 64.462067 56.028702 28.792362 12.976776 c +34.583050 8.179047 l +h +137.171692 133.482880 m +139.813660 136.720261 140.928268 140.649261 139.688675 144.177887 c +138.396896 147.855072 134.944427 149.949066 130.978302 149.958282 c +130.960815 142.438293 l +131.647614 142.436707 132.038910 142.268188 132.232162 142.142303 c +132.415222 142.023071 132.526093 141.877991 132.593735 141.685471 c +132.722305 141.319458 132.853668 140.085495 131.345535 138.237488 c +137.171692 133.482880 l +h +130.978333 149.958282 m +123.319649 149.976151 110.381828 149.985077 99.363159 149.970398 c +93.854500 149.963043 88.820534 149.949799 85.164253 149.928741 c +83.337914 149.918243 81.845848 149.905746 80.807587 149.890915 c +80.292084 149.883545 79.869400 149.875366 79.568008 149.865906 c +79.424759 149.861420 79.269722 149.855499 79.134247 149.846558 c +79.081985 149.843109 78.947746 149.834045 78.793877 149.812927 c +78.748192 149.806656 78.519775 149.776611 78.242943 149.696655 c +78.151802 149.670319 77.732826 149.552322 77.269096 149.258408 c +77.008621 149.077545 76.415733 148.516479 76.117638 148.114105 c +75.779449 147.451843 75.526001 145.882538 75.668030 145.039062 c +82.893646 147.122528 l +83.035507 146.279633 82.782158 144.710938 82.444359 144.049255 c +82.146645 143.647491 81.554413 143.087036 81.294876 142.906754 c +80.833023 142.614014 80.417023 142.497208 80.329964 142.472061 c +80.061295 142.394440 79.845428 142.366760 79.816643 142.362793 c +79.744957 142.352966 79.691727 142.348022 79.672020 142.346252 c +79.647842 142.344101 79.632370 142.343079 79.629318 142.342865 c +79.623993 142.342514 79.635231 142.343292 79.669975 142.344788 c +79.702553 142.346176 79.746620 142.347809 79.803734 142.349609 c +80.038528 142.356964 80.408836 142.364441 80.914993 142.371674 c +81.920067 142.386047 83.386047 142.398407 85.207527 142.408875 c +88.846886 142.429810 93.867622 142.443054 99.373184 142.450394 c +110.382942 142.465088 123.311569 142.456146 130.960785 142.438293 c +130.978333 149.958282 l +h +82.893646 147.122528 m +83.006897 146.584534 83.032082 145.826370 83.012100 145.618286 c +82.997322 145.512054 82.967239 145.342422 82.954086 145.279816 c +82.929825 145.169250 82.905975 145.085785 82.899849 145.064331 c +82.890221 145.030609 82.882523 145.005676 82.879196 144.994995 c +82.875359 144.982666 82.872734 144.974579 82.872002 144.972321 c +82.871216 144.969894 82.878197 144.991119 82.898605 145.049866 c +82.937004 145.160400 82.999245 145.335785 83.087128 145.580521 c +83.261513 146.066177 83.522041 146.783676 83.860504 147.710892 c +84.536697 149.563309 85.515999 152.230911 86.721222 155.506134 c +89.131294 162.055588 92.440521 171.023193 96.025543 180.732605 c +103.194412 200.148285 111.466515 222.531342 115.847252 234.450256 c +108.788910 237.044525 l +104.410728 225.132568 96.142120 202.758942 88.971054 183.337341 c +85.386108 173.628113 82.075554 164.656921 79.663872 158.103104 c +78.458214 154.826691 77.476135 152.151535 76.796440 150.289520 c +76.456955 149.359528 76.190903 148.626892 76.009583 148.121918 c +75.919609 147.871368 75.846710 147.666321 75.795128 147.517853 c +75.770538 147.447083 75.743820 147.369186 75.720924 147.298798 c +75.711311 147.269257 75.690361 147.204407 75.668648 147.128326 c +75.659912 147.097717 75.634186 147.007111 75.608788 146.891342 c +75.595062 146.826141 75.564598 146.654419 75.549614 146.546570 c +75.529434 146.336914 75.554611 145.577637 75.668030 145.039062 c +82.893646 147.122528 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 9809 +endobj + +3 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 4 0 R + /Height 676 + /Width 544 + /ColorSpace /DeviceGray + /Filter [ /FlateDecode ] + >> +stream +x݉umkITgD[d|X+DmҾ!7K>5@jfFfUя>>>>>>>>>>>>>xxtU蔩:u? ՛h.%_[U?]՘51L?Hҵ \S>hEx dxuOj>1OɥauŅk@ nعQgm8V;K?Gu?lV;;Vo>1@?iGy\.M|I}#i(v`"k}6?:'v~yf&xLd/dwX͜479эXW2p'5J$|wlKraꋽ86V"?rT=s{Iei斡\ZjҕAHԔI٧1=ts:m9\ U[4,Drd&Zh-W>gq<2wI٩E"kpU| ɄloH[kh;p._8^20(7;B:D^jJЋ?64Tk\`54dEF)YQw{ŹN;z1L8\ZUa = !o'ƚMjP/c؊!LY2wlȅbN¨U)´3wfF}(/J䕯$b_*U|Qi!wZpwq6$J8C+צ15`V}2iLv(%\ͅ>\:9=T{e0 +k_um[h]? r [Jxls&ы+1aY iե`? ˝[xhGk;kp0tFW_B2~ P){$,WT!j/=yVKG E~~SW`SyO')5d㑆/&V45c?^6AKg }Ci.k.wS|w>~)'{NhÕvʴNOOkх}~qWj8hKYD_\r&G8Ȫj$0^Ӫn^bTejϊȋ& NU ^\1dlԫ>6ώ/;ɇojPvfKM`0B9_t8`}i62GA6qoFdB};^]LpVFD̋mFƆ;/o/Z7{q siL;0ρ` w /&J@bRLSY:(q: Xwyd.=|DځW 40 &QU1^K~0at}LESk"ZyZV-L3 IrNPN靾9ޛI:EݩͥC&-3 ?kG{ڲ*U!J0gN{ qç4zwʂiƜCVzeKJdN eU.ٞk0CUbVY,uvzשxd|Eև%ͼEeF{K3?3`XQ@w'' %Uh+xew-&S5n)ED?<\/ۄ켐6\EIiEԼ8 gڦwf=/}k? \/LH YfЗ[i 'ݶs;LU=İޒ kϧ0D.  P:ɪt]qsL\,?@WUs&CPCCߧk%RuNbmˎdkLTcz> Ȋ|9tڋIN @gSЅFMwff BÊ|߈WHfG * HַPTqoXqn S& fMXFRDᘂP9q] +RχNv3:fnEŚ^0,|1eUNmrؖ0M8^ߘdhZjS4t|?^)D^E{ Fǹxih 1qm 'HTHĶKC[Pva&y/,lǮE ^|!'ZEfSRALFYU4g$,Nw./u9.|t~blN[No 7B*v($.@f0u\:ٵc=O>)\/ȋ 4 %@ɢiT^C Fc8ս]XZkh[, }f&a H!H,C>3ֽf]tױ \C׾?miQ$Jw!2z! @EWjo:^X0Ub-yQb+N'_ulN)u(g_KoE7 Mua)G(ԡ,W|]#{\iDy &^P aEWfG:2N9wJ1Ei@_RTQJSfU_LW/1(WtY?5ALo }X5`@?DBżf('jPJ:J]2LGU^J)gz->Ez|#)~C86 |Q%YsAZ}cNIcVإHYX?) +>]`r[ ȯ$_wz@/~OaNݘd3Ok(7LcY3 DH5l'9 C¤ ca\ zbOO R_w6.enQi }pNywh%N &2Qy!K%iP*:)~! \7nksUt,?dleuUwhVrPx7˘ Y)C_,i8O ~9H8-cepMUWLr*[è>K @.(;G<@Pt,bv )@cF!ɑiN1I@Uii+Ngs:s0m+ԑY٥Ա&Ŗwpjt4h~ ՛VbdU:.}$ͬ¯*UAiƬ`cHcca4;r;|GxnaMVW$qё*ɋoI3Nq.^ MZYy7˃c|7K-վpNY'čLtAկU׺0m[7]8l!g:0اٗ:whƄf YY*i IZ#[$q`n0 9\60B'2h ĨɃ.ʛcOP7Óm>IL_xG0s HՐX1*m-Ù_7>}]!CUs EBHC9)]R*R\"_G Ug c4&,7F3rI(T_|7f >HBHJP!R*( Uqp%7um$ݙZ,RƐEQ,e}pv I(hÃ$*}T`Z9lC-~­02yjnJM]EguZ)$k1eI6;2K6R_!~7W#Ա +,B!8bǂ5NZ~ըituK? * +u?)đCj~ _v$m,H*uk_$Ҍd?@ j򘂮^cؙF"h¢`C8U}q\ƌ\7m O` +"!օL-27:Jj"㫍0SJ$ `g2BԡЇ tenlnc͎lޜa}2եnml?&o0fC2 5e# tHi5udZ')Q;̫0^aZZ͒+A ,r 6ZUbOs赪kݼyb=P!)CY֨BI~Po~N;ñ94J!kG0-ŶƲ=1޽D_}vA]\T$[)s"-Q7^oHcA |M:+2Jʭ 4~T.\[:hǁ:eU#^BY<_{:T8o^>fT;ֵ5pYۺkin&b_dCV!U% i#eTI8˵˹28ꍺ3=zì֙LQî-d)NgAvauEwO#{j nZ5 {)1(@^7qFKlF%2L]U_9P͜xmj=8w دQu n_蔋Q59ꍨ:6ꒈ$y(̒2I_5JW+.9l$%yA {@8v<8ha53+ߤOK=&TK'X nZLC^%b!;2D~#&_W#]TɖrU] /JU˄1ӷR⨂L} { ċCU >} " Guv[EBB$&)CҘC/h ^}(:cYLw$bbL GG +y\nl +xctsnI1V:W8Lȋm[Jc4xa*k`Ow!%>RH,,-@;2*ouGz̳VW 5Lbi '^;}\.{Ǥ1<LSj[΅G=t[_V^6 cGđ:C`>RJ$\ﵹG<9&J G#/q+$/{츶¸Ƹ#ά3kO_k?g /Rycԑ ܓȼĴ`W3+7h]4ެ/3xy@ #Qz$)yw(#< RXhzXyԸF"3Sܑ +Ff G.w '5vN.'T./z@yqo^9cbn +7q(OJL-u@CYqgٖ5CKΕ7x=&VjC+gɃ@`qhh+cتib~gVrq_C /R=&YM 켳My&EA daQv;2?oV2L[ىnk+ J^V?a/N Ąz^ +/*NY)5:>$ #}t2,fREm'It_Wm ZG2l!S@82n+B!}42I!#:ؑ-cCӅ|7X Nt׷ Y̨kCtD;'f6F[~԰q{L3ѨBF2Gҁ+pď1P` ^Q ղ`#z !0N3 bO/LWt7{ skϿ0ߙ̕il{uQ?ڨH H2m'XY8H TnPy>e҇@.X&}X,*Q `/ó};ݻrtF\U9X(~#ّ[; j [(,@wL^T3ml/~Y1(UR6ȟ,P fHv80V>V ^ +O&NyH;C_ӰoR$oUOmN|nRc4oX8GH9GAGr,! :=\ryWG1=y!A:YYBSk1 ?~M0ogvʕe98ulf/?dErdh"ecJ$)'j(R ?i+/vDS G#FV;A,F>Luھ;X3BmOnP 4Üo\x2X:Vr>nu>F!2xԑʰ<H@]6/kG=ay(Y6Ơ`P2`HÚ: 8JU{hl30A4?SO6C])lxv6b_{A!;}5]u$#"r+!߶ajP9̝caGbQ+~~#d-$8H,q,=9Q@?C/x58œ0+G05Dk<M` } lύh?xwQD:î7^wgnu {>8Wì=X>9=4@SYL׊A ġVhcձ6_\aO2^UOnzyo 4qS*Ow[KncfE!R\ K!G?I,5utgDF!fF +Q,ia1 :>&+.`$}bg"އri +{Ã#2i8)1Õřr_gfxp`*ߊ>PYLebNT yu6mNAYGPzt#TE#L<<.< >Kz1ɐ@TJᕕrrz;i (CCp20zk*? GGҹd:_ Rq FaQcA+X\s7Wydqԋ(xr-'j_VE 3v W4wB72$e +K|H +Tԑ<>DR<rNJ _:@{T0-q$7 >Zr䡤&y`pHg 9,qf ?b%PKIi:5bS}a-3 &Tڹp7N<eHȒT<.}eۧ8%5iic0E +!8eѯg8Q(w$7!3vt˚\݇9%pxJl9;Mvњ=_ ʨpFV'`۹S| -hKsBC"zJ qGHo`.}򨃝9񅨿C3dA?]X~n,X CFOGST]>[b)S_\g?z:Ɗ2h v,&IiV45=jy>vd$S#xxBn yX|J^\KES5x}!Ckjr1OhLn1lႋaArR} +DS#dYG+?'gNq#:T4BF +y0(uT'`yNc ] u >C3wrTC#rkOM1rF: nQҲq: HncF V./ngBo&6=,dEHg~w."7 +mc,e0U8Cֹ4T:MU|^Rq`C+A%1 X/x0z" YBȃ@dCfWa7}.4IӺ2t DffN! +XA`!ߏE#0Ȣ y0c Z猭5\nM '?s2͍3{`J(\2#\x0Lsā!'}>*uvy$ڈK +1 s6#%~b]Gl[u(B<e=%X %%s$I ʏSc_88 06sXwّ+.z،DۇwC@$P"_A!#AqhkFU7(f>`>Rؒ@Xӌe10pJSR6 7 Q61 QmorH[5x^;)(ڼZ#V])ism =틁IuO˿%Cu:L?@Jm ¦2^W%ze`cn2p*6?6{/N5p`*̴79UյWNaٴ##)ry@.i#uQQzӘ(5Oo V Ç>X1>_،>nT!iadyiM8jˏN[frj *Lf`}ח]y&3$l9M y#4B,hYcH 3Q]9=la#CZ^r^QːV9=aRʴN:z󄊺Q1:ʩ S/dBӾAAr}HO Pn ÿu.{5#B$XG ~ 838Q^1D^ʻ1ԁ>T)܍|:V)F?aSP۪*CNIS`& $A>Ve˲\ǁ **7G TSG'Ӎ<kX +}%}>X!A:ut{5 k;KjrnIP$M!YF;~2WY=bj]-;)QGȗɟc= j0ycܟ><<8I~n-?K۵m lJ 9nb)0BnLYfta&dIz>s{? bؕB ]+\y1/pɣC@iP1gBp $y(Ě)@X4`${r#ɐ`mY?ːcqY҅\>&Zf4 ۧYy$O^"Ink,3F 3{`ZmL$H6L"4=7DZB&Շoч򸷏p!ĕ8?Hλ)c=`E8-i^a_!(is8unZWlM7X 4pgJ=k*38 /~zxBc1!UG ]O +9]uNcwWTP҇lAj0,DG!b;hBpEW==dğ zf0-*w%۞D ",s`ZEscϪx3#Ѕ2Ջ?>SJwmHPG #Ȁ6hrpl b3>|Q@2`[uQЊ2 `MyhlKp tȲ)uԗ2MULof_Q9ģ[sL̛<)Bb;cVei Qyr"B@bc f7s ?V 9֯4 E + + {5foU/b\QG C(O1SP<@G!O%@4BH H¢/EQ0T\꘸ǐq?:vGuO8+ J5"p#Y{' w25ݽ2<0xɃ>q$fp Oߓ^<`ڥ[BtrczFؘP_`x'`jQFGlf8w\x_&:tiqp%Tfx̣qKUa9R՘5ɓ8{!ywA!6H2?P׿RnXk")Ī N_,e:BBPEV0H{%l>/!}ITe;tRދnW[QۻfOmiaf+hMW]̗:=[r#CZ#q )w<+u<Nj!O2}/=j{`%qpc /.KUuӵ*83il{͋6ycy'&SBba)!Gr>Z8@1 gG@Lng_967w+ᣏWH  ?](]˲`rPO$pA!Qg.HcQqy;`}>Gm. +  + k@P+E,g>yp7YoX;ٚFnEƔ7쫇'ft`SZO&x޳Ԭ*/H%[r&s" +ZqpyđT__ ֲ"} ki`!G =o9i hEg^Dw{<8`V#@i!6lTuA!b'>'R.`C^eh< [frJr.Z'AS4]L:52 'q<.}>HYa/=nC1#q< Í>E9Df&5GPCc"9 ٪,9Pzxm<߷ڀ`J|c%m.\|:7s0 }3,=+)Xxo兗?$>_9Z6Ag'ȣKE==rP:'ql,uyLfɦ IW/e {mScb +ȃ>SN$V&tbA|qގu>W{`kZ~Ѕ +_5W  ȈN&;Ō=lH=]QHCN(C-u_"}=>>Rzav #0=u|(YxPMB(#:&]5TF`^Yԝc=FFMd{#-:q2y@#?w(~9ۇ ֔>:YPEFq^ÖC[fA1K}I#'+UA~3^q Dzȃ \+ц7QGP&嬰HUP;FƺZ|Q۴9ߢi&m;@џ":u@QF, GZ}>2.1"D!p?GE@k_ޗ<( C163< N-$ï39h\i8g3CEUOSRmZN+L1>\F mA~kڱHK vcH ֱ/ AOc#}>{oC>H%)bkB{ h +dMdm :V<O◗.\Qqnz +I-a8}d6F sVS /.H;c&^cD#qLb$THq[yәyz7f0? }ȑƨC].4?^s'' +1Ljx1eA a`IAF!>H -r`{.ö'gRBSF idCdħ0J G@4"7<,rT H""5(#~ +:|/-M1G@Y< +y9tbtqpz.׺}QlÂMc}PH1 !8!+2>`Uw\¾I\Ҁ/+ {G InЖj?)74 D!7GQCÛ%w!!1Kĵm(}@C_ UA(D8ç `\F!*uq.c^vW#S Ҩ /#Drm=3o}fnԋfJ~zn>ZhߗLJ7~.(a7t%u@/}+y>lGaX)< 1].ze[ޱ.7 +əo^AC$HnZ|%zY>phtj5=76\j)'}`ܽ>C!UuhBnwA!#nsVNs+q< !?܈FwIBikآ^4g7#CACr:cx}Bp fpO@_c^<]{mXyLJ:V{Od]5QF%MNa[2U@i1xo@4FRG#DUxxMyՆZޅM⨮@x:.*Sԝ=C m<Ց>D><DGy PGpH +}(AE #>X@Ύq7 ӇP<H0yt$EE>DV{05ꐕ@ ԡUo|4׿*.XEoY~+*O\ )O`A,[v| +cA>ͪ҇L7򠎇> +!0_oSGP(҇ {/U`6,>B_AXwH# +e^<>|ts<@r?y{S,I>B3Q,҂>Ews0Tp>Ö5.(qB+m"|aTkf>WƪC&|d:\#DF #.Z>e>dXA0s'X/|0 @Aic!Bi0!'^tb(UuFxqP :FJ6>F * +ϐB/% VM ߨ!?<ĪA8d|8LKF&p@\ |O } YRT +jz$=8gӹSJA![>D#H,}@e|7TA Vy>ؓ<;y@Q2]EPr7anQ;ՃA +|ș%A:G*:/>143BÍU^AȀD +`8BEBd`f?N:4m8P@\F|c6+zr\](5#M/x&$:12Cr ⋦lC꥟ Hq:}TG>fR}>3Z9QH3 A%y(Gb-aH!-L@T}Y@Hۇu pD.{ dCngwb#}i|DuPȁ: +h((aG`rW"mȃ>uq17QہM@}HtG$U.X+CAAڤt@2U.h!"7al!N$KɑL8&o +h#x>hAC1ESUk 'Dps\[RTGg U9*x( w]ïLl,iiV0 +!1\JxB8 E?(Lfp̓CsՁG}$I"tC HW.ޗ%B*CҠ Hg=z{}AAG@Lu<}4)lkч L7b bBmtB(W bR+E?(,`Ł($y,bzz)sä|+$:ıIowH#:PhBzWÍ䡑I̵A9kcL +v!V+TdiDգfm\ qYI "aHX$"Sg7 F>F҇t&hwo@JʾL  y\?}yi dvH|u4V>{@D ;aDVoDӔ a1w.{ԡ}>j®>|~HсQG_û>iM}A G]>28dC{$@:u"&r1Ջ'\020#旨́0^p[[x; +E˨#Dv,S-UEUqB0:9HB6Fy!u(U o#q(a4@le11` ף!:DFN5b\$McYPggS8x3bݓ:fALDfm .(CE`E7[ayp,э:旦̾y#?PWQ /@> y\Cpv)D0u[F̦?Y:1뇖40G AG EIDlvۇjQHH2#HA׾=Mn!r+tT6<^}x +.}p|)K$B `"W1~z `h}'ㆆK>V}Poy2X}O`N>i %B ƪC7<ʥ&7P$ .]| y<G*X*G"98< b /ojBXc@csZ1}DC (PV"Ƞ \,jL"@q@#ys-SJ-&r@xIk90Q9XͪTf_qAAB"&n䱈 4M7Q.YS9XSa)$>}q d:^>!GXU`0K59eHڱ򐖭\c{i%yH{XR-s[a0ɨ>^|!P/@|6#@A(-Nό%hα+y @yC`@JhhC8_ӿ+Ӯ"9-qHd +RװpI%cqıAGe,J 6Sx+" "81l,7EX GÜV.c6U.z>ԡ + ADB!zB$QLiBM#be.$ g )SvBc{?B6!ic'JDo-|uhQPb7`"sf|->ϗ4rAˉ0(Rps\yKg/lf;xqByD:c-jL.A> 8+k`H//}wIuuX 1>qrTK mB#yKr)mlhf7Q6OiBF;-&W>S@ +@QD0Ks* +$b.t~}6`,a$cUoH`!("sA $B$e" \ԗFdqP >$::BiCIu_Ʃk1&b)KZ)c1}pRH|Є( X<@lb=L7 >HRfXxtPc9p^F %K6M@/A.$CC_#e(U@pvȃ@B__5]q{l⠮H?` "$o#܀-9JȀ4IהS}6,زl2z6"˙?r4rv#~..ԑ @ C_8.6vܐ'pҌGHB\.${sH?߁>B 1Db4|_Y&2Hg"9JXOXyPMR"4QF!+xq~~:u\He"4A +u@*RrIcR- 'rg!+̛>~jQ{26?g$B"$'VFbA$s?><ika YHz=F|'u<=c~IFC!:x t!A| '-yTƴ8$I|HCCGXq cH Îpd5ۇ(b*M;n@BVq/}E\!*z2SSi,5`3H!ܧ AV Co?]%8VC!)c]4!r9)XPzQ4@ _}fuђg&j06T>? q@B$b+օZoA&a2uTXzLZ,D&S?씫NAh#U>#*!}@ouܼPdHdVGBGH1TA_Aző8s2>R<|O!q"_.;ІB>@ WQҦ~3-@DŽGQi!JLd)ȣ(kQLH@Bd,c"YRd8JFv:eLqGxģ0y`d[}ԛ|>&>[mb)A~c4%V+Y|Leypc1Q>F4A.C@UpQc1 .AU8d0r8ꈯA=>GU}1eD|+@q*ZB8"9Ut~1je-Q—1\ygB"# +AɅ@F"ތ!kҘJ+[o1uL#˥{)4t~1z\f4ĺ` 7)y\G}W@2ݠ vTb E<(mgÒo` #}G#|N!'\8"ꀘ艟(>H8:BA(P+SJr!g2Gr(:X乽`RC /(DH+V1kf ;c 07@D+}B@W~ +MnϧE`L&"v@(@F`qB"qtA]uHi(YVqIDK6aDWoMG Ac/H '@B1- OCcW2y+y7,^?w+aUT1*P%dġ BA +}$KiC3x{|HjR*A$g%tQ M<GYld+}6}PJ>F!1;H7 Ad|7ܳm]LXjX[oy{|%śc}A.}#uq Di4 ҈<>8!V0> ~ ó$^_H > j#Q81~!gFm2yT˩7, U?̀o(FB "T+ +D: l{o>&VIܭB-40:H0<B LOF <yxW8%BV"C;ʼn<6"%zجIUfqf Dpg?!/AK!qVPq BJQ.u\ QcBÛB򼿥q1eKsHl1Na7Z<@R\em#8>AtP}7bzeb$;modbřN3wA)DXAc"ıHbK!CV |( -v BFR(YVeWe;y _5x&Sؒqly^63ă:BFČ::c18D\e`O-C $2²QG΃ԡ郷uA H!#-UH`!GKAyP&9+qB҇RH1m^mG K#A\'9c~_ BHFx%@"G$&qi%*y;VYv!.|@.!8x@H$Dg)u, ]!K dII,:\҆ n #lw=4=uǥ *O s;yH#}!F|("(J[7\ɨX^NB΂}d,։xaч 1 -AHa?_IL]u>J*R._;nΐ2V"1NL3@xR(VA Hu))bB/>-5JLwb]XI268/M wYyh 03HHӌ8*B +}\;Fe0y?EQ+kykVa.KX"eS}#Vs@6C d"&p+BC@ԡS$EyNgz@K0§HJ0y=ȧypB"w?6:iҕ8$ϡN>_dwmVA,]pYB,d=<Qg=G.˻{#8%Bn:dei>:1_?q#  5q,Rw!f+Fȴl`0r xD10G1I[!YXa#A>A fqRY,OLRLe/8Ny˄A0/>F"9%A!>KZn8Ji$oR;l}t67eLNauF@:ǟ H/㭏{ QF0[cbBkf},wh*CV^ 84!"}œ/ +TA& eh@~ AnhB +qFȥCo( kq_I3[@l `F=(<@V $#$~QF$+ِ#TT.v3ς, 2ldrJ G8 +;' q#yTcQ݊80HCr$(c1qPEڸ~6U$ 0bִ4҇ mc +_m `B%bAg?ݟtw^رIэ HSoV_F,=k +Ab d'm #>-9 "H !\icP>4m/sk}(mc#ypl$ H0#8 r!' uTGmBO ȉ$(PdnH<4Q 20i=n}lXWRe61oNR2ef"21(ql/'F"~uQZ85ƊʾmGGM m(xAc2A:ުӅ@mE^Y~0yQ}zF5H<#rL`(D< G!1y +u>K"&8%R`{D[PDHGx;9:sm6 Se#2 +A!&B )hHw +@S\GEj G2Uz +GFʏ +x{<&$[ AW +KW!O#x8>V!"6#68H!"#%-&c}c)Sշg]\5iZcGp_ F":t>0CYqP} ޟ\ǪCBA& 9@_}|=KuCK}tlp/N"GP è#DGDIČ:[!_L ux&L]Ad{aS1A1"{"/}Te#y:J W +%|&WYWT--|8|9uǡt07,>|(g!x""¢jxA(C'%CJ.UwZ4;4|-C/x#: B +MX}e.{՘e ^TB+ @[ }&xm!x}$Bm?цy PEi,c2yhKh1/SvN"} ︙:Vm v),/}y ]>> 2 \U.6R+s|Wq6Ő^^}YLwh@n}d}()90裒>H! >xG bEWE8P;~`}68fᚬ8Ʈ|q:Txg +l )<"}FUP!©~! +^7dK" 7PDzXE,gOJ ņ#`#}$AAA H(e(LU:}(Ct, k+㐕̝OFp:gy<ǛW>Iicч}>D>$!dDZBnu8yC'oHRLDHUڧ vY`vvx;oK!bo!o$@@#q<lRdf#Kч􅄪_1w,ǵ-ɍll͕%7"2 >'gef@2bƨ,\MBG}>IA.[lW$MHPWSPHqWowN;edRs FE-2?!C7BZIV50qsHst~ vj3e nt#-B `DbM   o#. VtU CƎ +;l<; s\41Ň,>H;BJEB\G|>B +n +uaZ_ Tv 8i,ّ'Zl}8 B=J +)|X@V 6W 3U8ԭuE" +<Q`Ӿv=!F5 "‘>=IB*bq <|,!La#D84xCcG;官:@bCwq&aX!d. + fF >ub#;]s|HGZ>q+~WEoÕGR큓:m2ߪWI.xf$3 +;xB3 'qvumk +3#S/9CĂBvX>CGwq3[>1tD=cX)UDLJkMtv70GAr&G +>ķ|HxKA"1|h21@>"@Xb104^70~~% 5JϤ/#=0 ~duX%,bTB*1|$vҩ)zGtmGDLTtKq"r Gt0)aBth+RhvX>GѾPLH\4 +Q$_j2ҵ5_r/>4ut>I>:NKWZx G^ۣVN"SYȄ&Fe!"RDI `J|U#έd7TC=/9[^5tQK%JnI$?5oű]@8?܅GA@P #2TLFʨ#Mۃbb4}~RAv#g߹Q$p- !tW !"e|-Da6C;sfyMR/TXf,t=ʡ.|C[>V8âW`С.4˷"ᓩ +dvd0Aa\|<.*fNm[2yBt mY=x. `l\cZdŃ٪|^.9 +2yw'4t4_á79S58)Fv,eA*@ 4#D!uW DvAqsiӲ+@Rmph_)K)TH ږdY|84WdP1r+0rNA70BuLN?3iO*6=F' CHwLj%L ˇB|}R}`jRMO\IX#Z>M9Y%(Q2>vhwps֏HPrB/^>=< O͓˩#a S #/"DsA<] H>m X@GX:jDFV0 4:%2<$i]-1\@\N̯*UR%R"SMI[UHHj!%!Hf By+Hv,(G|>V]|9dAgd_- +&s -;? (D᱀p n0kKGx1| 3HP.uS8])e]ac4%oF>'N匫chCA~D"CH. 1ƸFfGΎ+R\öG p$F"$dr 0I~a;tua"74[RM9v[O^'yj9#<9ąx- EN|eEJ@Þ¬ԗd?RdR2aհH$i9TtHBr u t)8]&x zhP,R7SBdؔ46},%R?3N7$ Kp"<| GdCrpq?U9<~`cLu:<,Jx4[0- Hd& y^rHE|#@8cq C5VsXv4;u>==tҜʉQGG[cYF"rDZBd YH!k~J=bwT1tlh!S6H`$ W J~K+ >/<|D8f:*`:րA[G- O`Tgy[ThK:t. 2&:fb`t .œfʑJL;R/W+ + +Nx==#<ˇm7b1# +jA2akm]ݿc㶣9KL&e@H$r#@$(A̙'<=c)@Vk;(jonIZf6 GHvˇ6X >o@cj`;)A|hxITꡣj Tzzv7C;- f5;B2@(~Y ҔhA@|x#?Ap߬RG^!S*C^Q!N1l1+?u)-b<0i[ZBA(ba%#@ds7d +|D8q%_{~F}JUZPjSQ]-$)|P|!|,!?-@xzG+BCO_:R)AAO+P{e104ۑ H8Q+8"v:Hb'#<ƃa@طx3s~qPJ2k3@F Mg' 68!}|ظ bᢚRc] +᱀Gb Cr>EJOR)<A<_"UB}<<waBS+z}Lnvz}m@ Ȯ1xX<퍏&s< pH{As \L A$=_`3-t$R51 B B.|D84bdqa@&r2>ڥcJۍ$YB! I ֏   kсw +H*TTLhv}jJ^]= &|$g8|>CE UK/žǯp|d_|hԔh +2uhExt<$$Kxh=|p!)CL6{ߓEEƭUUI)mO]?ڑ#>@t,iG{<&DN>T}ҰA~ ;^Ƈ+>>ޤF 2H4Z #IIn !ݿÅIrc:x(:-qj#TPrWGTx=ܩ h%,#%wKKC}D_@!@H #d(Tէ?>_sʋ<7iЯQ7q%lO~ "oh G|B?tTⰥI1BT8|X9AKg7RkÌvs(uX9 F&41d%ZE/#>"d1|X@+CH,:M_DxZ_/\" z"ltGcHr :<} #sŒ8u2(j +Դ򶵧9%v?9>Ο̶CF]@HbtG/ $@aС D_Ǭ;#+w** Iѱ|Ǡ#< B~0na刱 V"TfI54}OCAȑ@IBָ|\<AtU:p!BY)pPG}z)!'X@'*~ ⚃Fq9LITMlԠUS_Dn+ya IIdi"{pbn0 F- U\^x oR/W`>x @ #' v$+k?[L| >~$x:LEj^*6u3(˄/x8Fu2~w~Y@621 +U!"EG-{|P_/tX>Y@82CUZ_.S&# #O$F->#Crx\>Ed -@o:HW +퉾T/:yǑ)gAd$Hn:BFy|Dc>~ćv 3#T:FA]mԁ8'Rђm 9Y?>! |p bᦞTd4+Ū`QIM|i99l{Õaҡ'‰)EJ#@>c/>~K8f#rn=C,8D=Kk'!+@W.<⡑(ER_P-*wzpiG^<7_U $N1|G߃ +`X>dB`.5vt n\ +v!Ӛ:-MǑ4J%$Y?'~̢c~bb8D**GXx4%G5jR0ec`ң"22&GuG dvb`|knMjqL jfPfW$ M~T  /9xw<4N]>VpUꥩg}֍=<])-eKK!$ +T|HY@Rć  Is5 KX2߶tԯeZ?|J3.R1:dAa @>4p- FV$A"U<`B$>4x,"Q _<@"ŽH]#GWx"++mSIcڃ^;#,"CM8| v>8ۗ'>c 8_r~ Ř6CRYw<4:KIc8 +whC^!8p<>:DFUfUt:RCCݗR{OZ WW=w#IH; @$9W@t!.| ,bBwn/\'_)䗙| +Q5*R;Dv|h na!'0 +-jCSU=m0rO{JuhSg^g-bhE&hO2$> qrE@x 6ļFըW 2j),]#E=IOu +YA o =)Ցi+'^⧮N@ +jhీ DBCG#@~KL3 N 261Cqx$#݅f| i𰂀>o-|,..Gt`o#@V'JXF=z6h:Vo_L>-6~}?kWJfG6F- .yHGVfB  1K(#doLLg7 i6R?<#q l1{yxp}踵S+5GQ՗}70Fsk_Y7&14|Ȉzhh2q:}bݔG\f53 M(>| dx.z(@ƒs#C'3z6pCKJ6GJEnO>wXvF>٥G  +Qdd%7X6e>1 K\ 6b! VǮi8ˇFd]x|E *|'6Oy3'p1γ&2m͐A "KQxhv@GxC{JYVSo;`u$ء+Iaټ|h<~ ~pLj]*q,X g7بwN=|hoz}/xU}tdȜ}JdžƛW_-ʼnɼNS(7 HBx|TIёaf70d(BX3!{M J.SG+~G< ;hV\< + Rh/v|XE~x%p*G߳~vUDۓX.M KH #@,@;16 +/e`R=c7CQHl/'{DJodґ`y^|̯J,th؈!K2xL GūQqNߡ5Q1+TxجHɐҵ &cчS|ڡsTV+H(Λz8SF9}|D޲+=~&(o!dIu@1xc E^c`NגTx2)ח/ +"kd!K n:>c3b$!7t +s F5yhujc;yiC7IAB]A=tㅇ/th7>4x,5Gjߙ +\_.l1 :##:Cc@H@(J@U<ڇ3Rxc0#@8ܚ#1B1xD~ ]\$jߏ>5>KIS(>㳖WLN`B ~mB9t |ϲn@-%vSOT6ԷͿė*KY=⣿X Ju )C B .ل 5@Ji*᪒U@zJG)jq0jW^nY<:H\?"7>1?g@\hh4GRu2|vQ"*)`2"]?, +u7я5z'Rph W"4zGj{G^鸊j`ЍW^ЋP.!Ć!‡~ViN@E僁\%^q +R*)/l«oB'Y$K~GH:a?K jv#U)R0PIFrF zt;Cw\|ZrO7|ahA4!'%|ĉcHLc~pH,B\Jkn*'QORڒ^b׏5/Q + +!Q t.d.S\9 Xƹი QrIe 0dl)4-Irw{ϧ&x`,s$1<^ >HYT'94\g)?^H+zl+sS QG@\Z?x@@!N&+ݩJIVs ׾CRqbL1|9KK`Gr?^<\]c졣{|:KT*yG)3/ |.7H&Jx$8aZC!>>|>D -uGSE;ďFhC|Q2:kq#! \Uc>/)́"ݚuGjn\`[籃p#5 a%<³x!rK14@3@.Gc"(#Q-w8n9/h1+*$n5< ) %b+"Ɔć?In|ƽ\d&> m gRhW:ח˴5r{I"Kb'!!!$>x@a-ڑZ?"PG)#5TE퓦>+sF0敶Ŵd$|| !z!SZ>~pG'Br78>RUlP-h S9->RhҼkێ@bHWCG}t}>eaH071:?ڎ˴>>252Y*y[?#@!CLj Wt㣔IW>AX> ?"tv읟L6&L_>|&?\ez#w2<<fmWUIAf6 +u ~P HH5~   d1x$|FM|$c|KjvR4@մvnsE4xVa::Y|1;>"Dh"eBLJTHN~LR x0|7؈##g oacjBʣP bXJ#ٞxX@,;>Za|HT>~߁#[u 7AhWlPyeZjW`DTLcI n.+#cYCf#?G@HF{H@[zj@ʬ>r>=c&HMX@ "G"+IqacĸG)wFG\LW +Џde!`]Gh#H Π7/r>˯2#PH^m8#D#BБ6n/Zx o> ?b8s><4 U,9)R#G]5Zns^sĦ r$ȑ|s{(]?‡Õc˯ǰA,uqwh.A ! Z[$BЎ@Ab>_ p!}x!-Ȭ"L%Bz39%G6od*ׂ@?Ru z!~h GC3@-PELGTHf"[Y|D<;[x1x)ˑ/yGi$xh&EbB C(c$;<֎kchz!idx%%s<n9߿031ACT \( +4'Noa!fX͡ ȑf3-e}+D(2|<\89I`VNƑ(V{84бx1|H2G0"auH:7a1<4|ޫjI SS)FPԃR!Y$lC~<>8N_.42|L^1\ +ѡ- }cXCvGBH`FԚ>@1eC2'U݃P~/LxFG|"%4Õ&pi`Xb^|%CfQK>HV$?$y@Z<1Gfc/Lf4/ S_L*ܣt?r5Ctгw9ߛLfRtg$:tc 1:$ܖAG5PN23 i`GE/ #M >ߡ> Mw $|+*T QFÆ]RJ|Ӕzeu%zy 52}ec]#⃃B؜xIC' @$RIr |;talK n.Esp)CMJ4JloRӫp2槽A6NA0]ݤ1L<4pX>ft Hybq7FHv1dأRs79(#?%tZ0e; \:qqa28TcrjG~^%+7GT}Ѧ:7 N!&x$1|_c~yWv3x*xA^\7׏b}̌śa$Xp$!χa,y<|Vj4|0I.ͿI- ʣFʦz Tig;ڧȧynglţ=̳D:8R:G[?X̱ƒŃgѽe2g0H`3r}Lq; C:r6 壎ზphKHvC0c#dYQȦ!*'8U7ʑ+g֚|&&QF@Kx \!/bވYZb2>[+߾|gv+Pj𐍴QGG|DGxH+Gbmˌr=-!|Of|h+B* ͨXUԎ{LEUګWp,W^9K\fђ |J ~Hj#Npc׏ u{a(R2;4S׏ +()"OH2!#/"hKAC6 quNQ(պU*AaW=Px "!+5Wl $?^OE#2AZ?p!/ER75Zc#N ʠSv2qS\?1 NC>Ij$G1t|$<Ber7QGBƶK;"#iLe$Z!'tGG/C!>^p3b @Xe#,FWȨ:}/b$$8'0W")HcG DY? :epS8RZ~bh=U_w93ɌH"! Q/>dM@a£:@N] +ir2 +|jJ!ċ" aS|,!F|,! `~|ԓŒT)UZ>g!@فyͩ8cᨋC0EBLTD 44t/CGx0 tQ!BGÉ:L [):Y?(S3I)d >1D8! )1)MW2-^%TJK[FV ~HS!>H <渎ƒoKH?P pYByIx< DABI}Ã@o!>Gh3o*-u*BƎz%+G +$ }S龞w+5&x!m!ɹ +:tAa^en⃽oǕ} c$4EJ &#}!]2li}hlt~x@czTBTk計3*'U\Ez@<ּh l?o$>FŇz:Gt7SS};Fw% e&SN-/Ebf2 q-R!K +R%dQBU]* h׼)/LZxj#œ [ሡ21%Ro It"LH)<M:I@;!̢G#c bQJuHhʨUul»|AkΫ)c0Kmd4tKAѓd@/Acn/̀x4pԩ,q4GW7U :4vKH,Q/>h8ƒ - @ wa9^!Dq1r)j96f}kz#'he֦E>c|#>$Oz|oxF@%B$O^7bÌȊ%H*,DX@MPUNTQ7Dj 3$$Fqw@;)6" X>n+KJv#>H": SȮKb%)I~5y:6H|PF ЈSGh\GhjC);$^BWvlop6fy.Rłxh>GIn]=bG|0(t%>&ʼn-@{HTPn1b&w$1>Njv- Lć tAUCE !Z d(pc0;"BBjD ;RNS"ҮScpt3 IJѿ/&DIPى>1+ˈGbٝB}l#D6u*1nb#Wgmfq_*>FP ;<'y~u;흅5!#Nk~ +?z_HxsC2GHO.9 ]&?' 9PGh \|TGH%IRφ1""I(_&UA)oj !m!T]WLJ1bvo_:ƣN[2|r$d;%B~1|pŃw!D Fǭ&2M"'Yl.v1|ãN<>#":!!! GMĀx|bY=竫s;22сXm AXx r +IJ\ ˜qQ>2-|_%IW򏎹pf:C_Cu@|ӷI0DY}pF< 3Bxh,gXgHOxhCGH`VeJ%EKJI + 2PN44Rg1c3 n|bk!Y {h !]>x CxW#e"/[&I +3w/?Y/ʕѤ`-M&<$13 Bq+<oa!@"DHFʩWS -ّ/P]yػN22t3_<Gt!PKY}qWlP HjPߤN0 U#qJ3Iy 18 Jjb86FR⹼UR= N acO"ipĆ~xC GB:i/I$gްh#?!u..1$ꗤs%=|h! iQeNzw"í [>TF}BAjJUX[;wj_ɹLLKb`"d"I -z5oGməGf5H(Hg@"tB8Iox< +!|,!هi4zEuWs 8AE,p,j#RHᑸ1GlXd:с$JS HQzo yr+ LV1M0 @,N +Ȃx$!bֈ͚ NWNe53ā (_ 8(2jHі.r "2| #1##J KZ>{>Dqq:@o_FH{~ؑ 7l !$:SB6IDN,|lD\ʬ[84(N^ ѕ)jd"i#y$>,k!\c 01;< n0n1JL +En!IaIWCzv*ރ { +"vodNphŠC+2*^|FCw) KC;|p@Ȉ[l723C9>l V}oyd_Ю +S?J*Inɒ#@¿%d/]/>*z(L%fY)gR[Rx8K~M޹rtZDƒ/t\NjBÇ$e*ca>xt%+BhTr5u&-@Q +b;=Cy6,!ψO1gwl|dY?cQCA6r+=>%G Blh2_4 Daw%'Epp>LGt KH7@Vs2_G>ãH:$- bL.y #:!X8+6| !$4)]tTTʽtPO}9pzWw#C2ddUtIpH=|D88|B}d$B8񘁓KG2tv@gwjf_Q75??Z,Gr"ɰۨm$}<߻$u{<23񡩀B !j"~C5- * +:N ݴÆidsA#55B$iEHNMb$)+#D'dH,BV\c֓hVMeR/ M +"'0|̰|H.%JR,"\| Zp4Bh />T,)BV)%D'{e}qdC]ǒeؘ!8hGB 'Q]?o tbC_qrHG +*Iɾ^5Xn60@ ҒINO bdM!B*IAǿ{4):>eVt@ wxWS79IЁ258Ql$>l-2$Ɏ$.>'Ё3H +aSRJžT; b$Մ7.|H >  ڈ?lbnG|A B|JBy[@TSeJ}9GN4rЃcEZI&~!~tECE+pϗֶC9J5ۗj~Wnl#pqjxb ј]?柿LHKq07@\Lg=>B9MTMCHRJ m`w9sϦNMcBtQ#Z>_,<*R$.~' +Q[@ ygDOW|KqakeƣC bѥ&DŽ I>eO=H)gy .XTN + HWLȈTjcN"N84v <>[H:sM"h#v.I UÑ"}j#4@IR{b-Çk㟳0+VV'|:1t(]A+,)_<Q~lj{)G-0GBHߒ$)9|DHXsGgkd_3/94 +.+iMR^:3, c!qpg@{ +z8|Ts +oL}GNK?X;4xA +u(Qˠ\%9I~$p >4Q+#UZ5gXlhtbn'ZKҕ0J>, kw|8f QLRդH :>^˽ +:`+Q8xJ C:ZGb. A6t䭍JP~xjA>6HFLq!#N# -:2ժ(-GJZGp}=^r:FN?Q#Q .hH  "M|hQGf#Ϗ%3{KeR/T5h;(4k Ӥj4xrC$>r>2OP %Qx(ÕR+BÂU)ބ|DW -|8zId. !$` uı.&O^>2|b}fFûKĮ!>4(,_y >#<gCIP]%V H坩6`29 iEG*^Q +# "'YC oZ?FL#rf.WWumf_*S>*QXٿTI*%%3#:$j1 CsR|usASEHW\+^|BCU\gX5ϧ/X4xt.g98/_ɞ0?D&@|\B$cC7:d!!4qG|N]d_,shƼHf$Ǖ"4t  |rAeBPb/:mOHA6t&Vh!A ?vOc'%tGfl^+#tH`/ҔJ҇:"Va9p< <u|c:ɂ)qmo;zN2 Y>z\5:p0^c%d @D6Hz$WcV$ƠqGsӎ +RվZFC@;DNuW ^1CoIj |Ρ1Rc끂F4>ɂ˼aLcm"Ǩtt԰Q'l !R#Iv!Hw@H|DWXC, ++.l`<~9V*|-"fkGRoO 2g:j!pd~ ù2ϮGH|Z?x,/`9e/Y:۝%>RlԠ26̼dITjzAAH"K G1%6͍l{*E2ggKlj2&RܗUz8 D!R!ddP= +yf᨟^r /8tG'7 0 vPGle 'i|!/ol loE}LyWV%pCB<. l?Lfx`C<>Hi*ӱC}+mp +}Wή| ԅp1ácoE=%挐 H$Ç<0\x}B8Hɬʙ*ecG?yS8⁍訛q +l.FGBGBǰA]@H ȒH q &oWpGʜf?HpOtj=E e]?4 %q(2)1+,sF,"8F:iFˇ BLgNH(vB;Z> Dl "Ql +84c1G g?GHɾ\ +륂M% HIMv-@@÷c]f<g3דw::X:LhvBhiK Q'hV9cDzAz#qqe>bg)חYMMwp1&Wphrg+%@(XgrǿVBGs[AaK>T!v?P_y%&o9(찱ҎGf;HPT F " "=knXXm4ߧbs \UXMjЦ'EU_~c$7Y<|>#Sf`|p^))j*{lL]ym|x':'5:I  ƚ ~7Bb##?K[6tGbWg|rH[N;oH !y{@ men$s&OFE4h^/:URH ip*<!ĈhHegph26v3>NfP/TS9MabJҘBD䪘%d'/+Yjo`CR6|&ҏaaf{u舏@H  |8HV =Y5 o>h8f #w+\1Ī,#G|skeFh^ u *$Oã䒲|P|8l*>ljG*Rt/Xy GaQ7)o#h Y>wG-jx С ded YBF঍GT }L:A e"!l%˝e^:EL].ы`&׊qO9呷`NGrhc ,dG2jc$H -f}(fƊ=OSe6{OJI e΂3&|Dcy@T UQEQ5ն]@pUGks0d҇a.LWa",tK/p<:e{3x]OW!rͤhbyv<H&$IRJ|k_ߺy *NVDrѫlE&Ɔv +l:lh /rcبВɣBٓȄRph$v$|80:1:DGX1QG@#D;p$B +Mjj!|Ѝl $R 鲓d-CcБZ=N|0*C +ѡi G!8EF܌`> phxɯ@1*ƑybOa )X/EP뇖x0|p!>%x#aq +So8_GaS9<1?`r~| +mjXʻwT} Bc s鎖+hӍGH0J`F@1+>Oɖy#@2tQ'oYlhL7)}LfY'Ĉ~,H𸀰С ]3_%PBt>-=@,Fˇ{z׼7l2< 2ڨ!@&_dUK84+c׏[舏u^&6VuV CX76T@Q^Kz|x>u|   +ڪ^r]9:t2ɤG"8Hc*!0 L&#BIrv!,\q3fHy[(<'7<*G џ"c XXYs%֏*/ !*fxv ^6 ozLb6xfH Z= 1GUx:?y+^rcvt)7K1q8rHa}< ! btxER47*o/պ+=@A牾:>p  Xw qvЌ =Cw  \ +$xh+I"%c/;mWJ,-H ~˾esa$|h\ :ƹ2"X<UVVftЖ=A7y|еQplǁ&`hct@J} <1_hٳw(P## +8d>IW] b + GHΈ:th#S4\@Ď?%L"9Y%NMJ? GOy}# 4Rnӏ#>bN[֮$ M3荳O;b(']nDe p&{^o,|ȠeoVogخ: ^H^y#:׹ѡdCAfo}hiYHE2R +,!Ċ ȰX2W_6nګGSL~.T}*5OQ.I1|r׿zå hlmD!#{r> A[@8):n PcV&QLx GFã-yq  # 8CepG]/ liI1|, mgБx@|(UBY!Spt?|{֫F⽃S%'}#e@ :Сأx鵂x0_uo::H&DWbyS3؎:X%iv  eD:i$v >3](? GT+?Hb,C|?o3(>XfQ]>[ H'B4/Nphǿqx"C`Ѯ}m}FE;$Jn:@XckU8F.2?y g9Gr|$* d0 e*e>+\>N|AfPBj]@3– zy@|2jmG5[rxWzAHIXB\$FlJ<;|Ljk+{Qt+&?]&K  C9p a_֪Z@|j`@MȈ +_%>xÉ[}2`Q<;G#x<@d:: +eЁ6bCc%?9#|$ Oؚv>.E5JjtJ>q4{+WԜ"@,GG#@̼vt@#muQhG/ui6:1bFcY<6hX:jʰg#ጊL 7#<e~0ePCx@,PirH8Z:H9]|f24q$GERuh`N~G$p$@xt lj*#{UhȬnv^!7IS1UH*cr**<{Z!u:/>|<7Ň9d֎԰B[~bӓ&t|c Z +NL#d`Cr-qg$] M.@HɅ4ƑUᑱU$SzЪmPxtEc| zAVLhV\'5f?ݣΑCi#B +|DH6 ipzr$uAnA?f*]E88:T<!>x8\>-}*=|(h[4<yKr phǬaPFqND2s:%y!4C$/@# |O@Å <UAWzQ|b?|0!#4꣧BLy˽ip_ yuS8)KA ~t,JU~F0ŇC^Q?ttapgps>DDIcGw t.>:ņ,-ckZSWCuay{ԙ4$Q<.2Gn{GF1@7gb3UDD΢>#ėwyH +x(>б|h3C Gtt>g t #Ӣ`.7 3hҎWͫG;|E3 -W[t~ΎR5>-14@8뇟<|:4bvutԽRƹC ÜX>!&' =H;'|1DD <tJ~BG/ӄJ 4w9ԣ y8`äa~Ї26G861?"9ыXCJs_sz“4)( 1Xc!b ESPLʓ<'-O߅N!v5i (zn^ '!>R!5 ]R'FKc#<8UHdQ[øxƏ4=[[36" +>-GQ}@\s?ƕ~iV` +Da uD" +HWGj\ԑ6о#\DCTCwٷ?V,FlꀄK܃ .wA.xoE]諥GLrpS)xzu9A +7 C;xz{!LgUHy>x9#q"yxڵ$1*` yz ZSbG"K;+cF:uD>"yG_/R1X}xꃕ|2 +^]N!虞H!yȃ87pF40ߩC=~mc26e/^L +Pp@ X +y%qU=pLZ 5 /wm'!1*$M d!r)Aʥy'zp3 Gڀ-<Ց6F=\Hטדxz^ Wmy0g85b +|^ +b;/Q +o";o7q?kac^a"pP X_CF!ԱIĵѕǥԑ8ho.1-i.z +h+}0?0l؟O "ChDpAy8sCOҵ C!q,yh58k=幭bE㟊K)BD{}2p37_뿽>RG[A>NyЇK*u>RGntЅpk j }"B{,3\PL9'bgPkd2& !),+" P,ET)k*z< +Ƭ4f+#uؠR@eK/> Oc0]PS_]310u>|ekşȯ1jNNc35y_i ՑF%ke.y{py{ut*J'^*D63ЅID|=8GA[!CuB)Wܸ1ɧU:HwʒǨc^ԑc~Xa n Br1]H +J9"#UqWiPYR{o6vg,"$aHC| Z_0>_ɣ@WFr<-Q}ؤqy}Ȥ@ . ;DͥZ8hׇ&3eڸjâc2uٛ 2H2=%m#2BBD:ܥQ芯P0Yw'F G`veg6hXyЇ[A*>Ƿ>@A(DG v԰FaݞA3aRfJUGpPiqYiݯfaĽYG( oCQQoWE?&!yrKܷ ?y@[C46RVǿC!^ 2?Hf4ʨS^q\}{ЇwuKqKNMij {ΚVoo6$%u4KA`UAN1> +yb^c#{wa }rCQ*0(*㡥a'ԑ>/ , iN );8\y2Xv.| E' gsy@ê*vF} 9w`qGނ6@ʃ@`Ͻ@&)R"#l,mW-ף^%dac50d1 Z4ž>ȃ@s(>q,ʊܼ /9o{av (j>VQRGʩ/(G!O)&FFkmJ6{y +3a]L(T\'H}Fr\y%K  >p0IA^RG(syiyPGC#V f>v|qe\_S^pnxȣ AdjNC0ӘĊbR(X-E>X.J8;r8VbvA4&BB?}Dw%(ӝTn<;l +/ȣb9Çȏ"gҎ(aqC#gD)]޴Tzj̼x{"~m!F:OW޸X4{fm\dWL1&GsMpAib TW7VE}#SMU_yi}7l<*bCɃ> u!}PB$ȻHUhdyOky媣Tɏ䔠/{y 2)5ueP2WYkAρ#c!!/H_!o}Ox{H7A?} 2}@ [}8Q#FZm:ȃ:`+CܚaFQ WQÚmġ\H(9tu65~ Apo߁-yxʃ:UG}(i,IKCPct_bȃ8ѫkZxR6pW|sW_2ڸ{p +x(zQdCՈԯ@<47x)}Pݨ#}ȏ/KKMɩCcnDb،^B8]+b[} GcEU\>uB s{7VmzؤNHES^/G u0ooVK$<.B2H y1vCa MO55{biQ2I O,SS3T3#SLBQ\v0 l! + +~AO "9O$q bC^66^P)# #?(҇BNbhs +u( KX<`ez9٪?*8<$ +kK33&  ß#qQBӝÏ;%PT0}aCHDJ$?#w?@2 +Iw#V +WKQG4@P’ġI@Ja*պTR1NVVKWuqb^ EP&H>BeGYǛ1mpcjG#yGK ##%0S :>d Q馀GOȝca3ML+q8zh·R48 +J ֢^vxxu؅}ت7>BGSUģ'CB9j)#m:: Ū/Y\Sj&9tߎ.iolmVg``35Q +털Hn/"5P'/8.liʼnohB_/FRg 2^i#}GS'5y$N$%?Gskc|2ֺpw0M3sfC dT)FcCh CyBjCz

!Cu!kBhyfÏ>L88Qy$ x7L9 {ƙyT"Y6R[bqԑ-ZH?}\9I$bQqȼ>//~?@CfWpB%QD'ScF9$Qeh#u>Vr+)?tOGwӎ_cP<eTiV[wVG5 et1` SFRcLNs};a(6CcA>d7#}Add`gV,>WVJ.K4g'?p gPLOwKlgZe2t3GHqah9kZzCr|?8>hgA;Q4BH e\SGQ58 +4 $y{̜xxa 𓣪vKlhn5tLz1+X[p};|1gΠo\\} V0p/;{ڌm b9RyHl9N! +yae 7d1*UWf@Aa=҆٨GtQc*x&16|*̡3̺ѿf)h2k ?p 7Ce߰ [ 'l^L>,K~D%r0]GPys&FˆyL֤nS-@҇TH沚Ր$GxĀ:=LLL֕@UfqFx3͊Zyh\|F sDTs/%D`ǫB> Tٕg̗}@ʀf.T4ȃݡƈure^|P6٭-ܜj<_M3~L3bEB^;n7٭܈robgC[$Va;أ-od&4}H>D.%u.ՂcX~P! %iT@r :i ]?O96}i?Ĝh$fuޚe]45Vtk|4`8 |3\xw87y\!!HP_`>ȪJ3(v8ڨAQrh#Oo _lȘv豆ifd-C1NZGq/ +3U~<.o.j*kUɔ7L!ːvP=YmJ5Gf2A-8xHIx=EۉiyeRzM4t[{nu yr%P +ц~3p;Ll'`h +nxqY؄ؒ61򨒇\0HH_MBiD!EW3y0āԡZpklR$we v6&00z؉X9V:[5cu sLzEU<@?VG C<ќ6Ϋ'1BPi[5;XmUT7 +5=13&Sԥӎ^};Gpn໭`e{-uHL: d2(\<:@gzŃ=ܨ:fiL\F%9e*}8i\c]"N Φbh~]<nMl-u A/Aj\s7ϣյM~+u>RID.~+XgEJcXUOjo3yyiϢ"#!;J 7B%|HTp`>A :ӎB,5>ݡrjfI?>n~ ._0(C=qt:3QgeR2Z'A;-Xς{H8ދ>2O" +;¡A=Ѓ +o3#ytͅ8+ո>v}bձ{0KaSvVshǎIfʧX>|;ތAV;0 !Mq@8Ez/ywJ -0Y{49.inJG5wƱr:FbF1Ycf1cN\4ͩ$,h5k3UlN x3ﰝݜu e!sBH!}Z +4AS:8n6e)S1 ~a3gJͬi~`ɗ3cVbİɉ|:h!!)}n{ytȗA@F#J^-r$IDUaHae> \ sVV'sOe^bCU;߶9Aj.!6\ +\R=߲RRQ6QvjaS"=!#@RHa :w+n/3%aG~';3B[ͭ:L=fŜTa.IQxF[l^\(;.}DCqLf=ȺBvR'zC=<@3GêVn$ }{6Nʹf'ȴ̨QFqyMX-v9!-own8ul^Ƀ@4 _Hcɜ0z_(tjOToy:K<8p@]lF3ؼ'Fwlԩh:Lw+#0k9Y-#6ƑwgaW7HqA2]І\+d"ۃ@P0vfa f7`YC ^؂M8I ~^K}5u(LauЛ&LUЛفkGp"m.l>HRJ!SiD ++FNR,ג8@,dyF3 mXca)(C!V_\'Xt!e!Vl:Ê^v.;Qw".{ RH8U˳_Fz +̸lcb _.ii/1pO2.ǵɽ?dJ=xΧjutRN NsJ&6?oȈ>V!RH +J.$z2Bzd`f0 [bjK#ꘃ݉a(z8͡g`dqwC<]Os,VWK"yjKb@F!D SNU 0%Y<'<31ua!>Qxw;ex O}0̨^}L,3TSjʇ7KM u5dBrB@ӣ<*d4"Hh<Ă*f1:fbGȑsTm>^b8m eMf+V}>Z#MَuMV<Gk!C|͇8eQKGuF-CZC[5Ҵ*1kzohCu[Bgjۜ`Oer.qBy*`T:NIxHJk5VO?jh#qPGhcBS:sP +!m"8g1{ݽ'ݽg/gWOhn`-}) n={fᴡN>.dH%o)Ȍt v%[={\&R`ΦtLuX78e.}1*|zG8` iE)JO5m(i7|J*$Z"-LKUl#o"ǐ:Z˒q\׭3;Wi~BVɬV2;MͺYt|M<{3rq0iXy,~D$ra~X=՛{XJ#YduX%t,X.u0>[seBO0U3͘%VY+.)Oi tRiE0٬0өG~";s| 6AEB +e92㚋O708Y6':r*c2[䔌FɘOft>7tϥ5&3suy=nխ)فئȀ*شzqXb&G6(b^#L}W:QLFa~`/.\Po VYM?UPL>KOF6̄ad~<%N +Îhڡ%hNs~3N`Vb7;NAR#S-SvK@Je yNn+S(x+qU:jA79yn=Nk+f툽oB⳿J{\-5R {pFp3gsGLy\RڵqgKL1%~KpvC|㥒YI—KO1]EYG{\;/#Dsc`kb/!edaYdew#1{do=߸U ~`øIzBZ8i5`6:|d)#aK),Bh`<=O?AUp-ee\es;c&aO֨ƮݰQF#<4i&HlNa=pո|00kbbV0O:U~Y? ;hE,:ӎa.2A{=]!_C.JMLLlzW-djC#V! hU$;MT~ӡ0ة.G'rQ2)U"e^'R΅'}N¸_I1KX.?p<ߍ>2]3zp"'jH҅4ġE+Wzbq,8pxrf?b8_PIc)uRUvz`;\fjX.ʃ%k{ M[ɚTJf߁cmFZڡqԑ3fh9+Uxŵ ,i|K#'اՅ,Ls'<lǎAL0r;Xĺ2Nqnb, Q(ɥ{ġ -w=԰iQnᗥOʼnD@EW_B/;tzg:W!# 7R_q"!R;A%,N:Liuٛ/'f2SƬ`ŋ~4B}C(9ELb ['eWhFo0[$?XF8F,q*N+dzǯ0C +UзgRr@ S)${alhD`a^VFQ4B-WԟqQ.M P^ ӛvFQL I?]JZy23;B# +ggx"ǹFEdc*S,$!FW0szЯt,qqqrU>*h/E> I]J9EލKSэ*[; +*C_J8 +ZDf^)yepkLfeW;Oƺw3Zd`Q=  q7s"R?iDgc:fϴV_QvTplbXP `}Вu;Gˋ1K +]>U(BBɐ+xf=f + \f8 3٘W_>a6dY"x!ϔ 3LPLxO;bmx;pï ɐs7:ۥ,O3 ^xв1pR9ïFҢO#c?a+2JPpF\۞4n|m[h>zW6%tTFFeu* my0;1i@=v5z be:_!N=?!oEƝ*$p*LMGjP.tq0Wު~MV)wGW_ + +)֫\sojru.YXRW +jC>6@G{=A2CMۘ{:VyŝK~^ +Vk)tloSD +Gp'5Neة\U,{}4\IfR'( e *DpwnUi{ڋ|,Ts; *S*LW_M&601U.:>|ɉFخ42le_d਀SwW=4LmmE>/?| RjcB}E _g0(=eZn ?%ԜK>$b3TweIgOH Cnk:Cr\ $+jcz!gȷΤ"9 n$sFʅ/s}mp~h~}x $|lgיgh՟GzaRyAcV&+_wI7ck\b͇Cb(dŝ2ܿfu}Zf/=%4W{;Y9"JDU˨a¡kϤwv`ὐK l+ 7.y=2ա[] 9Ep'nKثǦLskBYvs]ރ9w,|n I jz3Ui릾X;y7+c:Ty̅N'~u~#H=zvq%j W$u~m YD?Oi -3$E'AkwKCU*X{5>f5ϸ7m2j&3⢂4bN3hi>f-uu^(՛Џd,VFeqH%Uyuw'nLwnFC,E'**;͘ '۞rLԙ;Y?^(*32{P3ރsZ͚)U +8a0uѡOwJ.N.:qQSxEW>|Ç>|Ç>|Ç>|Ç>|Ç>|Ç>|Ç>#r8 +endstream +endobj + +4 0 obj + 74637 +endobj + +5 0 obj + << /Type /XObject + /Subtype /Image + /BitsPerComponent 8 + /Length 6 0 R + /Height 676 + /SMask 3 0 R + /Width 544 + /ColorSpace /DeviceRGB + /Filter [ /FlateDecode ] + >> +stream +xM-KUi'$?BaQމ"DFNҔ _.G IUDg uA In,]_tNw\.w2$2pA B3_qgĞj J~x$  r2r\.dBJ1dv8SSA?Ig6LB$폮^K\.ˏ̐$H ^}DI L3ul:rr5դ\YVFdHGqz3 _.?&;ӏ$AIb D$eHd5q|PJ|_8hbĪ$2(W*Hrca#/O:M_GS]>L3ȉ%>HA@%F0IrSc )jO%2pd:$(3%(|GȠԬ# CJ;堬 &җg'Lb; A@@<7)Wvg4A6:SƓ\M\(>GDqHr\2d d_=~_j֖`)\krY"$"$RA2HfKT2.A1r\. αg|D#I9HP;$%Hx9Ny$GA 2LIjo S#rm1 X=C >%y!}AgP$jj; XM}kI:PRJDG$j3(A;cRf4ACӃV<=դ,$pkY |X:g_[@~rt"YMSsDD\.4ʠ x?ràS?kPMj8"KՉڬNDuY:fmAvNZP&D'"6Gr\~2 C$"23L:&kgmof<VZZ>m㫪WHTo9 d5duH~PJr0¯)wJa܆j : &:լjfo mu\-!eaJK :eБՂ! rs\.[0'&qUU;x 8kլu צNDlQV6fm?#"HGDnNhp2tRJr!3G9#.Gd\YLKTwڬn"I)wFdDdu:Y2pKjC>}\6,`r\Y0U$"ry D'ޯzaI՘~2T $V/a$=': W HC)_%tY-z\X-\QHa8;H\.? Fʯ +xÔ3_=e1 JHejD Ôdp .ھ ]՜լ}Y՝0X!HD^.? FJ8ŹdL!"a5p/;uPژ~ԁ\!d͵ӯAGHض#{.̐pz0sÔ{Y=P_6$Y_m#A=I}G3<@Z.G!.K դg6_JulPJ(A.埄с0刁#w, #լٵ;;jw0C<~՟TlLP*4ʠ#ꫨ#F)T˜**!yr0:%:HjrP\MѬM8V2VvdVހ8| ?#K^U/Ot#OOw$29d5MV{Au$);XDLx`t "1qa5:v)*\MmV{|1MSsAUq BV~7 2I8Vjw>HmJʰSP/LpT :0$\݌%j3 ':^lj{}T{wG ]J տS>x JhZtd5eD5F^#V˙r_&c$4d%(, Փ ^$P""ìt2pui@BKAn|䁧A}!RkR#WS"%GnJ"2H;A&F.2v\=&W$"z?F0}CZ@t']) jŪ%'Q a'19&jƶ$q +"I\.f0'NTOURb=1l%krPZ<1<Lw($4I$ЏWN<[8tT~T^$栴T[& r\̊$"A8"dXƇ՝$Hp:N fՓeVy3?2)G~砺#s2oa 2]NTKn\C9^-oҒT""A0r\1 +e$1RNb8:d&nZu2QO5S&@@QF@ÿ:5Dt OOȧ 0N#}̒A=h} ^AS)ɠ̪L)_t1LWPtoMuߠ/1դڂQBJ(du!b (! 'O̧P= I:uP3AfДA 4durPOd5OS>@A0ݔA=X} Ւ qy\1@0Sa)$pTPkO_JOd'#5%"60HP"G~R8ӌY ÌH}<{6:KnD훞ї$y\19'|:$FVMrrmAӤ|y6<=EO4'ɬ8/1%>"Mp|VOedq%Hp!EID:D>8fINŧ%aad2A 2(2e4P'CmLQHFq$ H%O@K %}431KAnRi+sVS-n+H$jt\.'$%֞9Tx5G$|ݬnr=I?pM:ROSI 2ۂ $%I}B/44JjgoDGRm# FN&\.8WNǔ?=a!&ՠ ql:PjJ3%H/YIsPO|:ɠ駒29(]ioV')~"f29(R*Í#Y=8T1螞~A)kw I.I"d09Li1s4?o?߰Y:I>xKWΫ)i4:C|6a`7GD`r8eBjCs AVa%CFbm2pCuxq.~ޣC=CN9R~Qԁ~φ\E h&A¸3rȓv>-/ܗŨ!z?B)1)t_j'Q{6b<2uPjд_#O/dZ|S.R-hVrҒ$Nq#"5_>yLyY}7e^-;\^ݔ\.Io\~GX=+CZj( OFY{*NxҤE`d0Cm>M|t"yHu)G5csd~Oxe$$tP4^OwC=E䎣Π)r{8SՃBV7%"r;CuvZ2e@ F$"S԰$qJV. HǧIiA}KhYLIJçe5H*eQHr"!i'H$A5kS۝ UpWſq!4P۫Yt%/1ܞQVOBF'3SȕqD 5g2RzKtiTP2<;?;&?M:iidI8ϓեHL C&m:x-U'j`k""A@02_%'Ib:ړJTij_,W /~l>}s/j;"2&~DP[GB)A._3!Pe[!\#( @i 9G`UżR%ȋ^=Dj^'2d[LK>(6uxƵ|17e s+Iu_Mj?&r 2ēt(_hft (Ϟ{OS=>O!,1b OSͧ泩ƜRZykL_ n) iK|99H89G.ğN噚k_Cީ~*$ ':寊 5YxfmjcH<T˘yIIpRi`FzP+JtR&_ɫc"?%BCp2$pAӤ335 լ?_Gb5ɳ rȿISBO@T6QД A٣̠KGtB܌ OSͧ$C:^CJ N~ )e$>R.C.SL] C8V$Zjդr2/_K':kO pKXM5뛘B!E`j8ȉyA*#Fq<:|CdȄGD{ֈUҞtB:D"%"~gLJ+%v8A ӝJrOuPY Obqy\7>GDP MVR;M8sP]o 4N_63Hm !Bk MeD"erP0ZTqw/|+eHeD:CݑO B'Kv^.*83.:)j AeFIdIt C ^n&a|Z>dBP)Sbt$#!.?}-TDNFBS*D]Ծe AemCrrvⓗˋx9tFBd8$gӑ2"IOS[ˇq\aܑpB[qNۚf"KON}(A.#p x+/7I S0N@0ӑA8NSOx7cm1|_G3iLUϫj 7 ӏKt)9RO[|zTdN2Βc^#N~;y%"et9Yݩ0 Co#Jr&P&!~ʐ=RS\.? /G$9LIB|2%"d}D +S%"X=(ꐰ6n$Wգ4P$xҸ aC!)_\\G'Ƞ4I3iGr/ڂڸqC2֪NTKO% r<ÔB$>Ƞ2$G”$A#֞f5uLLP=ͪ'~/$^*Q=fG."IaDGƥA rHf&4$ルQj>[X#HܚCʹ a5Yʰ:\^Cd8= grDLoOo\`$  $A3< <}\Ԗ`V/5':'O0@rJb6 th[xQ Y"ɔp43~ "I.69OSF#4Dā<6 5SZ<HIБ!My` ̓OX 8Fp~'2hҠëNK3pv5լj dg ̷IM2c02|z~~6ݼY StDAP" H?$t0K $? \*K .ux~HȯX§QJTijw-a>ȉ $G5jJClqMA7n}>hJT?Br,E2_'a$Xg$K2K$  SD$|4T/լ^% ) p:X?2[A x r?&{< T"9"Os:)k>{ZC foЗrq9<ΈȠ'Y tAqa9䑐; Ϧʠ j~dCAe ) ) \4$r :f*8s;!)"Vndu)WjjWcKSWd#)=OOElMId0Af8fȄH D$$*O ) VJ(WɜɤSQEd9@nKkN4[= ՒGhWXpgd32Rధ +kOW R$rP/_d NrP .C\jRJD# q5,mQ1duO4I~t!4c "#5>h&A@ALD$rJ2hKpE|2dBNӯXͧ$2  a. ͼMp +,)~L|Db\M9GGDrF`CXMmWv/1%~8=MKё $WdXMmJCJ9Li"!.צ8"V'4OR&%ȏX#V_ Y Oϥ砺="$sDX94xIRrMr cJ "AN=q_%9GDCDuh9(s~%F܋$w OK dylPR.W'4A@@.0N@I0 dmk7ױYݗa5RC J})㫩H իdPk)+Ap~- 3C|r2H3'Q' Y[d&OxڟRnnЅIXsrPAip6IR)9>>9xYHMMxa̟&2 s-$(]x !rtj>-nA} :J7/K렺~&A`I*A@.vr%"WA~ ko@:au_d} JjU P\M|R$ q֪vD&T'V"RݬP|CzO5YjqDd'S*H8 AՂ8$9 $>+Q9..͙2T4N]':l4RDŽSFq2cA+7%'3ɉDDӑ:R'2.I~3F I'xqt}4,m"2?ZZ q rsk)$`RROV #e>[ՙ8%j e')GP/'$z8ȉNX:L624{Rb$KIWz^f*It6)zt bԜd7@l0BܜI3`IwPA| JMrM҆$t$|PZMpmʤ)J(qI0'I' !A$w7Qͳ2:!j~L\wxI2sӃN~NM "H{P_YMPݩN-'ItfؐN5kI+- |jkD"%t@NnSpJ'Ot*qT͙Al1񅈱V=ܒ^5S F+2xDĈF$|2 A'xAL6 8i~O?Rg\&epJږ ͜6q_qio O3T \z5˟ 74OYMN$%A? :rP"{Njj?Xj^`5&XM5e9՝:E*v_)%QpD4%FIZCCJzfIyzRiEV_$ZxR 73 "\:4U bΏ`$pJ$"wI%`!oJ'*B +($Tu9L8 +՝:ҥ :YGi 'Rnvg<$3s\  Y_#TO̻@çd<8O""_{r$\{%p8}Qͧ #HW?}sg <P 8c JKEwZj簺\X'G-#Q-2LqT;Vw֑YZqC5D^sgAh "A H#Q]碪/၃ IDkOO9c)#0*)-Atd0MIՠdPb dHyiti'0(ϤcD$lO؁#`1M8g#G,]\k!x૗$Q ;rԒJYA}euN%"IV!R0~A5MΡԾvșvrPH0_AY:c 3LzԁY$At%aUO4I&Y 9؍:!%ӟ4I2ePBG'%r$'5s'l|{$%R]~c cșÔgĽ n zk 'Gҩ.<{u`Mp H.%F՟¸DmV7I2p!P `/Sr(0}dI>}ϑxECL$ j=ɍrStP 2э$ȜMƤNP)2M /Uq]IK81$~o M)n+jx<}$"=Q _h^tqC$Fo%xAmCJ*idAÍW@#HXJ+&ʉIxbT&MQ b +㗟ìFJ v̫DDI(>(Otq?J\".6p̙vxa;,X}H'L[~ xF) N:nhugD'>VS-)UMWV99I'aFm&!ns?I$A?9Ѭ-//Q:Q-s#a5iNIbt.$Dd$q.͜$71}i  bH8!x2chN@DnÐ&|!0NNHA$w.8F$MN{*$GY $ǝ +wշ)A'n8*n*GΔdң+ x`RVuz>D3)KjK@YPG<8䫗 eK<]49g |HOau/CRVoCΖHT/6:Qu:A$4kA '؆&xFD:,d1 F)k"4R&yȄ'A DjI*e ':v#At0.D!WcJ"sI"s2; MA tQIƣiY ':]?q2pD$gm KyԁN g(eJ|2^$uPkSTZVNjVj:Q IH0ed^"],TI2Č)K+r}! 6,YiJ/n}S=N2( QTF4""qA + +)s N;/C5NאW?<~6@@_=RrPOAn+_st:a5x`xc$;j,ɠ3 fT?)k{:Q-u$:Q-vDڝEN VoHGN eZ+2n\jy\&#Q{H`J 7`)4QY +.3c% ^hp1AA'pcgt: ~Uov]$ s$&Ar =.Tߋ ˹<$矀Fy&NML}HXjj_ TKuѪ.H: KO '_^gT栓nJ<i)GV^ڮ/`&KT C5GD%Irp:i +%|zęx1dfJ4(0ZA2~'e"A2&L~2%^t@_8\K9Aacuu PvE=:$T7]x7v,OQ$+| 4e0&$73A`"+i 4#xq69\D?G~H~㓚s 9 +9(sn0  Ͼ5~ɐ[)s@5/!"2w}HPb)%HTODիkSlX#$=pJ{5T{5O_K !qpD!.צ6O7%O5IehϞW_(K*%?1' LpST"bK;(dD$ D$%ƳĈt7?8Arҵ:N ġM:TD~B37TȧgSxg  (prp_B\&JCշ{5YMղx5)g#Ƨ e=) OaJj O? AuY} .]K:lI: z5I)AyITKER)̱jR” 38蘙 0H:Lf'I>|/y;ȔA>xRq)_;R%p Wǃ˗姜#sSD7'SR <#VTctL#A./2pwViVkwHX Nf5umP,%CB"4<[N<)Ay9Il5$ē |%y /$s#b#?V IK{NHB5,i C $tdq[ Mb#b>dOj$N':HrP=zJ S49dnA;Lϝ%n7 <<]j$!WKPfx8GTE64H=D<|Zٖ$C\b$z;'Aɿ#|oI J9UsP(}xHd:#&|mΒO߉WeJ8$2.?ե9f O0!2Б4f\ |N*ǘy#8GDwBYf W q 1t N9mx>jܾ!(T҄'g_~+ܔpzA$շ[\M}rݯOHH9c`Ox{&gw~މ G9k ƚҔ{e0AěE'`X~=R{F$F^}ū .9dH$qC39w$|TG$`Sti 03A.!5xə{] ǔdXa5K/Q&c)Iqõix$/rg~f}7JwH\-:R;C3CJ|gf: +$ɐ 4AYs%4A qg_ԖPӗ|fwAs$씵?c7$˙mHr =ym"6vU,.KXuA+G@h|k%WG#)Sk4& :fD,Lc|xFz胄%t$^)Dޕy+Ǐ5i@*K _@PSM׃T/lt 8cqﱀ|m3  %H_~$FbJ2(}}*FJ2:c_{XXS%%Ofp#*%*D"=zYLI$Nw%K3|k?+,%t@?[>;٬CkpyǗHL'=ZF h%ai<&q:B ^xA4%3cJ&*#q|P)eI ļd>a @XД +pP./H䵱,t@>iN4.bȀKԑw 򗟀M*%9е;Iܛ#'$t{#ё8okdsP2TSPB)+TI^#C)_%қ$A`)4'?Kl֖ՖHfs4{ĵG*`d4 FH4'R92\DɥRdN HKy ח N +Rx|Q 9{,A @W^}.?;٬^g6k6)%yI ?z%3$2cM)Y'H0L46a:,(}*ep_/93rǓI$xpoAЧARFfV>Jle( NHs硫{:0|IM y \ެͽY[Ng|0 P)'巜rD߃k߸z ?y EV:D ?SSZ gK?6k%Hxc׳%5r(yѐ`H aSMly[DS )n8F`"vȎmJ= 0t|!lP(^;0dP~GLi[זM ެGuth=g)zzdJ8<Β)A@e#[#я_J:g$*K-ΒKX+9k/lw>g9B9984-^[IgLI4i2 :AkEjJü5~ N`G12|O2r +3(% +\Z|bS`w]:v|p0t^8}+AGGM{ ak"/%HEr rP~6puǟN5p'A<jJDkRtx +y oJJ9%HIe~&YY㑌^3 $2z4o{б2\f4"7K0Lš)\`B4A@܎0A">:It{L,ȃoGP)mhS8!$^Oެc'3A~Qc!Y:d])W MOK$2~~=&A:H% #C D2 dY@p=|mG ,M"xk +rP7/ah&2/-Ta Zp[%4- }hfm>[L ڜB zg}8 Jam4l|D'SGC)3/WƵ##~ډpSSQ;gJP>LK+A~^:% k7VR+A¬ sO)M0$ir6?'5$LAiD vM ?7E(IO8?=hG"@)-,՛ nm3: ,$Nӵ6nF)_󗟆} ': {wl3yX uP_ph<H\O?z+^ӵk緯ĵ^/)I@ 9zGR Jw\c&k'CIԂr0@ :{0Kь4uC dЙ n9Aׇe^WY VòZI$ vaG_N6HyH#Pam\Y{6{'G0% _~vdPd8K~?_%,R{61{ agZ6uep_wYr_#+~Ndp}}D #,z%ayAbh:嵷٬}هf=s慧<Mn YɤJG2t@UMQqm֖s}H ?Y/\NHJHه*AJ/&,ĈG\tNx +.O4_/an%W9& %:K30b٨aFU X s.Ovjt#M)A4qzTzL#`KwHdiPiK_LSIBD.q ~牦I ͖҇X.|HYFml_;8ȷ8Brޓ(6:C2}ˏ¦ )H?acm->0Ht&3|:-' h鄟%܅rm}!׆="'uztT#]K\JMTѢ%'e!HO(A@; *riia$Q9TJP5f%L>iO")H^qD)JDIޞI K"Vo:[PkC8B\Akͽ1pN'{ɴG%{o\۱Ȩ#ڢ#iK&^9(l:dБ׾ qm6kz&KJ/PG"^R%ھ? |@TJG L@2&ᣅDMÊV2F)M9$2As5W R4OC$/AȾdH$ ~p;Mw't>X{ + $/?R֙ S(f=8 HΌ>cڲH0\1sȻ\O­Y5Y>O=vB':X\;G(Dļ bU%PU8=tJ\}^;q=B5dsiI{/TJ4 0idЙ4N >T Az&hThboMk>BjJGODs):"%8Ɖ- aG^eL oG"F -4KX +XknA·9oep[D{ĵyH$Ybm>D^#F\{:DD.Qȥm][BY:T}x(%:Q Hs:RGb$'0i.ĵY`r=wĝ<;_^eԔ~Pr GõY[h 䤎E#2G\x)r7+}pp#KQֈ 0aĜ cS)*2M`^ 8%ri- }9[IV*OtO +1XX.Q)-{p/횄]{m\ (;;k44d{>ddPӰ/ >OJܛl{"A"/# t>'Ntǔt)X{MDBy,W }%tyf=\{)GR#)*$]^M;>` */0ܠl&aiB2n X9TJ8rinEHDDqH&AX$S-Acm֖s5ByK.H?U)+'TJDG`$?OJ Bde 'dw\4`w0 H1L4<++e &< %WH(ep/t@Go`w>L -l,Zi%eyia/lA{gk$ȵ˵\|(GRެ- 8 䗟}HY)19ެsg,/?9$ S:Hgɑ|>\t5"?s$H:%HpJDG"zު q/Jpmvz$\8<xHj i 0 18 ϓ #Ԝ :lˡRby9* eh~HD  -5/A`/] v- kkTv|6G"9< e/?sR^K{ܛ;q?8apG(ep'skw$^%!ZeoN%HpxY.9ARbz>\cJ$r6oX1O@p S1׸ >|ԅM4ٮ)Oeu@@2D"Oy:DBX@XOK- $lF`7F}ΌS]۲{9X 8xH6 /)ؚvc#>" |p؆Bg2's 7tJcz}K [Nt%Jhz۽vDz 3sFN41O1>;Kd9_+rmx>Kȱ>kJt>֑rP@Г(IO''~^0dЁTKX,e ks=k`_ߌw%8J&piw f`0!/P6Ee|셭!$l1}†K;i$atf:Ё<kS ~XqQDv3C^φx*AM.(9f JɥR|~D>oRf&)QԇGXϏX^Vk>tL*0`#iF),XINL2?sJ})@$8HaJ1}JM-/Rxj@,lC)씽 \8XHOISᳯvgX{K$g`䗟M\")H񵹷83>HbAkrTA"u@P$xtr#剎ɑHb$9F5'k?J'C@"H$zW#)0+ $NqD{dA9(>|YĊI+I`y6,Gv.DčJrw6kO,1rzҴA"1_~v<3MG'!)G$*!8D"|Ck"cn܈ep$ M? OLցqBYH,d28ZҘ_̣|f\7BFb03P\y+eH1~rv^y_~I$8>˙ }v9(C[ŗ; #=-mD"Юtsӱ;EX{cȬ(l + /P:y?L)gH|6GqюnpMïk;{AB)Ax|.NT9o&d2rg%Mԑ 8_H(n<ĵ_o :H$@ FiT4B'ALop. Ldp:*'eeA x$H@s:D[@JXs/aaO2F鵿_ܾ$҅E-'2` AR3;xDR ~ﭿ7ޏ(}H>:rJ<I\:rڸ ;ݔЩ,#) ʚex𴷧/< +IAS$2|]ƚ2 3`J)Atʦ̑H3\"1c[  CdxZ޻I$N`G"k _J82LP}6#Z'9訇1SQ(]28Hn22:1^ @.5ep* |$R~- JD,Q)u%eX1Gʰ ߎ@9iaaw .r鞂K%>k_oȬ8}I(ᨯĽY}H|ߙH'g+6n< HGJ$2G$GgG^D %PB $8z }F erJ2C'5$ @$L0Fc :I4%o̎ȚrFw|=< *{2CT*Y4%F:} hʰH>q,U%wA־k_~K`m1pbkbV)ެG {C@a89(ĈG2:̒wȕõY{0"nPY7k]Sr)HM"syO"C,A2.}DDBeHBIDz1F4;#0d#.C_1椄YFP)39-7cԙx5PJKJJ klmJe͕p$,}־ nH悗_(X{&O'A"q/? +rbCA@67ͽtdJdyy3A.N(%}e(tˈD"A\F9h9==/ ~-A"O,D <8P)%F,'HxP:Rf/r0es2HLWp \οԑ C7#3[we +Hx'ಲX*C }PxyMMe:C34f=Bcy̼;(o-A7ޥLaDF^J(A|4 FkrcA]͒\#SH,$HT~5_{,'{4dzP(lʋ;JX{6 +F>u%tJr~m:2Kw$N $\FYR 8ŏH>L$%|'r܏J 2:Ht&{'K$V"pЇ!˅ F+H0IM$%^R Vp Ai5勖n +[j"mx[b+AvW I(]a$/L"{Cd2-zy ּvc9| +}//LC&)$kqDAEnq=Q'A. ˞NHCp A"ϞQL)9N9~Ho9&o>DJJO~!OPE+g|$L$= ё𞡔IͰ1NкZa6 >\pVr 4'pǥ> ˆ$#X{`H[#[)Oz?;Ag8KypBdPѕqnO˅H_NHg]7ŷ~דR)9Πeu8_BK V@0K7Ho>̱X ^Ё(c3ri9 .#7<1ϡ/__r8MxK#>\X@V5 + ptuڷ+)B)]s}]S)cB{w/?q:,$0(/;g8ݩ:6n $҉+<$zT5AtTJ!A#s  rRыy`^xR4/RY'h$Ҝ\\291%h.l9|-} ) 2mae2<Ak(ayCm;aB_žpM\p6D%J{=|_{Hc8G'ڸ{/$yr K|s%јsK$J%Ci_.] +$K*]18}HDY[N>I8Rk:]͵}P=Ҵ e w ^\g1%D o uHSG2# D\N%C.c28FI CxLti hyI^ߖ DR%ex>|IPJ%!-]XRi[S#a\7.bk+]ls ͊{Rx:D))$xp9DIuam>|&rp%<=7H@\A##MNshJNp7+GRIM" Dz8һ)IIT|*K`D&\ ѧD >L #)_bG<=i,<ִYe8Mq4%ȵoY\˅H n{l1}ߟ="z) G% $4*CŽOŽN ց&H>lIS"prp @&$x> I`Jꁃ_eJTN9勚2Wˡ& P#WHxo.)PN8\-/4Mf$e4*CEM- J-Д}>(Ł$|ŀj^qH$p;q\;JamwŽS)=Lp$e8_~틌Dƽ7ܛбzkpPy9yt S DR#7.J>hJM7z\*A$ DH>)QSBkzZB\7(%|j@ _UBds6lNΰM^i8: x('[Od ': Kȥ+eX"ÚB@K\t+ _RSr_L4Xֆxi؝5tDޛȜD|?$ZvA7H.[=f! ONJN"){%M"sw.$ +{)tpʠb?x+CGjI38 =3pQ) PN c7#Hdi^Dbd&?T8/41+`^h~Ś= [ymJK2qCB(_wD .mkI_(%L)[X\ᡔCee 8eP|E?>'V2`G`2rwLk_tOC!{D$ :kOh|گ'Ha9WkLbm>[8㉌qNM2BY=" 5% !28H_(|/H,_4?Ci&%|,Aqd2 2(撃>W/[aa-h>9Nbp\Db 7$>1B$rmK&S$ksafe 7H8 .sK.I 7#);2 ҕ‚D2 ~0*A@$I/H\ЙɑK7iI$f]i S*O4PN~eߑZ&*%FN##%"TPZAI&[Nr⾀t+s$H H>k}~~&*A@'6[6s?y⩦SזIS9AW~(A\ nxӑ2H$HWx}vRcSb:\J^$PJ)'AS*A7tД r:DrR&)e3I$8eP 1~{ģēP$ԑ%N1icyБhnyQohPJ$%|F +VXGc'Kq鲸;Htp=z鶂 zm+{2ȵ}ڳEއ#K/xmYlqoS4| GW@w/΄J_D LD?踛: pK:qz|rNj#r8~ +u_RĈ;1/+y'fx$19sC["/j~M8\C7xT§ +3-W +6Aq nHh6.]t-.a=>ý~ҴY{y&/?{xz{G~X8%}=pRġ_ r/Wýt:r&1Ox9oo?2q %7#I b&ޠ4 A&<1c%Hpts["vooˈO8A"#/A :Lb剅AH$lqB$wtdŃtaH1RH}$Rq plS{CO PG|6k=َANzhH&: CJbɓ3b-AWR<2xp/^D: uЇ ME53i016;x 򅉭D<o~Rg2rK,L\C-엽'nTR : C.ow 34'M4t?{N7>XOAڲ6BtD/)g28p ,$]fMHd҅TJBΘlփx& s}"k.1ȹ;)$ӏ`p1'_;0 ^pfx:% S Pʯ'<=N" ~v=8x$/!29 # _esd:8 0~ЗF4 -hdM!~^?t@ S& O'ɬ)[a 2Dsػ MڬʡKZY[L3ãa=Ľ 8{ dڣ䤎- ӂ{3%AL:gztjL)9sJ\+#u;tH(K251w :0o23A\ Dr"9ȼShJ}$%^^Ocd/aAYrpD5aѦHD{L pHkߩkڸzp1MJ|UĽY +^"A@H$N-v!lDrfܻsĽhg|sHt*s+񞼞cupGB)Ht*[6Nrn"NN@B_85OJH 22*p%8F0Np +E$4e ICO )J$ M I$ >(ϯTQ:%28, aGIK5/aGJ&:Ϊ; gp ~uѤ Bm yr|/RbެCLR/e(AJ/-(qݼΆ#ks?_+HJg?Hs̯aE*'ݠ. I :xSx𵌼YRFpKc 3*e1AR4Q% [ (_DRb+L$rPb_2(A@> l!FA kެ=a$4c= _;Kh#$f7 :8=Hlp2Kx$,A +kR5e. WE n(T#1G@#QL2rpJ^甡 k M :p< n0 OabĘe9=t@ /彌 %ķ|#+ bMɉtV'nO\HwJk_k_2p718ٝ 7\6@XGG8%_#Kx{ɽ_pM'G/~^yX۝IpY }ڸ82H7'n䡌D0G9T۲\sPJ#/HM|R'KM.} `&s(%ȉΉHp$}GIAJ'A-BXAߒ}pfUBs .2mµqѸ SN:}ד÷{k Kţ:/ԃތ'2)r E^:l$>p,C;h:ے's.9ItJnt %,OC\D8J$^'˓㧠s8"HA  FWixOØCǸoR)OH(8WF^8З -$ i%:^2g8.B鎠+#A\1k)t$7z=W} xzo֖S_k'3jIΉVO;+~XDSd=8 HJgXb ω/\qq49AfA_ tAqgΠ5$M M.c: N)DR4ic*DrP Vb<9$|GT+^ Oz^'|.xM HZΤ?3AA 7:ʔp0u{.kw;>Ts?@~fmБw_d$8H'ayh{3 F}@>Y c,PJK: JN 6*u59L2N25T` DOxI?[BgPEHfD"1IMp$$Hd/<-bCd#/2N?G2 [Q"H1:(% zvH6cqb9D筑s6'^n)FtaB>З 48^"A)6I $xp;d sdgrmxtheTsOO}\P Wp7NG7QI$Oʲ,9k4%aPTN~28^RxD +u&ᑬHt$)f`"se a g D1aWAX;e E)xK/$,oģH~X} 2F/LJ__O8+ %\OHte)9'k)ueLT*A"/H] C$g A`Z2:LIpA328ȷPm3+|8.CFteL JK_XvT$<½މs8>$8|PJK~/]M 6ir9r&GB .A^yOG2itKKɠ)g) Q&*Wb b'<_f$~HJ }^o$ ^ڝvCq,quC.Bǥ 2w%AK\s%G"A;$.4^R 'k7%/$rJνYt0P)|Rjp)A@+=;*CtRC ́0D':K?>\\S YrFjh``jRo(G"A +?Ô{$'%t8}yrIHK7XahJ>< yC.fp<\P#'K:(٬G%>QԔ 'z2e/&':Íca-Ӹw|G鴼Tz8 '0|g~m\ @ W)b y./# AzD)=4@")R fY%z<8b; Fefi% >(L# Ld?i?[<C rb A`1ʰ +N<;e8ip\vd5’Rq%Hxt0<ڏ^D^{K*mڛ6:e~X><dYf=/''SM@@}毝_%ܠIW줎D.# %hpt)lC5C9({Ƞˤ<8)^%D'AFB APkMPNS%D8AH%HtXpiȉiq +g/X#t'.+#\k3P耄]vR~A`rz$;c׆Ko `1q +{B|-pl>tV%D'2tp=ۤ F_JRvGJ7tg.rM,2JA.'!HJ#3AGStGg"Q Dd$8_ A"A1cA"e+\#aYʰБaA>8-8f wKeD> ׾VФvMtgt 3"yor)R Xe=½;Sdd>QG߂Ō_ئJ%epƤ&>y CK$* O;\ 9அ M$مE"åVpyTbD3LWD*2GR~JF?r&H֓#9iHJ$DKfr"8+=-%ݓ錜hb>oX$qPZ篴v?Є3)-;p__pb=_.%sPk")f=7νҴ)㫟 Y2/]*{Y^܏g::% EAңAg. VB ^FW[#9 ޔ2xRVJ$o8Fq<^~bH%F+J$[GyiH&#/SzY[$<8|LrVU"hlqJ9' 9:.&:p0݌ERk_"scAzv[LDԑ#H&?5,{2am6J9JxTqc,_y=%\"hpUrD' yR̙uDG?ԑ|dZcӣ!yѰER'8?ipry9|[K\" Ixd$,fVN:odk8ι&qׁzXq7CYsEs4(^Ox$a^p ڥ~(W/cs;s>Xs>[>'9q\KYrk{\Kp/nP']4 vOm(]@”8 SuK /oGLCrvHlA"P F.\|@$8iOb _;'4)QJ>xV :'6r^Ai%!8]<N8$&\wMt%Ýρ! H~Y{_9|PYG_ tOOw6ν3p|.N8tbkq@=B$_%Y#Y. Dǖ) )Of%ztx2MDY( (XO 2;Kl1)OHפD"$DrDj"' /2 Ose8D"uE"Ab<) Ht4c)A1K@"1DRzD" aYf9A/AH$gD"V>Y;xɽїk`=־B~~Aet@02Oo { {;- "8F=`ΚvK94%ϳ-"b$O0GAE|w$5tJ%:1DjJY H$FNtP 93:#L"JDD]u 2^D"#Β֨SYw5җ, _(ѼI nbA`g%57< /$D$ epC.4ۗhk$>fٵఏ2,t$Xaeڲ(A^Oޢ#`m*%g?CeDO軒KLOroCk_`v9hb}q=F&\bcYX~?+ h+˰aȎK'OKGБJ"ģ D##(tepD 73V%N9 IaJ2(A0\D^ S" +"_h!A c g"i/m, <+7G }kΠs?DBb=ܛ/ҠxE(]b{Qy6Ƅ%ȵ_hVq:/٬#քLG %9]}}$Ζ-4It@,!p' #ѥ ~2hJ%2#w`# 4rG'!/4rrG<JP%CܐH'bA p&hd.g}H 5|vi>hΪnksmփk}}7ޢެͽoQH {svo,8I9(O=ey1 vPF#])&%1%+)+9,mTRheMr\"q:JHA )GGHAy")#;Y:GR")%~/qP٘Y& p0(\| nA@`w,VV}PJhye}m֖q r4WQ~\B'w: zrP/UUPb>r?y 6E Yax6"׆EIԇ+Az2,2*KH$>tHIg S + p@SpB#ARbD3q%xH$%F(r$ȋWRL )}ɠ#Ad"dp&̉A9hnK#Y KB}2JW\\O \Pbz:p$Hp `{;|PI7Hk/1r=Cu$L2GD~_K!A, iJq5cb#Ĉ5r_2Y[n&f\-A7g7s Ov}DSUUJ$ΤN(;']%d) px(חS#H$F‰+O 9(wT ?q5e'Χ.A0SxuVg")}]IBA"p3h4O 9G"m )-T\XK7 b+IbXlz 9!D#QEb$*I_i qWDI(=]P^%%0 <>ϯ I%A 4X4ItlskDe(!5eSI%<L*qQKLpɥD OG ':':}3rP'AK,Al\A?L̤tfzRSbvvhm>[>;be+ë_;,8s"ڷem>).E9hݙ 0~W%鐵楯=3C#מژF$H|\2K̊9kh+LJVFK]Bgw0ҭ/1~ (C 2xY#<"sJ0G2ƝSY?G"HDe.ANGC_8y _F/|"Nz#h7XT2(guiCYΎV욄,uJ Y['ޗ~yo!P(en'k? ?B~v({GzjmA +҈9 n7Ԕ&e<,}$ZFB)OzԂOG@2s!#/ g' YoRD^:'gtLA$UNM[w38Ô^1H# I3Y50])їvjm< 48Hھ 27n2xp':k FއڏB=?q=nm4  O1kXnSNt^X+ '+%&t@`dn:g˃S28F07 Ste .*D=Nz9L C\F~&W_I g9ȾRfߞoO%L̕y947̡b@Yρ ^ \9 fHO|`m8rJrL_ ˰ Hܻ_$מ`m->CCؠ4gogg)`OHpkA=am'a1b߹D"݄p*%Gr&FbJLJK$y38e' @epARxy'%\x /A':#/LQS" k>wəI#KNjH?.>f}v"hz\G:]_y^ JT)o{UK$ro| 1L\_-ksm1> QKr&&##V~YSj@r':A:e,A@r$8[*9)O8q'gdrNC 7Sw{ Ai~*!4q `7: v| zDа:_;ڬC#׋6: at)A@P9?oL y%ȽN^>W6zxurra1 as&>AZhAI@`@0b[OFC)aك8]8qp241%.(K %Fr=PG[Z$xI#R8 霂HdI x/RIJ$r^:DI0fUx4i4G`Y\ByX:e8 r?t^;u)#=jd)1;Yf=p +kߋSX*0Cn))C3lSkV{ +bs-2:xAjʨ3kD")_h" H%GJaʑjrDRtIA D g:ƣ`${M') hAilP 3 n ~k x 26Թ)vƵoʼn&{{rX{S~J0%:SHܻt02`mAk [I.a±u(+siز2Փr: gC)A@@[I#y9TNF O%Fb)ӡtƉ7%\f"耀^`dyq~E|P7N7&T<&SXF3%rsM`Wam>[lbiOC)m.Y[NtDN}d5#u{ 8 +D"s9(OtN/Dz*u@4q֞(MccdFڄ#ľ!J+naag w$.q%$i0#q d2x +[WbG/A@\ѓ:9ښ\DYS%D/3(OtNtrD'\zeD7.#/X0H6晄ҜK4J{1RZp$Vl=wkÃ-aq.u0G%Ƚq9hݑ8_ +Po|fm-^nroї06D7c/ gyh%HcJ鑴DatlenU+%JN% JWȼ|q6kåHI #] ^"JxJ 1N@# 9DRA"qz)%NARRGH$/\:қzcx|1Cbi4wI1m Je8~˽Yam# 6 M#sal$'IeXIۊXTJpm>9DB$Rޏ*4K_G|QH O*{6w-A^\uf g["9>dcG\e8lڂv5':Dp$2ݙ3#/N ܭ+A4ɉ\rī M'639IedA"}Qy 7bN!pJ 'L`Ouh;?8b}~ɵ}D (D,p\/\\_[ny tgBIhޏ': ʵ'bR |Xk_w#:&gҰh%H^{N+QxD&DHzDJ"s. '#mA"ɟyLiNGn2 9||lydH&%}J(@ %|#ݑۇdc#ØxJ<1{-iGlM"At– e'l6fmYYHJ3%D{?~U~E`=:upOX['dfT^;Jc gT/h}:DrWjJ[)1{jS‚#>@JۢIW nFbĹ %3ƻ$8#hG} } 'zZJa>0e)=y}H/bPz}C&AL.$3~|J}pߗP:Д͵D$t}m/)J$g'o)H$~J$ң{fm IyƘ@J%JggH[ #'m mX;t&N/KAgd:c%-D/勚 pCG<^o<ΒaDH$/HC^b(A$(A76DF}aMO*A) QZOF\\8:Nys?G?"=Z[$)Nl4ɵRp+"ח}aJ}WP>h '>@_]Dt%dm_dY{Y: h,e?%I(٬8FD"#Y~\@J:7js1J$$rd2xi28ȋig8[]H(+Hq: /jJ$0\D׈ `H81KѤB"A ' $9HpX%$uqx[)[ؓ:jǵU^;݊)8>Ü{'<єALW'#/] K畕x赹w`rm3]%CfU4Xb eEtVD$9'%#"ctʡR)c"9FLtJ|Px=ȋA@^g ᥀%ҫ$g"1!+R0Ҹ 1Ƙ&<,FBVmD2GJJKJj M-u,>a=t1$_u?'Eߛe=9ܻt@ .N޿./Թ7 sTBJ,_$ sNe O7`f~*Mo0̇EHښI6k +)G;kG׳>_^p^bf2%xp :g#HJxGqLYV;絮 t5 ^h -)?{aKhJybe)^ZPJDʰҾq qLpA .t??RD JsaJEM r22(A0ep$ߜL,}ryhJ^40]f,,aJ,O1 u`%F+#f=%,$rG#$>ܛʹHQ-7/P{MdDɵ+ӹ1cHs+A>{ <"H fXvHw` tR" +{$tgD3ˑL" DKId28ȉȠ  q9<O݆扷#ATHlIiDfAgg֜%kkٿ2%fm Ik_;p$GHsdep͸R1* ^X]x ДFcYj"/ dZKf;"?QiNt,Np2Vm%OJXa {LנwUp$O˓~2N$PY•\_gɑ8~> WxM )#RMܛ%2x +}iBYΔ&fCGvրP8ź!13?-u 6ƕ2NeX{}CX1?K~.x 6|2떸oJ6J]^ۯzČ Y%0͎=S}+&A"qk#6>bn*OtHD 9LY&F SdZ{$9MC +rأ2lىbr Kj. $$%t$xc GO| $xSHJ>J9(AeA 2"HJa2' d2xG%O`/|,iYepub*lGD( pD MUqo#MK/| $x|{G~ A_ĵ\fPJ#JJ|6FJB@lP[bKY(Fnp$ɹ!dk_,AH ëqJh^ѽ + >)\|J1>~orɱ>֦CœHD FIig:ȉi)kl< +ϔ5h% +L3%AOx: 2_R:$8IJe9% Ĝ& kgֆt?M_528Gu=(6k#'0zdP1TvxH$F,iV=)a3yw)L`$:SM4A@@~Ryqvrp#Q/IǷHU,O?y}:A'8WSN0%1lKsQbz% 1- ;RZI"A@ڸ[oa K \ +$2z$v97 +\D6|Д>?8J񝯿 +ݹ'̉(A@ *>,Ay~M>Zo^|?X4l% +siJtF$Ar^I$#'trɃ`s#"LA 2$ 'QLUG^Ifa R2LlAߨK4TN*ƭ 8GĪ^ S^:rqwMJ}J':aJy/ :{qkwH 1' k+ !GfFgc%8DrX+qЁ nM/Otn$E@Hq H$0 's$4G|`dD2NǫwCB9 2J'#%r4 f n؆J. K و#aq%*%Z: ˨DH,C6)qrD"Aproj޻)6sJ}OTo- k`mMMC &0N*QF KKK'у2}nE9L0# )4e7:9hЌ2Nw1glr|2Ik+eyf#^ s})'N/0JD|Dz +5(lb\򡲽 OVhϝNB v_DfWe{MDjkR:ҜgWIQ^4ɟ:ĴKؔASPƍb6v`1\­ P?I8S K8kPJ%N$^ N_#H9yu^ OA^Myhz}$NG3 $)z/A!cI`\%̰!0e3gXRp +AXc;zD~o8R$WeJ_|#9ܛ߬`\{Z'c=bK}eWϞdϞm{$E*$aJVOZol.|@A.A8 $s$ H'ߝoL$ \`D"%ȏ?2ADW0(ެaB_$IdfopqP] )䇦E(#a_"٬gLA+,-82[<%XǪԑ?ڟ)A~o)FvSޏG./W_[Nts&<ƈ| v$vA $Vf)m]# a7m-Yj Ɏ't)tmR?ΠS%^RgoMA@ч?[yf堌o3tsE̔$&:&Y,am>l+Y)[sNwC҅Hzpkw$}߈\_/s2PA9֓=] cmsU |g=H؈Бlu/<2fIX,1C t3c7w턗 P 7~dJ$elC) !|izZUwvPF^=?CD q <AR"Y>єyr) H$>#Drf ?ux:9TzHt$FLH(i|abǤt`^2Hjnwk٬,#6Jk6זk!A@}a$.JAH_[>OxJݹ-ڏ4xk9HkSHIg+2;DN[^ t _tI2!{| ΚPGbD"Jg\N SJ': D0Pķ'^Jf޲D"g6$4W (rW)d.a Ҕj,>{?Ožr [ )7:tDc7kFaD_r _νߎ\i~D C F8zy=,0JX + b6aaJpRp:i!J&|ДQ@<=tGXB;A8;$Nѣ3?˓3N Jx//<~!rsu)C%*Lo#MQABDqvY@3sbHY#|RA1DΤ1IXR/}b QMyI +\ՒHJ$u hBza&#h'"Hi5m/kX4l.|2&ܟW^%1ҥO:\fB)S#e:2Ɖw .C :1nd䥁7iM0b%l /'v'|K5$%a=Xd NdJnoq?8AgkSR_WB3\'>pm#h[OИX9H_1 b;$HS&)ɥH,2Nq GC";.sj $xhbsrDRF.A!<8^2qA%f?q>}D72̌ OepÆD"È40ɼn%, 4[-_D"k$^UYZA׾::ĵzp@@|F忋o5y{{gIM.a*Lj4{噟b5%eXx+#+m֠w2l"l%vV%NFµ rpP)9Nr9(Sd : ?ң\b#/?21dH~' $4A#il0NN4%F ' le42ڎBIպ%\;:4e .`.Iœ8G*R#Ӕ+OL}_o'x݇%ɡ\ ) aNgec SF[bG@XIV ʰJyP82PD.v䤎\ˑHN>|?1 2D|TʨqD9xR3A29c䃲IQG0Hq&L2(N{?(r\{Caa_h  +v_.#'qyp +~A˿7ɽA@v88#'dQ1?rJsP9x: v$RN 1Nl=d> GJ#%H͡ YwcRI(8 ɯJ|wbI F5d8o^\7rP"ixIM!IM.[h$f`$.?{UO>Zp݁kSy/4Ԍ{ \8H/%~Myoý_z('M 'qZ{Jh 1b\O?bqJX8Jk퓜e 28Nq%#?U' A@@(_xG3T'Ϋ y1}" Mď )A*$2/1'H#jVIpl^*a-E\XB̯Lv0>%-;0kdR΋ƙ +9_ %K_rh=KA9\H4{$ F7֞nPI؝Y%wДa1aa_" JAH*ᑜry3$Nǔ#x9E|_[z3<8:ErPܛ"/xR""كRN9T](K,aa湄],~ ]J\9p8/ _Eq#c|>C~F.t&_h^\ϻ3'TʆLAvo i/@@Uu n AbC+,HkID8eP½:qjp:d%ȟ}LF~拿i^"x{Je^ٓ  3 2mA@ieZ-ݤ5g/-A`A@,;8Nq bmw: NM 7*'{aex#r8yc=ՇX^"1Ep9(&3gJ 3K% '6 >GjyK]819Ír 2TKtT18Dc%gF>y$>HpG9D/Tb$*`0i4x &60;F}HX .} NH;f|[8jݣ9Dvډkʐam_{U_2һ#1J^ԿP)%LԈ =0aC'6(f-}2 Ͱ Hd8/ΒG^"'uDH(~"A y)/4$ Y&y001?(-VF*h k#~hxD's֑]{LN9FMJ_ |t״7<6kFS`39X==uiI9XҾBE {2 UD"epoGל/<\ ''nG:M'u$FA~':/\D.6^}~~t8# e jӋ4%~|7%"vjwз v}'_ $td"xR"Ks'A< +7@lJVC? qI4qy{# 2=!,)abPT70C0 o7 ZXOVl%t$΃pnH$%}MMIbG.-ވZPM'GgƉL4g|1ma1[@iiqO(T{b l+ H,8H$Nܖ>VIGQ9Ldԁ&H$ ȉo: #H&qo%HNIhԑfG0\R)r#=*A#P9aPbLCydR63@kbq%e㤒zF?+#Df.3ܛzʵem40˿|äo~8KR9h*|D")#x@~6k ; : #ȝRS98H?r>'T?)CA"?9S`>I/79i$ bG '  5gNA)A`CV9Zv%kkɵ\WܥH{ ^ڌ{6^D"A= ׃ +o Kme/:(%Z+ٮIk:%u֔H&; HDhÔ#Q)1U)y:T8=t@"?Pr8N)A/$ Jp$g%Dⵂ(O bH**ngiHNl2%lVXД06 ]G>X8MR")#Kyw^tT\_fmWkq#MѲ_hm*%v;#_h:#/I`<)9ARF")S ! ߜӿM8D,^#Q|A"+ D 101 #-Q¥2,KYAށDpbJamԔP"є: 2G";+2r˿G߰<ӛJ$5O\}{{g\!k4l0Pxxm䡔6E%M +t䋳ɝ?g&_ԜD"3pWh Jd._h 99N>3A?ң1*epI$a0Ѡ 3<$L{&BeAb}Nl<,ylk5zNJ'$}gMi3u&2/}2i&J-'2Ni ѵi$[iO̧&\$JEr]GrP"qAbJ wvșHdM$)Q 2Jd: Nx#qzșHH MI$2N?=N$o0c'>/K a&|P7~e8"-`$G.lI?. qR2>pvSu6D"KK" e35]]޻6:PQg&jhu>u+Ͷ#)-,HAĢÞDI$9(89 '|Rr8>vyꃟ耜|w~OH%)T3#H&%ȉzёˡi yDNs0r>9Հ}iq88ux;(5A&^XLha1vyC{.ܻ|˿G߰|#$%ȽYG"מ +&H#'M3٬-kcmbe+@$'#8R b@M,ѶN2N@t4.O M#=:#߼U2ҡ*'?m|S_GRF^b$2D2 +Lyr6}FAs͏4[&2'p%%i2q +16imH$2m6, J|-P¾t$+AN<{{vI**'{>d^ЉG~oV O1Bk ^b A@` b-5Scѐ;&'Τ}O$ HJwc켌:A.勚%3 eɠ#krD )A0~sNə GKA<%F  >F 7`$:H,ˉ=җY6NdpR5sʵoEnνs7Tk2$_2ۑ%Fpo֖^kHb<ݰh8?em 0 Q/5KXXKKT**%*&Hp+G$%#2pm0rr6W ʓ:ÔnipMP)`$?\~3 AH8})@#ˡ?(KcdNRK !^%t`^hQ 2M }i͹T(^H$␰\v + |ݰJRkv(5%ܙok>Xu>Gd^}8k7)R°A)"8g%3%l3/ w;VIfqFd 6hL$sM$cr,e|7Id$NIA_B?sT8\ TJDҘI$g30li-.ښYbBҕm7vhX|"Y!N .eO]_=|1~ +FD ^\:3$ks=STMS'a aVeh[=ۑ{AGvjzJXa$⏔n8u$\NrPF> # Mc|S_ OA?-ÔəOFD#t@0ңo|``#Ƚz7-/93FefDb7x{ .=#(A+-&HJն%j^>#rPIY'qoƍ8#/?W=oM ޻+E0:':zȀɵg/ڲv ɷDiY8r K$TVI+,# H8ДHhAdr.LI(Yb$r x/OL Ja P9r9?3oMIADŽ7?8 &R#iX=pJh6eEpD?ryRDR[;9 3DƫCOA$8y?I A@Adr OGБ8OS&^=H49KБAEbͶTIcb[2]ӑa#8(cѾ Xz[꬝9Pkwx>/[$2u{>Yu`r쟘O/0^ vDhC;x,[ol3 H +p9IӇOrO3SD|dP"*cHgJ03ST00ҚGڣ,'%l|6k/rpXvyymW X{˽4IT4_m%wެeڬ-I4?M4`r4 ne=au(Ad[,J+O|藕9s&FDr&;ȍ$Y mG}_"G&pV͂!nd6U-Xor .Ajr2; D9&ȉq䤎Ĕ)%2DxHdy`\a!`ll`Db`! =X2!71rr;:.("QY"9W~IɁOfcm8 xm%Ⱥ;6ϥ/^@Dz!m + X"ΤEt$,>rpQ %K>})W$FPS>Q8P92H?=%%Ύ_t(фX HX:RxmցG⮈ox/nڇ{I^WL"/?vkg tbJA"cS_o$T`m{r@ƀ Jx3s`KCJ-lb3ə> g[I#%9T<ܙ(%CD =L$*%\s#req2'ȥ~"#/ JgbC %FN4 *FL2GbaOa)lG{4/8#MJ6kcѲ} oy"\6:ڸ,}F"Hx5 5\DalH:Ҥ!?1ib_E6:\*%r#ҲLڠ]Z@i+9+9Fz M$A@駟 r$p $Gr,YGr$:G#K|?4̙D"C?.ˡӣNۇ$="ayW"]ladD^> D"OCwmr#tR*c<938g@Xp|'28~[.C# 'P1x1n>%0lRFXrV&PKHV"_[܂<yw7k$\;kD"O_~>$NYxکDެc`%!$H^{JS[Sm%Hpo#x #ANAVv#SH$uJnД$Ɠr<3Ip$%t Cp@DIq0%H&AGN$FÔ J@1BF.I%f͹Gb#vx$ Dڬ{-xt y/ ]AUk_;N"A*eu'*~Hk).1CakO)^PŽ)Ri@,TY42kh1h}t䠌 OqII{2AH$Ԍq䒃TJeTJ$ dPߦ#9T$'9y4+Kbh6B%F 1J`4ieK![[EVDWH"JDJ:kY/?vP\ (q+&r0!0-" kcnp4RFVAbAljZ.9f [2͍m8F12tH$%ȠD"?9.ݙD J%rPG"A%|ShG耀 KTH;ID~ieyb3N9A+?hK`MvJ- ۊyqX{}=\t;][e䓿4>FuIu}9{cI?ɑ׍$&JFGвvv*t$|p\-A@M A#n29(# jJ$%'ݫGRJ$rP/ Yr$$xOrK%Ƞ4 %I*ɠ4o_b\=LunG#bB)a@[S { |nkJpmk6T'㕃f\us )1?sfcv⛥Lu`}蟴v : uGhIA@*iAHBJ`KF~2(A>vp6]#9 eA9gNR":\I}NDAR%\P#5IpԌiuyb0#A}oaau$uľ* ʖn;m1 b#~?q@dPn*˓j_~eɵɵ2~_.|o10RIkO&''F<29)mMZ1Mt>ӾH\DH{ə ԗ'u$ɡD"1&QKd)1HCYIdQ $#<ecx`l>q/ sJ-tJ (-晖t+RI&(&}W FwT drP! ywzm׽NiDb-DdNH*KA9e.H7d3%#At@ᰌg'HdxP~1 y_AOe.#H䃚%zR)O )/!Xkk'ց[k8\b䗟 ߚG 6{16Kc&ANt` %^@Y6 `/6%,tڬ]q%J$nK F>d,A"HJ<DBw'a{OY  P)bҵy2l3o} iM%Gkv Ա_bsHI k'N  D")CnN3@'Iԑ JIOy2r:dԉӡ<## ^}('%fBi' +M402,EH؝Ph@wD zb=Hx?Nym͵ڬS)>a$ea^]z4k 73\*Acy,fR´싴D}X4$eTJX}iى|NH%P)9q:O~2Oꔃ#IO%'e.A>G#ON>;I|>fp$eOBr#8=LO AS'7d0Ҳ!/: 2Z k%l%+VPf \fXX e׾H$xǧ u/?O8u\mJԑ`1NX{Daby$JxR ,akid3asO츧DI_pmd𓳓1wU#qRB pऎÔѩLE$ p5 ̀* y+ Atr1 KaG0+ o kΰ9\ D{?zOJ,)s K$ex5Eqaʑxo /- e=B)<10\pXK}e$, -t$Hp@t<_6Q쌻B#8˳ã?$:1}2(A"/4ˡ&FBy9(c^)M ͳR&:m_ /Ѧ PqW`mWLywIwu ':}“ ~{k;hn8fc060,8k6&H)G헯X:%tlb$ K$~,#8/hD.#?L,K|crLJ8ƑhNSL" C˘peH"y,A7b[55L鑌ܣu}rP\78r/ =B%HUC9 6xvdP@)ao&RJ[<" >!"A) F:xA"#'tdPC)OHg&t"/<Hu剓 %d#'5K$0  #A<*1FQ4rPHÜ~i# E"P\+nvm2ЎK8F7n:U!ɵ$"WםkĄD#49(a4|b/ȍ1 [3i@fY7X1v\dAM8F9LI@%wO??%PD.Lbį +h GJ udp?+S$N9(a>9Ÿy4aJOt`4Ϳ5m2`$ {wÔ׾s$O>^A0ҫ쵮ݔ׍rx=0M{+hJ |1LL825 R3kC[)#5!8HpL >TJĔ$II9|W5Lv&xM9(#':~~x*%ȃik/A >6o~h GC"=3(eh׵P$t4fpaSy*ՠc`M78 : 8#%/q>x KgJYRd.O8Gdp$e,sN}N{# D4ø0䞚.Ȱ#5H" 6 ۊy~!2@d +ZM5/h)ukFbSB ژ<0҃/ bm`_ZIfٸZFc2E\Z$HK&)$g|R_|יDr&N.Q>4cs&)R\DSQ瑃rG"ל*Id#CW07 m5JZ9 z७|QXGޝ󺮵3Ap?eePI: OxJ r~QB|/dil@NCD .A`8, }kvMYZJ$r[Lδ '%Fn RgHTJ|vP U9)ЁTH(2NbA;z:9L=*OHD"{2t@!2ȕ8p#9 yARؑJ)X.nFb⵱h bӉ;n r]{Aֆw/] ~ف/?O8xpĵ,|ݝPJ L(7iff2^ۛ^iK6.p2(QX+Cl,a1CGb"C)whAB擑n|PY"9D"?L.)2<)<2?щܱ# 8#xd-A)Y\%N cs&AbD#7̡4hv'lŤQXhO_7XDZpv%Fp0|mkșH$ ',ѫGeuy$4HuDp0 ZF.g BZDE#,$8b$$D"ýd,?ytL9ҕK$3OLߣ $PY"~ HJ%;Kq>&䜥+͡D|mAkA`Al`AپH3/-M eiߑK~$F1 H>TJR8ە:r< ^B_ + Cgs:I9TJ`:'_v|x/ ! Xb,Ԕ{.+D'ltgmDbrxw q]ډuCG!J$Wyk11NJ9nJ0s)=iC E>h26 (-PKc$~ 8dn%/t(#d29#[OKTӌJPYzz)tFSbD_DSFK<2xpCg?h4mA9kҗMluCbC)m,q{Ҁr_[Je\^rA99Ot~ l'1Xzg<)FG !40 f MG'V,<:~itDtK .2K<: '! H}$.[ eWЁ3y`$$ ?$A1%#|P"76:Pd8|280a щ} 4ҁ[+_NJW C{y\JD rs}(1 ?2f!_l|%}mpXj;RFWڗ[Hk7Nt"?ɣ6k^:esrJotd=1'H#7rЄ5 H,b2fD~vY) QK%F@B)A02aJr99u/_Je7v\䤎 N2A"QY8#) P) 'uPJ$&䁾 na&M3L$lh#l)Qa`([I)R [2u1Թ]u㚂iӇCxh^ /HT2ڼKhE#J@Cho$yQWi*iGl8Hdf/#oKAG/qu N"0%F,Ory0d-C#Qr2tLȉ>  _ ޯT?'DD" PY6M/t4$ |RLAo5JaׁEf7fm\%F\8X7vGUqh*9| K<sEd^4pX6\^;M,aK (ÎXJ-ĬE m wiOGG':H$HgrP_ңtԙq?'h")G2׆# rr"Ko62r0WY"12 p,a{*a)BǾ%,5 >n0t?3ƽQHdJ$O<GC~dfݾk Zw9 }]JTI&[810uM8U*-3%,`pK + <*O¥O9)Bɉ&Hrљ2C$\##@$N3qHNB)NԏCǏ<я'%3ʲJd.Ϝ #'f#FFz00] 6\f]  +~Y;}9|ĝp7εڬ`J^OΒc0 ko֖uђh !4X{V Sr +Hf nGl Ve\5TZ@AI{n5dP")%::D"P9t y/,sDzHTB#g\g#= <)#H4' rh&A)AJr$\šH%Av*a@!׽a5%`A6xoP"}R͵tAk/?əã5ŵ:=rCT1|mM=%Z ,6BnIa[}$I.A @٥y)D"L1N\D\ח\M,Ǔ~O.R)MP~ qI}^1lRDހqP"1 00ꍽDFHK+&ѷD"_7kc򽯋N7ƙ 6nO'Cu_bə }%HYM({.uڳDʵKiނ)^9/e>6 #‚G 2(@bn :]4JR"YA%{ ,,1CkD" ?N 6D+f@Gx%X7V#m=FIĵYԠ~9T_~p%Y)A@\ĺ_CgxcdpGJ_.HkO ! "lfJfzA.A%'rq"L 1N1A<9љhJNB)x r9'u@*%9Tqz gy40KX:[ɡ_£ehJ+Jf}ݸ@U`t9Kx5<=f.?U&G 61K 0r%L#kal 2<ѬO[ibNĎ.$gM"AGur28ȉȗLA;Yr2 '5G&uP)%2<8Iy%x HH0q<& 8PZrDn -mn be7Jt}JɯKI}Ak{\) _~kk'z$1i{ጄބ@yb<A(׵G18ШveZ"~ɉ]tNvVFҎ3H7 3RJ$"AR`dPƧw A86(5e _v"/?$13Cym:a0rbJ(ì/3HT2$:\{OY9藾S$ b˹|;d.ډk_AqzLs KxSkÃ#x dPi192.e&Iݙap$|1e_+RI$ .,b׸\J$2<'u&BA&ҍthgt(A02|9)H$F ] D:I|J#L$98 dPF^+ f  +#(NAU Ϳ9  +ꅧD;[b .JمC@ڬ-k_MD5MOs>39(A6s$ AP9NQ4 f9: al2 V ܊>z.-,4%ꔰ<8 IK(&"A@>ɜ$ ': H˙H\_2=)%tHA#8 Hr06#退A n÷Iu/PzB/'֍E"RkX7ï'\~;+>T^7voˡO=`4 ᄹym 8+ aA#;a)GQp[ޓ̋4h^7k}LdfkHi)^{z*EX)9)aZ4itJ,28Fc%ɣA"A"sw僎A%H Ô#O>38' HD>Da} C_Bh>(Ռ< aAVji&ABXp ^u Eؙx"t}))& >8SO+ /_A2]扎9$7wIye))x+,h JL9(ANѠ 灼 yyRGҥ'!1hB)cqdP .)'<{A'> 3#t~MHfm 67'JXik $†P9%(#??g .AG")9L9/';\הtR!rߓЙA"S{Gr/{GRF3#c/ø*yiG"o_`lF£k+[ĦJ= u2/.qm@b?‘LWrI^ԉ޻C{=Q0~cJsni9x2 bw@&iH k%4:Af3(cDsG^")ѭE,9QB#ãtiK<8R9"9>% CD$2*At3%Rh^} yӴLAĜ#iSHk%t LǞC;l )ǵk'ztݥK dp$O}2K _eALrxcfdI$4L|/ͺi6e8,fv6[X(r{ +$:rP7 GR" 1N#AqhJ.LA ^>R|RRD~&28#(<ݫ9i#FD kr:XD}P2l\ H& |}IIv rk\(Ab!| q7E*[r>`r94A%w_.{0"[ 2 G +$Na7amK%wҥ1 p="ept}ɼ9 m/CYO,<-  2*C$NL)B$N{MPN!Qr90Q 1 nwi$t",͕': sE2\)HYk?k__ڬ͵_~NBKbh>˙t)M)MڣKTK0pR}k# Qg ̥..'ѽ!)]; |)8m>y2C _ңҭNK  II$~9NH<ʨ9<-অ ?1`8XYa&#]&3,$v ||ݬ\F.=ʿ־()_ns݂S"/O_~0FA@"67^ %J*%0~ SB_{t_1HD6HX"$vmĺGЙ Jד'$xtiL<D t)SYÃcyysk22cP MzJ@# J H$(f/AL\ad1f03l;lAbq,QSZ87Qvi[- {_AjnO~$w3*K_~'6Wi97Q'(DnVOpX #V&Q%t%RӆJpˋj'';A"9ۆL)Hdߤwr࢞(8j:2t0$xgxp$##H>H%'ħqEguDnT#ٓaby4Dr"9,ѴDV Jzr5\{Uٓ:y3}r ~&k_2 :ȉ/?DwԻ<\\wP}O ARH0i_;C.a9F`M@ca n1A;KTZ:Hd]Kz%ȉzœ MH(K2! چy0ARp{bB'm\.-,^(I 6:u'־$`#/&}#*NhۗX+ Qﻄ;1{C)Q`l$ 1DGŠMJJeA ~<s%86r)OyȓAy>$A>џ@b\_*J`$w1&hg"lMp{T`^K%;<ޛO\) sAM˿I (8Fp@0} gi4o%oDe,vwJO؝Ё +n@,)E P;ᓹF HE % 7dI jJ$r솗PrS̓:~38tǙA}dr͖ ME4TN?(ybtF3 ZLZ-8^W{*Hδ׃;ցKM69fݏ4A"A/~7X*\_;qm6 RYb&4F^]׆K}&("H*a<,栄DrOFWO `3A.%>A>"4L?99w`R'x%3'#ANKh@ʘ 3, PZr:mE2i)7CE9\'YnBoaP^q#K"s>0`ra1`n|vKe$2Zdg{\IiO'"@[: e+.?///D%Hp$2:/k& qP J= 3OMM2x:4B%Fh$oPVF"%Nx$a,A8 $H7.Rusmꯛk _/!^Hp Xxgm[fx&1e|#]@26HZ]+>*tU--`sctHhTD^z4(1+?ݱ's&. ?(%*'2Ip<>}ҋ >I<1CMtMG_&;\V)5e,3Y3G':':1N3#e&arPoҔ&KJC;"l[D"m +, E! pXKM^­:]J2t@I~ϙAb^S\kŵYk˵Yx$b$}2.q$ mC +ݱ5 fĈՓ mʑK(CgPƸ $0J<I ȃn/D"c<)Ng]H%A"HK$F9щ 8ŧ-9 %r~(Ot0c! Ǥ% H^*;mB+|m֍ {ʥV=dP^7~ĵe(@ނ OGk_xh⽿huެ0aV% ï)sv$4')"E'н2M"w r28rx.vh:Ot:H9LH 8*#ѣ|PSD~&F|&8FoZ ~b"1440cGdX{rBݔuƈ}}_6k\[M7Op_|_;-;&qmۃ9f\nmI)9%{YDmJeX7v0ZI:ev 6)A@*KteR7ȠJ%=*򤎌ur9U~oA o s0W0{283ab$r~6I%\at̛=-AE|߸Pgm\w *XPIDu⨃1<ε :i2vE Vc/ _./a=AZXvv.KtHp@gO$ \"=$2Ɠ2rDgyz: :H`S~Lg0ydp$2tH)cx_':ucO:VA0RH?lǠ_*!K:OZpոIJO4%#;AD.!Y{lrA3-A@s~H_(ehβ&> b鈄M,-D]Zs@LtA wRI.1p:%^:\243 ⑄IOdp:A'8H4G}%5!)CpF˘LT3% 6'ۈIJ- gZu ^[K vԗwԗ.6sĵ;O gI}9(A_^GGD. 4>HrmlHv5 +EC5 DhHqu@Dϙ nd{O28>yz(CS DIW!%>A}9(I 5 C/z&> +MBhz'`+M>yi)`S28-+8?iC Fe9(ߛu ZHusmE4\GW/ǵNy7xH46yq`ݼ=L (4&D|vgPZ ʖNw +i]£r5]5g!%cPN7!D%C\epx8KHT"JPJ$_Jr'{#AtBW|bBBY\pF$%L;ҾK6k]a%ã3[@}cJL}猔Q<8/&}%RB rR11DqfxodPa0~6y0 B@$a 2J8};, p$kHJW9(nB$×ey2Ow@@J~P G#8H$Ƞ99;}Β@%z#rP "yAi(216d9, "km^7k 場 hӅ@)Aݼ6:rH3~<8Fdp}IJ { 䒛|m^i)ۄ+aCm#-`5#o%d.prPFW|5x)1rR >TH\gS(%F0>~ +PGw#EDF$fI ِ )I 4D["A@ 6Oʖ׍5$ 8#]6s`N(ڏt"C%R˒|mi@t@NCodR\,A⵿_v%SJ7H>X[ ,A,8IWk!A.rPSn$: H$I/i8I+8]BG^pA僳i4D&1{he#_ګ7ZF.m18w 2.q k7@rPbwiG^#%rpX7L$F #k֖u$%'gh_eҚ[X4$\rӾ\$H{]β 5epW +FBASp:\ˑ$$8;~ +Ը{EyDFg5~,"JsϪL8yYJ#]1A@cv2ͨ p51N >h8}Y=R?IuļAMi*P)O sDWF DFlM;&go,ND2d5c#Nq΋i{K;̃#﷎7ki0QJ\k;IhkSZEi#B)_6H$을5A*bRet Jg9C I @;єpHD"1O|˨~>F\dPHd'qJP E(A"-O1 43fih&$hn+}O$F >[DV ֖ hLp rs>NA%ۂf_̓''z$ ,zXk4 :Ҭ&2o%ϓX[|S²@sU -d6Vu:D9TZę)qSer28٬<D %n:I 0(K2tLM :7/18FLBH$82[%Փl~$^E p.,^˵6Ĕ>O{'8PY8L] fKK7 uPqL `%Di#85cq fa` + >L@2 'Hp8dxȥ*8HN3DRt<R9XJr2pK$xIpMA9TND.OtĔ/jJb8ܫ/]~er9:/?) Nt\"$FNaJ|ilJ+^6u $A#PpTJX=h+ [ I,8KJp A쌒 3MKQBd,pK Mn8K <䗟 )q9S~ˋHTJ\^ fK r!LJr-96J<MѪʰk/5>}wvp.;陗2K"9Ƞ;]O6frmM); Y[ ^"rX% 2(;p^I52ow`")D"# pJ9HTNF. M9TrPb :" Ap,ϰ5{ķjs&<[c_I9X6kɠ{=9gARFW'Ƀ`)3(1DђkbZ"҄ |6Y .9-t@f-%-Ak/<SҢ> k+A%=pU:D e  e >j*IAM[,_"A<cǔ$xpxA-/8ԕ0PU`o"a?_^~ .H,#o%f>4X Ao˿O y5iJ׾3&DCuKDBzXۍ'NA6򍾙O>8Y :dhѤ`aC,- >. M阒 #OwNJȿnq:sO"9 /4A"~_Q)HdpDzz'rء)t(/}o,tNM81N*8ݸ9Y|paRgm3Af˿p +BSW124ɽ%I"0H44DBٻŽ]G\"фu Kb=(aX2EM9*) I;*GɡLJAGB =)2|m27(ўNBzX[м{[NxXհkk7' G˿$Fq +yqofpos8̡Iitep}b%'Ht>l2#qJ(MiKArpWGwR CG٬ M$28'H43p㉄J*P)ȓtI$8NA"{^G.q\giB#A>(R)7bCyN)1 d~ Y:\\ORyǵYNG7Jhrc*w%FHUHK/} 1DQ=uׁD42)5󟇵8 -x&# 5U| K:DA ɿ&HG=Nr{0oU"$xi$_#.ɿ)A@@;p\͌K$HHNtfE*?{q>_[76TJZai/^;&q{6:$ 1住He`=Pƽ7ƀ}gJܛчR*9sXbѕyJX:, BEޮ J9([[pĬt$a8B"':w cSo/#/c_)1__R$?Rp ߝ+A݁+2x0$%Ɂqti*Ad`W62{9>41d%ZC%IXy(OtpX.$Xr?WA/ _'`[3J"''kP :O!$%\'': +l됡d[L=ɽS 4zɑh,eI,s&:g}!X: 9$Ht`ՖH;e8.\p@ tYSN'/K$ҁ^" frPD^S MHe2F\p9%t U F.gM~9X +XG$^6n:VڬGt80powD.O}@]B/ =Rd%ýr6f=13C&z4rIT#`BdV/|ma%ZNl ΢k8H A9(1Wn+%I)_rdəSPx{Yo3yˉDRfIMd +bAcYu;ѷ 렭(cDDäA_@hȥy2:eK!8<(%Z M8.X[?8s2ȵ~.!G/8WUXi<{8n=]e:DF$1A~YgDrX |Z2[p/ҩK2rW 8sK K824NBR )@y5I(I /hД ^k>2L FbfHv&ȏQ9ݦ:{%>{q`7ED> ~{r$ڗtrti=(%_q<,ǓI{{'Evp0rk!OS" S&&-XǦ٦'-,|P: $2-҉N-DN8=N^9GdP*Q)c<)wHJ$8A"OH'Β{HJ$%}%%W<130F:ĘIN`cyU"1h%,ګT*%ZX@]ۓkg;qoÃqv_)z# xټ73'w*A˲v*%|k̰R/퀭9Ba5ͅ pPG+D/4;$IXFg)ȋi&g")!P)ep$剎 +ə1NcdЙGL"`%K^9xA8Jaެ:a*H3MiF.bP~6V ĖINN,&ȵY[^k/0'7ޥGQ_ș H%I./@'F'8['MN(AfH=P6Ƙ ?<X/('A8̪Xv?N 8:I$ +%]q::K$yA<Ô#t@38;_).Ak\k/\A g'ڝ;1 } :x HKC}%4Xq?kP^kژ28^ %>o_rmLVIBGVJb ANneXyĉ. 3G'8%Hǩ <8P9 %٥ID"/'I <4 HdP\H1IoD%Hp4 1nl0BDb`9l2% $ria,C+٬?P-PJk+å|>U܏8m8n* D_#< Z{˽3r4H adkO\ 24Ml$gjk lO x[,pqh J' Q (;eP: APKsԃԑN| 7. JD_q^aĻI^1PYi)u$$9nhXjin1^KudY;["X1?&J\g >"p\ X2r y ?KOU*]]a@7`IN4AڳJ$7: P?(m|6m neXp88¹Gʉs VBDHX\rl=DeS-ig/x|6٠6 흴JrvV!\rh¥pαS&Tt$8PyOx 2TAD"AwRN #8!_Ԝ&F%yDQG dp!$M*#q|\*M/xjM23O@`/a=kUƕ:I$V9F#ky?Tf=8g0/ROy )HyAf=}1螸O +.- ׆˵U^T#qy bE.m= ;8::ND\ڠ0L@t':x:PJ|q69SRL)A )A0J"8ȉHpOIɃ(APL$%wZBG*# n$HS(Kvbc=0.FbAxyX;x&ȵ7amKp?28v r2(')A@b<$Gro {am_Gbm"<;ڬ(K4$|_@>.zgY a,c(mm Z8pHhp +8֤> `TbV~W' o%He8FRby9AJxrq$G\*W J$D4!:\%4rQ,A6egK(s,yksm,x%D׃çY:/\O#2/)A{gr$ ]fm)%%a0AZ ++2kƕLܲtHgE(gKG&A@Dґ+NG=xЗϥJT<9;D$^2y>:=v(eI KLBJDc#IE$Ll堄5#!7 Hlgog'>Vt pYvĽYެO!:==I뱿ȽoV6U{D^{ +MiO4s 9X>Pw5,o\Z|pHMpѠ鸓yg yM}yRg2;A'wm?(1~X9%“K2xYF/A_pa$@~ l$ [pMrb,5KW[rc؈gL|ֳ\dVumz-5F0NAo\lTiT*\ 2@_e~&Lj>g?: 38?lr#OaH}.O檄4oa)#*e;"ae*vMeȸ6JY?7k9Udԉ}$#x'gY׎ 7JL?13ǵ S'[̐#/?'fmLpMDbOa[pKD($/A ҉7(8-Gn胀ץ b~ M<K/41?/20%'V>HxeM.ëě1%F$K@hJgtϑ7gZ |6X%\2r} ,o͵7TA{JpebyROyg9 W#L6f=43ktyL, 6"y_*ٵKqP %G")Dp֕H$t'P4 D ΙHJ$#~'HdH%Ag8KE )AݡΔD$8LH:8I4pI 2ru V/\$\{OڎpmH$}KXOQPK&A0^QB9T{/eTJ(7~0Д1ؼ4%}Y MA)jb +G4%ܑd2Q/4x/:'ό9P)!C$rD[!)O#Wr"UD2sK&A0SzPe#i hHiޠ9YUrr17ғulK2>{-Z zFs}>`$wމ{e?{3T 'u$[%Hqor DSwm3f`SJSҖ%bFk bC ࠀs.8Jǚ ypA"A@b<)}Nn9D"ci p@G&hʡOR dp.IA[@M9M%?&FjeCkuk&Nj^DXpP)]rupF}5/j_<8$PN{{'gI/s=ٸJi^g"HdfЇup8D:7dpF4D"8-AL ': /)eqygA:#K##Cela\[eXr=(Sn8"堌\")gJ$23q7=R2IWX[@9)BNNkZ#lHۄ>+JXaAN GʠqL#9 D8p#'2DqȠ3N`UÃL"MMN$\*w eeW䉄rA4W J"}AʆS]$F&\Zi;%>Z 흌:X{AS*%{6fF8=3%z*?93~t*̏\YOjĵ1rrmtJ nA|Rp¥2lӫe5Hm,zt8% ӑpJ%9*% 2(%Ht/.I|h<JOLuC:dO"s.(ƘHr, hAʓϳMR)QkcA2;%Q&N y{  ˿)lroA@N=q?=? YkOJBE>{& h٥s 8UBG8q$8NA@%eK$I>DG_57=9TK$d:^T&gj2 Nyq69H4  ' O X_ :H$HׄLb61F$ )kyh+c1A*gakĂ{E4z`(Lo.1o32M^b%ן54I>|E҉-~ 8YXaNJ8 +$p,} {v\WJ$$r%=43H$M] piM;AR&v {d$N{'&C`֜KGt \p83Ah%4~ W +:onK$ 'ggI$3qύ/2A"JDx2JpY9?zcF-#Rq)A`YF:!H^iCyr=͵Y[v4\{* WAPs/xDRF.UH?Y'k޽;\*cڟ KF`őawZ.VUE ; p\ytMtBJ8ii V dJJ__ tPgH8'a.+epr(A~Kc)q +H?Zi G"5򅇯'әLJ%J #K2x4L/ adP‚HXMmrP\{}ݔ&ǽ/I2 ޿?WD#eLdܛ)IS0 `=Hʖe n%F|Ιrp 䠉I4xAD_R D"SrARң÷c|%F~Փ:9x3Dy :R38GDɉ2x;frͿ k|G_'n^{a9dmр(q2< 'ߝ_G"@@G"A9'&*r b vWF n)[CngC m))+љYTc$!c:'I9e^O5=qiPs#&G"ȡRJ ]G"Ề)Ip)_F d&< <]J2B`}6[I3k/,-N\e׹Shr=' xp,#/+fG\s)rRk"Dy% |Xl) ADFg%CiOfN ϙN:e83K8iC#oK^ȡ揉_B<*xbD?HFAy%Fxw|>(A*y蒒 wIaK9(A`z\l$M{ VTab=k&[I)ڻ|8 pͰѱ6N<ޝ_/9F #)q1wbK\c"aKXۭ8f ˰PJsDN?8-:dF"GR )}be%yg9є ~p$%F=HdV* Mxw#M} nHT#19ʚHʹ'K&S0:HCp"9[v`s~|YԱ#ep\{͕q?MGJ *K_egޛ'; )~aކzG٬GIMkn-/c' fhJD|ARK$!kʳDR8\?GKSyq>XeL2TzY IR/Oꀜ9(ь?1ᒱ-HmMvmtBN|;i ՑX{sm49H$HʸaH$pV"';'uD"A7&ʸ7%̼ nAq!!Vzbt 8e8&A*Ot28H8ep/^M|_#?ZY?TDY )q>(xȡK\";eFbP"0?dP@ RipJN$O AL83OJ[d/D+ +MDz=(ڝ`A{ 'IvfhaJ^P)H$%Ƚg@YFDCH$k7 *fѨ6VC ݑFloeO)Ψ:ҡp D HJD'Rm%GRJϧ M 2Tz 9 Eq,ASɃAB C@*%tI .($2xCdJ`3Pl o\~6kyAvHnm6זkvJ {fmY|P _?ѹ$qެ=K~|sm{$>V#4KXi,#œ(z G"FUbzK䡔Hd _~X ^") Gg-D}H Pwuxu)QS"TJGDF#$LR}V| gV>"6f-fN%ȸǽ_  6(;?O8Cĉu,OD"_Ԝ,AtIK$ Ĉ|e. M :"y"sJc0OKWA':43J`OjͿ#3;E>ko\r7З±rmvtn8C% ,/uxqC,AsJ3S@ړV ffm&<=jp_h0.I$J$RptJ$ġ >9P"'u$Hp'##bm=I!y×9v4DY_%"ASGAGPnG"74'Vv⒴Dڮ fmY;y g>(bq'u$/K&AeI֑F0 pxO~Ckb_dXk }XXXd8ptphOLgGF< Z>9I'=\%һH&>.y('12>(c?BVJ$Ryb%ȉ;47e2ypXX;\צi%m ;Xjk F/}I}2`{H_@{k$HD%y扦4D]gB(K$m7VIHtBtuqnKĔķ`<#/%/K?\x&8%8<@( s%xvFJ2m:18A.+9Hp$ps.O MH$o +>+2ii%7֤SkӢ}ĆJJ_ r}}er?_{ D"G^忂^ f`I'WM3נJ<>R¦ʖzv6wҹx WO:WHtGr rRD"A`kFX bAl gsmt9 '$ts MQ\48ƿ>9qIKğ}?NA$8HxnJDb/P[@R7%)1R)At$ȠD6.CӐHД8ZYA?3\+Hp3 i#r %l:m,2iׁ@߻INp8,Q_7<82{O)#מk%ht s C V#U"ʶL}3h}piN6^p )Ag 8e%pHJ$% 'G+A!zb1N>PJt6/N*8P%|pgNʹ<82&RЁTfib%'%H#-QmUggcxn9 ;\RRYމN䤎%?+͵%RbYnk?1 ffmY;HM %,,X}ı9([dkD d8S$A9.K KDp 6I(KJ$V\VNG" ,QS}*K24A@,A@М!5J l#vhC%,,ױ:9@$qcAX[>^d8//xY{kÃ`t&p=%i%g'R)-K(EEb}p#pJ861>8o}'yydI O 8%$PC?) ~-1T"AH JL$2I q~`~qHSY0RIexD b}p:gS>K >,k/8?}I~._hD>>4x@\fL\u-$+PF+`5l}@Ov : O'n\%^R")OtD")1N8?&i\SJpx /Ejp8iDyT")Aj" INrRC$OK)R|qK }neǂ,u{Y)Q:%|oA@N L kL&Y~ p_*nH\uH\kg4Ɠ"]Yݠ +i[R#HH$Stpޞ8c|P߸ +28=:S> SC .H+Q& ') } 2/`~əOt樔ಒu$H}a#w . +cYJ%8>;٬g0rbIBG 7:O\rdp|ڲ6 =r0r<g.K̲X%k5{6eC` >t˝qz8ա/A aJ:D)m1 ~mI} |C#J\ˡR75TzX{{MI3[[ab]1oD -WiaC¦:'C$rP^A rķ8;?zCK׉$O OLO| $5הJ!)ϾT" C +DTo .V{qI"h_$H2- 4򍦬f<\ K(7pO(A@s%t8tSg^R ަZLz[I$oC$ q`=b%UyϝQx_*KM'oykqd=DzG-AYe[)KZ{;FhҔ +8VLx$ A-XYH$S,I K0xXްK6bQڦ+ =*A)> T KeIX'>]*MxX(һ?&zװVJ$& 3x%M0U@-XYꔿZ G$3 W&΃QpK^h;JhzIAnI% F4HW$wl Ax7ߎ- rQF^w _j<B*AtH(6A@\.7:_I%\-u$NJ\4;K/E[C{$gc~VJʥқd9_hރؒ )e_rlO/8'׃WjHX$s(?sj$^/G$^geP/Kh )1uJ&`Y$)?q MD畋fpJ/fief.s_4%Vm}R/ +xH%Vb˷_ }W RIdpxpDr%( \4۰/ot~ %Έ^9(ǞH/fx}ޯ𺕽 QRYf;+$82xJ }./;'oX loJi1amCG"$%Tє䍦 :A@F3_5p6;% :I+ .QGz.Nx7eeUJ(hg~YP.Jip0zuጘo47l” E  p5\p <8!C5z"qI:p%μ;YV%\Л^zIM90C&r4eۡ)AvpHX_?я_U3r\)[XCW[OXd,_MY,@BS F :PF.r%V\̿ w%V'~zT.K,-p$ w*>I|WKx+9 ){ Hʸ3H(Ky_H[:q:g <\q;\:$xe xE ҋhHr4KE bv9/]J(A$KeT~%Ip_HT=TJh)$ KWJr /hež~c]&o4A*Hr͕MDIJ%%|ǻ}PB͞W.y(AJ%H֜HJ3 VB,$Cͥ+ 5X>gg9Y0YS^sޮ3rm8pr8华 +^M6nf.,lPsHJE.J$I |Q:ACA otbM@H+2,87$VRvl|>d;RIJO%%HM w_ث!uV^`A{Ddoy>x1uJ﬎ q}=~M$pF2 V~}! r_AvNh]p9҉Ebf8 ){Jx2 qӨ a7̑Ho n#D"R_K~f?CYN6#v5i#@/\+a[K.\7KJqIdq\ RSRF}"Q B38H2H6=DbY7EO*J' wyfl"a +kԼ3r ߃ (m̃ӔtpuZrk8s&-Xp'990>Hzh ^F7>c` 3MW( +8/ HJqIrh6hZX ˾v'8HpM(At@@HITmXe7pãAJ ѝ: [~/6(7hx)7D{$Xy%VxUȯsMH$1rO3X %_M HO5ILy:7́w7Z[7x+!&䦙t \O0)Ax79țw#%{G"{. bKiaeV C C`xp% M:?3n$}7nQ!X e|9z|JF'u~&|=nnx⭉ޣ_+8skJ=]ƙ_ +RI>$y5yx(3yhHMuo}y8sZkN2*) K, ^W,[~VIJ 0c &77:HK.F=>He ?(nXqՒF.c1 \*%ƺRJlց2O,.SPLWO$Ó.JxH(yM$.{?{q8}qbHAY7\]_u$IϾ#2A:sΟ8ҙDR+A>s?:,^x&z{e .A@X$\|DD.[&AD"X-}C%<*-~V@@,,n%{<8HJJ|@$O$ @=p(QI@b=)A<,|R?Lwj7K +@zkxRF)25xYJ/䋎7]By= p#-2pFp/D"_/䞝 g8ǖ|5M簄N;ޚNN,%ļ}c /.&9w߅+%ț:% 9xp$rU#/= Y=xp$V D ([2,8v a28DwKI! u$F.\rJXv(٦7 DB CB:K(sLJ_ 9rA"/'oQSHz4JDg8þ?7Kx5kzXЏ{pfHS.:'wޝ\>n3;3tH)9:%KK]r RBDFM eMD? 싕C%/JKGdp#G{[+?c$f}O' fQo}H$~=|y7zpҔqM :Igg~\e>^(x˰o+v<\Ie* bҡ  ~,o c%G\ +?.ZE.- KeavMžK72N(C>HtJ_2) AȲʨܬwMY\|2r' rI|5WڂW;gw?33gёwbwV{a(\g&VNUr0Ǚ/{)$E{]H/_pQ{B.g"C n## v,uODe ǔx/-T%c%X7_#$lw28Vw%8H7K $HO!s$K?c"[|;*AH$+g8JJ:Kk^̊ LEfp_*lJ7:#7s g30x^ ++#Pz U5 EfJ58-JF(?ĸ +7>7kcJ . h$ nC^,JN$VKvI'WGpTx'A"5h$$u %V7[@@sޗ^i l7*)Kd1J)%7A f4K[ _nn/D",ķ2D"-erWKK n%IK Y*΃䋲f'D_rpL_\nM )QTEY/L~P|L<|\FU e o43rf}ܸ3b/΋nO)olx! } ) +]dn:po^>\l-f f&&*8 Eis//_>H=5Oo #)=] -e2#a1aKXp$a/lk޹҆_*y8O$H>[ԔQ -{RDKĢITe)ep7=~ę^Fy_{K7!ߌ{#H?'/+۴R89g-: p,8 9g$#Љ^7e Cf`0*AM$&mpXmlI/w'uIy}al ]"u$vmeޚSn7TX']#e|tΠ O* ܗM/amRi%A'otRS.5}Otxg3|E *RM\KgP3yG)_B r?U\\9s"פ ,1I&AlƖo.$߅H )/Ju⻕ X%ፎ([Uڛv!awگhABS)Q)Ayr9g3?4;$ CI@˻lqJD^W"5B)#A|f}>|c W$^k~SARO{N8 mؗ/4AvgpΜ 'y!A ^ۇF_! I.ɩ&2iH/4e&Hp9/oǻ}R\҂ DPZ^ ؂ť7fb߸T~''?#g|fJ)} . ߸dDޥ#K JԗRGOxKO' :%⽆]*yI x\~PtRɱ/'vr5%?=~pr8#p,g8g0L⌜'䍯.-JN@MRS:R*GWcKJ>W* yq<#B@)mE/ek8g~5N״9=>/6+q_Nțk85a@B);?sEAo7(tta-M$YFnHL$H4K$~ P|'2|IA S a}V/t,fpee/a;qN(C<|^#n993^SK\řo.A4C}X%(%t~.olMKJhBY:|%43D/u"q87q8S')U(lѾqg5jqt$*A9μ 8H%Dz 0e,5`C-p =r,A.s/· +=2zXɭ]bk+òHؠw3/\>yϏNp~?oeD7\PArW,)- _y- pfA,?#ϨuD"q$YpTyv4DBSD%tϒg8?J|Ox@ H@_rIH,_: %Q g A"- x[ MF׌yk:~q, FM7:HvD͕H\gW H3 9uN%a|KoŠAn14-\U C847~1A +ގ-K$K/%|*AYoʦR.vPj8l7AP9J"R?r:YOL$Μ3E̥A"sɉJ(atIB N\.n&H|/j3kEr-%M\D3K8*A@@phZJ7`Gkqag:gRyg$I_Wf~!A1!I*yUk&XXA\V~e}& D+}%޲x:ϻZZX(A`Xp5 +{\R.σۤð@DgEr$Km H6A>:B!Gr#?24vMTZkx_4WSy(gD\rQۚH(׼q 24D3#{%V/߆A" py|8zquD093g4Δ.y+_dQZ(p Sb%9K3IxБH5?~\ċ/qMG.]_h}&##(8Y[~XA^M<BGY N)<7e0xi TL 4]9 bEGދ\vsE{;Amuo4e%A\4ZpYioiyKk(avkB}3$r&22޾h$xU̙$LOxLEM bB) +߸__E^F^<-󘭏:o#xKy\^e}Y3* 3D '|mn򙾼ɟ8!ÙWwDA‘G ^+xe{7f .M* . /aC)\h I,%HpDBN@WO'Ht]D3?/?L#K- ͒Dh^d9D=;Y*ŻvKh) %# Yg0p +/\ x @)|u,K8t{"Knc&!8?(M[A0K4epWEۂD+-+Dzv%]"&u  ˥%{Y3)%H:ADI Y>tEBo8{vy^{$tZ23N&[&i~,= eH(Jyqk~S;rr&O藸f (H 1)`R\Ρ2 |f8C65J ٨DSFa S7e뤑^.28XHDuɃD@"J|t*K k* m#V#}ipfCϳt)5egC{:d R[B)AG̉9S Y e|)Ϭ#2A"5Kh 2<S"8%ǺRi8 +Ly?1/ȥR )?(et0p3\%:1x|:y#2eK4aBr bl.j b8cES.h>σeu}g8#g?/yB_lZ4Az#8p^&g~8=g(@_bn $%yolX9*7^8jxG;ciIn bi6~$lb~A6KA-Ix( VC*ªZP "}<ȑHL6u/|ɞ+eRfn.5e|'R V#ΔJ MKnH|33w"g /'^ʙę!M pO' D%Hp_El ϾsHJqμ+ gn 3; ЗH ND~Wj#%ȲODzR/,fy8 2I|/|I|f_=p =2Z䒓E<53/$x=+ehK8Nԑ.$]'ze~?p΋kJ#JǛϼ#^ x`-agi\ 3y1K钁. |9M$>J_  X7XF(:sN]6+24; _3fԇkNT- H$V|=:%O@Зi~3|߂3Z$4$_ b$#2yH3}gg &7%˿]W_y>渧t6rzt6w8+{|kIxMN4H%v&6?q NR:?JB,a‚@Vo6B٣ޅdR Wc\Bynq/ /s(}R:k?8#gg]r7n@}FD S2ҥkPBNeK3SL "2toR_`Y^9MkV|.NC)F4f_h/OE(ANJGuy|gKd2 [yN γS&/v|\y9gp g!5?K+Dp$+VND)Έ\Hdp냀x"dKeř 5pX89yS<8<}{:RgNs(>8Zf4`*l. ^$N#TXyne?"VA@@w@.2|EXłqsXTJkϋ3_& ^_9CcMq1hH134NÌف;N2rD ˖+_G +߇˾Dr%A4g_iq@ZCXБ?lJ٣3*Dc@4\!kn 2: Q r&t O%t["9V4墄:< #oCG>%H\,ggfI ͍{8# yhKT-V4_El6ATJg8U|^g^.k2 = 0xR0H1JX6% shJ4r/4#/&ѳppkX%hZరXD3G٠/^u𚜔JɉJ(ѼfIX8yL ^煙oߣJ|ߥ](q~VBL%S%ґJt ex~|[C$%K}c9I h\u_S@}K%9z+r b@`ܲ:O GbgOq0;Ih.3 lLmę/ q\?x? }s- E7$8 } 5p(2,)ș8Ϩ!gfퟑ5%` l4pΔNTƙ.vÙQ`ā( KsPM@CpM쥩'R/?;?D@*;#фIyKTJ?"x;g~m ;͕OYllKrQ"RY^_?qk8~y){ MtlMAxY*[&}%D.5}Pz]@"wbg~?Rf  TI%;s)#2&7>KeY~qYV53eW\*K|uwgW+xЗ 5,+X'%&,TT/)evy|y?7@D/[@@2ow<Ȧc/9sJߜ║Ho~Yx)KP 'HM~rK$! D2zRS"ãICZHkV,,#Vf| -1vb%T5& 35Agr9_XC>rM_L3r&qɢ\v)ܶND"i#A@HPB)rS\3kpfHWIP}'q%rї\B{?8#]9ωqKo|<Yfcil˝fl(McҸ6s'n'+#JR)_LK_+hԡ_Z i$)-r(?/ll7et;$p/Dr^PSKq.\3b3r sgiY@PFV[r=*7So4Ao{ G=p33܏ ΌTJW%șN D.{# \fgNHyΜE̿h3T~;~K{`NoJ%\TfєKP/p&9͕̎8 H\:#*"93pyxs&;g.;R)QYB5_H(q$oaq gDf b:_(, 8ϋU^ÙufYU7S+↸ F䗿vDb%yA'dVyh\Ӂ#-ὀa܌9Y"1EDr M} A*o(X 3=N +Ȱ> \*$l٥&VllT/+esuN|vQ.g=' zz$z|#hK 7#I6Aꃇ5_LA >~R w^e=K z?UOS~_C;"ߴ:g紐2ጜy^: Y'xQZbmW.fb\*%FpU7>H\FRJJ$S#ʰ\J gJp{5lPJ(A8B28H9lqww&ə?7j3y~ّH /%:8otSKX%kI\bv|I$c :q3(7iOj3pəތ)KAV\ 3MyP%$׏;ϫ2x%[\BN#3'ϋ҃_y^{t`+/#Iyώ). SHMk8NDRz/9t~bpe'\ c6L %s\bruBN$$hp5}SϻXEV5Zmb/B mVጜzFqDyr=89O$3FG~8=gaAR^ϗ'GH37kR5YS,68HWJ 2p={d:2G/B3O. CTbf) bd|%\Ou| bEzLHd8eWRia +6tIy?Tqa̙΃2\=3A!2DO<5᚛[3\y-y<)/]X5#ҧ7\tHy_KUp?}v,5I(Us[Ԕ ٭IA(A\3=DByMsJS JiAS)94H LZ;RiKR)W%_#OqJ gmWz"zo,Ҳ?A}r\u$D=l;S~qy&wdQAp^VB{:gn28Ϋ/ϟkkz$4_PS5 8RySSJ~F.Gf)ϠJ.#2/6e}絉 J9KXI%_9WWp Y 1-YJnݔ#=vԓ7:T#A;ԑ|+}oN< +<)t]i ЄR|7HlJ$wnZ&og2Μ{drRMyO)| - -=M繊#y|As gNp UYTGrͥk8k<Jl_R~Ynp=] +_BFp怕 3\s%p N>/A5Ip|W3i$œi(-ap/$ vbK)PrҟNp$w؄$RBGZĺm~L>Ĵ v3k2l5N9=pO?G"̿+{3%;=G\g3+ k85/qpp "xA_^S^sxy=roRyM)5)q ~8\8SIgΰE+o_*lJpeR3pO™yiJ7mJ|0p>)C3&*F.LU4%Q/G"".et/2\ +^U7=/u=f5Ye (,˻M˶7u!9woXj.XJR3m + +5q}~pIwEƝT:9|(t{8C'gnxԑȥR"ןhZ^rg3rkz2˿)cOoi33܏vGc;Uv :] g[ _;_ b|-' 0xi W˝;opvW0/ud0e R2xUK%x;( 8/KOR5o 3\g܎ip=Wc;J.~eOr搗БQSyןᚅ?9sL™NKl͕2֓5-=~8w&=}p|݃`S"ysF8KK ˞[3oRɖ,pF`N)A g㗓7Fr[NM!oO+HV=5At=ub5b3V>tM Clܙ̾q9r7=Kg:d9?)+ϓLL.qq $JWKX75Gy3rk\Ӽ^\p}\*{8%kyfqq?%=EPY)/Js,Ir噃3c+/$ܢܜlpSC@3J:OdoDR,JcD}q\ U&x䛞HTJhΒ,\4awжA"iG3T(\~p4pΔq8GqO~ҥR){#(Qb@9sY_V/ȵUob>%)HwU\Eaʰ|Gl[c&tU(󋍧O!G$E:PYgKg9*DqId5FT/$Id3QRW$ +(ux9]-C#|J(}H ijO%jեO4yƝ,Xm>iLjKe&j?%t}$)2ܙ|lKp)몌iPpD$3II6,QFF 7LW~Q(Ò/J*gŨqINH06sXb̡%'LϟQQӕбDAH^Q](O.>4|gG/]lէ"uZ8q eruf#R2˔%I+YU9;uRՑ%)Vq5pI#)v%fCLJ% +)"Y=bd})RFN?}|객NF0GAa}%K_6RG1*/%92NƤ׏23G/`+.VmT[]N0JIZ͈)I7V?O[ ڏUb1j՛|ٯ?(e5eVߦz:~ZRjzXXJVmY)՗i˸3$GW!c (5^O&R$"QP|1j"&bʘMJÈ6CXQZ d+pn6t9NIRG!E қRF6޾:CQ|2tRxZkO6~kTHG%KU=21=EKR$ +éZƗ:n[?TWRb\{\}U%/[CV.siuwaw*PR2,7%cTm5?ai+3:(S}lL=(R?.j(~<&tRrOztRrg̦$cwa2^#6P~SC1HzGw\RXO}ꫬV3q #\nj.MZ./?)SGEH˨NV\>IrsIhCWdttNy Kʋm)(eKW'2%c{^|rT}QP|tILG&M+V?|5'CMI0>] sݧO'rdIGNH*aӪ}伭.($s\{朗['1'QP 0db]0]-:4>=OX}d֞Ncaoc؟l2fsƌ +SЙBDʋo0=oVIj SGl.qiuC'/WWչnO=uX[m((ZdX}IGa +u|gT'.ĥHuս,98r$Q>P7E䈖n,eY*a$%9$11,l2$6 +)y:oF2f/.ɰסTVmJGIb$sO6ՅW$GnfD1.W[]֡2jե}oJVKe6Աv7QAr6J*6~~#x3ɷ:ʩz㇪<[5P=VtI8tyBZ{ Hw;љb?wH$&I^7$#/KUC-Q|VH,RWni e +|b0$ +cBɧtM?qj(f4d_ #<YMQ_V/W>vY(1} +V]vau_^9JR"cR؄H?E/ɫHQNVƹ$-ђSWN6Lz$s~u9<2&(NFS6gxrŨH3_*{YƧg.}b>I "%{$/:K (/QNHx,2oBtU;-1䨶#u }T_v(%֡zW~aDA9es&SdLr&ss̞[d5#J 6(OH8B'$Ł9l&qF0:M2OqXp[rFo%>(d +>XRjʪ +JI6Q +le3=''x`輊\f '*W zj/)tV߶Zʨ$:KcS: +.2F^&C4_bLw=B.PmmM%Q}0E0JOIOɱl)((WƤ%,ny&gf:^OFbD٪{ucD!E|z(F%y^W藠n<Jdy& +SC}ظ$1).YWrF+(d1tR$Gs(秊b=wQFdUԶ6LcʿH\x1oGGLFz%uI"Qr$au{2tx9c/%Sr)y.I+Бq~6(Gzn[qIED ՞.o9Ù)CqrfNcYӪ993Q@l{Q.IcFꡇ]I#~#a|X2S,/#,Mm%$QG9aʿqd,(y +K"٣rLKu\Ms]ddK#\?/I0^*C?ٌ (u\2R#aOiΨ>lp`JltR31JX?%j>8ßt.lH%[m(.G$d>dA;Z킟AQ/2'b'_}_L|82SC}u&%I%)-lPPqǢ˳r!%'^36II z'q4Zr4%Q!FJmOP}0JRdؔsU<ד(7IrCƤ'|O?3D%)Cm^yJ7KR&5O)J蜅ߣ ~%Wߓ.R.(LAGAA7󏒒 %ғÈ2xent92JRu<'6}H W~(kw$ʋ%)rI;=KcV}Uq蒌ߜcٜ>Èc2)TIOߐRMK8NY((u<ۧM^gFA,δ$QK<[>#DYkUr +YWmkb1ﷺ+OG}pU}fգBDYRr5$ + =ғ2,%)4%~6տ:2\O.$OY&lԥ%YpNHlHK雍2Vs)>W 3tsd0ce>s OV-WY-kup)%ka$)faDA!%ɫLzSTw#]rXiꎎYHI~I\3f]"׾1] dÔ*_KLa/pmMÈdDyڽXGv +hi#1>};S{f|R䯌"?ON}%#7S;ٓbID,U +ETT JV:{$Йpoh^ث$B FHF%gAyF]&H_܃=g$_#J7~of-iduCIR͘tvQ_\O_zZIHϹ;xu_g8>Oo>2|oW}נ'?=%)#JKR! jOY T/:Y^MG!ELCy' )C}?7d%>܃tl$׬ #)u$$)8l:տ@lEpQ?O9*]ڣ cei$ڧ)OOOwYk4ωB# +Aqɻ_EQfRQ+Q&9(26|ݲc~.uRD^xsTn2II$1% +JЇmOK2E>})Ti)QPYq҆((RG =ǻ"꽒_HOEʘ͙^f[ed$]jQeVъQaU6;9~wO?|f +:k)#㙡"WAJLOd2(f\)տjO@>t1cOtOg8^}F]lI31E:իK}'??UY\J29^d%Kot%tY}I>TSd}u\F+aDIyeN2Jz,1=%Iʙ((߲uᇁheVmmN<{A'dzx|Up81PPH(3kݕ(8Qד"FtIw*Q(6(O5もb:$V]/]H(( /0EAAy9g'UgZoTu(I} Ÿʙ#c۟B~ sQ{ᰲ鑓Tu8i1JHDHd +(K(#c2q˜$E~(O~JArDɥ|# WY2tXkKE(ky%S$nj?BJ2'Q뿝֘1?WlHY}jϥ0I]HuNRj'L]]rݡ.C"'K>} ]2ex^6(O2O]z#,HZ:.3|rڣFb**IR&QB'EW$SQ^MW~W1??%W߳cK҉'u4DGQГ_lgk2^#$0&QB')2l#y6O>2|<6{^%6vZ>VoV2DY}sxL>Y'-t.΂O$|matH +)&/Rb#)IlH9ȳ?gٲ~Sf5_q˓jzr$Qdq짜fpQ^׿B~]3dn@1aũd:!(Z%߲"aD1 ÙR6(f"CgugY&Yn  'RMwډ?GIR>f|6)(2(j7ˢDz2%SF6딓AzL,{tda)͸|9OD~yXN:QB'WI,a#-Sd:Q¥S6I/cd|ڟC,Oof}m`CʟQPy,>|IFlPKԶZM*>2,y|Z՞.l”l\ + +z褸?璂.?R1}ެ4c(C__ꍬ}5_eȘ}u]ׯLqS,ed)Q\RB'ee'&x6mՇm9VMJ5N+KtL(n~ɨDzr.b5j[+CG!%X=&RzAq>[IR$SV).s4rQrz[?9,yHɱDTȝT/2: QuIЇg~QΨt(2lHY]icL_6_JRg"cu]-gD9g&QX}@l1 J'yGKejzFOF$SCIRDyc.o[1'P:OkoVd铜庮/8(ӦZeQ戣T[F$].jIKB5cȰ0|U6x:zs~eO)ipȆڗlPF$)r] %22s0Wc<0ʵ=h =W?=oP98χK)nK8/NϬg?u+cTogB>Hh_:I%%7 u]N 8XPB2_z:sd#30%fT=Y}jϾt c?aJ}`l|z?% )2tNX͘RUt۹ׯ)8^fri6'g)ӇD;>S3#1ғQdtzz/6cT{Z+OM<^ޗĞoƧP V~q/J~-u]Ash)#$)rٵV٤Pv>t\YF6(nӜ(uCr՞V]o_{Yi~zjUF3(ԶZue0I׾?뺮WNⴑ(Gj3s/iDAAqw>}8+((1]!erԣQr4]lPVFڠk3ƫ|mZeu7j(>y]gR$gA }}m96閒i.%z#Z'Iq]b|ZlNO_GzIgs62F\}ZLMժJT +):0#u]_sVDzd8$Spj8Xf/OW&`#Qb1 .Q[Y$FRX}簡zH2&V?yRT)(WAY[FS9X}%r]䕧!Ec̨>N7U..IV߆>lr˧Of2@dDqdCD{j:a^?vZLg)뿅#2YT8;jk>QOVsťHO0%\z)SN6()1c¸$Ulb3#݃% +(q'Edu]NS6I1̌u$.ܖq,SI]Я[8RBKE 8QatS2}I$a|femF_L\CV(lHY{6'7T[z)@d庮yYf K9\/)L!=Ty.Q=VsA,!2+Q>#KT_~dF֡겶:>jI˵$(CoH\ d3fT2lNʵyv&ډےs$4YVЕ$ + +"=JWM_;dKaG%Q(|뺮9pl8q!Rެ.]f%B[~djoVPoF #S^K:/YzT_bs,H$Я=9(('QB_RMMuFmUj gf&q7(%r^'%RӲ~ZժKuzD7ˌaD_u8|8K褬>e\lPr2V]d=QW_oAG tR,IYڪ{څ}1]YzT뺮NsvXV[}UaJd\NlfQ]X戦ZNmU/gTX})֡z̞]ƟzT_;Eۄf.*?ar]!%92G"Æj>3׾Szt%Ie_́;{~ZKQNWi=R}ѝ +,%#(.#ϏKGA!E^u,tIatɗ\bfF>0S׶>Sb59V&DJXYڪ{S3S2l`0N9K뺮\GƜudID9e#W?dQ j'm꒓}j^NJtiZm%} +$;CGf]s̱S0rr LF\[tVu$U[jCPmmnHb~Z$Im =ғl(W?<}oN/L1N>W%~lR-{VȨu橔)o.ru_c=LXSV̘.>KDz>)tMɒ(jcW.E=zM8GOhs6(LMdq뺮P2%p*R="c;1ty%Ttp(Twڪv>%J}xk:Sq + +2YP +Jdy̓ "2u]*R\?:FR^yB,QH(k? +# +)Hj#ԫKQը^J\u/ K(#crQ8 +)g(c0C +{R\ mL!]#u]N@RdLy ې2\_a8fLI_n=rUV/&lbYbvAz_{3$Ok"뺮ɯ)1:jcӕqz'IIC'\"W%)2$_,뺮(ioٟIJzKI =t1ӕ+d#뺮_I˔aIR$J9ӓl$ + +ȘVcYwqƓKu]׿)/WP't%oO(((1=%B)I뺮_I{M$e~6)aDQHI)_r]uK9lQPeDUqJIG$g_[qU.Mzɫ$Gd衣"Q뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮뺮! +endstream +endobj + +6 0 obj + 134207 +endobj + +7 0 obj + << /Type /XObject + /Length 8 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << /XObject << /X1 5 0 R >> >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +272.226562 0.000000 -0.000000 338.357361 119.886719 90.744507 cm +/X1 Do +Q + +endstream +endobj + +8 0 obj + 104 +endobj + +9 0 obj + << /Type /XObject + /Length 10 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 15.562500 15.437378 cm +0.000000 0.000000 0.000000 scn +0.000000 0.000031 m +0.000000 451.298706 l +303.743164 147.476624 l +311.959686 139.257233 325.283234 139.257233 333.499786 147.476624 c +341.716339 155.695953 341.716339 169.021393 333.499786 177.240784 c +29.756639 481.062866 l +480.937866 481.062866 l +480.937866 0.000031 l +0.000000 0.000031 l +h +f +n +Q + +endstream +endobj + +10 0 obj + 420 +endobj + +11 0 obj + << /Type /XObject + /Length 12 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 187.691406 139.141357 cm +0.000000 0.000000 0.000000 scn +112.318077 230.817093 m +114.451881 236.621597 106.507004 239.951080 102.709404 235.070602 c +70.894165 194.178711 37.289165 151.643723 1.607707 108.848343 c +-1.328853 105.327103 -0.157611 101.418564 4.426768 101.506927 c +20.839167 101.823715 57.499165 100.883713 57.172985 100.331924 c +57.264164 100.648712 34.854565 40.328918 24.311525 11.312057 c +22.424946 6.120438 29.099884 2.524002 31.687706 5.647614 c +67.369164 48.713715 102.736664 92.306213 134.258621 130.929871 c +138.408722 136.015289 136.225098 141.255768 130.969559 141.267990 c +115.661659 141.303711 79.236656 141.303711 79.280838 141.150482 c +79.236656 141.303711 103.559158 206.986221 112.318077 230.817093 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 139.141357 cm +0.000000 0.000000 0.000000 scn +112.318077 230.817093 m +114.451881 236.621597 106.507004 239.951080 102.709404 235.070602 c +70.894165 194.178711 37.289165 151.643723 1.607707 108.848343 c +-1.328853 105.327103 -0.157611 101.418564 4.426768 101.506927 c +20.839167 101.823715 57.499165 100.883713 57.172985 100.331924 c +57.264164 100.648712 34.854565 40.328918 24.311525 11.312057 c +22.424946 6.120438 29.099884 2.524002 31.687706 5.647614 c +67.369164 48.713715 102.736664 92.306213 134.258621 130.929871 c +138.408722 136.015289 136.225098 141.255768 130.969559 141.267990 c +115.661659 141.303711 79.236656 141.303711 79.280838 141.150482 c +79.236656 141.303711 103.559158 206.986221 112.318077 230.817093 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 134.211060 cm +0.000000 0.000000 0.000000 scn +112.318077 235.747391 m +108.788986 237.044724 l +108.788910 237.044525 l +112.318077 235.747391 l +h +102.709404 240.000900 m +99.741928 242.309937 l +99.741806 242.309784 l +102.709404 240.000900 l +h +1.607707 113.778641 m +4.495335 111.370483 l +4.495601 111.370804 l +1.607707 113.778641 l +h +4.426768 106.437225 m +4.499225 102.677933 l +4.499327 102.677933 l +4.426768 106.437225 l +h +57.172985 105.262222 m +53.559685 106.302261 l +53.036144 104.483368 53.946251 102.558853 55.684322 101.809479 c +57.422398 101.060104 59.446564 101.719513 60.409737 103.348846 c +57.172985 105.262222 l +h +24.311525 16.242355 m +27.845428 14.958176 l +27.845484 14.958313 l +24.311525 16.242355 l +h +31.687706 10.577911 m +28.792362 12.976776 l +28.792278 12.976685 l +31.687706 10.577911 l +h +134.258621 135.860168 m +137.171616 133.482788 l +137.171692 133.482880 l +134.258621 135.860168 l +h +130.969559 146.198288 m +130.960785 142.438293 l +130.960815 142.438293 l +130.969559 146.198288 l +h +75.668030 145.039062 m +76.243362 143.043762 78.327271 141.892639 80.322571 142.467987 c +82.317871 143.043304 83.468979 145.127228 82.893646 147.122528 c +75.668030 145.039062 l +h +115.847176 234.450058 m +116.701851 236.775009 116.619583 239.131058 115.641846 241.171753 c +114.692940 243.152252 113.039856 244.550491 111.229118 245.332703 c +107.622391 246.890778 102.796585 246.235626 99.741928 242.309937 c +105.676872 237.691849 l +106.419823 238.646637 107.465248 238.766983 108.246910 238.429321 c +108.630371 238.263672 108.800774 238.046219 108.860069 237.922440 c +108.890541 237.858856 109.001205 237.622040 108.788986 237.044724 c +115.847176 234.450058 l +h +99.741806 242.309784 m +67.937111 201.431458 34.361767 158.934479 -1.280186 116.186478 c +4.495601 111.370804 l +40.216564 154.213562 73.851219 196.786575 105.676994 237.692001 c +99.741806 242.309784 l +h +-1.279920 116.186798 m +-3.258407 113.814377 -4.442426 110.586319 -3.346803 107.497406 c +-2.147628 104.116547 1.122458 102.612839 4.499225 102.677933 c +4.354311 110.196533 l +3.731298 110.184525 3.509113 110.314011 3.518988 110.308304 c +3.525570 110.304504 3.570336 110.276199 3.624385 110.211884 c +3.678738 110.147202 3.717865 110.075287 3.740574 110.011261 c +3.787205 109.879791 3.727518 109.904022 3.795444 110.156403 c +3.863918 110.410828 4.051676 110.838486 4.495335 111.370483 c +-1.279920 116.186798 l +h +4.499327 102.677933 m +12.622606 102.834717 25.825699 102.680542 36.987919 102.416489 c +42.562939 102.284607 47.599640 102.125992 51.211418 101.966797 c +53.024452 101.886887 54.443344 101.808319 55.382313 101.735413 c +55.867409 101.697754 56.148544 101.667206 56.272243 101.649170 c +56.369865 101.634933 56.239033 101.647369 56.024120 101.711349 c +55.987492 101.722260 55.526577 101.847763 55.024200 102.194839 c +54.785992 102.359406 54.143162 102.837708 53.738750 103.746353 c +53.220856 104.909973 53.351353 106.186203 53.936230 107.175613 c +60.409737 103.348846 l +60.999710 104.346878 61.130787 105.631775 60.609016 106.804108 c +60.200726 107.721466 59.549557 108.208527 59.298603 108.381912 c +58.770741 108.746582 58.266712 108.889893 58.169785 108.918747 c +57.834274 109.018631 57.514095 109.067612 57.357121 109.090500 c +56.971634 109.146698 56.482571 109.192627 55.964401 109.232849 c +54.896851 109.315735 53.376972 109.398651 51.542557 109.479507 c +47.859436 109.641846 42.766521 109.801895 37.165764 109.934387 c +25.976433 110.199081 12.643328 110.356522 4.354208 110.196533 c +4.499327 102.677933 l +h +60.786282 104.222198 m +60.809654 104.307068 60.897423 104.745392 60.929855 105.103455 c +58.742435 108.680466 54.195972 107.559952 53.865387 107.051224 c +53.814571 106.953873 53.747608 106.813766 53.728859 106.771698 c +53.718666 106.748245 53.702507 106.710175 53.696033 106.694595 c +53.676624 106.647614 53.659908 106.604416 53.655502 106.593063 c +53.640240 106.553726 53.620911 106.502960 53.600471 106.449005 c +53.557949 106.336761 53.495045 106.169464 53.413906 105.952957 c +53.251022 105.518311 53.008900 104.869934 52.696381 104.031525 c +52.071083 102.354004 51.161480 99.908997 50.034843 96.876465 c +47.781475 90.811142 44.658810 82.392624 41.203846 73.057480 c +34.294983 54.390076 26.053322 32.046478 20.777569 17.526382 c +27.845484 14.958313 l +33.112766 29.455093 41.347427 51.779816 48.256329 70.447327 c +51.710243 79.779648 54.831825 88.195236 57.084080 94.257553 c +58.210258 97.288849 59.118813 99.731049 59.742771 101.404984 c +60.054882 102.242294 60.295158 102.885712 60.455673 103.314026 c +60.536228 103.528992 60.595272 103.685959 60.632847 103.785156 c +60.652458 103.836929 60.662716 103.863693 60.666313 103.872971 c +60.671341 103.885925 60.661373 103.859802 60.646000 103.822601 c +60.640533 103.809448 60.625214 103.773361 60.615692 103.751434 c +60.597614 103.710892 60.531147 103.571854 60.480659 103.475113 c +60.150402 102.966995 55.604095 101.846634 53.416664 105.423340 c +53.449085 105.781082 53.536671 106.218628 53.559685 106.302261 c +60.786282 104.222198 l +h +20.777622 17.526535 m +19.123594 12.974869 21.347857 9.066910 24.252548 7.152466 c +26.852655 5.438782 31.562777 4.533432 34.583130 8.179138 c +28.792278 12.976685 l +28.944626 13.160568 29.138533 13.175171 29.096033 13.174240 c +28.995413 13.172043 28.716915 13.216492 28.390881 13.431366 c +28.077776 13.637741 27.886385 13.893036 27.802916 14.105087 c +27.740448 14.263763 27.684525 14.515381 27.845428 14.958176 c +20.777622 17.526535 l +h +34.583050 8.179047 m +70.276260 51.259323 105.658882 94.870407 137.171616 133.482788 c +131.345612 138.237579 l +99.814445 99.602615 64.462067 56.028702 28.792362 12.976776 c +34.583050 8.179047 l +h +137.171692 133.482880 m +139.813660 136.720261 140.928268 140.649261 139.688675 144.177887 c +138.396896 147.855072 134.944427 149.949066 130.978302 149.958282 c +130.960815 142.438293 l +131.647614 142.436707 132.038910 142.268188 132.232162 142.142303 c +132.415222 142.023071 132.526093 141.877991 132.593735 141.685471 c +132.722305 141.319458 132.853668 140.085495 131.345535 138.237488 c +137.171692 133.482880 l +h +130.978333 149.958282 m +123.319649 149.976151 110.381828 149.985077 99.363159 149.970398 c +93.854500 149.963043 88.820534 149.949799 85.164253 149.928741 c +83.337914 149.918243 81.845848 149.905746 80.807587 149.890915 c +80.292084 149.883545 79.869400 149.875366 79.568008 149.865906 c +79.424759 149.861420 79.269722 149.855499 79.134247 149.846558 c +79.081985 149.843109 78.947746 149.834045 78.793877 149.812927 c +78.748192 149.806656 78.519775 149.776611 78.242943 149.696655 c +78.151802 149.670319 77.732826 149.552322 77.269096 149.258408 c +77.008621 149.077545 76.415733 148.516479 76.117638 148.114105 c +75.779449 147.451843 75.526001 145.882538 75.668030 145.039062 c +82.893646 147.122528 l +83.035507 146.279633 82.782158 144.710938 82.444359 144.049255 c +82.146645 143.647491 81.554413 143.087036 81.294876 142.906754 c +80.833023 142.614014 80.417023 142.497208 80.329964 142.472061 c +80.061295 142.394440 79.845428 142.366760 79.816643 142.362793 c +79.744957 142.352966 79.691727 142.348022 79.672020 142.346252 c +79.647842 142.344101 79.632370 142.343079 79.629318 142.342865 c +79.623993 142.342514 79.635231 142.343292 79.669975 142.344788 c +79.702553 142.346176 79.746620 142.347809 79.803734 142.349609 c +80.038528 142.356964 80.408836 142.364441 80.914993 142.371674 c +81.920067 142.386047 83.386047 142.398407 85.207527 142.408875 c +88.846886 142.429810 93.867622 142.443054 99.373184 142.450394 c +110.382942 142.465088 123.311569 142.456146 130.960785 142.438293 c +130.978333 149.958282 l +h +82.893646 147.122528 m +83.006897 146.584534 83.032082 145.826370 83.012100 145.618286 c +82.997322 145.512054 82.967239 145.342422 82.954086 145.279816 c +82.929825 145.169250 82.905975 145.085785 82.899849 145.064331 c +82.890221 145.030609 82.882523 145.005676 82.879196 144.994995 c +82.875359 144.982666 82.872734 144.974579 82.872002 144.972321 c +82.871216 144.969894 82.878197 144.991119 82.898605 145.049866 c +82.937004 145.160400 82.999245 145.335785 83.087128 145.580521 c +83.261513 146.066177 83.522041 146.783676 83.860504 147.710892 c +84.536697 149.563309 85.515999 152.230911 86.721222 155.506134 c +89.131294 162.055588 92.440521 171.023193 96.025543 180.732605 c +103.194412 200.148285 111.466515 222.531342 115.847252 234.450256 c +108.788910 237.044525 l +104.410728 225.132568 96.142120 202.758942 88.971054 183.337341 c +85.386108 173.628113 82.075554 164.656921 79.663872 158.103104 c +78.458214 154.826691 77.476135 152.151535 76.796440 150.289520 c +76.456955 149.359528 76.190903 148.626892 76.009583 148.121918 c +75.919609 147.871368 75.846710 147.666321 75.795128 147.517853 c +75.770538 147.447083 75.743820 147.369186 75.720924 147.298798 c +75.711311 147.269257 75.690361 147.204407 75.668648 147.128326 c +75.659912 147.097717 75.634186 147.007111 75.608788 146.891342 c +75.595062 146.826141 75.564598 146.654419 75.549614 146.546570 c +75.529434 146.336914 75.554611 145.577637 75.668030 145.039062 c +82.893646 147.122528 l +h +f +n +Q + +endstream +endobj + +12 0 obj + 10585 +endobj + +13 0 obj + << /Type /XObject + /Length 14 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 15.562500 15.437378 cm +0.000000 0.000000 0.000000 scn +0.000000 0.000031 m +0.000000 451.298706 l +303.743164 147.476624 l +311.959686 139.257233 325.283234 139.257233 333.499786 147.476624 c +341.716339 155.695953 341.716339 169.021393 333.499786 177.240784 c +29.756639 481.062866 l +480.937866 481.062866 l +480.937866 0.000031 l +0.000000 0.000031 l +h +f +n +Q + +endstream +endobj + +14 0 obj + 420 +endobj + +15 0 obj + << /BBox [ 0.000000 0.000000 512.000000 512.000000 ] + /Resources << /XObject << /X1 11 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 13 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> + /Subtype /Form + /Length 16 0 R + /Group << /Type /Group + /S /Transparency + >> + /Type /XObject + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 177.791992 177.791992 cm +0.000000 0.000000 0.000000 scn +156.416016 0.000000 m +0.000000 156.416016 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 177.791992 157.981934 cm +0.000000 0.000000 0.000000 scn +150.035095 13.429123 m +153.559174 9.905029 159.272858 9.905029 162.796951 13.429123 c +166.321045 16.953217 166.321045 22.666901 162.796951 26.190979 c +150.035095 13.429123 l +h +6.380932 182.607010 m +2.856840 186.131104 -2.856840 186.131104 -6.380932 182.607010 c +-9.905023 179.082916 -9.905023 173.369232 -6.380932 169.845139 c +6.380932 182.607010 l +h +162.796951 26.190979 m +6.380932 182.607010 l +-6.380932 169.845139 l +150.035095 13.429123 l +162.796951 26.190979 l +h +f +n +Q + +endstream +endobj + +16 0 obj + 761 +endobj + +17 0 obj + << /Length 18 0 R + /FunctionType 4 + /Domain [ 0.000000 1.000000 ] + /Range [ 0.000000 1.000000 ] + >> +stream +{ 0 gt { 0 } { 1 } ifelse } +endstream +endobj + +18 0 obj + 27 +endobj + +19 0 obj + << /XObject << /X2 1 0 R + /X1 7 0 R + >> + /ExtGState << /E2 << /SMask << /Type /Mask + /G 9 0 R + /S /Alpha + >> + /Type /ExtGState + >> + /E1 << /SMask << /Type /Mask + /G 15 0 R + /S /Alpha + /TR 17 0 R + >> + /Type /ExtGState + >> + >> + >> +endobj + +20 0 obj + << /Length 21 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q +q +/E2 gs +/X2 Do +Q +q +1.000000 0.000000 -0.000000 1.000000 177.791992 157.981934 cm +1.000000 1.000000 1.000000 scn +150.035095 13.429123 m +153.559174 9.905029 159.272858 9.905029 162.796951 13.429123 c +166.321045 16.953217 166.321045 22.666901 162.796951 26.190979 c +150.035095 13.429123 l +h +6.380932 182.607010 m +2.856840 186.131104 -2.856840 186.131104 -6.380932 182.607010 c +-9.905023 179.082916 -9.905023 173.369232 -6.380932 169.845139 c +6.380932 182.607010 l +h +162.796951 26.190979 m +6.380932 182.607010 l +-6.380932 169.845139 l +150.035095 13.429123 l +162.796951 26.190979 l +h +f +n +Q + +endstream +endobj + +21 0 obj + 632 +endobj + +22 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 512.000000 512.000000 ] + /Resources 19 0 R + /Contents 20 0 R + /Parent 23 0 R + >> +endobj + +23 0 obj + << /Kids [ 22 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +24 0 obj + << /Pages 23 0 R + /Type /Catalog + >> +endobj + +xref +0 25 +0000000000 65535 f +0000000010 00000 n +0000010069 00000 n +0000010092 00000 n +0000084945 00000 n +0000084969 00000 n +0000219409 00000 n +0000219434 00000 n +0000219813 00000 n +0000219835 00000 n +0000220506 00000 n +0000220529 00000 n +0000231366 00000 n +0000231391 00000 n +0000232063 00000 n +0000232086 00000 n +0000233473 00000 n +0000233496 00000 n +0000233673 00000 n +0000233695 00000 n +0000234343 00000 n +0000235033 00000 n +0000235056 00000 n +0000235235 00000 n +0000235311 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 24 0 R + /Size 25 +>> +startxref +235372 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json new file mode 100644 index 0000000000..bd16466e97 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "SubtitleArrow.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf new file mode 100644 index 0000000000..cecbd934c3 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Contact List/SubtitleArrow.imageset/SubtitleArrow.pdf @@ -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 4.664993 10.125000 cm +0.000000 0.478431 1.000000 scn +4.470226 8.989735 m +4.729925 9.249434 4.729925 9.670488 4.470226 9.930187 c +4.210527 10.189886 3.789473 10.189886 3.529774 9.930187 c +4.470226 8.989735 l +h +0.000000 5.459961 m +-0.470226 5.930187 l +-0.729925 5.670488 -0.729925 5.249434 -0.470226 4.989735 c +0.000000 5.459961 l +h +3.529774 0.989735 m +3.789473 0.730036 4.210527 0.730036 4.470226 0.989735 c +4.729925 1.249434 4.729925 1.670488 4.470226 1.930187 c +3.529774 0.989735 l +h +3.529774 9.930187 m +-0.470226 5.930187 l +0.470226 4.989735 l +4.470226 8.989735 l +3.529774 9.930187 l +h +-0.470226 4.989735 m +3.529774 0.989735 l +4.470226 1.930187 l +0.470226 5.930187 l +-0.470226 4.989735 l +h +f +n +Q + +endstream +endobj + +3 0 obj + 767 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 5.329987 9.330078 ] + /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 +0000000857 00000 n +0000000879 00000 n +0000001050 00000 n +0000001124 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1183 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json new file mode 100644 index 0000000000..37accc5389 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "check.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf new file mode 100644 index 0000000000..c4dfcbde1b --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/Apply.imageset/check.pdf @@ -0,0 +1,150 @@ +%PDF-1.7 + +1 0 obj + << /Type /XObject + /Length 2 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 11.669922 11.450928 cm +0.000000 0.000000 0.000000 scn +16.557842 15.662290 m +17.172628 15.260314 17.345146 14.436065 16.943171 13.821279 c +8.443170 0.821279 l +8.223975 0.486040 7.865371 0.267438 7.466962 0.226191 c +7.068553 0.184944 6.672771 0.325444 6.389548 0.608668 c +0.389548 6.608668 l +-0.129849 7.128065 -0.129849 7.970175 0.389548 8.489573 c +0.908945 9.008969 1.751055 9.008969 2.270452 8.489573 c +7.112782 3.647242 l +14.716830 15.276961 l +15.118806 15.891748 15.943055 16.064266 16.557842 15.662290 c +h +f* +n +Q + +endstream +endobj + +2 0 obj + 584 +endobj + +3 0 obj + << /Type /XObject + /Length 4 0 R + /Group << /Type /Group + /S /Transparency + >> + /Subtype /Form + /Resources << >> + /BBox [ 0.000000 0.000000 40.000000 40.000000 ] + >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 0.000000 0.000000 cm +0.000000 0.000000 0.000000 scn +0.000000 20.000000 m +0.000000 31.045694 8.954306 40.000000 20.000000 40.000000 c +20.000000 40.000000 l +31.045694 40.000000 40.000000 31.045694 40.000000 20.000000 c +40.000000 20.000000 l +40.000000 8.954306 31.045694 0.000000 20.000000 0.000000 c +20.000000 0.000000 l +8.954306 0.000000 0.000000 8.954306 0.000000 20.000000 c +0.000000 20.000000 l +h +f +n +Q + +endstream +endobj + +4 0 obj + 472 +endobj + +5 0 obj + << /XObject << /X1 1 0 R >> + /ExtGState << /E1 << /SMask << /Type /Mask + /G 3 0 R + /S /Alpha + >> + /Type /ExtGState + >> >> + >> +endobj + +6 0 obj + << /Length 7 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +/E1 gs +/X1 Do +Q + +endstream +endobj + +7 0 obj + 46 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 40.000000 40.000000 ] + /Resources 5 0 R + /Contents 6 0 R + /Parent 9 0 R + >> +endobj + +9 0 obj + << /Kids [ 8 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +10 0 obj + << /Pages 9 0 R + /Type /Catalog + >> +endobj + +xref +0 11 +0000000000 65535 f +0000000010 00000 n +0000000842 00000 n +0000000864 00000 n +0000001584 00000 n +0000001606 00000 n +0000001904 00000 n +0000002006 00000 n +0000002027 00000 n +0000002200 00000 n +0000002274 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +2334 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 85a01c0c16..be58331f0a 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1828,8 +1828,8 @@ public final class SharedAccountContextImpl: SharedAccountContext { return StickerPackScreen(context: context, updatedPresentationData: updatedPresentationData, mainStickerPack: mainStickerPack, stickerPacks: stickerPacks, loadedStickerPacks: loadedStickerPacks, parentNavigationController: parentNavigationController, sendSticker: sendSticker) } - public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any) -> Void) -> ViewController { - return storyMediaPickerController(context: context, completion: completion) + public func makeMediaPickerScreen(context: AccountContext, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?) -> Void, dismissed: @escaping () -> Void) -> ViewController { + return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed) } public func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController { diff --git a/submodules/TelegramUI/Sources/TelegramRootController.swift b/submodules/TelegramUI/Sources/TelegramRootController.swift index 46737ce2ab..c5e1f9464f 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -26,6 +26,7 @@ import AvatarNode import LocalMediaResources import ShareWithPeersScreen import ImageCompression +import TextFormat private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -196,16 +197,8 @@ public final class TelegramRootController: NavigationController, TelegramRootCon guard let self else { return } - var transitionIn: StoryCameraTransitionIn? - if let cameraItemView = self.rootTabController?.viewForCameraItem() { - transitionIn = StoryCameraTransitionIn( - sourceView: cameraItemView, - sourceRect: cameraItemView.bounds, - sourceCornerRadius: cameraItemView.bounds.height / 2.0 - ) - } - self.openStoryCamera( - transitionIn: transitionIn, + let coordinator = self.openStoryCamera( + transitionIn: nil, transitionOut: { [weak self] finished in guard let self else { return nil @@ -218,18 +211,11 @@ public final class TelegramRootController: NavigationController, TelegramRootCon destinationCornerRadius: transitionView.bounds.height / 2.0 ) } - } else { - if let cameraItemView = self.rootTabController?.viewForCameraItem() { - return StoryCameraTransitionOut( - destinationView: cameraItemView, - destinationRect: cameraItemView.bounds, - destinationCornerRadius: cameraItemView.bounds.height / 2.0 - ) - } } return nil } ) + coordinator?.animateIn() } ) @@ -288,9 +274,10 @@ public final class TelegramRootController: NavigationController, TelegramRootCon presentedLegacyShortcutCamera(context: self.context, saveCapturedMedia: false, saveEditedPhotos: false, mediaGrouping: true, parentController: controller) } - public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) { + @discardableResult + public func openStoryCamera(transitionIn: StoryCameraTransitionIn?, transitionOut: @escaping (Bool) -> StoryCameraTransitionOut?) -> StoryCameraTransitionInCoordinator? { guard let controller = self.viewControllers.last as? ViewController else { - return + return nil } controller.view.endEditing(true) @@ -299,7 +286,6 @@ public final class TelegramRootController: NavigationController, TelegramRootCon var presentImpl: ((ViewController) -> Void)? var returnToCameraImpl: (() -> Void)? var dismissCameraImpl: (() -> Void)? - var hideCameraImpl: (() -> Void)? var showDraftTooltipImpl: (() -> Void)? let cameraController = CameraScreen( context: context, @@ -326,7 +312,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return nil } }, - completion: { result in + completion: { result, resultTransition in let subject: Signal = result |> map { value -> MediaEditorScreen.Subject? in switch value { @@ -342,17 +328,37 @@ public final class TelegramRootController: NavigationController, TelegramRootCon return .draft(draft) } } + + var transitionIn: MediaEditorScreen.TransitionIn? + if let resultTransition, let sourceView = resultTransition.sourceView { + transitionIn = .gallery( + MediaEditorScreen.TransitionIn.GalleryTransitionIn( + sourceView: sourceView, + sourceRect: resultTransition.sourceRect, + sourceImage: resultTransition.sourceImage + ) + ) + } else { + transitionIn = .camera + } + let controller = MediaEditorScreen( context: context, subject: subject, - transitionIn: nil, + transitionIn: transitionIn, transitionOut: { finished in - if finished, let transitionOut = transitionOut(true), let destinationView = transitionOut.destinationView { + if finished, let transitionOut = transitionOut(finished), let destinationView = transitionOut.destinationView { return MediaEditorScreen.TransitionOut( destinationView: destinationView, destinationRect: transitionOut.destinationRect, destinationCornerRadius: transitionOut.destinationCornerRadius ) + } else if !finished, let resultTransition, let (destinationView, destinationRect) = resultTransition.transitionOut() { + return MediaEditorScreen.TransitionOut( + destinationView: destinationView, + destinationRect: destinationRect, + destinationCornerRadius: 0.0 + ) } else { return nil } @@ -367,10 +373,71 @@ public final class TelegramRootController: NavigationController, TelegramRootCon switch mediaResult { case let .image(image, dimensions, caption): if let imageData = compressImageToJPEG(image, quality: 0.6) { - storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.2, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) + switch privacy { + case let .story(storyPrivacy, _): + storyListContext.upload(media: .image(dimensions: dimensions, data: imageData), text: caption?.string ?? "", entities: [], privacy: storyPrivacy) + Queue.mainQueue().after(0.2, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + case let .message(peerIds, timeout): + var randomId: Int64 = 0 + arc4random_buf(&randomId, 8) + let tempFilePath = NSTemporaryDirectory() + "\(randomId).jpg" + let _ = try? imageData.write(to: URL(fileURLWithPath: tempFilePath)) + + var representations: [TelegramMediaImageRepresentation] = [] + let resource = LocalFileReferenceMediaResource(localFilePath: tempFilePath, randomId: randomId) + representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) + + var attributes: [MessageAttribute] = [] + let imageFlags: TelegramMediaImageFlags = [] +// var stickerFiles: [TelegramMediaFile] = [] +// if !stickers.isEmpty { +// for fileReference in stickers { +// stickerFiles.append(fileReference.media) +// } +// } +// if !stickerFiles.isEmpty { +// attributes.append(EmbeddedMediaStickersMessageAttribute(files: stickerFiles)) +// imageFlags.insert(.hasStickers) +// } + + let media = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.LocalImage, id: randomId), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: imageFlags) + if let timeout, timeout > 0 && timeout <= 60 { + attributes.append(AutoremoveTimeoutMessageAttribute(timeout: timeout, countdownBeginTime: nil)) + } + + let text = trimChatInputText(convertMarkdownToAttributes(caption ?? NSAttributedString())) + let entities = generateTextEntities(text.string, enabledTypes: .all, currentEntities: generateChatInputTextEntities(text)) + if !entities.isEmpty { + attributes.append(TextEntitiesMessageAttribute(entities: entities)) + } + var bubbleUpEmojiOrStickersetsById: [Int64: ItemCollectionId] = [:] + text.enumerateAttribute(ChatTextInputAttributes.customEmoji, in: NSRange(location: 0, length: text.length), using: { value, _, _ in + if let value = value as? ChatTextInputTextCustomEmojiAttribute { + if let file = value.file { + if let packId = value.interactivelySelectedFromPackId { + bubbleUpEmojiOrStickersetsById[file.fileId.id] = packId + } + } + } + }) + var bubbleUpEmojiOrStickersets: [ItemCollectionId] = [] + for entity in entities { + if case let .CustomEmoji(_, fileId) = entity.type { + if let packId = bubbleUpEmojiOrStickersetsById[fileId] { + if !bubbleUpEmojiOrStickersets.contains(packId) { + bubbleUpEmojiOrStickersets.append(packId) + } + } + } + } + + let _ = enqueueMessagesToMultiplePeers( + account: self.context.account, + peerIds: peerIds, threadIds: [:], + messages: [.message(text: text.string, attributes: attributes, inlineStickers: [:], mediaReference: .standalone(media: media), replyToMessageId: nil, localGroupingKey: nil, correlationId: nil, bubbleUpEmojiOrStickersets: bubbleUpEmojiOrStickersets)]).start() + } } case let .video(content, _, values, duration, dimensions, caption): let adjustments: VideoMediaResourceAdjustments @@ -388,10 +455,14 @@ public final class TelegramRootController: NavigationController, TelegramRootCon case let .asset(localIdentifier): resource = VideoLibraryMediaResource(localIdentifier: localIdentifier, conversion: .compress(adjustments)) } - storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: privacy) - Queue.mainQueue().after(0.2, { [weak chatListController] in - chatListController?.animateStoryUploadRipple() - }) + if case let .story(storyPrivacy, _) = privacy { + storyListContext.upload(media: .video(dimensions: dimensions, duration: Int(duration), resource: resource), text: caption?.string ?? "", entities: [], privacy: storyPrivacy) + Queue.mainQueue().after(0.2, { [weak chatListController] in + chatListController?.animateStoryUploadRipple() + }) + } else { + + } } } } @@ -400,16 +471,12 @@ public final class TelegramRootController: NavigationController, TelegramRootCon commit() } ) - controller.sourceHint = .camera controller.cancelled = { showDraftTooltip in if showDraftTooltip { showDraftTooltipImpl?() } returnToCameraImpl?() } - controller.onReady = { - hideCameraImpl?() - } presentImpl?(controller) } ) @@ -429,16 +496,28 @@ public final class TelegramRootController: NavigationController, TelegramRootCon cameraController.returnFromEditor() } } - hideCameraImpl = { [weak cameraController] in - if let cameraController { - cameraController.commitTransitionToEditor() - } - } showDraftTooltipImpl = { [weak cameraController] in if let cameraController { cameraController.presentDraftTooltip() } } + return StoryCameraTransitionInCoordinator( + animateIn: { [weak cameraController] in + if let cameraController { + cameraController.updateTransitionProgress(0.0, transition: .immediate) + cameraController.completeWithTransitionProgress(1.0, velocity: 0.0, dismissing: false) + } + }, + updateTransitionProgress: { [weak cameraController] transitionFraction in + if let cameraController { + cameraController.updateTransitionProgress(transitionFraction, transition: .immediate) + } + }, + completeWithTransitionProgressAndVelocity: { [weak cameraController] transitionFraction, velocity in + if let cameraController { + cameraController.completeWithTransitionProgress(transitionFraction, velocity: velocity, dismissing: false) + } + }) } public func openSettings() {