diff --git a/submodules/AccountContext/Sources/AccountContext.swift b/submodules/AccountContext/Sources/AccountContext.swift index 7071f7a3b2..82b8b05825 100644 --- a/submodules/AccountContext/Sources/AccountContext.swift +++ b/submodules/AccountContext/Sources/AccountContext.swift @@ -892,7 +892,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, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController + func makeMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController func makeProxySettingsController(sharedContext: SharedAccountContext, account: UnauthorizedAccount) -> ViewController diff --git a/submodules/AccountContext/Sources/ChatController.swift b/submodules/AccountContext/Sources/ChatController.swift index 41028d8b5f..4767446896 100644 --- a/submodules/AccountContext/Sources/ChatController.swift +++ b/submodules/AccountContext/Sources/ChatController.swift @@ -313,6 +313,9 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { case textMention(EnginePeer.Id) case textUrl(String) case customEmoji(stickerPack: StickerPackReference?, fileId: Int64) + case strikethrough + case underline + case spoiler public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: StringCodingKey.self) @@ -334,6 +337,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { let stickerPack = try container.decodeIfPresent(StickerPackReference.self, forKey: "s") let fileId = try container.decode(Int64.self, forKey: "f") self = .customEmoji(stickerPack: stickerPack, fileId: fileId) + case 6: + self = .strikethrough + case 7: + self = .underline + case 8: + self = .spoiler default: assertionFailure() self = .bold @@ -359,6 +368,12 @@ public enum ChatTextInputStateTextAttributeType: Codable, Equatable { try container.encode(5 as Int32, forKey: "t") try container.encodeIfPresent(stickerPack, forKey: "s") try container.encode(fileId, forKey: "f") + case .strikethrough: + try container.encode(6 as Int32, forKey: "t") + case .underline: + try container.encode(7 as Int32, forKey: "t") + case .spoiler: + try container.encode(8 as Int32, forKey: "t") } } } @@ -426,6 +441,12 @@ public struct ChatTextInputStateText: Codable, Equatable { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .textUrl(value.url), range: range.location ..< (range.location + range.length))) } else if key == ChatTextInputAttributes.customEmoji, let value = value as? ChatTextInputTextCustomEmojiAttribute { parsedAttributes.append(ChatTextInputStateTextAttribute(type: .customEmoji(stickerPack: nil, fileId: value.fileId), range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.strikethrough { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .strikethrough, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.underline { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .underline, range: range.location ..< (range.location + range.length))) + } else if key == ChatTextInputAttributes.spoiler { + parsedAttributes.append(ChatTextInputStateTextAttribute(type: .spoiler, range: range.location ..< (range.location + range.length))) } } }) @@ -464,6 +485,12 @@ public struct ChatTextInputStateText: Codable, Equatable { result.addAttribute(ChatTextInputAttributes.textUrl, value: ChatTextInputTextUrlAttribute(url: url), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) case let .customEmoji(_, fileId): result.addAttribute(ChatTextInputAttributes.customEmoji, value: ChatTextInputTextCustomEmojiAttribute(interactivelySelectedFromPackId: nil, fileId: fileId, file: nil), range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .strikethrough: + result.addAttribute(ChatTextInputAttributes.strikethrough, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .underline: + result.addAttribute(ChatTextInputAttributes.underline, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) + case .spoiler: + result.addAttribute(ChatTextInputAttributes.spoiler, value: true as NSNumber, range: NSRange(location: attribute.range.lowerBound, length: attribute.range.count)) } } return result diff --git a/submodules/AttachmentUI/Sources/AttachmentController.swift b/submodules/AttachmentUI/Sources/AttachmentController.swift index 12fc208a8a..d78ef8bdb4 100644 --- a/submodules/AttachmentUI/Sources/AttachmentController.swift +++ b/submodules/AttachmentUI/Sources/AttachmentController.swift @@ -678,6 +678,7 @@ public class AttachmentController: ViewController { self.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.3, removeOnCompletion: false, completion: { [weak self] _ in let _ = self?.container.dismiss(transition: .immediate, completion: completion) self?.animating = false + self?.layer.removeAllAnimations() }) } else { let positionTransition: ContainedViewLayoutTransition = .animated(duration: 0.25, curve: .easeInOut) @@ -745,12 +746,12 @@ public class AttachmentController: ViewController { let position: CGPoint let positionY = layout.size.height - size.height - insets.bottom - 40.0 if let sourceRect = controller.getSourceRect?() { - position = CGPoint(x: floor(sourceRect.midX - size.width / 2.0), y: min(positionY, sourceRect.minY - size.height)) + position = CGPoint(x: min(layout.size.width - size.width - 28.0, floor(sourceRect.midX - size.width / 2.0)), y: min(positionY, sourceRect.minY - size.height)) } else { position = CGPoint(x: masterWidth - 174.0, y: positionY) } - if controller.isStandalone { + if controller.isStandalone && !controller.forceSourceRect { var containerY = floorToScreenPixels((layout.size.height - size.height) / 2.0) if let inputHeight = layout.inputHeight, inputHeight > 88.0 { containerY = layout.size.height - inputHeight - size.height - 80.0 @@ -933,6 +934,8 @@ public class AttachmentController: ViewController { fatalError("init(coder:) has not been implemented") } + public var forceSourceRect = false + fileprivate var isStandalone: Bool { return self.buttons.contains(.standalone) } diff --git a/submodules/Camera/Sources/Camera.swift b/submodules/Camera/Sources/Camera.swift index 99d22b5afb..9e01b6f13c 100644 --- a/submodules/Camera/Sources/Camera.swift +++ b/submodules/Camera/Sources/Camera.swift @@ -37,12 +37,15 @@ final class CameraDeviceContext { private weak var session: CameraSession? private weak var previewView: CameraSimplePreviewView? + private let exclusive: Bool + let device = CameraDevice() let input = CameraInput() let output = CameraOutput() - init(session: CameraSession) { + init(session: CameraSession, exclusive: Bool) { self.session = session + self.exclusive = exclusive } func configure(position: Camera.Position, previewView: CameraSimplePreviewView?, audio: Bool, photo: Bool, metadata: Bool) { @@ -81,6 +84,9 @@ final class CameraDeviceContext { } private var preferredMaxFrameRate: Double { + if !self.exclusive { + return 30.0 + } switch DeviceModel.current { case .iPhone14ProMax, .iPhone13ProMax: return 60.0 @@ -95,7 +101,7 @@ private final class CameraContext { private let session: CameraSession - private let mainDeviceContext: CameraDeviceContext + private var mainDeviceContext: CameraDeviceContext private var additionalDeviceContext: CameraDeviceContext? private let cameraImageContext = CIContext() @@ -162,7 +168,7 @@ private final class CameraContext { self.simplePreviewView = previewView self.secondaryPreviewView = secondaryPreviewView - self.mainDeviceContext = CameraDeviceContext(session: session) + self.mainDeviceContext = CameraDeviceContext(session: session, exclusive: true) self.configure { self.mainDeviceContext.configure(position: configuration.position, previewView: self.simplePreviewView, audio: configuration.audio, photo: configuration.photo, metadata: configuration.metadata) } @@ -306,9 +312,29 @@ private final class CameraContext { self.modeChange = .dualCamera if enabled { self.configure { - self.additionalDeviceContext = CameraDeviceContext(session: self.session) + self.mainDeviceContext.invalidate() + self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: false) + self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) + + self.additionalDeviceContext = CameraDeviceContext(session: self.session, exclusive: false) self.additionalDeviceContext?.configure(position: .front, previewView: self.secondaryPreviewView, audio: false, photo: true, metadata: false) } + self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in + guard let self else { + return + } + self.previewNode?.enqueue(sampleBuffer) + + let timestamp = CACurrentMediaTime() + if timestamp > self.lastSnapshotTimestamp + 2.5 { + var mirror = false + if #available(iOS 13.0, *) { + mirror = connection.inputPorts.first?.sourceDevicePosition == .front + } + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false) + self.lastSnapshotTimestamp = timestamp + } + } self.additionalDeviceContext?.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in guard let self else { return @@ -325,9 +351,29 @@ private final class CameraContext { } } else { self.configure { + self.mainDeviceContext.invalidate() + self.mainDeviceContext = CameraDeviceContext(session: self.session, exclusive: true) + self.mainDeviceContext.configure(position: .back, previewView: self.simplePreviewView, audio: self.initialConfiguration.audio, photo: self.initialConfiguration.photo, metadata: self.initialConfiguration.metadata) + self.additionalDeviceContext?.invalidate() self.additionalDeviceContext = nil } + self.mainDeviceContext.output.processSampleBuffer = { [weak self] sampleBuffer, pixelBuffer, connection in + guard let self else { + return + } + self.previewNode?.enqueue(sampleBuffer) + + let timestamp = CACurrentMediaTime() + if timestamp > self.lastSnapshotTimestamp + 2.5 { + var mirror = false + if #available(iOS 13.0, *) { + mirror = connection.inputPorts.first?.sourceDevicePosition == .front + } + self.savePreviewSnapshot(pixelBuffer: pixelBuffer, mirror: mirror, additional: false) + self.lastSnapshotTimestamp = timestamp + } + } } self.queue.after(0.5) { @@ -394,11 +440,33 @@ private final class CameraContext { } public func startRecording() -> Signal { - return self.mainDeviceContext.output.startRecording() + if let additionalDeviceContext = self.additionalDeviceContext { + return combineLatest( + self.mainDeviceContext.output.startRecording(), + additionalDeviceContext.output.startRecording() + ) |> map { value, _ in + return value + } + } else { + return self.mainDeviceContext.output.startRecording() + } } - public func stopRecording() -> Signal<(String, UIImage?)?, NoError> { - return self.mainDeviceContext.output.stopRecording() + public func stopRecording() -> Signal { + if let additionalDeviceContext = self.additionalDeviceContext { + return combineLatest( + self.mainDeviceContext.output.stopRecording(), + additionalDeviceContext.output.stopRecording() + ) |> mapToSignal { main, additional in + if case let .finished(mainResult, _, _) = main, case let .finished(additionalResult, _, _) = additional { + return .single(.finished(mainResult, additionalResult, CACurrentMediaTime())) + } else { + return .complete() + } + } + } else { + return self.mainDeviceContext.output.stopRecording() + } } var detectedCodes: Signal<[CameraCode], NoError> { @@ -559,7 +627,7 @@ public final class Camera { } } - public func stopRecording() -> Signal<(String, UIImage?)?, NoError> { + public func stopRecording() -> Signal { return Signal { subscriber in let disposable = MetaDisposable() self.queue.async { diff --git a/submodules/Camera/Sources/CameraOutput.swift b/submodules/Camera/Sources/CameraOutput.swift index 967bdb3246..65e0ed5e4a 100644 --- a/submodules/Camera/Sources/CameraOutput.swift +++ b/submodules/Camera/Sources/CameraOutput.swift @@ -6,6 +6,28 @@ import CoreImage import Vision import VideoToolbox +public enum VideoCaptureResult: Equatable { + case finished((String, UIImage), (String, UIImage)?, Double) + case failed + + public static func == (lhs: VideoCaptureResult, rhs: VideoCaptureResult) -> Bool { + switch lhs { + case .failed: + if case .failed = rhs { + return true + } else { + return false + } + case let .finished(_, _, lhsTime): + if case let .finished(_, _, rhsTime) = rhs, lhsTime == rhsTime { + return true + } else { + return false + } + } + } +} + public struct CameraCode: Equatable { public enum CodeType { case qr @@ -272,7 +294,7 @@ final class CameraOutput: NSObject { } } - private var recordingCompletionPipe = ValuePipe<(String, UIImage?)?>() + private var recordingCompletionPipe = ValuePipe() func startRecording() -> Signal { guard self.videoRecorder == nil else { return .complete() @@ -288,18 +310,16 @@ final class CameraOutput: NSObject { guard let videoSettings = self.videoOutput.recommendedVideoSettings(forVideoCodecType: codecType, assetWriterOutputFileType: .mp4) else { return .complete() } - guard let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) else { - return .complete() - } + let audioSettings = self.audioOutput.recommendedAudioSettingsForAssetWriter(writingTo: .mp4) ?? [:] let outputFileName = NSUUID().uuidString let outputFilePath = NSTemporaryDirectory() + outputFileName + ".mp4" let outputFileURL = URL(fileURLWithPath: outputFilePath) let videoRecorder = VideoRecorder(configuration: VideoRecorder.Configuration(videoSettings: videoSettings, audioSettings: audioSettings), videoTransform: CGAffineTransform(rotationAngle: .pi / 2.0), fileUrl: outputFileURL, completion: { [weak self] result in if case let .success(transitionImage) = result { - self?.recordingCompletionPipe.putNext((outputFilePath, transitionImage)) + self?.recordingCompletionPipe.putNext(.finished((outputFilePath, transitionImage!), nil, CACurrentMediaTime())) } else { - self?.recordingCompletionPipe.putNext(nil) + self?.recordingCompletionPipe.putNext(.failed) } }) @@ -318,7 +338,7 @@ final class CameraOutput: NSObject { } } - func stopRecording() -> Signal<(String, UIImage?)?, NoError> { + func stopRecording() -> Signal { self.videoRecorder?.stop() return self.recordingCompletionPipe.signal() diff --git a/submodules/ChatListUI/Sources/ChatListControllerNode.swift b/submodules/ChatListUI/Sources/ChatListControllerNode.swift index da8ffefc24..c00792058a 100644 --- a/submodules/ChatListUI/Sources/ChatListControllerNode.swift +++ b/submodules/ChatListUI/Sources/ChatListControllerNode.swift @@ -1191,18 +1191,19 @@ public final class ChatListContainerNode: ASDisplayNode, UIGestureRecognizerDele return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range } - let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false - if selectedIndex <= 0 && translation.x > 0.0 { - transitionFraction = 0.0 - - self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width) - } else if translation.x <= 0.0 && cameraIsAlreadyOpened { - self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0) - } - - if cameraIsAlreadyOpened { - transitionFraction = 0.0 - return + if case .compact = layout.metrics.widthClass { + let cameraIsAlreadyOpened = self.controller?.hasStoryCameraTransition ?? false + if selectedIndex <= 0 && translation.x > 0.0 { + transitionFraction = 0.0 + self.controller?.storyCameraPanGestureChanged(transitionFraction: translation.x / layout.size.width) + } else if translation.x <= 0.0 && cameraIsAlreadyOpened { + self.controller?.storyCameraPanGestureChanged(transitionFraction: 0.0) + } + + if cameraIsAlreadyOpened { + transitionFraction = 0.0 + return + } } if selectedIndex >= maxFilterIndex && translation.x < 0.0 { diff --git a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift index 82cdf31ba5..b2cdd7680a 100644 --- a/submodules/DrawingUI/Sources/DrawingEntitiesView.swift +++ b/submodules/DrawingUI/Sources/DrawingEntitiesView.swift @@ -321,7 +321,7 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { if setup { text.referenceDrawingSize = self.size text.width = floor(self.size.width * 0.9) - text.fontSize = 0.3 + text.fontSize = 0.08 text.scale = zoomScale } } @@ -415,9 +415,16 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { let newEntity = entity.duplicate() self.prepareNewEntity(newEntity, setup: false, relativeTo: entity) - guard let view = makeEntityView(context: self.context, entity: entity) else { + guard let view = makeEntityView(context: self.context, entity: newEntity) else { fatalError() } + + if let initialView = self.getView(for: entity.uuid) { + view.onSnapUpdated = initialView.onSnapUpdated + view.onPositionUpdated = initialView.onPositionUpdated + view.onInteractionUpdated = initialView.onInteractionUpdated + } + view.containerView = self view.update() self.addSubview(view) @@ -516,6 +523,12 @@ public final class DrawingEntitiesView: UIView, TGPhotoDrawingEntitiesView { return nil } + public func eachView(_ f: (DrawingEntityView) -> Void) { + for case let view as DrawingEntityView in self.subviews { + f(view) + } + } + public func play() { for case let view as DrawingEntityView in self.subviews { view.play() @@ -700,15 +713,15 @@ public class DrawingEntityView: UIView { return self.bounds } - func play() { + public func play() { } - func pause() { + public func pause() { } - func seek(to timestamp: Double) { + public func seek(to timestamp: Double) { } diff --git a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift index 3bab1ecc44..2e9a513d4a 100644 --- a/submodules/DrawingUI/Sources/DrawingMediaEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingMediaEntity.swift @@ -43,17 +43,17 @@ public final class DrawingMediaEntityView: DrawingEntityView, DrawingEntityMedia } } - override func play() { + public override func play() { self.isVisible = true self.applyVisibility() } - override func pause() { + public override func pause() { self.isVisible = false self.applyVisibility() } - override func seek(to timestamp: Double) { + public override func seek(to timestamp: Double) { self.isVisible = false self.isPlaying = false diff --git a/submodules/DrawingUI/Sources/DrawingScreen.swift b/submodules/DrawingUI/Sources/DrawingScreen.swift index d9f7e95196..74a7ec4011 100644 --- a/submodules/DrawingUI/Sources/DrawingScreen.swift +++ b/submodules/DrawingUI/Sources/DrawingScreen.swift @@ -738,7 +738,7 @@ private final class DrawingScreenComponent: CombinedComponent { areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: true, chatPeerId: context.account.peerId, - hasSearch: false, + hasSearch: true, forceHasPremium: true ) @@ -749,7 +749,7 @@ private final class DrawingScreenComponent: CombinedComponent { stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], chatPeerId: context.account.peerId, - hasSearch: false, + hasSearch: true, hasTrending: true, forceHasPremium: true ) @@ -761,7 +761,7 @@ private final class DrawingScreenComponent: CombinedComponent { stickerNamespaces: [Namespaces.ItemCollection.CloudMaskPacks], stickerOrderedItemListCollectionIds: [], chatPeerId: context.account.peerId, - hasSearch: false, + hasSearch: true, hasTrending: false, forceHasPremium: true ) @@ -1153,8 +1153,14 @@ private final class DrawingScreenComponent: CombinedComponent { controlsAreVisible = false } - let previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778)) + let previewSize: CGSize let previewTopInset: CGFloat = environment.statusBarHeight + 12.0 + if case .regular = environment.metrics.widthClass { + let previewHeight = context.availableSize.height - previewTopInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: context.availableSize.width, height: floorToScreenPixels(context.availableSize.width * 1.77778)) + } let previewBottomInset = context.availableSize.height - previewSize.height - previewTopInset var topInset = environment.safeInsets.top + 31.0 @@ -1646,7 +1652,7 @@ private final class DrawingScreenComponent: CombinedComponent { transition: .immediate ) context.add(fillButton - .position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0)) + .position(CGPoint(x: context.availableSize.width / 2.0 - (hasFlip ? 46.0 : 0.0), y: topInset)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) @@ -1678,7 +1684,7 @@ private final class DrawingScreenComponent: CombinedComponent { transition: .immediate ) context.add(flipButton - .position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: environment.safeInsets.top + 31.0)) + .position(CGPoint(x: context.availableSize.width / 2.0 + (isFilled != nil ? 46.0 : 0.0), y: topInset)) .appear(.default(scale: true)) .disappear(.default(scale: true)) .shadow(component.sourceHint == .storyEditor ? Shadow(color: UIColor(rgb: 0x000000, alpha: 0.35), radius: 2.0, offset: .zero) : nil) @@ -1988,6 +1994,9 @@ private final class DrawingScreenComponent: CombinedComponent { var doneButtonPosition = CGPoint(x: context.availableSize.width - environment.safeInsets.right - doneButton.size.width / 2.0 - 3.0, y: context.availableSize.height - environment.safeInsets.bottom - doneButton.size.height / 2.0 - 2.0 - UIScreenPixel) if component.sourceHint == .storyEditor { doneButtonPosition.x = doneButtonPosition.x - 2.0 + if case .regular = environment.metrics.widthClass { + doneButtonPosition.x -= 20.0 + } doneButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + doneButton.size.height / 2.0) } context.add(doneButton @@ -2105,6 +2114,9 @@ private final class DrawingScreenComponent: CombinedComponent { var backButtonPosition = CGPoint(x: environment.safeInsets.left + backButton.size.width / 2.0 + 3.0, y: context.availableSize.height - environment.safeInsets.bottom - backButton.size.height / 2.0 - 2.0 - UIScreenPixel) if component.sourceHint == .storyEditor { backButtonPosition.x = backButtonPosition.x + 2.0 + if case .regular = environment.metrics.widthClass { + backButtonPosition.x += 20.0 + } backButtonPosition.y = floorToScreenPixels(context.availableSize.height - previewBottomInset + 3.0 + backButton.size.height / 2.0) } context.add(backButton diff --git a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift index 3b1dfec20d..bc710c2ef7 100644 --- a/submodules/DrawingUI/Sources/DrawingStickerEntity.swift +++ b/submodules/DrawingUI/Sources/DrawingStickerEntity.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import AVFoundation import Display import SwiftSignalKit import TelegramCore @@ -21,6 +22,10 @@ final class DrawingStickerEntityView: DrawingEntityView { private let imageNode: TransformImageNode private var animationNode: AnimatedStickerNode? + private var videoPlayer: AVPlayer? + private var videoLayer: AVPlayerLayer? + private var videoImageView: UIImageView? + private var didSetUpAnimationNode = false private let stickerFetchedDisposable = MetaDisposable() private let cachedDisposable = MetaDisposable() @@ -63,12 +68,27 @@ final class DrawingStickerEntityView: DrawingEntityView { } } + private var video: String? { + if case let .video(path, _) = self.stickerEntity.content { + return path + } else { + return nil + } + } + private var dimensions: CGSize { switch self.stickerEntity.content { - case let .file(file): - return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) - case let .image(image): - return image.size + case let .file(file): + return file.dimensions?.cgSize ?? CGSize(width: 512.0, height: 512.0) + case let .image(image): + return image.size + case let .video(_, image): + if let image { + let minSide = min(image.size.width, image.size.height) + return CGSize(width: minSide, height: minSide) + } else { + return CGSize(width: 512.0, height: 512.0) + } } } @@ -119,23 +139,64 @@ final class DrawingStickerEntityView: DrawingEntityView { return context })) self.setNeedsLayout() + } else if case let .video(videoPath, image) = self.stickerEntity.content { + let url = URL(fileURLWithPath: videoPath) + let asset = AVURLAsset(url: url) + let playerItem = AVPlayerItem(asset: asset) + let player = AVPlayer(playerItem: playerItem) + player.automaticallyWaitsToMinimizeStalling = false + let layer = AVPlayerLayer(player: player) + layer.masksToBounds = true + layer.videoGravity = .resizeAspectFill + + self.layer.addSublayer(layer) + + self.videoPlayer = player + self.videoLayer = layer + + let imageView = UIImageView(image: image) + imageView.clipsToBounds = true + imageView.contentMode = .scaleAspectFill + self.addSubview(imageView) + self.videoImageView = imageView } } override func play() { self.isVisible = true self.applyVisibility() + + if let player = self.videoPlayer { + player.play() + + if let videoImageView = self.videoImageView { + self.videoImageView = nil + Queue.mainQueue().after(0.1) { + videoImageView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false, completion: { [weak videoImageView] _ in + videoImageView?.removeFromSuperview() + }) + } + } + } } override func pause() { self.isVisible = false self.applyVisibility() + + if let player = self.videoPlayer { + player.pause() + } } override func seek(to timestamp: Double) { self.isVisible = false self.isPlaying = false self.animationNode?.seekTo(.timestamp(timestamp)) + + if let player = self.videoPlayer { + player.seek(to: CMTime(seconds: timestamp, preferredTimescale: CMTimeScale(60.0)), toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: { _ in }) + } } override func resetToStart() { @@ -184,10 +245,11 @@ final class DrawingStickerEntityView: DrawingEntityView { let boundingSize = CGSize(width: sideSize, height: sideSize) let imageSize = self.dimensions.aspectFitted(boundingSize) + let imageFrame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) self.imageNode.asyncLayout()(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: imageSize, intrinsicInsets: UIEdgeInsets()))() - self.imageNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + self.imageNode.frame = imageFrame if let animationNode = self.animationNode { - animationNode.frame = CGRect(origin: CGPoint(x: floor((size.width - imageSize.width) / 2.0), y: (size.height - imageSize.height) / 2.0), size: imageSize) + animationNode.frame = imageFrame animationNode.updateLayout(size: imageSize) if !self.didApplyVisibility { @@ -195,6 +257,16 @@ final class DrawingStickerEntityView: DrawingEntityView { self.applyVisibility() } } + + if let videoLayer = self.videoLayer { + videoLayer.cornerRadius = imageFrame.width / 2.0 + videoLayer.frame = imageFrame + } + if let videoImageView = self.videoImageView { + videoImageView.layer.cornerRadius = imageFrame.width / 2.0 + videoImageView.frame = imageFrame + } + self.update(animated: false) } } @@ -226,13 +298,19 @@ final class DrawingStickerEntityView: DrawingEntityView { UIView.animate(withDuration: 0.25, animations: { self.imageNode.transform = animationTargetTransform self.animationNode?.transform = animationTargetTransform + self.videoLayer?.transform = animationTargetTransform }, completion: { finished in self.imageNode.transform = staticTransform self.animationNode?.transform = staticTransform + self.videoLayer?.transform = staticTransform }) } else { + CATransaction.begin() + CATransaction.setDisableActions(true) self.imageNode.transform = staticTransform self.animationNode?.transform = staticTransform + self.videoLayer?.transform = staticTransform + CATransaction.commit() } super.update(animated: animated) diff --git a/submodules/DrawingUI/Sources/StickerPickerScreen.swift b/submodules/DrawingUI/Sources/StickerPickerScreen.swift index a35bd6e844..8f203534e0 100644 --- a/submodules/DrawingUI/Sources/StickerPickerScreen.swift +++ b/submodules/DrawingUI/Sources/StickerPickerScreen.swift @@ -35,6 +35,7 @@ public struct StickerPickerInputData: Equatable { private final class StickerSelectionComponent: Component { typealias EnvironmentType = Empty + let context: AccountContext let theme: PresentationTheme let strings: PresentationStrings let deviceMetrics: DeviceMetrics @@ -44,6 +45,7 @@ private final class StickerSelectionComponent: Component { let separatorColor: UIColor init( + context: AccountContext, theme: PresentationTheme, strings: PresentationStrings, deviceMetrics: DeviceMetrics, @@ -52,6 +54,7 @@ private final class StickerSelectionComponent: Component { backgroundColor: UIColor, separatorColor: UIColor ) { + self.context = context self.theme = theme self.strings = strings self.deviceMetrics = deviceMetrics @@ -129,6 +132,7 @@ private final class StickerSelectionComponent: Component { let topPanelHeight: CGFloat = 42.0 + //let context = component.context let keyboardSize = self.keyboardView.update( transition: transition.withUserData(EmojiPagerContentComponent.SynchronousLoadBehavior(isDisabled: true)), component: AnyComponent(EntityKeyboardComponent( @@ -153,7 +157,44 @@ private final class StickerSelectionComponent: Component { switchToTextInput: {}, switchToGifSubject: { _ in }, reorderItems: { _, _ in }, - makeSearchContainerNode: { _ in return nil }, + makeSearchContainerNode: { _ in + return nil + }, +// makeSearchContainerNode: { [weak self, weak controllerInteraction] content in +// guard let self, let controllerInteraction = controllerInteraction else { +// return nil +// } +// +// let mappedMode: ChatMediaInputSearchMode +// switch content { +// case .stickers: +// mappedMode = .sticker +// case .gifs: +// mappedMode = .sticker +// } +// +// let presentationData = context.sharedContext.currentPresentationData.with { $0 } +// let searchContainerNode = PaneSearchContainerNode( +// context: context, +// theme: presentationData.theme, +// strings: presentationData.strings, +// controllerInteraction: controllerInteraction, +// inputNodeInteraction: inputNodeInteraction, +// mode: mappedMode, +// trendingGifsPromise: Promise(nil), +// cancel: { +// }, +// peekBehavior: self.emojiInputInteraction?.peekBehavior +// ) +// searchContainerNode.openGifContextMenu = { [weak self] item, sourceNode, sourceRect, gesture, isSaved in +// guard let self else { +// return +// } +// self.openGifContextMenu(file: item.file, contextResult: item.contextResult, sourceView: sourceNode.view, sourceRect: sourceRect, gesture: gesture, isSaved: isSaved) +// } +// +// return searchContainerNode +// }, contentIdUpdated: { _ in }, deviceMetrics: component.deviceMetrics, hiddenInputHeight: 0.0, @@ -225,6 +266,39 @@ public class StickerPickerScreen: ViewController { fileprivate var temporaryDismiss = false + private struct EmojiSearchResult { + var groups: [EmojiPagerContentComponent.ItemGroup] + var id: AnyHashable + var version: Int + var isPreset: Bool + } + + private struct EmojiSearchState { + var result: EmojiSearchResult? + var isSearching: Bool + + init(result: EmojiSearchResult?, isSearching: Bool) { + self.result = result + self.isSearching = isSearching + } + } + + private let emojiSearchDisposable = MetaDisposable() + private let emojiSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var emojiSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { + didSet { + self.emojiSearchState.set(.single(self.emojiSearchStateValue)) + } + } + + private let stickerSearchDisposable = MetaDisposable() + private let stickerSearchState = Promise(EmojiSearchState(result: nil, isSearching: false)) + private var stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) { + didSet { + self.stickerSearchState.set(.single(self.stickerSearchStateValue)) + } + } + init(context: AccountContext, controller: StickerPickerScreen, theme: PresentationTheme) { self.presentationData = context.sharedContext.currentPresentationData.with { $0 } self.controller = controller @@ -249,8 +323,52 @@ public class StickerPickerScreen: ViewController { self.wrappingView.addSubview(self.containerView) self.containerView.addSubview(self.hostView) - self.contentDisposable.set(controller.inputData.start(next: { [weak self] inputData in + let signal = combineLatest( + queue: Queue.mainQueue(), + controller.inputData, + self.stickerSearchState.get(), + self.emojiSearchState.get() + ) + + + self.contentDisposable.set(signal.start(next: { [weak self] inputData, stickerSearchState, emojiSearchState in if let strongSelf = self { + let presentationData = strongSelf.presentationData + var inputData = inputData + + let emoji = inputData.emoji + if let emojiSearchResult = emojiSearchState.result { + var emptySearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !emojiSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + emptySearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: presentationData.strings.EmojiSearch_SearchEmojiEmptyResult, + iconFile: nil + ) + } + let defaultSearchState: EmojiPagerContentComponent.SearchState = emojiSearchResult.isPreset ? .active : .empty(hasResults: true) + inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emojiSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: emojiSearchResult.id, version: emojiSearchResult.version), emptySearchResults: emptySearchResults, searchState: emojiSearchState.isSearching ? .searching : defaultSearchState) + } else if emojiSearchState.isSearching { + inputData.emoji = emoji.withUpdatedItemGroups(panelItemGroups: emoji.panelItemGroups, contentItemGroups: emoji.contentItemGroups, itemContentUniqueId: emoji.itemContentUniqueId, emptySearchResults: emoji.emptySearchResults, searchState: .searching) + } + + if let stickerSearchResult = stickerSearchState.result { + var stickerSearchResults: EmojiPagerContentComponent.EmptySearchResults? + if !stickerSearchResult.groups.contains(where: { !$0.items.isEmpty || $0.fillWithLoadingPlaceholders }) { + stickerSearchResults = EmojiPagerContentComponent.EmptySearchResults( + text: presentationData.strings.EmojiSearch_SearchStickersEmptyResult, + iconFile: nil + ) + } + if let stickers = inputData.stickers { + let defaultSearchState: EmojiPagerContentComponent.SearchState = stickerSearchResult.isPreset ? .active : .empty(hasResults: true) + inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickerSearchResult.groups, itemContentUniqueId: EmojiPagerContentComponent.ContentId(id: stickerSearchResult.id, version: stickerSearchResult.version), emptySearchResults: stickerSearchResults, searchState: stickerSearchState.isSearching ? .searching : defaultSearchState) + } + } else if stickerSearchState.isSearching { + if let stickers = inputData.stickers { + inputData.stickers = stickers.withUpdatedItemGroups(panelItemGroups: stickers.panelItemGroups, contentItemGroups: stickers.contentItemGroups, itemContentUniqueId: stickers.itemContentUniqueId, emptySearchResults: stickers.emptySearchResults, searchState: .searching) + } + } + strongSelf.updateContent(inputData) } })) @@ -258,6 +376,8 @@ public class StickerPickerScreen: ViewController { deinit { self.contentDisposable.dispose() + self.emojiSearchDisposable.dispose() + self.stickerSearchDisposable.dispose() } func updateContent(_ content: StickerPickerInputData) { @@ -363,9 +483,224 @@ public class StickerPickerScreen: ViewController { navigationController: { [weak self] in return self?.controller?.navigationController as? NavigationController }, - requestUpdate: { _ in + requestUpdate: { [weak self] transition in + guard let strongSelf = self else { + return + } + if !transition.animation.isImmediate, let (layout, navigationHeight) = strongSelf.currentLayout { + strongSelf.containerLayoutUpdated(layout: layout, navigationHeight: navigationHeight, transition: transition) + } }, - updateSearchQuery: { _ in + updateSearchQuery: { [weak self] query in + guard let self, let controller = self.controller else { + return + } + let context = controller.context + + switch query { + case .none: + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + case let .text(rawQuery, languageCode): + let query = rawQuery.trimmingCharacters(in: .whitespacesAndNewlines) + + if query.isEmpty { + self.emojiSearchDisposable.set(nil) + self.emojiSearchState.set(.single(EmojiSearchState(result: nil, isSearching: false))) + } else { + var signal = context.engine.stickers.searchEmojiKeywords(inputLanguageCode: languageCode, query: query, completeMatch: false) + if !languageCode.lowercased().hasPrefix("en") { + signal = signal + |> mapToSignal { keywords in + return .single(keywords) + |> then( + context.engine.stickers.searchEmojiKeywords(inputLanguageCode: "en-US", query: query, completeMatch: query.count < 3) + |> map { englishKeywords in + return keywords + englishKeywords + } + ) + } + } + + let hasPremium: Signal = .single(true) + let resultSignal = combineLatest( + signal, + hasPremium + ) + |> mapToSignal { keywords, hasPremium -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + var allEmoticons: [String: String] = [:] + for keyword in keywords { + for emoticon in keyword.emoticons { + allEmoticons[emoticon] = keyword.keyword + } + } + let remoteSignal: Signal<(items: [TelegramMediaFile], isFinalResult: Bool), NoError> + if hasPremium { + remoteSignal = context.engine.stickers.searchEmoji(emojiString: Array(allEmoticons.keys)) + } else { + remoteSignal = .single(([], true)) + } + return remoteSignal + |> mapToSignal { foundEmoji -> Signal<[EmojiPagerContentComponent.ItemGroup], NoError> in + if foundEmoji.items.isEmpty && !foundEmoji.isFinalResult { + return .complete() + } + var items: [EmojiPagerContentComponent.Item] = [] + + let appendUnicodeEmoji = { + for (_, list) in EmojiPagerContentComponent.staticEmojiMapping { + for emojiString in list { + if allEmoticons[emojiString] != nil { + let item = EmojiPagerContentComponent.Item( + animationData: nil, + content: .staticEmoji(emojiString), + itemFile: nil, + subgroupId: nil, + icon: .none, + tintMode: .none + ) + items.append(item) + } + } + } + } + + if !hasPremium { + appendUnicodeEmoji() + } + + var existingIds = Set() + for itemFile in foundEmoji.items { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + if itemFile.isPremiumEmoji && !hasPremium { + continue + } + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, + subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + if hasPremium { + appendUnicodeEmoji() + } + + return .single([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )]) + } + } + + var version = 0 + self.emojiSearchStateValue.isSearching = true + self.emojiSearchDisposable.set((resultSignal + |> delay(0.15, queue: .mainQueue()) + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result, id: AnyHashable(query), version: version, isPreset: false), isSearching: false) + version += 1 + })) + } + case let .category(value): + let resultSignal = context.engine.stickers.searchEmoji(emojiString: value) + |> mapToSignal { files, isFinalResult -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for itemFile in files { + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + return .single(([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )], isFinalResult)) + } + + var version = 0 + self.emojiSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let self else { + return + } + + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: true, + items: [] + ) + ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + return + } + self.emojiSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + version += 1 + })) + } }, updateScrollingToItemGroup: { [weak self] in self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) @@ -570,7 +905,95 @@ public class StickerPickerScreen: ViewController { }, requestUpdate: { _ in }, - updateSearchQuery: { _ in + updateSearchQuery: { [weak self] query in + guard let strongSelf = self, let controller = strongSelf.controller else { + return + } + let context = controller.context + + switch query { + case .none: + strongSelf.stickerSearchDisposable.set(nil) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) + case .text: + strongSelf.stickerSearchDisposable.set(nil) + strongSelf.stickerSearchStateValue = EmojiSearchState(result: nil, isSearching: false) + case let .category(value): + let resultSignal = context.engine.stickers.searchStickers(query: value, scope: [.installed, .remote]) + |> mapToSignal { files -> Signal<(items: [EmojiPagerContentComponent.ItemGroup], isFinalResult: Bool), NoError> in + var items: [EmojiPagerContentComponent.Item] = [] + + var existingIds = Set() + for item in files.items { + let itemFile = item.file + if existingIds.contains(itemFile.fileId) { + continue + } + existingIds.insert(itemFile.fileId) + let animationData = EntityKeyboardAnimationData(file: itemFile) + let item = EmojiPagerContentComponent.Item( + animationData: animationData, + content: .animation(animationData), + itemFile: itemFile, subgroupId: nil, + icon: .none, + tintMode: animationData.isTemplate ? .primary : .none + ) + items.append(item) + } + + return .single(([EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: false, + items: items + )], files.isFinalResult)) + } + + var version = 0 + strongSelf.stickerSearchDisposable.set((resultSignal + |> deliverOnMainQueue).start(next: { [weak self] result in + guard let strongSelf = self else { + return + } + guard let group = result.items.first else { + return + } + if group.items.isEmpty && !result.isFinalResult { + //strongSelf.stickerSearchStateValue.isSearching = true + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: [ + EmojiPagerContentComponent.ItemGroup( + supergroupId: "search", + groupId: "search", + title: nil, + subtitle: nil, + actionButtonTitle: nil, + isFeatured: false, + isPremiumLocked: false, + isEmbedded: false, + hasClear: false, + collapsedLineCount: nil, + displayPremiumBadges: false, + headerItem: nil, + fillWithLoadingPlaceholders: true, + items: [] + ) + ], id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + return + } + strongSelf.stickerSearchStateValue = EmojiSearchState(result: EmojiSearchResult(groups: result.items, id: AnyHashable(value), version: version, isPreset: true), isSearching: false) + version += 1 + })) + } }, updateScrollingToItemGroup: { [weak self] in self?.update(isExpanded: true, transition: .animated(duration: 0.4, curve: .spring)) @@ -667,6 +1090,9 @@ public class StickerPickerScreen: ViewController { } func containerLayoutUpdated(layout: ContainerViewLayout, navigationHeight: CGFloat, transition: Transition) { + guard let controller = self.controller else { + return + } self.currentLayout = (layout, navigationHeight) self.dim.frame = CGRect(origin: CGPoint(x: 0.0, y: -layout.size.height), size: CGSize(width: layout.size.width, height: layout.size.height * 3.0)) @@ -691,7 +1117,10 @@ public class StickerPickerScreen: ViewController { } transition.setFrame(view: self.wrappingView, frame: CGRect(origin: CGPoint(x: 0.0, y: topInset), size: layout.size), completion: nil) - let modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + var modalProgress = isLandscape ? 0.0 : (1.0 - topInset / self.defaultTopInset) + if self.isDismissing { + modalProgress = 0.0 + } self.controller?.updateModalStyleOverlayTransitionFactor(modalProgress, transition: transition.containedViewLayoutTransition) let clipFrame: CGRect @@ -761,6 +1190,7 @@ public class StickerPickerScreen: ViewController { transition: stickersTransition, component: AnyComponent( StickerSelectionComponent( + context: controller.context, theme: self.theme, strings: self.presentationData.strings, deviceMetrics: layout.deviceMetrics, diff --git a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift index 6a475beccb..6d89ada6a5 100644 --- a/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift +++ b/submodules/LegacyMediaPickerUI/Sources/LegacyPaintStickersContext.swift @@ -146,6 +146,8 @@ private class LegacyPaintStickerEntity: LegacyPaintEntity { case let .image(image): self.file = nil self.imagePromise.set(.single(image)) + case .video: + self.file = nil } } diff --git a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift index bd39727822..723884294e 100644 --- a/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift +++ b/submodules/MediaPickerUI/Sources/MediaPickerScreen.swift @@ -668,6 +668,10 @@ public final class MediaPickerScreen: ViewController, AttachmentContainable { let transaction = MediaPickerGridTransaction(previousList: previousEntries, list: entries, context: controller.context, interaction: interaction, theme: self.presentationData.theme, scrollToItem: scrollToItem) self.enqueueTransaction(transaction) + if !self.didSetReady { + updateLayout = true + } + if updateLayout, let (layout, navigationBarHeight) = self.validLayout { self.containerLayoutUpdated(layout, navigationBarHeight: navigationBarHeight, transition: previousState == nil ? .immediate : .animated(duration: 0.2, curve: .easeInOut)) } @@ -2167,6 +2171,7 @@ public func wallpaperMediaPickerController( public func storyMediaPickerController( context: AccountContext, + getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void ) -> ViewController { @@ -2175,6 +2180,8 @@ public func storyMediaPickerController( let controller = AttachmentController(context: context, updatedPresentationData: updatedPresentationData, chatLocation: nil, buttons: [.standalone], initialButton: .standalone, fromMenu: false, hasTextInput: false, makeEntityInputView: { return nil }) + controller.forceSourceRect = true + controller.getSourceRect = getSourceRect 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 = { controller, result in diff --git a/submodules/TelegramUI/Components/CameraButtonComponent/BUILD b/submodules/TelegramUI/Components/CameraButtonComponent/BUILD new file mode 100644 index 0000000000..6aa84906a4 --- /dev/null +++ b/submodules/TelegramUI/Components/CameraButtonComponent/BUILD @@ -0,0 +1,19 @@ +load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library") + +swift_library( + name = "CameraButtonComponent", + module_name = "CameraButtonComponent", + srcs = glob([ + "Sources/**/*.swift", + ]), + copts = [ + "-warnings-as-errors", + ], + deps = [ + "//submodules/Display", + "//submodules/ComponentFlow", + ], + visibility = [ + "//visibility:public", + ], +) diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift similarity index 85% rename from submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift rename to submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift index 867f42e71e..a2435d4512 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraButton.swift +++ b/submodules/TelegramUI/Components/CameraButtonComponent/Sources/CameraButtonComponent.swift @@ -2,14 +2,14 @@ import Foundation import UIKit import ComponentFlow -final class CameraButton: Component { +public final class CameraButton: Component { let content: AnyComponentWithIdentity let minSize: CGSize? let tag: AnyObject? let isEnabled: Bool let action: () -> Void - init( + public init( content: AnyComponentWithIdentity, minSize: CGSize? = nil, tag: AnyObject? = nil, @@ -23,7 +23,7 @@ final class CameraButton: Component { self.action = action } - func tagged(_ tag: AnyObject) -> CameraButton { + public func tagged(_ tag: AnyObject) -> CameraButton { return CameraButton( content: self.content, minSize: self.minSize, @@ -33,7 +33,7 @@ final class CameraButton: Component { ) } - static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool { + public static func ==(lhs: CameraButton, rhs: CameraButton) -> Bool { if lhs.content != rhs.content { return false } @@ -49,8 +49,8 @@ final class CameraButton: Component { return true } - final class View: UIButton, ComponentTaggedView { - private var contentView: ComponentHostView + public final class View: UIButton, ComponentTaggedView { + public var contentView: ComponentHostView private var component: CameraButton? private var currentIsHighlighted: Bool = false { @@ -74,7 +74,7 @@ final class CameraButton: Component { transition.setScale(view: self, scale: scale) } - override init(frame: CGRect) { + public override init(frame: CGRect) { self.contentView = ComponentHostView() self.contentView.isUserInteractionEnabled = false self.contentView.layer.allowsGroupOpacity = true @@ -104,19 +104,19 @@ final class CameraButton: Component { self.component?.action() } - override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { + public override func beginTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { self.currentIsHighlighted = true return super.beginTracking(touch, with: event) } - override func endTracking(_ touch: UITouch?, with event: UIEvent?) { + public override func endTracking(_ touch: UITouch?, with event: UIEvent?) { self.currentIsHighlighted = false super.endTracking(touch, with: event) } - override func cancelTracking(with event: UIEvent?) { + public override func cancelTracking(with event: UIEvent?) { self.currentIsHighlighted = false super.cancelTracking(with: event) @@ -155,11 +155,11 @@ final class CameraButton: Component { } } - func makeView() -> View { + public func makeView() -> View { return View(frame: CGRect()) } - func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } diff --git a/submodules/TelegramUI/Components/CameraScreen/BUILD b/submodules/TelegramUI/Components/CameraScreen/BUILD index 91519fbd9c..8e9359b72a 100644 --- a/submodules/TelegramUI/Components/CameraScreen/BUILD +++ b/submodules/TelegramUI/Components/CameraScreen/BUILD @@ -73,7 +73,8 @@ swift_library( "//submodules/Components/BundleIconComponent:BundleIconComponent", "//submodules/TooltipUI", "//submodules/TelegramUI/Components/MediaEditor", - "//submodules/Components/MetalImageView:MetalImageView", + "//submodules/Components/MetalImageView", + "//submodules/TelegramUI/Components/CameraButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal b/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal index 8947a1fd22..fb4e2d3d53 100644 --- a/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal +++ b/submodules/TelegramUI/Components/CameraScreen/MetalResources/cameraScreen.metal @@ -37,9 +37,9 @@ float sdfCircle(float2 uv, float2 position, float radius) { return length(uv - position) - radius; } -float map(float2 uv, float4 primaryParameters, float2 secondaryParameters) { - float primary = sdfRoundedRectangle(uv, float2(primaryParameters.y, 0.0), primaryParameters.x, primaryParameters.w); - float secondary = sdfCircle(uv, float2(secondaryParameters.y, 0.0), secondaryParameters.x); +float map(float2 uv, float3 primaryParameters, float2 primaryOffset, float3 secondaryParameters, float2 secondaryOffset) { + float primary = sdfRoundedRectangle(uv, primaryOffset, primaryParameters.x, primaryParameters.z); + float secondary = sdfCircle(uv, secondaryOffset, secondaryParameters.x); float metaballs = 1.0; metaballs = smin(metaballs, primary, BindingDistance); metaballs = smin(metaballs, secondary, BindingDistance); @@ -48,22 +48,32 @@ float map(float2 uv, float4 primaryParameters, float2 secondaryParameters) { fragment half4 cameraBlobFragment(RasterizerData in[[stage_in]], constant uint2 &resolution[[buffer(0)]], - constant float4 &primaryParameters[[buffer(1)]], - constant float2 &secondaryParameters[[buffer(2)]]) + constant float3 &primaryParameters[[buffer(1)]], + constant float2 &primaryOffset[[buffer(2)]], + constant float3 &secondaryParameters[[buffer(3)]], + constant float2 &secondaryOffset[[buffer(4)]]) { float2 R = float2(resolution.x, resolution.y); - float2 uv = (2.0 * in.position.xy - R.xy) / R.y; + + float2 uv; + float offset; + if (R.x > R.y) { + uv = (2.0 * in.position.xy - R.xy) / R.y; + offset = uv.x; + } else { + uv = (2.0 * in.position.xy - R.xy) / R.x; + offset = uv.y; + } float t = AARadius / resolution.y; - float cAlpha = 1.0 - primaryParameters.z; + float cAlpha = 1.0 - primaryParameters.y; float bound = primaryParameters.x + 0.05; - if (abs(uv.x) > bound) { - cAlpha = mix(0.0, 1.0, min(1.0, (abs(uv.x) - bound) * 2.4)); - + if (abs(offset) > bound) { + cAlpha = mix(0.0, 1.0, min(1.0, (abs(offset) - bound) * 2.4)); } - float c = smoothstep(t, -t, map(uv, primaryParameters, secondaryParameters)); + float c = smoothstep(t, -t, map(uv, primaryParameters, primaryOffset, secondaryParameters, secondaryOffset)); return half4(c, max(cAlpha, 0.231), max(cAlpha, 0.188), c); } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift index 1c6a1a4fdd..424ea07c81 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CameraScreen.swift @@ -18,6 +18,7 @@ import LottieAnimationComponent import TooltipUI import MediaEditor import BundleIconComponent +import CameraButtonComponent let videoRedColor = UIColor(rgb: 0xff3b30) @@ -86,6 +87,8 @@ private final class CameraScreenComponent: CombinedComponent { let camera: Camera let updateState: ActionSlot let hasAppeared: Bool + let panelWidth: CGFloat + let flipAnimationAction: ActionSlot let present: (ViewController) -> Void let push: (ViewController) -> Void let completion: ActionSlot> @@ -95,6 +98,8 @@ private final class CameraScreenComponent: CombinedComponent { camera: Camera, updateState: ActionSlot, hasAppeared: Bool, + panelWidth: CGFloat, + flipAnimationAction: ActionSlot, present: @escaping (ViewController) -> Void, push: @escaping (ViewController) -> Void, completion: ActionSlot> @@ -103,6 +108,8 @@ private final class CameraScreenComponent: CombinedComponent { self.camera = camera self.updateState = updateState self.hasAppeared = hasAppeared + self.panelWidth = panelWidth + self.flipAnimationAction = flipAnimationAction self.present = present self.push = push self.completion = completion @@ -115,6 +122,9 @@ private final class CameraScreenComponent: CombinedComponent { if lhs.hasAppeared != rhs.hasAppeared { return false } + if lhs.panelWidth != rhs.panelWidth { + return false + } return true } @@ -186,7 +196,7 @@ private final class CameraScreenComponent: CombinedComponent { } }) - Queue.mainQueue().async { + Queue.concurrentDefaultQueue().async { self.setupRecentAssetSubscription() } } @@ -229,9 +239,18 @@ private final class CameraScreenComponent: CombinedComponent { self.hapticFeedback.impact(.light) } - func togglePosition() { + private var lastFlipTimestamp: Double? + func togglePosition(_ action: ActionSlot) { + let currentTimestamp = CACurrentMediaTime() + if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 { + return + } + self.lastFlipTimestamp = currentTimestamp + self.camera.togglePosition() self.hapticFeedback.impact(.light) + + action.invoke(Void()) } func toggleDualCamera() { @@ -256,7 +275,7 @@ private final class CameraScreenComponent: CombinedComponent { case .began: return .single(.pendingImage) case let .finished(mainImage, additionalImage, _): - return .single(.image(mainImage, additionalImage)) + return .single(.image(mainImage, additionalImage, .bottomRight)) case .failed: return .complete() } @@ -282,9 +301,9 @@ private final class CameraScreenComponent: CombinedComponent { func stopVideoRecording() { self.cameraState = self.cameraState.updatedRecording(.none).updatedDuration(0.0) self.resultDisposable.set((self.camera.stopRecording() - |> deliverOnMainQueue).start(next: { [weak self] pathAndTransitionImage in - if let self, let (path, transitionImage) = pathAndTransitionImage { - self.completion.invoke(.single(.video(path, transitionImage, PixelDimensions(width: 1080, height: 1920)))) + |> deliverOnMainQueue).start(next: { [weak self] result in + if let self, case let .finished(mainResult, additionalResult, _) = result { + self.completion.invoke(.single(.video(mainResult.0, mainResult.1, additionalResult?.0, additionalResult?.1, PixelDimensions(width: 1080, height: 1920), .bottomRight))) } })) self.isTransitioning = true @@ -316,15 +335,13 @@ private final class CameraScreenComponent: CombinedComponent { let zoomControl = Child(ZoomComponent.self) let flashButton = Child(CameraButton.self) let flipButton = Child(CameraButton.self) -// let dualButton = Child(CameraButton.self) + let dualButton = Child(CameraButton.self) let modeControl = Child(ModeComponent.self) let hintLabel = Child(HintLabelComponent.self) let timeBackground = Child(RoundedRectangle.self) let timeLabel = Child(MultilineTextComponent.self) - - let flipAnimationAction = ActionSlot() - + return { context in let environment = context.environment[ViewControllerComponentContainer.Environment.self].value let component = context.component @@ -339,6 +356,9 @@ private final class CameraScreenComponent: CombinedComponent { isTablet = false } + let smallPanelWidth = min(component.panelWidth, 88.0) + let panelWidth = min(component.panelWidth, 185.0) + let topControlInset: CGFloat = 20.0 if case .none = state.cameraState.recording, !state.isTransitioning { let cancelButton = cancelButton.update( @@ -363,7 +383,7 @@ private final class CameraScreenComponent: CombinedComponent { transition: .immediate ) context.add(cancelButton - .position(CGPoint(x: topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0)) + .position(CGPoint(x: isTablet ? smallPanelWidth / 2.0 : topControlInset + cancelButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + cancelButton.size.height / 2.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) @@ -423,36 +443,36 @@ private final class CameraScreenComponent: CombinedComponent { transition: .immediate ) context.add(flashButton - .position(CGPoint(x: availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0)) + .position(CGPoint(x: isTablet ? availableSize.width - smallPanelWidth / 2.0 : availableSize.width - topControlInset - flashButton.size.width / 2.0, y: environment.safeInsets.top + topControlInset + flashButton.size.height / 2.0)) .appear(.default(scale: true)) .disappear(.default(scale: true)) ) -// if #available(iOS 13.0, *) { -// let dualButton = dualButton.update( -// component: CameraButton( -// content: AnyComponentWithIdentity( -// id: "dual", -// component: AnyComponent( -// DualIconComponent(isSelected: state.cameraState.isDualCamEnabled) -// ) -// ), -// action: { [weak state] in -// guard let state else { -// return -// } -// state.toggleDualCamera() -// } -// ).tagged(dualButtonTag), -// availableSize: CGSize(width: 40.0, height: 40.0), -// transition: .immediate -// ) -// context.add(dualButton -// .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0)) -// .appear(.default(scale: true)) -// .disappear(.default(scale: true)) -// ) -// } + if #available(iOS 13.0, *), !isTablet && !"".isEmpty { + let dualButton = dualButton.update( + component: CameraButton( + content: AnyComponentWithIdentity( + id: "dual", + component: AnyComponent( + DualIconComponent(isSelected: state.cameraState.isDualCamEnabled) + ) + ), + action: { [weak state] in + guard let state else { + return + } + state.toggleDualCamera() + } + ).tagged(dualButtonTag), + availableSize: CGSize(width: 40.0, height: 40.0), + transition: .immediate + ) + context.add(dualButton + .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + topControlInset + dualButton.size.height / 2.0)) + .appear(.default(scale: true)) + .disappear(.default(scale: true)) + ) + } } if case .holding = state.cameraState.recording { @@ -494,9 +514,17 @@ private final class CameraScreenComponent: CombinedComponent { } } + let flipAnimationAction = component.flipAnimationAction + let captureControlsAvailableSize: CGSize + if isTablet { + captureControlsAvailableSize = CGSize(width: panelWidth, height: availableSize.height) + } else { + captureControlsAvailableSize = availableSize + } let captureControls = captureControls.update( component: CaptureControlsComponent( isTablet: isTablet, + hasAppeared: component.hasAppeared, shutterState: shutterState, lastGalleryAsset: state.lastGalleryAsset, tag: captureControlsTag, @@ -537,7 +565,7 @@ private final class CameraScreenComponent: CombinedComponent { guard let state else { return } - state.togglePosition() + state.togglePosition(flipAnimationAction) }, galleryTapped: { guard let controller = environment.controller() as? CameraScreen else { @@ -550,45 +578,50 @@ private final class CameraScreenComponent: CombinedComponent { }, zoomUpdated: { fraction in state.updateZoom(fraction: fraction) - } + }, + flipAnimationAction: flipAnimationAction ), - availableSize: availableSize, + availableSize: captureControlsAvailableSize, transition: context.transition ) + + let captureControlsPosition: CGPoint + if isTablet { + captureControlsPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0) + } else { + captureControlsPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0) + } context.add(captureControls - .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)) + .position(captureControlsPosition) ) if isTablet { let flipButton = flipButton.update( - component: CameraButton( + component: CameraButton( content: AnyComponentWithIdentity( id: "flip", component: AnyComponent( - FlipButtonContentComponent(action: flipAnimationAction) + FlipButtonContentComponent( + action: flipAnimationAction, + maskFrame: .zero + ) ) ), minSize: CGSize(width: 44.0, height: 44.0), action: { -// let currentTimestamp = CACurrentMediaTime() -// if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 { -// return -// } -// self.lastFlipTimestamp = currentTimestamp - state.togglePosition() - flipAnimationAction.invoke(Void()) + state.togglePosition(flipAnimationAction) } ), availableSize: availableSize, transition: context.transition ) context.add(flipButton - .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - captureControls.size.height / 2.0 - environment.safeInsets.bottom - 5.0)) + .position(CGPoint(x: smallPanelWidth / 2.0, y: availableSize.height / 2.0)) ) } var isVideoRecording = false - if case .video = state.cameraState.mode { + if case .video = state.cameraState.mode, isTablet { isVideoRecording = true } else if state.cameraState.recording != .none { isVideoRecording = true @@ -607,6 +640,13 @@ private final class CameraScreenComponent: CombinedComponent { transition: context.transition ) + let timePosition: CGPoint + if isTablet { + timePosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 - 97.0) + } else { + timePosition = CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0) + } + if state.cameraState.recording != .none { let timeBackground = timeBackground.update( component: RoundedRectangle(color: videoRedColor, cornerRadius: 4.0), @@ -614,19 +654,19 @@ private final class CameraScreenComponent: CombinedComponent { transition: context.transition ) context.add(timeBackground - .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) + .position(timePosition) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) } context.add(timeLabel - .position(CGPoint(x: availableSize.width / 2.0, y: environment.safeInsets.top + 40.0)) + .position(timePosition) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) - if case .holding = state.cameraState.recording { + if case .holding = state.cameraState.recording, !isTablet { let hintText: String? switch state.swipeHint { case .none: @@ -656,8 +696,15 @@ private final class CameraScreenComponent: CombinedComponent { } if case .none = state.cameraState.recording, !state.isTransitioning { + let availableModeControlSize: CGSize + if isTablet { + availableModeControlSize = CGSize(width: panelWidth, height: 120.0) + } else { + availableModeControlSize = availableSize + } let modeControl = modeControl.update( component: ModeComponent( + isTablet: isTablet, availableModes: [.photo, .video], currentMode: state.cameraState.mode, updatedMode: { [weak state] mode in @@ -667,12 +714,18 @@ private final class CameraScreenComponent: CombinedComponent { }, tag: modeControlTag ), - availableSize: availableSize, + availableSize: availableModeControlSize, transition: context.transition ) + let modeControlPosition: CGPoint + if isTablet { + modeControlPosition = CGPoint(x: availableSize.width - panelWidth / 2.0, y: availableSize.height / 2.0 + modeControl.size.height + 26.0) + } else { + modeControlPosition = CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0) + } context.add(modeControl .clipsToBounds(true) - .position(CGPoint(x: availableSize.width / 2.0, y: availableSize.height - environment.safeInsets.bottom + modeControl.size.height / 2.0)) + .position(modeControlPosition) .appear(.default(alpha: true)) .disappear(.default(alpha: true)) ) @@ -734,12 +787,30 @@ public class CameraScreen: ViewController { case instantVideo } + public enum PIPPosition { + case topLeft + case topRight + case bottomLeft + case bottomRight + } + public enum Result { case pendingImage - case image(UIImage, UIImage?) - case video(String, UIImage?, PixelDimensions) + case image(UIImage, UIImage?, CameraScreen.PIPPosition) + case video(String, UIImage?, String?, UIImage?, PixelDimensions, CameraScreen.PIPPosition) case asset(PHAsset) case draft(MediaEditorDraft) + + func withPIPPosition(_ position: CameraScreen.PIPPosition) -> Result { + switch self { + case let .image(mainImage, additionalImage, _): + return .image(mainImage, additionalImage, position) + case let .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, _): + return .video(mainPath, mainImage, additionalPath, additionalImage, dimensions, position) + default: + return self + } + } } public final class TransitionIn { @@ -846,6 +917,10 @@ public class CameraScreen: ViewController { fileprivate var previewBlurPromise = ValuePromise(false) + private let flipAnimationAction = ActionSlot() + + private var pipPosition: PIPPosition = .bottomRight + init(controller: CameraScreen) { self.controller = controller self.context = controller.context @@ -994,9 +1069,13 @@ public class CameraScreen: ViewController { self.completion.connect { [weak self] result in if let self { + let pipPosition = self.pipPosition self.animateOutToEditor() self.controller?.completion( result + |> map { result in + return result.withPIPPosition(pipPosition) + } |> beforeNext { [weak self] value in guard let self else { return @@ -1070,6 +1149,9 @@ public class CameraScreen: ViewController { let tapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(self.handleTap(_:))) self.effectivePreviewView.addGestureRecognizer(tapGestureRecognizer) + let pipPanGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePipPan(_:))) + self.additionalPreviewView?.addGestureRecognizer(pipPanGestureRecognizer) + self.camera.focus(at: CGPoint(x: 0.5, y: 0.5), autoFocus: true) self.camera.startCapture() } @@ -1128,7 +1210,30 @@ public class CameraScreen: ViewController { self.camera.focus(at: point, autoFocus: false) } + private var pipTranslation: CGPoint? + @objc private func handlePipPan(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let layout = self.validLayout else { + return + } + let translation = gestureRecognizer.translation(in: self.view) + let location = gestureRecognizer.location(in: self.view) + let velocity = gestureRecognizer.velocity(in: self.view) + + switch gestureRecognizer.state { + case .began, .changed: + self.pipTranslation = translation + self.containerLayoutUpdated(layout: layout, transition: .immediate) + case .ended, .cancelled: + self.pipTranslation = nil + self.pipPosition = pipPositionForLocation(layout: layout, position: location, velocity: velocity) + self.containerLayoutUpdated(layout: layout, transition: .spring(duration: 0.4)) + default: + break + } + } + func animateIn() { + self.transitionDimView.alpha = 0.0 self.backgroundView.alpha = 0.0 UIView.animate(withDuration: 0.4, animations: { self.backgroundView.alpha = 1.0 @@ -1185,8 +1290,10 @@ public class CameraScreen: ViewController { view.layer.animatePosition(from: view.center, to: destinationLocalFrame.center, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring) view.layer.animateScale(from: 1.0, to: targetScale, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false) } + } else { + completion() } - + self.componentHost.view?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false) self.previewContainerView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.35, removeOnCompletion: false) } @@ -1288,7 +1395,7 @@ public class CameraScreen: ViewController { } func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { - guard let layout = self.validLayout else { + guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { return } @@ -1337,7 +1444,11 @@ public class CameraScreen: ViewController { override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { - return self.effectivePreviewView + if let additionalPreviewView = self.additionalPreviewView, additionalPreviewView.bounds.contains(self.view.convert(point, to: additionalPreviewView)) { + return additionalPreviewView + } else { + return self.effectivePreviewView + } } return result } @@ -1379,6 +1490,19 @@ public class CameraScreen: ViewController { let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 let bottomInset = layout.size.height - previewSize.height - topInset + let panelWidth: CGFloat + let previewFrame: CGRect + let viewfinderFrame: CGRect + if isTablet { + previewFrame = CGRect(origin: .zero, size: layout.size) + viewfinderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: 0.0), size: previewSize) + panelWidth = viewfinderFrame.minX + } else { + previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) + viewfinderFrame = previewFrame + panelWidth = 0.0 + } + let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: 0.0, @@ -1408,7 +1532,7 @@ public class CameraScreen: ViewController { self.hasAppeared = hasAppeared transition = transition.withUserData(CameraScreenTransition.finishedAnimateIn) - self.presentDualCameraTooltip() +// self.presentDualCameraTooltip() } let componentSize = self.componentHost.update( @@ -1419,6 +1543,8 @@ public class CameraScreen: ViewController { camera: self.camera, updateState: self.updateState, hasAppeared: self.hasAppeared, + panelWidth: panelWidth, + flipAnimationAction: self.flipAnimationAction, present: { [weak self] c in self?.controller?.present(c, in: .window(.root)) }, @@ -1452,16 +1578,6 @@ public class CameraScreen: ViewController { transition.setFrame(view: self.transitionDimView, frame: CGRect(origin: .zero, size: layout.size)) - let previewFrame: CGRect - let viewfinderFrame: CGRect - if isTablet { - previewFrame = CGRect(origin: .zero, size: layout.size) - viewfinderFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: 0.0), size: previewSize) - } else { - previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset), size: previewSize) - viewfinderFrame = previewFrame - } - transition.setFrame(view: self.previewContainerView, frame: previewFrame) self.currentPreviewView.layer.cornerRadius = 0.0 transition.setFrame(view: self.currentPreviewView, frame: CGRect(origin: .zero, size: previewFrame.size)) @@ -1470,7 +1586,36 @@ public class CameraScreen: ViewController { if let additionalPreviewView = self.currentAdditionalPreviewView { additionalPreviewView.layer.cornerRadius = 80.0 - let additionalPreviewFrame = CGRect(origin: CGPoint(x: previewFrame.width - 160.0 - 10.0 + (self.isDualCamEnabled ? 0.0 : 180.0), y: previewFrame.height - 160.0 - 81.0), size: CGSize(width: 160.0, height: 160.0)) + + var origin: CGPoint + switch self.pipPosition { + case .topLeft: + origin = CGPoint(x: 10.0, y: 110.0) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: -180.0, dy: 0.0) + } + case .topRight: + origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: 110.0) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: 180.0, dy: 0.0) + } + case .bottomLeft: + origin = CGPoint(x: 10.0, y: previewFrame.height - 160.0 - 110.0) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: -180.0, dy: 0.0) + } + case .bottomRight: + origin = CGPoint(x: previewFrame.width - 160.0 - 10.0, y: previewFrame.height - 160.0 - 110.0) + if !self.isDualCamEnabled { + origin = origin.offsetBy(dx: 180.0, dy: 0.0) + } + } + + if let pipTranslation = self.pipTranslation { + origin = origin.offsetBy(dx: pipTranslation.x, dy: pipTranslation.y) + } + + let additionalPreviewFrame = CGRect(origin: origin, size: CGSize(width: 160.0, height: 160.0)) transition.setPosition(view: additionalPreviewView, position: additionalPreviewFrame.center) transition.setBounds(view: additionalPreviewView, bounds: CGRect(origin: .zero, size: additionalPreviewFrame.size)) @@ -1500,6 +1645,10 @@ public class CameraScreen: ViewController { 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))) + + if isTablet && isFirstTime { + self.animateIn() + } } } @@ -1563,6 +1712,10 @@ public class CameraScreen: ViewController { self.navigationPresentation = .flatModal self.requestAudioSession() + + if #available(iOS 13.0, *) { + try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(true) + } } required public init(coder: NSCoder) { @@ -1571,6 +1724,9 @@ public class CameraScreen: ViewController { deinit { self.audioSessionDisposable?.dispose() + if #available(iOS 13.0, *) { + try? AVAudioSession.sharedInstance().setAllowHapticsAndSystemSoundsDuringRecording(false) + } } override public func loadDisplayNode() { @@ -1611,7 +1767,17 @@ public class CameraScreen: ViewController { if let current = self.galleryController { controller = current } else { - controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in + controller = self.context.sharedContext.makeMediaPickerScreen(context: self.context, getSourceRect: { [weak self] in + if let self { + if let galleryButton = self.node.componentHost.findTaggedView(tag: galleryButtonTag) { + return galleryButton.convert(galleryButton.bounds, to: self.view).offsetBy(dx: 0.0, dy: -15.0) + } else { + return .zero + } + } else { + return .zero + } + }, completion: { [weak self] result, transitionView, transitionRect, transitionImage, transitionOut, dismissed in if let self { stopCameraCapture() @@ -1665,15 +1831,21 @@ public class CameraScreen: ViewController { self.node.camera.stopCapture(invalidate: true) self.isDismissed = true if animated { - self.statusBar.updateStatusBarStyle(.Ignore, animated: true) - if !interactive { - if let navigationController = self.navigationController as? NavigationController { - navigationController.updateRootContainerTransitionOffset(self.node.frame.width, transition: .immediate) + if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + self.node.animateOut(completion: { + self.dismiss(animated: false) + }) + } else { + self.statusBar.updateStatusBarStyle(.Ignore, animated: true) + 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) + }) } - self.updateTransitionProgress(0.0, transition: .animated(duration: 0.4, curve: .spring), completion: { [weak self] in - self?.dismiss(animated: false) - }) } else { self.dismiss(animated: false) } @@ -1694,6 +1866,9 @@ public class CameraScreen: ViewController { } public func updateTransitionProgress(_ transitionFraction: CGFloat, transition: ContainedViewLayoutTransition, completion: @escaping () -> Void = {}) { + if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + return + } let offsetX = floorToScreenPixels((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)) @@ -1713,6 +1888,9 @@ public class CameraScreen: ViewController { } public func completeWithTransitionProgress(_ transitionFraction: CGFloat, velocity: CGFloat, dismissing: Bool) { + if let layout = self.validLayout, case .regular = layout.metrics.widthClass { + return + } if dismissing { if transitionFraction < 0.7 || velocity < -1000.0 { self.statusBar.updateStatusBarStyle(.Ignore, animated: true) @@ -1842,3 +2020,109 @@ private final class DualIconComponent: Component { return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) } } + +private func pipPositionForLocation(layout: ContainerViewLayout, position: CGPoint, velocity: CGPoint) -> CameraScreen.PIPPosition { + var layoutInsets = layout.insets(options: [.input]) + layoutInsets.bottom += 48.0 + var result = CGPoint() + if position.x < layout.size.width / 2.0 { + result.x = 0.0 + } else { + result.x = 1.0 + } + if position.y < layoutInsets.top + (layout.size.height - layoutInsets.bottom - layoutInsets.top) / 2.0 { + result.y = 0.0 + } else { + result.y = 1.0 + } + + let currentPosition = result + + let angleEpsilon: CGFloat = 30.0 + var shouldHide = false + + if (velocity.x * velocity.x + velocity.y * velocity.y) >= 500.0 * 500.0 { + let x = velocity.x + let y = velocity.y + + var angle = atan2(y, x) * 180.0 / CGFloat.pi * -1.0 + if angle < 0.0 { + angle += 360.0 + } + + if currentPosition.x.isZero && currentPosition.y.isZero { + if ((angle > 0 && angle < 90 - angleEpsilon) || angle > 360 - angleEpsilon) { + result.x = 1.0 + result.y = 0.0 + } else if (angle > 180 + angleEpsilon && angle < 270 + angleEpsilon) { + result.x = 0.0 + result.y = 1.0 + } else if (angle > 270 + angleEpsilon && angle < 360 - angleEpsilon) { + result.x = 1.0 + result.y = 1.0 + } else { + shouldHide = true + } + } else if !currentPosition.x.isZero && currentPosition.y.isZero { + if (angle > 90 + angleEpsilon && angle < 180 + angleEpsilon) { + result.x = 0.0 + result.y = 0.0 + } + else if (angle > 270 - angleEpsilon && angle < 360 - angleEpsilon) { + result.x = 1.0 + result.y = 1.0 + } + else if (angle > 180 + angleEpsilon && angle < 270 - angleEpsilon) { + result.x = 0.0 + result.y = 1.0 + } + else { + shouldHide = true + } + } else if currentPosition.x.isZero && !currentPosition.y.isZero { + if (angle > 90 - angleEpsilon && angle < 180 - angleEpsilon) { + result.x = 0.0 + result.y = 0.0 + } + else if (angle < angleEpsilon || angle > 270 + angleEpsilon) { + result.x = 1.0 + result.y = 1.0 + } + else if (angle > angleEpsilon && angle < 90 - angleEpsilon) { + result.x = 1.0 + result.y = 0.0 + } + else if (!shouldHide) { + shouldHide = true + } + } else if !currentPosition.x.isZero && !currentPosition.y.isZero { + if (angle > angleEpsilon && angle < 90 + angleEpsilon) { + result.x = 1.0 + result.y = 0.0 + } + else if (angle > 180 - angleEpsilon && angle < 270 - angleEpsilon) { + result.x = 0.0 + result.y = 1.0 + } + else if (angle > 90 + angleEpsilon && angle < 180 - angleEpsilon) { + result.x = 0.0 + result.y = 0.0 + } + else if (!shouldHide) { + shouldHide = true + } + } + } + + var position: CameraScreen.PIPPosition = .bottomRight + if result.x == 0.0 && result.y == 0.0 { + position = .topLeft + } else if result.x == 1.0 && result.y == 0.0 { + position = .topRight + } else if result.x == 0.0 && result.y == 1.0 { + position = .bottomLeft + } else if result.x == 1.0 && result.y == 1.0 { + position = .bottomRight + } + return position +} diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift index e2efca1bfa..7927d31ae7 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/CaptureControlsComponent.swift @@ -5,6 +5,7 @@ import ComponentFlow import SwiftSignalKit import Photos import LocalMediaResources +import CameraButtonComponent enum ShutterButtonState: Equatable { case generic @@ -27,24 +28,39 @@ private extension SimpleShapeLayer { } private final class ShutterButtonContentComponent: Component { + let isTablet: Bool + let hasAppeared: Bool let shutterState: ShutterButtonState let blobState: ShutterBlobView.BlobState let highlightedAction: ActionSlot - let updateOffset: ActionSlot<(CGFloat, Transition)> + let updateOffsetX: ActionSlot<(CGFloat, Transition)> + let updateOffsetY: ActionSlot<(CGFloat, Transition)> init( + isTablet: Bool, + hasAppeared: Bool, shutterState: ShutterButtonState, blobState: ShutterBlobView.BlobState, highlightedAction: ActionSlot, - updateOffset: ActionSlot<(CGFloat, Transition)> + updateOffsetX: ActionSlot<(CGFloat, Transition)>, + updateOffsetY: ActionSlot<(CGFloat, Transition)> ) { + self.isTablet = isTablet + self.hasAppeared = hasAppeared self.shutterState = shutterState self.blobState = blobState self.highlightedAction = highlightedAction - self.updateOffset = updateOffset + self.updateOffsetX = updateOffsetX + self.updateOffsetY = updateOffsetY } static func ==(lhs: ShutterButtonContentComponent, rhs: ShutterButtonContentComponent) -> Bool { + if lhs.isTablet != rhs.isTablet { + return false + } + if lhs.hasAppeared != rhs.hasAppeared { + return false + } if lhs.shutterState != rhs.shutterState { return false } @@ -58,22 +74,20 @@ private final class ShutterButtonContentComponent: Component { private var component: ShutterButtonContentComponent? private let ringLayer = SimpleShapeLayer() - var blobView: ShutterBlobView! - //private let innerLayer = SimpleLayer() + var blobView: ShutterBlobView? + private let innerLayer = SimpleShapeLayer() private let progressLayer = SimpleShapeLayer() init() { super.init(frame: CGRect()) - - self.blobView = ShutterBlobView(test: false) - + self.layer.allowsGroupOpacity = true self.progressLayer.strokeEnd = 0.0 + self.layer.addSublayer(self.innerLayer) self.layer.addSublayer(self.ringLayer) self.layer.addSublayer(self.progressLayer) - self.addSubview(self.blobView) } required init?(coder aDecoder: NSCoder) { @@ -81,42 +95,69 @@ private final class ShutterButtonContentComponent: Component { } func updateIsHighlighted(_ isHighlighted: Bool) { + guard let blobView = self.blobView else { + return + } let scale: CGFloat = isHighlighted ? 0.8 : 1.0 let transition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) - transition.setTransform(view: self.blobView, transform: CATransform3DMakeScale(scale, scale, 1.0)) + transition.setTransform(view: blobView, transform: CATransform3DMakeScale(scale, scale, 1.0)) } func update(component: ShutterButtonContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { self.component = component + if component.hasAppeared && self.blobView == nil { + self.blobView = ShutterBlobView(test: false) + self.addSubview(self.blobView!) + + Queue.mainQueue().after(0.1) { + self.innerLayer.removeFromSuperlayer() + } + } + component.highlightedAction.connect { [weak self] highlighted in self?.updateIsHighlighted(highlighted) } - component.updateOffset.connect { [weak self] offset, transition in - if let self { - self.blobView.updateSecondaryOffset(offset, transition: transition) + func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { + let bandedOffset = offset - bandingStart + let range: CGFloat = 60.0 + let coefficient: CGFloat = 0.1 + return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range + } + + component.updateOffsetX.connect { [weak self] offset, transition in + if let self, let blobView = self.blobView { + blobView.updateSecondaryOffsetX(offset, transition: transition) if abs(offset) < 60.0 { - func rubberBandingOffset(offset: CGFloat, bandingStart: CGFloat) -> CGFloat { - let bandedOffset = offset - bandingStart - let range: CGFloat = 60.0 - let coefficient: CGFloat = 0.1 - return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range - } var bandedOffset = rubberBandingOffset(offset: abs(offset), bandingStart: 0.0) if offset < 0.0 { bandedOffset *= -1.0 } - self.blobView.updatePrimaryOffset(bandedOffset, transition: transition) + blobView.updatePrimaryOffsetX(bandedOffset, transition: transition) } else { - self.blobView.updatePrimaryOffset(0.0, transition: .spring(duration: 0.2)) + blobView.updatePrimaryOffsetX(0.0, transition: .spring(duration: 0.2)) + } + } + } + + component.updateOffsetY.connect { [weak self] offset, transition in + if let self, let blobView = self.blobView { + blobView.updateSecondaryOffsetY(offset, transition: transition) + if abs(offset) < 60.0 { + var bandedOffset = rubberBandingOffset(offset: abs(offset), bandingStart: 0.0) + if offset < 0.0 { + bandedOffset *= -1.0 + } + blobView.updatePrimaryOffsetY(bandedOffset, transition: transition) + } else { + blobView.updatePrimaryOffsetY(0.0, transition: .spring(duration: 0.2)) } } } let innerColor: UIColor let innerSize: CGSize - let innerCornerRadius: CGFloat let ringSize: CGSize let ringWidth: CGFloat = 3.0 var recordingProgress: Float? @@ -124,28 +165,23 @@ private final class ShutterButtonContentComponent: Component { case .generic: innerColor = .white innerSize = CGSize(width: 60.0, height: 60.0) - innerCornerRadius = innerSize.height / 2.0 ringSize = CGSize(width: 68.0, height: 68.0) case .video: innerColor = videoRedColor innerSize = CGSize(width: 60.0, height: 60.0) - innerCornerRadius = innerSize.height / 2.0 ringSize = CGSize(width: 68.0, height: 68.0) case .stopRecording: innerColor = videoRedColor innerSize = CGSize(width: 26.0, height: 26.0) - innerCornerRadius = 9.0 ringSize = CGSize(width: 68.0, height: 68.0) case let .holdRecording(progress): innerColor = videoRedColor innerSize = CGSize(width: 60.0, height: 60.0) - innerCornerRadius = innerSize.height / 2.0 ringSize = CGSize(width: 92.0, height: 92.0) recordingProgress = progress case .transition: innerColor = videoRedColor innerSize = CGSize(width: 60.0, height: 60.0) - innerCornerRadius = innerSize.height / 2.0 ringSize = CGSize(width: 68.0, height: 68.0) recordingProgress = 0.0 } @@ -166,13 +202,20 @@ private final class ShutterButtonContentComponent: Component { self.ringLayer.bounds = CGRect(origin: .zero, size: maximumShutterSize) self.ringLayer.position = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0) - self.blobView.updateState(component.blobState, transition: transition) - self.blobView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: maximumShutterSize.height)) - self.blobView.center = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0) + if let blobView = self.blobView { + blobView.updateState(component.blobState, transition: transition) + if component.isTablet { + blobView.bounds = CGRect(origin: .zero, size: CGSize(width: maximumShutterSize.width, height: 440.0)) + } else { + blobView.bounds = CGRect(origin: .zero, size: CGSize(width: availableSize.width, height: maximumShutterSize.height)) + } + blobView.center = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0) + } - let _ = innerColor - let _ = innerSize - let _ = innerCornerRadius + self.innerLayer.backgroundColor = innerColor.cgColor + self.innerLayer.cornerRadius = innerSize.width / 2.0 + self.innerLayer.bounds = CGRect(origin: .zero, size: innerSize) + self.innerLayer.position = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0) self.progressLayer.bounds = CGRect(origin: .zero, size: maximumShutterSize) self.progressLayer.position = CGPoint(x: maximumShutterSize.width / 2.0, y: maximumShutterSize.height / 2.0) @@ -202,13 +245,15 @@ private final class ShutterButtonContentComponent: Component { final class FlipButtonContentComponent: Component { private let action: ActionSlot + private let maskFrame: CGRect - init(action: ActionSlot) { + init(action: ActionSlot, maskFrame: CGRect) { self.action = action + self.maskFrame = maskFrame } static func ==(lhs: FlipButtonContentComponent, rhs: FlipButtonContentComponent) -> Bool { - return true + return lhs.maskFrame == rhs.maskFrame } final class View: UIView { @@ -216,12 +261,27 @@ final class FlipButtonContentComponent: Component { private let icon = SimpleLayer() + let maskContainerView = UIView() + private let maskLayer = SimpleLayer() + private let darkIcon = SimpleLayer() + init() { super.init(frame: CGRect()) self.layer.addSublayer(self.icon) + self.maskContainerView.isUserInteractionEnabled = false + self.maskContainerView.clipsToBounds = true + + self.maskContainerView.layer.addSublayer(self.maskLayer) + self.maskLayer.addSublayer(self.darkIcon) + + self.maskLayer.masksToBounds = true + self.maskLayer.cornerRadius = 16.0 + self.icon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.cgImage + self.darkIcon.contents = UIImage(bundleImageName: "Camera/FlipIcon")?.cgImage + self.darkIcon.layerTintColor = UIColor.black.cgColor } required init?(coder aDecoder: NSCoder) { @@ -241,6 +301,19 @@ final class FlipButtonContentComponent: Component { animation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps) } self.icon.add(animation, forKey: "transform.rotation.z") + + let darkAnimation = CASpringAnimation(keyPath: "transform.rotation.z") + darkAnimation.fromValue = 0.0 as NSNumber + darkAnimation.toValue = CGFloat.pi as NSNumber + darkAnimation.mass = 5.0 + darkAnimation.stiffness = 900.0 + darkAnimation.damping = 90.0 + darkAnimation.duration = darkAnimation.settlingDuration + if #available(iOS 15.0, *) { + let maxFps = Float(UIScreen.main.maximumFramesPerSecond) + darkAnimation.preferredFrameRateRange = CAFrameRateRange(minimum: 30.0, maximum: maxFps, preferred: maxFps) + } + self.darkIcon.add(darkAnimation, forKey: "transform.rotation.z") } func update(component: FlipButtonContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { @@ -255,6 +328,83 @@ final class FlipButtonContentComponent: Component { self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) self.icon.bounds = CGRect(origin: .zero, size: size) + transition.setFrame(layer: self.maskLayer, frame: component.maskFrame) + + self.darkIcon.bounds = CGRect(origin: .zero, size: size) + + transition.setPosition(layer: self.darkIcon, position: CGPoint(x: -component.maskFrame.minX + size.width / 2.0, y: -component.maskFrame.minY + size.height / 2.0)) + + return size + } + } + + func makeView() -> View { + return View() + } + + func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, transition: transition) + } +} + +final class LockContentComponent: Component { + private let maskFrame: CGRect + + init(maskFrame: CGRect) { + self.maskFrame = maskFrame + } + + static func ==(lhs: LockContentComponent, rhs: LockContentComponent) -> Bool { + return lhs.maskFrame == rhs.maskFrame + } + + final class View: UIView { + private var component: LockContentComponent? + + private let icon = SimpleLayer() + + let maskContainerView = UIView() + private let maskLayer = SimpleLayer() + private let darkIcon = SimpleLayer() + + init() { + super.init(frame: CGRect()) + + self.layer.addSublayer(self.icon) + + self.maskContainerView.isUserInteractionEnabled = false + self.maskContainerView.clipsToBounds = true + + self.maskContainerView.bounds = CGRect(origin: .zero, size: CGSize(width: 30.0, height: 30.0)) + self.maskContainerView.layer.addSublayer(self.maskLayer) + self.maskLayer.addSublayer(self.darkIcon) + + self.maskLayer.masksToBounds = true + self.maskLayer.cornerRadius = 24.0 + + self.icon.contents = UIImage(bundleImageName: "Camera/LockIcon")?.cgImage + self.darkIcon.contents = UIImage(bundleImageName: "Camera/LockedIcon")?.cgImage + self.darkIcon.layerTintColor = UIColor.black.cgColor + } + + required init?(coder aDecoder: NSCoder) { + preconditionFailure() + } + + func update(component: LockContentComponent, availableSize: CGSize, transition: Transition) -> CGSize { + self.component = component + + let size = CGSize(width: 30.0, height: 30.0) + + self.icon.position = CGPoint(x: size.width / 2.0, y: size.height / 2.0) + self.icon.bounds = CGRect(origin: .zero, size: size) + + transition.setFrame(layer: self.maskLayer, frame: component.maskFrame) + + self.darkIcon.bounds = CGRect(origin: .zero, size: size) + + transition.setPosition(layer: self.darkIcon, position: CGPoint(x: -component.maskFrame.minX + size.width / 2.0, y: -component.maskFrame.minY + size.height / 2.0)) + return size } } @@ -278,6 +428,7 @@ final class CaptureControlsComponent: Component { } let isTablet: Bool + let hasAppeared: Bool let shutterState: ShutterButtonState let lastGalleryAsset: PHAsset? let tag: AnyObject? @@ -290,9 +441,11 @@ final class CaptureControlsComponent: Component { let galleryTapped: () -> Void let swipeHintUpdated: (SwipeHint) -> Void let zoomUpdated: (CGFloat) -> Void + let flipAnimationAction: ActionSlot init( isTablet: Bool, + hasAppeared: Bool, shutterState: ShutterButtonState, lastGalleryAsset: PHAsset?, tag: AnyObject?, @@ -304,9 +457,11 @@ final class CaptureControlsComponent: Component { flipTapped: @escaping () -> Void, galleryTapped: @escaping () -> Void, swipeHintUpdated: @escaping (SwipeHint) -> Void, - zoomUpdated: @escaping (CGFloat) -> Void + zoomUpdated: @escaping (CGFloat) -> Void, + flipAnimationAction: ActionSlot ) { self.isTablet = isTablet + self.hasAppeared = hasAppeared self.shutterState = shutterState self.lastGalleryAsset = lastGalleryAsset self.tag = tag @@ -319,12 +474,16 @@ final class CaptureControlsComponent: Component { self.galleryTapped = galleryTapped self.swipeHintUpdated = swipeHintUpdated self.zoomUpdated = zoomUpdated + self.flipAnimationAction = flipAnimationAction } static func ==(lhs: CaptureControlsComponent, rhs: CaptureControlsComponent) -> Bool { if lhs.isTablet != rhs.isTablet { return false } + if lhs.hasAppeared != rhs.hasAppeared { + return false + } if lhs.shutterState != rhs.shutterState { return false } @@ -369,6 +528,7 @@ final class CaptureControlsComponent: Component { private var state: State? private var availableSize: CGSize? + private let zoomView = ComponentView() private let lockView = ComponentView() private let galleryButtonView = ComponentView() private let shutterButtonView = ComponentView() @@ -377,13 +537,14 @@ final class CaptureControlsComponent: Component { private let leftGuide = SimpleLayer() private let rightGuide = SimpleLayer() - private let shutterUpdateOffset = ActionSlot<(CGFloat, Transition)>() + private let shutterUpdateOffsetX = ActionSlot<(CGFloat, Transition)>() + private let shutterUpdateOffsetY = ActionSlot<(CGFloat, Transition)>() + private let shutterHightlightedAction = ActionSlot() - private let flipAnimationAction = ActionSlot() private let lockImage = UIImage(bundleImageName: "Camera/LockIcon") + private let zoomImage = UIImage(bundleImageName: "Camera/ZoomIcon") - private var lastFlipTimestamp: Double? private var didFlip = false private var wasBanding: Bool? @@ -424,26 +585,69 @@ final class CaptureControlsComponent: Component { case .began: component.shutterPressed() component.swipeHintUpdated(.zoom) - self.shutterUpdateOffset.invoke((0.0, .immediate)) - case .ended, .cancelled: - if location.x < self.frame.width / 2.0 - 60.0 { - component.lockRecording() - - var blobOffset: CGFloat = 0.0 - if let galleryButton = self.galleryButtonView.view { - blobOffset = galleryButton.center.x - self.frame.width / 2.0 - } - self.shutterUpdateOffset.invoke((blobOffset, .spring(duration: 0.35))) + if component.isTablet { + self.shutterUpdateOffsetY.invoke((0.0, .immediate)) } else { - self.hapticFeedback.impact(.light) - component.shutterReleased() - self.shutterUpdateOffset.invoke((0.0, .spring(duration: 0.25))) + self.shutterUpdateOffsetX.invoke((0.0, .immediate)) + } + case .ended, .cancelled: + if component.isTablet { + if location.y > self.frame.height / 2.0 + 60.0 { + component.lockRecording() + + var blobOffset: CGFloat = 0.0 + if let lockView = self.lockView.view { + blobOffset = lockView.center.y - self.frame.height / 2.0 + } + self.updateShutterOffsetY(blobOffset, transition: .spring(duration: 0.35)) + + Queue.mainQueue().after(0.4) { + self.updateShutterOffsetY(0.0, transition: .immediate) + } + } else { + self.hapticFeedback.impact(.light) + component.shutterReleased() + self.updateShutterOffsetY(0.0, transition: .spring(duration: 0.25)) + } + } else { + if location.x < self.frame.width / 2.0 - 60.0 { + component.lockRecording() + + var blobOffset: CGFloat = 0.0 + if let galleryButton = self.galleryButtonView.view { + blobOffset = galleryButton.center.x - self.frame.width / 2.0 + } + self.updateShutterOffsetX(blobOffset, transition: .spring(duration: 0.35)) + + Queue.mainQueue().after(0.4) { + self.updateShutterOffsetX(0.0, transition: .immediate) + } + } else { + self.hapticFeedback.impact(.light) + component.shutterReleased() + self.updateShutterOffsetX(0.0, transition: .spring(duration: 0.25)) + } } default: break } } + private var shutterOffsetX: CGFloat = 0.0 + private var shutterOffsetY: CGFloat = 0.0 + + private func updateShutterOffsetX(_ offsetX: CGFloat, transition: Transition) { + self.shutterOffsetX = offsetX + self.shutterUpdateOffsetX.invoke((offsetX, transition)) + self.state?.updated(transition: transition) + } + + private func updateShutterOffsetY(_ offsetY: CGFloat, transition: Transition) { + self.shutterOffsetY = offsetY + self.shutterUpdateOffsetY.invoke((offsetY, transition)) + self.state?.updated(transition: transition) + } + @objc private func handlePan(_ gestureRecognizer: UIPanGestureRecognizer) { guard let component = self.component else { return @@ -455,6 +659,9 @@ final class CaptureControlsComponent: Component { return bandingStart + (1.0 - (1.0 / ((bandedOffset * coefficient / range) + 1.0))) * range } + var scheduledXOffsetUpdate: (CGFloat, Transition)? + var scheduledYOffsetUpdate: (CGFloat, Transition)? + let previousPanBlobState = self.panBlobState let location = gestureRecognizer.location(in: self) switch gestureRecognizer.state { @@ -462,62 +669,105 @@ final class CaptureControlsComponent: Component { guard case .holdRecording = component.shutterState else { return } - var blobOffset: CGFloat = 0.0 - if let galleryButton = self.galleryButtonView.view, let flipButton = self.flipButtonView.view { - blobOffset = max(galleryButton.center.x, min(flipButton.center.x, location.x)) - } - blobOffset -= self.frame.width / 2.0 - var isBanding = false - if location.y < -10.0 { - let fraction = 1.0 + min(8.0, ((abs(location.y) - 10.0) / 60.0)) - component.zoomUpdated(fraction) - } else { - component.zoomUpdated(1.0) - } - if location.x < self.frame.width / 2.0 - 30.0 { - if location.x < self.frame.width / 2.0 - 60.0 { - component.swipeHintUpdated(.releaseLock) - if location.x < 75.0 { - self.panBlobState = .lock + var blobOffset: CGFloat = 0.0 + if component.isTablet { + if let shutterButton = self.shutterButtonView.view, let lockView = self.lockView.view { + blobOffset = max(shutterButton.center.y - 10.0, min(lockView.center.y, location.y)) + } + blobOffset -= self.frame.height / 2.0 + + var isBanding = false + if location.x < -10.0 { + let fraction = 1.0 + min(8.0, ((abs(location.x) - 10.0) / 60.0)) + component.zoomUpdated(fraction) + } else { + component.zoomUpdated(1.0) + } + + if location.y > self.frame.height / 2.0 + 30.0 { + if location.y > self.frame.height / 2.0 + 60.0 { + component.swipeHintUpdated(.releaseLock) + if location.y > self.frame.height / 2.0 + 130.0 { + self.panBlobState = .lock + } else { + self.panBlobState = .transientToLock + } } else { - self.panBlobState = .transientToLock + component.swipeHintUpdated(.lock) + self.panBlobState = .video + blobOffset = rubberBandingOffset(offset: -blobOffset, bandingStart: 0.0) * -1.0 + isBanding = true } } else { - component.swipeHintUpdated(.lock) - self.panBlobState = .video - blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0) - isBanding = true - } - } 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 - if !self.didFlip && location.x > self.frame.width - 80.0 { - self.didFlip = true - self.hapticFeedback.impact(.light) - self.flipAnimationAction.invoke(Void()) - component.flipTapped() - } - } else { - self.didFlip = false - self.panBlobState = .video blobOffset = rubberBandingOffset(offset: -blobOffset, bandingStart: 0.0) * -1.0 + component.swipeHintUpdated(.zoom) + self.panBlobState = .video isBanding = true } + var transition: Transition = .immediate + if let wasBanding = self.wasBanding, wasBanding != isBanding { + //self.hapticFeedback.impact(.light) + transition = .spring(duration: 0.35) + } + self.wasBanding = isBanding + scheduledYOffsetUpdate = (blobOffset, transition) } else { - blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0) - component.swipeHintUpdated(.zoom) - self.panBlobState = .video - isBanding = true + if let galleryButton = self.galleryButtonView.view, let flipButton = self.flipButtonView.view { + blobOffset = max(galleryButton.center.x, min(flipButton.center.x, location.x)) + } + blobOffset -= self.frame.width / 2.0 + var isBanding = false + if location.y < -10.0 { + let fraction = 1.0 + min(8.0, ((abs(location.y) - 10.0) / 60.0)) + component.zoomUpdated(fraction) + } else { + component.zoomUpdated(1.0) + } + + if location.x < self.frame.width / 2.0 - 30.0 { + if location.x < self.frame.width / 2.0 - 60.0 { + component.swipeHintUpdated(.releaseLock) + if location.x < 85.0 { + self.panBlobState = .lock + } else { + self.panBlobState = .transientToLock + } + } else { + component.swipeHintUpdated(.lock) + self.panBlobState = .video + blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0) + isBanding = true + } + } 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 + if !self.didFlip && location.x > self.frame.width - 70.0 { + self.didFlip = true + self.hapticFeedback.impact(.light) + component.flipTapped() + } + } else { + self.didFlip = false + self.panBlobState = .video + blobOffset = rubberBandingOffset(offset: -blobOffset, bandingStart: 0.0) * -1.0 + isBanding = true + } + } else { + blobOffset = rubberBandingOffset(offset: blobOffset, bandingStart: 0.0) + component.swipeHintUpdated(.zoom) + self.panBlobState = .video + isBanding = true + } + var transition: Transition = .immediate + if let wasBanding = self.wasBanding, wasBanding != isBanding { + //self.hapticFeedback.impact(.light) + transition = .spring(duration: 0.35) + } + self.wasBanding = isBanding + scheduledXOffsetUpdate = (blobOffset, transition) } - var transition: Transition = .immediate - if let wasBanding = self.wasBanding, wasBanding != isBanding { - //self.hapticFeedback.impact(.light) - transition = .spring(duration: 0.35) - } - self.wasBanding = isBanding - self.shutterUpdateOffset.invoke((blobOffset, transition)) default: self.panBlobState = nil self.wasBanding = nil @@ -526,6 +776,12 @@ final class CaptureControlsComponent: Component { if previousPanBlobState != self.panBlobState, let component = self.component, let state = self.state, let availableSize = self.availableSize { let _ = self.update(component: component, state: state, availableSize: availableSize, transition: .spring(duration: 0.5)) } + if let (offset, transition) = scheduledXOffsetUpdate { + self.updateShutterOffsetX(offset, transition: transition) + } + if let (offset, transition) = scheduledYOffsetUpdate { + self.updateShutterOffsetY(offset, transition: transition) + } } override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { @@ -582,7 +838,7 @@ final class CaptureControlsComponent: Component { self.availableSize = availableSize state.lastGalleryAsset = component.lastGalleryAsset - let size = CGSize(width: availableSize.width, height: maximumShutterSize.height) + let size = component.isTablet ? availableSize : CGSize(width: availableSize.width, height: maximumShutterSize.height) let buttonSideInset: CGFloat = 28.0 //let buttonMaxOffset: CGFloat = 100.0 @@ -597,7 +853,16 @@ final class CaptureControlsComponent: Component { } else if case .transition = component.shutterState { isTransitioning = true } - + + let gallerySize: CGSize + let galleryCornerRadius: CGFloat + if component.isTablet { + gallerySize = CGSize(width: 72.0, height: 72.0) + galleryCornerRadius = 16.0 + } else { + gallerySize = CGSize(width: 50.0, height: 50.0) + galleryCornerRadius = 10.0 + } let galleryButtonSize = self.galleryButtonView.update( transition: .immediate, component: AnyComponent( @@ -607,7 +872,7 @@ final class CaptureControlsComponent: Component { component: AnyComponent( Image( image: state.cachedAssetImage?.1, - size: CGSize(width: 50.0, height: 50.0), + size: gallerySize, contentMode: .scaleAspectFill ) ) @@ -619,12 +884,17 @@ final class CaptureControlsComponent: Component { ) ), environment: {}, - containerSize: CGSize(width: 50.0, height: 50.0) + containerSize: gallerySize ) - let galleryButtonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: (size.height - galleryButtonSize.height) / 2.0), size: galleryButtonSize) + let galleryButtonFrame: CGRect + if component.isTablet { + galleryButtonFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - galleryButtonSize.width) / 2.0), y: size.height - galleryButtonSize.height - 56.0), size: galleryButtonSize) + } else { + galleryButtonFrame = CGRect(origin: CGPoint(x: buttonSideInset, y: floorToScreenPixels((size.height - galleryButtonSize.height) / 2.0)), size: galleryButtonSize) + } if let galleryButtonView = self.galleryButtonView.view { galleryButtonView.clipsToBounds = true - galleryButtonView.layer.cornerRadius = 10.0 + galleryButtonView.layer.cornerRadius = galleryCornerRadius if galleryButtonView.superview == nil { self.addSubview(galleryButtonView) } @@ -634,69 +904,46 @@ final class CaptureControlsComponent: Component { transition.setScale(view: galleryButtonView, scale: isRecording || isTransitioning ? 0.1 : 1.0) transition.setAlpha(view: galleryButtonView, alpha: isRecording || isTransitioning ? 0.0 : 1.0) } - - let _ = self.lockView.update( - transition: .immediate, - component: AnyComponent( - Image( - image: self.lockImage, - size: CGSize(width: 30.0, height: 30.0) - ) - ), - environment: {}, - containerSize: CGSize(width: 30.0, height: 30.0) - ) - let lockFrame = galleryButtonFrame.insetBy(dx: 10.0, dy: 10.0) - if let lockView = self.lockView.view { - if lockView.superview == nil { - self.addSubview(lockView) - } - transition.setBounds(view: lockView, bounds: CGRect(origin: .zero, size: lockFrame.size)) - transition.setPosition(view: lockView, position: lockFrame.center) + + if !component.isTablet { + let flipButtonOriginX = availableSize.width - 48.0 - buttonSideInset + let flipButtonMaskFrame: CGRect = CGRect(origin: CGPoint(x: availableSize.width / 2.0 - (flipButtonOriginX + 22.0) + 6.0 + self.shutterOffsetX, y: 8.0), size: CGSize(width: 32.0, height: 32.0)) - transition.setScale(view: lockView, scale: isHolding ? 1.0 : 0.1) - transition.setAlpha(view: lockView, alpha: isHolding ? 1.0 : 0.0) - } - - let flipAnimationAction = self.flipAnimationAction - let flipButtonSize = self.flipButtonView.update( - transition: .immediate, - component: AnyComponent( - CameraButton( - content: AnyComponentWithIdentity( - id: "flip", - component: AnyComponent( - FlipButtonContentComponent(action: flipAnimationAction) - ) - ), - minSize: CGSize(width: 44.0, height: 44.0), - action: { [weak self] in - guard let self else { - return + let flipButtonSize = self.flipButtonView.update( + transition: .immediate, + component: AnyComponent( + CameraButton( + content: AnyComponentWithIdentity( + id: "flip", + component: AnyComponent( + FlipButtonContentComponent( + action: component.flipAnimationAction, + maskFrame: flipButtonMaskFrame + ) + ) + ), + minSize: CGSize(width: 44.0, height: 44.0), + action: { + component.flipTapped() } - let currentTimestamp = CACurrentMediaTime() - if let lastFlipTimestamp = self.lastFlipTimestamp, currentTimestamp - lastFlipTimestamp < 1.3 { - return - } - self.lastFlipTimestamp = currentTimestamp - component.flipTapped() - flipAnimationAction.invoke(Void()) - } - ) - ), - environment: {}, - containerSize: availableSize - ) - let flipButtonFrame = CGRect(origin: CGPoint(x: availableSize.width - flipButtonSize.width - buttonSideInset, y: (size.height - flipButtonSize.height) / 2.0), size: flipButtonSize) - if let flipButtonView = self.flipButtonView.view { - if flipButtonView.superview == nil { - self.addSubview(flipButtonView) + ) + ), + environment: {}, + containerSize: availableSize + ) + let flipButtonFrame = CGRect(origin: CGPoint(x: flipButtonOriginX, y: (size.height - flipButtonSize.height) / 2.0), size: flipButtonSize) + if let flipButtonView = self.flipButtonView.view { + if flipButtonView.superview == nil { + self.addSubview(flipButtonView) + } + transition.setBounds(view: flipButtonView, bounds: CGRect(origin: .zero, size: flipButtonFrame.size)) + transition.setPosition(view: flipButtonView, position: flipButtonFrame.center) + + transition.setScale(view: flipButtonView, scale: isTransitioning ? 0.01 : 1.0) + transition.setAlpha(view: flipButtonView, alpha: isTransitioning ? 0.0 : 1.0) } - transition.setBounds(view: flipButtonView, bounds: CGRect(origin: .zero, size: flipButtonFrame.size)) - transition.setPosition(view: flipButtonView, position: flipButtonFrame.center) - - transition.setScale(view: flipButtonView, scale: isTransitioning ? 0.01 : 1.0) - transition.setAlpha(view: flipButtonView, alpha: isTransitioning ? 0.0 : 1.0) + } else if let flipButtonView = self.flipButtonView.view { + flipButtonView.removeFromSuperview() } var blobState: ShutterBlobView.BlobState @@ -717,16 +964,19 @@ final class CaptureControlsComponent: Component { Button( content: AnyComponent( ShutterButtonContentComponent( + isTablet: component.isTablet, + hasAppeared: component.hasAppeared, shutterState: component.shutterState, blobState: blobState, highlightedAction: self.shutterHightlightedAction, - updateOffset: self.shutterUpdateOffset + updateOffsetX: self.shutterUpdateOffsetX, + updateOffsetY: self.shutterUpdateOffsetY ) ), automaticHighlight: false, action: { [weak self] in self?.hapticFeedback.impact(.light) - self?.shutterUpdateOffset.invoke((0.0, .immediate)) + self?.shutterUpdateOffsetX.invoke((0.0, .immediate)) component.shutterTapped() }, highlightedAction: self.shutterHightlightedAction @@ -736,6 +986,110 @@ final class CaptureControlsComponent: Component { containerSize: availableSize ) let shutterButtonFrame = CGRect(origin: CGPoint(x: (availableSize.width - shutterButtonSize.width) / 2.0, y: (size.height - shutterButtonSize.height) / 2.0), size: shutterButtonSize) + + let guideSpacing: CGFloat = 9.0 + let guideSize = CGSize(width: isHolding ? component.isTablet ? 84.0 : 60.0 : 0.0, height: 1.0 + UIScreenPixel) + let guideAlpha: CGFloat = isHolding ? 1.0 : 0.0 + + let leftGuideFrame = CGRect(origin: CGPoint(x: shutterButtonFrame.minX - guideSpacing - guideSize.width, y: floorToScreenPixels((size.height - guideSize.height) / 2.0)), size: guideSize) + + let rightGuideFrame: CGRect + if component.isTablet { + rightGuideFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((size.width - guideSize.height) / 2.0), y: shutterButtonFrame.maxY + guideSpacing), size: CGSize(width: guideSize.height, height: guideSize.width)) + } else { + rightGuideFrame = CGRect(origin: CGPoint(x: shutterButtonFrame.maxX + guideSpacing, y: (size.height - guideSize.height) / 2.0), size: guideSize) + } + + transition.setFrame(layer: self.leftGuide, frame: leftGuideFrame) + transition.setFrame(layer: self.rightGuide, frame: rightGuideFrame) + + var leftGuideAlpha = guideAlpha + let rightGuideAlpha = guideAlpha + if component.isTablet, availableSize.width < 185.0 { + leftGuideAlpha = 0.0 + } + + if previousShutterState == .generic || previousShutterState == .video { + self.leftGuide.opacity = Float(leftGuideAlpha) + self.rightGuide.opacity = Float(rightGuideAlpha) + } else { + transition.setAlpha(layer: self.leftGuide, alpha: leftGuideAlpha) + transition.setAlpha(layer: self.rightGuide, alpha: rightGuideAlpha) + } + + self.leftGuide.cornerRadius = guideSize.height / 2.0 + self.rightGuide.cornerRadius = guideSize.height / 2.0 + + let hintIconSize = CGSize(width: 30.0, height: 30.0) + if component.isTablet { + let _ = self.zoomView.update( + transition: .immediate, + component: AnyComponent( + Image( + image: self.zoomImage, + size: hintIconSize + ) + ), + environment: {}, + containerSize: hintIconSize + ) + let zoomFrame = CGRect(origin: CGPoint(x: availableSize.width / 2.0 - 150.0 - hintIconSize.width, y: floorToScreenPixels((availableSize.height - hintIconSize.height) / 2.0)), size: hintIconSize) + if let zoomView = self.zoomView.view { + if zoomView.superview == nil { + self.addSubview(zoomView) + } + transition.setBounds(view: zoomView, bounds: CGRect(origin: .zero, size: zoomFrame.size)) + transition.setPosition(view: zoomView, position: zoomFrame.center) + + transition.setScale(view: zoomView, scale: isHolding ? 1.0 : 0.1) + transition.setAlpha(view: zoomView, alpha: isHolding && leftGuideAlpha > 0.0 ? 1.0 : 0.0) + } + } else if let zoomView = self.zoomView.view { + zoomView.removeFromSuperview() + } + + let lockFrame: CGRect + var lockMaskFrame: CGRect + if component.isTablet { + lockFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - hintIconSize.width) / 2.0), y: availableSize.height / 2.0 + 152.0), size: hintIconSize) + lockMaskFrame = CGRect(origin: CGPoint(x: -9.0, y: availableSize.height / 2.0 - lockFrame.midY - 9.0 + self.shutterOffsetY), size: CGSize(width: 48.0, height: 48.0)) + if self.panBlobState == .transientToLock { + lockMaskFrame = lockMaskFrame.offsetBy(dx: 0.0, dy: -8.0) + } + } else { + lockFrame = galleryButtonFrame.insetBy(dx: (gallerySize.width - hintIconSize.width) / 2.0, dy: (gallerySize.height - hintIconSize.height) / 2.0) + lockMaskFrame = CGRect(origin: CGPoint(x: availableSize.width / 2.0 - lockFrame.midX - 9.0 + self.shutterOffsetX, y: -9.0), size: CGSize(width: 48.0, height: 48.0)) + if self.panBlobState == .transientToLock { + lockMaskFrame = lockMaskFrame.offsetBy(dx: 8.0, dy: 0.0) + } + } + + let _ = self.lockView.update( + transition: transition, + component: AnyComponent( + LockContentComponent( + maskFrame: lockMaskFrame + ) + ), + environment: {}, + containerSize: hintIconSize + ) + if let lockView = self.lockView.view { + if lockView.superview == nil { + self.addSubview(lockView) + } + transition.setBounds(view: lockView, bounds: CGRect(origin: .zero, size: lockFrame.size)) + transition.setPosition(view: lockView, position: lockFrame.center) + + transition.setScale(view: lockView, scale: isHolding ? 1.0 : 0.1) + transition.setAlpha(view: lockView, alpha: isHolding ? 1.0 : 0.0) + + if let lockMaskView = lockView as? LockContentComponent.View { + transition.setAlpha(view: lockMaskView.maskContainerView, alpha: isHolding ? 1.0 : 0.0) + transition.setSublayerTransform(layer: lockMaskView.maskContainerView.layer, transform: isHolding ? CATransform3DIdentity : CATransform3DMakeScale(0.1, 0.1, 1.0)) + } + } + if let shutterButtonView = self.shutterButtonView.view { if shutterButtonView.superview == nil { let panGestureRecognizer = UIPanGestureRecognizer(target: self, action: #selector(self.handlePan(_:))) @@ -755,26 +1109,19 @@ final class CaptureControlsComponent: Component { transition.setAlpha(view: shutterButtonView, alpha: isTransitioning ? 0.0 : 1.0) } - let guideSpacing: CGFloat = 9.0 - let guideSize = CGSize(width: isHolding ? 60.0 : 0.0, height: 1.0 + UIScreenPixel) - let guideAlpha: CGFloat = isHolding ? 1.0 : 0.0 - - let leftGuideFrame = CGRect(origin: CGPoint(x: shutterButtonFrame.minX - guideSpacing - guideSize.width, y: (size.height - guideSize.height) / 2.0), size: guideSize) - let rightGuideFrame = CGRect(origin: CGPoint(x: shutterButtonFrame.maxX + guideSpacing, y: (size.height - guideSize.height) / 2.0), size: guideSize) - - transition.setFrame(layer: self.leftGuide, frame: leftGuideFrame) - transition.setFrame(layer: self.rightGuide, frame: rightGuideFrame) - - if previousShutterState == .generic || previousShutterState == .video { - self.leftGuide.opacity = Float(guideAlpha) - self.rightGuide.opacity = Float(guideAlpha) - } else { - transition.setAlpha(layer: self.leftGuide, alpha: guideAlpha) - transition.setAlpha(layer: self.rightGuide, alpha: guideAlpha) + if let buttonView = self.flipButtonView.view as? CameraButton.View, let contentView = buttonView.contentView.componentView as? FlipButtonContentComponent.View { + if contentView.maskContainerView.superview == nil { + self.addSubview(contentView.maskContainerView) + } + contentView.maskContainerView.frame = contentView.convert(contentView.bounds, to: self) } - self.leftGuide.cornerRadius = guideSize.height / 2.0 - self.rightGuide.cornerRadius = guideSize.height / 2.0 + if let lockView = self.lockView.view as? LockContentComponent.View { + if lockView.maskContainerView.superview == nil { + self.addSubview(lockView.maskContainerView) + } + lockView.maskContainerView.center = lockView.center + } return size } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift index 5214c9a816..a4cc526c80 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ModeComponent.swift @@ -18,17 +18,20 @@ extension CameraMode { private let buttonSize = CGSize(width: 55.0, height: 44.0) final class ModeComponent: Component { + let isTablet: Bool let availableModes: [CameraMode] let currentMode: CameraMode let updatedMode: (CameraMode) -> Void let tag: AnyObject? init( + isTablet: Bool, availableModes: [CameraMode], currentMode: CameraMode, updatedMode: @escaping (CameraMode) -> Void, tag: AnyObject? ) { + self.isTablet = isTablet self.availableModes = availableModes self.currentMode = currentMode self.updatedMode = updatedMode @@ -36,6 +39,9 @@ final class ModeComponent: Component { } static func ==(lhs: ModeComponent, rhs: ModeComponent) -> Bool { + if lhs.isTablet != rhs.isTablet { + return false + } if lhs.availableModes != rhs.availableModes { return false } @@ -114,14 +120,16 @@ final class ModeComponent: Component { func update(component: ModeComponent, availableSize: CGSize, transition: Transition) -> CGSize { self.component = component - + + let isTablet = component.isTablet let updatedMode = component.updatedMode - - let spacing: CGFloat = 14.0 + + let spacing: CGFloat = isTablet ? 9.0 : 14.0 var i = 0 var itemFrame = CGRect(origin: .zero, size: buttonSize) var selectedCenter = itemFrame.minX + for mode in component.availableModes { let itemView: ItemView if self.itemViews.count == i { @@ -137,20 +145,37 @@ final class ModeComponent: Component { itemView.update(value: mode.title, selected: mode == component.currentMode) itemView.bounds = CGRect(origin: .zero, size: itemFrame.size) - itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) - if mode == component.currentMode { - selectedCenter = itemFrame.midX + if isTablet { + itemView.center = CGPoint(x: availableSize.width / 2.0, y: itemFrame.midY) + if mode == component.currentMode { + selectedCenter = itemFrame.midY + } + itemFrame = itemFrame.offsetBy(dx: 0.0, dy: buttonSize.height + spacing) + } else { + itemView.center = CGPoint(x: itemFrame.midX, y: itemFrame.midY) + if mode == component.currentMode { + selectedCenter = itemFrame.midX + } + itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0) } - + i += 1 - itemFrame = itemFrame.offsetBy(dx: buttonSize.width + spacing, dy: 0.0) } - let totalSize = CGSize(width: buttonSize.width * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1), height: buttonSize.height) - transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize)) + let totalSize: CGSize + let size: CGSize + if isTablet { + totalSize = CGSize(width: availableSize.width, height: buttonSize.height * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1)) + size = CGSize(width: availableSize.width, height: availableSize.height) + transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: 0.0, y: availableSize.height / 2.0 - selectedCenter), size: totalSize)) + } else { + size = CGSize(width: availableSize.width, height: buttonSize.height) + totalSize = CGSize(width: buttonSize.width * CGFloat(component.availableModes.count) + spacing * CGFloat(component.availableModes.count - 1), height: buttonSize.height) + transition.setFrame(view: self.containerView, frame: CGRect(origin: CGPoint(x: availableSize.width / 2.0 - selectedCenter, y: 0.0), size: totalSize)) + } - return CGSize(width: availableSize.width, height: buttonSize.height) + return size } } diff --git a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift index 46cffa39d7..a944705c84 100644 --- a/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift +++ b/submodules/TelegramUI/Components/CameraScreen/Sources/ShutterBlobView.swift @@ -121,12 +121,6 @@ private func lookupSpringValue(_ t: CGFloat) -> CGFloat { } } return 1.0 -// print("---start---") -// for i in 0 ..< 16 { -// let j = Double(i) * 1.0 / 16.0 -// print("\(j) \(listViewAnimationCurveSystem(j))") -// } -// print("---end---") } private class ShutterBlobLayer: MetalImageLayer { @@ -214,12 +208,14 @@ final class ShutterBlobView: UIView { private var displayLink: SharedDisplayLinkDriver.Link? private var primarySize = AnimatableProperty(value: 0.63) - private var primaryOffset = AnimatableProperty(value: 0.0) + private var primaryOffsetX = AnimatableProperty(value: 0.0) + private var primaryOffsetY = AnimatableProperty(value: 0.0) private var primaryRedness = AnimatableProperty(value: 0.0) private var primaryCornerRadius = AnimatableProperty(value: 0.63) private var secondarySize = AnimatableProperty(value: 0.34) - private var secondaryOffset = AnimatableProperty(value: 0.0) + private var secondaryOffsetX = AnimatableProperty(value: 0.0) + private var secondaryOffsetY = AnimatableProperty(value: 0.0) private var secondaryRedness = AnimatableProperty(value: 0.0) private(set) var state: BlobState = .generic @@ -309,22 +305,42 @@ final class ShutterBlobView: UIView { self.tick() } - func updatePrimaryOffset(_ offset: CGFloat, transition: Transition = .immediate) { + func updatePrimaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) { guard self.frame.height > 0.0 else { return } let mappedOffset = offset / self.frame.height * 2.0 - self.primaryOffset.update(value: mappedOffset, transition: transition) + self.primaryOffsetX.update(value: mappedOffset, transition: transition) self.tick() } - func updateSecondaryOffset(_ offset: CGFloat, transition: Transition = .immediate) { + func updatePrimaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) { + guard self.frame.height > 0.0 else { + return + } + let mappedOffset = offset / self.frame.width * 2.0 + self.primaryOffsetY.update(value: mappedOffset, transition: transition) + + self.tick() + } + + func updateSecondaryOffsetX(_ offset: CGFloat, transition: Transition = .immediate) { guard self.frame.height > 0.0 else { return } let mappedOffset = offset / self.frame.height * 2.0 - self.secondaryOffset.update(value: mappedOffset, transition: transition) + self.secondaryOffsetX.update(value: mappedOffset, transition: transition) + + self.tick() + } + + func updateSecondaryOffsetY(_ offset: CGFloat, transition: Transition = .immediate) { + guard self.frame.height > 0.0 else { + return + } + let mappedOffset = offset / self.frame.width * 2.0 + self.secondaryOffsetY.update(value: mappedOffset, transition: transition) self.tick() } @@ -332,11 +348,13 @@ final class ShutterBlobView: UIView { private func updateAnimations() { let properties = [ self.primarySize, - self.primaryOffset, + self.primaryOffsetX, + self.primaryOffsetY, self.primaryRedness, self.primaryCornerRadius, self.secondarySize, - self.secondaryOffset, + self.secondaryOffsetX, + self.secondaryOffsetY, self.secondaryRedness ] @@ -407,20 +425,31 @@ final class ShutterBlobView: UIView { var resolution = simd_uint2(UInt32(drawableSize.width), UInt32(drawableSize.height)) renderEncoder.setFragmentBytes(&resolution, length: MemoryLayout.size * 2, index: 0) - var primaryParameters = simd_float4( + var primaryParameters = simd_float3( Float(self.primarySize.presentationValue), - Float(self.primaryOffset.presentationValue), Float(self.primaryRedness.presentationValue), Float(self.primaryCornerRadius.presentationValue) ) - renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout.size, index: 1) + renderEncoder.setFragmentBytes(&primaryParameters, length: MemoryLayout.size, index: 1) - var secondaryParameters = simd_float3( + var primaryOffset = simd_float2( + Float(self.primaryOffsetX.presentationValue), + Float(self.primaryOffsetY.presentationValue) + ) + renderEncoder.setFragmentBytes(&primaryOffset, length: MemoryLayout.size, index: 2) + + var secondaryParameters = simd_float2( Float(self.secondarySize.presentationValue), - Float(self.secondaryOffset.presentationValue), Float(self.secondaryRedness.presentationValue) ) - renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout.size, index: 2) + renderEncoder.setFragmentBytes(&secondaryParameters, length: MemoryLayout.size, index: 3) + + var secondaryOffset = simd_float2( + Float(self.secondaryOffsetX.presentationValue), + Float(self.secondaryOffsetY.presentationValue) + ) + renderEncoder.setFragmentBytes(&secondaryOffset, length: MemoryLayout.size, index: 4) + renderEncoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: 1) renderEncoder.endEncoding() diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift index ec1c9d3ff7..097295eed9 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/Drawing/DrawingStickerEntity.swift @@ -8,6 +8,7 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { public enum Content: Equatable { case file(TelegramMediaFile) case image(UIImage) + case video(String, UIImage?) public static func == (lhs: Content, rhs: Content) -> Bool { switch lhs { @@ -23,6 +24,12 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { } else { return false } + case let .video(lhsPath, _): + if case let .video(rhsPath, _) = rhs { + return lhsPath == rhsPath + } else { + return false + } } } } @@ -30,6 +37,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { case uuid case file case image + case videoPath + case videoImage case referenceDrawingSize case position case scale @@ -64,6 +73,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { return file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" case .image: return false + case .video: + return true } } @@ -92,6 +103,8 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { self.content = .file(file) } else if let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { self.content = .image(image) + } else if let videoPath = try container.decodeIfPresent(String.self, forKey: .videoPath), let imageData = try container.decodeIfPresent(Data.self, forKey: .image), let image = UIImage(data: imageData) { + self.content = .video(videoPath, image) } else { fatalError() } @@ -110,6 +123,9 @@ public final class DrawingStickerEntity: DrawingEntity, Codable { try container.encode(file, forKey: .file) case let .image(image): try container.encodeIfPresent(image.pngData(), forKey: .image) + case let .video(path, image): + try container.encode(path, forKey: .videoPath) + try container.encodeIfPresent(image?.jpegData(compressionQuality: 0.87), forKey: .videoImage) } try container.encode(self.referenceDrawingSize, forKey: .referenceDrawingSize) try container.encode(self.position, forKey: .position) diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift index f8bfc0c06f..eca08256ea 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditor.swift @@ -420,11 +420,16 @@ public final class MediaEditor { if let self { let start = self.values.videoTrimRange?.lowerBound ?? 0.0 self.player?.seek(to: CMTime(seconds: start, preferredTimescale: CMTimeScale(1000))) + self.onPlaybackAction(.seek(start)) self.player?.play() + self.onPlaybackAction(.play) } }) - player.playImmediately(atRate: 1.0) - self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) + Queue.mainQueue().justDispatch { + player.playImmediately(atRate: 1.0) + self.onPlaybackAction(.play) + self.volumeFade = self.player?.fadeVolume(from: 0.0, to: 1.0, duration: 0.4) + } } } }) @@ -460,6 +465,12 @@ public final class MediaEditor { return self.values.toolValues[key] } + private var previewUnedited = false + public func setPreviewUnedited(_ preview: Bool) { + self.previewUnedited = preview + self.updateRenderChain() + } + public func setToolValue(_ key: EditorToolKey, value: Any) { self.updateValues { values in var updatedToolValues = values.toolValues @@ -481,11 +492,20 @@ public final class MediaEditor { } } + public enum PlaybackAction { + case play + case pause + case seek(Double) + } + + public var onPlaybackAction: (PlaybackAction) -> Void = { _ in } + private var targetTimePosition: (CMTime, Bool)? private var updatingTimePosition = false public func seek(_ position: Double, andPlay play: Bool) { if !play { self.player?.pause() + self.onPlaybackAction(.pause) } let targetPosition = CMTime(seconds: position, preferredTimescale: CMTimeScale(60.0)) if self.targetTimePosition?.0 != targetPosition { @@ -496,6 +516,7 @@ public final class MediaEditor { } if play { self.player?.play() + self.onPlaybackAction(.play) } } @@ -505,14 +526,17 @@ public final class MediaEditor { public func play() { self.player?.play() + self.onPlaybackAction(.play) } public func stop() { self.player?.pause() + self.onPlaybackAction(.pause) } public func invalidate() { self.player?.pause() + self.onPlaybackAction(.pause) self.renderer.textureSource?.invalidate() } @@ -531,6 +555,7 @@ public final class MediaEditor { } } }) + self.onPlaybackAction(.seek(targetPosition.seconds)) } public func setVideoTrimRange(_ trimRange: Range, apply: Bool) { @@ -558,6 +583,7 @@ public final class MediaEditor { private var previousUpdateTime: Double? private var scheduledUpdate = false private func updateRenderChain() { + self.renderer.renderPassedEnabled = !self.previewUnedited self.renderChain.update(values: self.values) if let player = self.player, player.rate > 0.0 { } else { diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift index dc069ab269..4619f09758 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorComposerEntity.swift @@ -20,6 +20,8 @@ func composerEntitiesForDrawingEntity(account: Account, entity: DrawingEntity, c content = .file(file) case let .image(image): content = .image(image) + case let .video(path, _): + content = .video(path) } 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]) { @@ -69,6 +71,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { public enum Content { case file(TelegramMediaFile) case image(UIImage) + case video(String) var file: TelegramMediaFile? { if case let .file(file) = self { @@ -90,7 +93,10 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { var source: AnimatedStickerNodeSource? var frameSource = Promise?>() var videoFrameSource = Promise?>() - var isVideo = false + var isVideoSticker = false + + var assetReader: AVAssetReader? + var videoOutput: AVAssetReaderTrackOutput? var frameCount: Int? var frameRate: Int? @@ -118,9 +124,9 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { case let .file(file): if file.isAnimatedSticker || file.isVideoSticker || file.mimeType == "video/webm" { self.isAnimated = true - self.isVideo = file.isVideoSticker || file.mimeType == "video/webm" + self.isVideoSticker = file.isVideoSticker || file.mimeType == "video/webm" - self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideo) + self.source = AnimatedStickerResourceSource(account: account, resource: file.resource, isVideo: isVideoSticker) let pathPrefix = account.postbox.mediaBox.shortLivedResourceCachePathPrefix(file.resource.id) if let source = self.source { let dimensions = file.dimensions ?? PixelDimensions(width: 512, height: 512) @@ -131,7 +137,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { if let data = try? Data(contentsOf: URL(fileURLWithPath: path), options: [.mappedRead]) { let queue = strongSelf.queue - if strongSelf.isVideo { + if strongSelf.isVideoSticker { let frameSource = QueueLocalObject(queue: queue, generate: { return VideoStickerDirectFrameSource(queue: queue, path: path, width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), cachePathPrefix: pathPrefix, unpremultiplyAlpha: false)! }) @@ -180,6 +186,27 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { case let .image(image): self.isAnimated = false self.imagePromise.set(.single(image)) + case let .video(videoPath): + self.isAnimated = true + + let url = URL(fileURLWithPath: videoPath) + let asset = AVURLAsset(url: url) + + if let assetReader = try? AVAssetReader(asset: asset), let videoTrack = asset.tracks(withMediaType: .video).first { + let outputSettings: [String: Any] = [ + kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + kCVPixelBufferMetalCompatibilityKey as String: true + ] + let videoOutput = AVAssetReaderTrackOutput(track: videoTrack, outputSettings: outputSettings) + videoOutput.alwaysCopiesSampleData = true + if assetReader.canAdd(videoOutput) { + assetReader.add(videoOutput) + } + + assetReader.startReading() + self.assetReader = assetReader + self.videoOutput = videoOutput + } } } @@ -187,9 +214,52 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { self.disposables.dispose() } - var tested = false + private var circleMaskFilter: CIFilter? func image(for time: CMTime, frameRate: Float, completion: @escaping (CIImage?) -> Void) { - if self.isAnimated { + if case .video = self.content { + if let videoOutput = self.videoOutput { + if let sampleBuffer = videoOutput.copyNextSampleBuffer(), let imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) { + var ciImage = CIImage(cvPixelBuffer: imageBuffer) + ciImage = ciImage.oriented(forExifOrientation: UIImage.Orientation.right.exifOrientation) + let minSide = min(ciImage.extent.size.width, ciImage.extent.size.height) + let cropRect = CGRect(origin: CGPoint(x: floor((ciImage.extent.size.width - minSide) / 2.0), y: floor((ciImage.extent.size.height - minSide) / 2.0)), size: CGSize(width: minSide, height: minSide)) + ciImage = ciImage.cropped(to: cropRect).samplingLinear() + ciImage = ciImage.transformed(by: CGAffineTransform(translationX: 0.0, y: -420.0)) + // ciImage = ciImage.transformed(by: CGAffineTransform(translationX: -ciImage.extent.midX, y: -ciImage.extent.midY)) + // ciImage = ciImage.transformed(by: CGAffineTransform(rotationAngle: -.pi / 2.0)) + // ciImage = ciImage.transformed(by: CGAffineTransform(translationX: ciImage.extent.midX, y: ciImage.extent.midY)) + + var circleMaskFilter: CIFilter? + if let current = self.circleMaskFilter { + circleMaskFilter = current + } else { + let circleImage = generateImage(CGSize(width: minSide, height: minSide), scale: 1.0, rotatedContext: { size, context in + context.clear(CGRect(origin: .zero, size: size)) + context.setFillColor(UIColor.white.cgColor) + context.fillEllipse(in: CGRect(origin: .zero, size: size)) + })! + let circleMask = CIImage(image: circleImage) + if let filter = CIFilter(name: "CIBlendWithAlphaMask") { + filter.setValue(circleMask, forKey: kCIInputMaskImageKey) + self.circleMaskFilter = filter + circleMaskFilter = filter + } + } + + let _ = circleMaskFilter + if let circleMaskFilter { + circleMaskFilter.setValue(ciImage, forKey: kCIInputImageKey) + if let output = circleMaskFilter.outputImage { + ciImage = output + } + } + + completion(ciImage) + } + } else { + completion(nil) + } + } else if self.isAnimated { let currentTime = CMTimeGetSeconds(time) var tintColor: UIColor? @@ -262,7 +332,7 @@ private class MediaEditorComposerStickerEntity: MediaEditorComposerEntity { } } - if self.isVideo { + if self.isVideoSticker { self.disposables.add((self.videoFrameSource.get() |> take(1) |> deliverOn(self.queue)).start(next: { [weak self] frameSource in @@ -371,3 +441,20 @@ private func render(width: Int, height: Int, bytesPerRow: Int, data: Data, type: return CIImage(cvPixelBuffer: pixelBuffer, options: [.colorSpace: deviceColorSpace]) } + +private extension UIImage.Orientation { + var exifOrientation: Int32 { + switch self { + case .up: return 1 + case .down: return 3 + case .left: return 8 + case .right: return 6 + case .upMirrored: return 2 + case .downMirrored: return 4 + case .leftMirrored: return 5 + case .rightMirrored: return 7 + @unknown default: + return 0 + } + } +} diff --git a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift index 75098f129c..b4acc02387 100644 --- a/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift +++ b/submodules/TelegramUI/Components/MediaEditor/Sources/MediaEditorRenderer.swift @@ -150,6 +150,8 @@ final class MediaEditorRenderer: TextureConsumer { self.renderPasses.forEach { $0.setup(device: device, library: library) } } + var renderPassedEnabled = true + func renderFrame() { let device: MTLDevice? if let renderTarget = self.renderTarget { @@ -181,9 +183,11 @@ final class MediaEditorRenderer: TextureConsumer { return } - for renderPass in self.renderPasses { - if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) { - texture = nextTexture + if self.renderPassedEnabled { + for renderPass in self.renderPasses { + if let nextTexture = renderPass.process(input: texture, device: device, commandBuffer: commandBuffer) { + texture = nextTexture + } } } self.finalTexture = texture diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD index f7058eeb00..2c3f064635 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/BUILD +++ b/submodules/TelegramUI/Components/MediaEditorScreen/BUILD @@ -37,6 +37,7 @@ swift_library( "//submodules/Components/BlurredBackgroundComponent", "//submodules/AvatarNode", "//submodules/TelegramUI/Components/ShareWithPeersScreen", + "//submodules/TelegramUI/Components/CameraButtonComponent", ], visibility = [ "//visibility:public", diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift index fd384c23ee..bc3c5a6c00 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/AdjustmentsComponent.swift @@ -386,3 +386,73 @@ final class AdjustmentsComponent: Component { } } +final class AdjustmentsScreenComponent: Component { + typealias EnvironmentType = Empty + + let toggleUneditedPreview: (Bool) -> Void + + init( + toggleUneditedPreview: @escaping (Bool) -> Void + ) { + self.toggleUneditedPreview = toggleUneditedPreview + } + + static func ==(lhs: AdjustmentsScreenComponent, rhs: AdjustmentsScreenComponent) -> Bool { + return true + } + + final class View: UIView { + enum Field { + case blacks + case shadows + case midtones + case highlights + case whites + } + + private var component: AdjustmentsScreenComponent? + private weak var state: EmptyComponentState? + + override init(frame: CGRect) { + super.init(frame: frame) + + let longPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.handleLongPress(_:))) + longPressGestureRecognizer.minimumPressDuration = 0.05 + self.addGestureRecognizer(longPressGestureRecognizer) + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + @objc func handleLongPress(_ gestureRecognizer: UIPanGestureRecognizer) { + guard let component = self.component else { + return + } + + switch gestureRecognizer.state { + case .began: + component.toggleUneditedPreview(true) + case .ended, .cancelled: + component.toggleUneditedPreview(false) + default: + break + } + } + + func update(component: AdjustmentsScreenComponent, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + self.component = component + self.state = state + + return availableSize + } + } + + public func makeView() -> View { + return View(frame: CGRect()) + } + + public func update(view: View, availableSize: CGSize, state: EmptyComponentState, environment: Environment, transition: Transition) -> CGSize { + return view.update(component: self, availableSize: availableSize, state: state, environment: environment, transition: transition) + } +} diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift index 6c577a80a5..efc391eebe 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaEditorScreen.swift @@ -1,5 +1,6 @@ import Foundation import UIKit +import CoreServices import Display import AsyncDisplayKit import ComponentFlow @@ -23,6 +24,7 @@ import ShareWithPeersScreen import PresentationDataUtils import ContextUI import BundleIconComponent +import CameraButtonComponent enum DrawingScreenType { case drawing @@ -41,6 +43,7 @@ final class MediaEditorScreenComponent: Component { let isDisplayingTool: Bool let isInteractingWithEntities: Bool let isSavingAvailable: Bool + let hasAppeared: Bool let isDismissing: Bool let mediaEditor: MediaEditor? let privacy: MediaEditorResultPrivacy @@ -54,6 +57,7 @@ final class MediaEditorScreenComponent: Component { isDisplayingTool: Bool, isInteractingWithEntities: Bool, isSavingAvailable: Bool, + hasAppeared: Bool, isDismissing: Bool, mediaEditor: MediaEditor?, privacy: MediaEditorResultPrivacy, @@ -66,6 +70,7 @@ final class MediaEditorScreenComponent: Component { self.isDisplayingTool = isDisplayingTool self.isInteractingWithEntities = isInteractingWithEntities self.isSavingAvailable = isSavingAvailable + self.hasAppeared = hasAppeared self.isDismissing = isDismissing self.mediaEditor = mediaEditor self.privacy = privacy @@ -88,6 +93,9 @@ final class MediaEditorScreenComponent: Component { if lhs.isSavingAvailable != rhs.isSavingAvailable { return false } + if lhs.hasAppeared != rhs.hasAppeared { + return false + } if lhs.isDismissing != rhs.isDismissing { return false } @@ -175,6 +183,8 @@ final class MediaEditorScreenComponent: Component { deinit { self.playerStateDisposable?.dispose() } + + var muteDidChange = false } func makeState() -> State { @@ -239,7 +249,7 @@ final class MediaEditorScreenComponent: Component { case camera case gallery } - func animateIn(from source: TransitionAnimationSource) { + func animateIn(from source: TransitionAnimationSource, completion: @escaping () -> Void = {}) { let buttons = [ self.drawButton, self.textButton, @@ -269,6 +279,8 @@ final class MediaEditorScreenComponent: Component { view.layer.animateScale(from: 0.1, to: 1.0, duration: 0.2, delay: 0.0) }) delay += 0.03 + + Queue.mainQueue().after(0.45, completion) } } @@ -452,11 +464,28 @@ final class MediaEditorScreenComponent: Component { self.component = component self.state = state + let isTablet: Bool + if case .regular = environment.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + let openDrawing = component.openDrawing let openTools = component.openTools - let buttonSideInset: CGFloat = 10.0 + let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 + let previewSize: CGSize + let topInset: CGFloat = environment.statusBarHeight + 12.0 + if isTablet { + let previewHeight = availableSize.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + buttonSideInset = 30.0 + } else { + previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) + buttonSideInset = 10.0 + } let cancelButtonSize = self.cancelButton.update( transition: transition, @@ -534,6 +563,16 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: doneButtonView, alpha: component.isDisplayingTool || component.isDismissing || component.isInteractingWithEntities ? 0.0 : 1.0) } + let buttonsAvailableWidth: CGFloat + let buttonsLeftOffset: CGFloat + if isTablet { + buttonsAvailableWidth = previewSize.width + 260.0 + buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) + } else { + buttonsAvailableWidth = availableSize.width + buttonsLeftOffset = 0.0 + } + let drawButtonSize = self.drawButton.update( transition: transition, component: AnyComponent(Button( @@ -549,7 +588,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let drawButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - drawButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: drawButtonSize ) if let drawButtonView = self.drawButton.view { @@ -576,7 +615,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let textButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - textButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: textButtonSize ) if let textButtonView = self.textButton.view { @@ -603,7 +642,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let stickerButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - stickerButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: stickerButtonSize ) if let stickerButtonView = self.stickerButton.view { @@ -630,7 +669,7 @@ final class MediaEditorScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let toolsButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - toolsButtonSize.width / 2.0), y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), size: toolsButtonSize ) if let toolsButtonView = self.toolsButton.view { @@ -719,6 +758,14 @@ final class MediaEditorScreenComponent: Component { timeoutSelected = timeout != nil } + + var inputPanelAvailableWidth = previewSize.width + if case .regular = environment.metrics.widthClass { + if (self.inputPanelExternalState.isEditing || self.inputPanelExternalState.hasText) { + inputPanelAvailableWidth += 200.0 + } + } + self.inputPanel.parentState = state let inputPanelSize = self.inputPanel.update( transition: transition, @@ -765,7 +812,7 @@ final class MediaEditorScreenComponent: Component { bottomInset: 0.0 )), environment: {}, - containerSize: CGSize(width: availableSize.width, height: 200.0) + containerSize: CGSize(width: inputPanelAvailableWidth, height: 200.0) ) let fadeTransition = Transition(animation: .curve(duration: 0.3, curve: .easeInOut)) @@ -802,7 +849,7 @@ final class MediaEditorScreenComponent: Component { inputPanelBottomInset = environment.inputHeight - environment.safeInsets.bottom inputPanelOffset = inputPanelBottomInset } - let inputPanelFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) + let inputPanelFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - inputPanelSize.width) / 2.0), y: availableSize.height - environment.safeInsets.bottom - inputPanelBottomInset - inputPanelSize.height - 3.0), size: inputPanelSize) if let inputPanelView = self.inputPanel.view { if inputPanelView.superview == nil { self.addSubview(inputPanelView) @@ -839,6 +886,7 @@ final class MediaEditorScreenComponent: Component { component: AnyComponent(Button( content: AnyComponent( PrivacyButtonComponent( + backgroundColor: isTablet ? UIColor(rgb: 0x303030, alpha: 0.5) : UIColor(white: 0.0, alpha: 0.5), icon: UIImage(bundleImageName: "Media Editor/Recipient")!, text: privacyText ) @@ -852,10 +900,18 @@ final class MediaEditorScreenComponent: Component { environment: {}, containerSize: CGSize(width: 44.0, height: 44.0) ) - let privacyButtonFrame = CGRect( - origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset), - size: privacyButtonSize - ) + let privacyButtonFrame: CGRect + if isTablet { + privacyButtonFrame = CGRect( + origin: CGPoint(x: availableSize.width - buttonSideInset - doneButtonSize.width - privacyButtonSize.width - 24.0, y: availableSize.height - environment.safeInsets.bottom + buttonBottomInset + 1.0), + size: privacyButtonSize + ) + } else { + privacyButtonFrame = CGRect( + origin: CGPoint(x: 16.0, y: environment.safeInsets.top + 20.0 - inputPanelOffset), + size: privacyButtonSize + ) + } if let privacyButtonView = self.privacyButton.view { if privacyButtonView.superview == nil { self.addSubview(privacyButtonView) @@ -866,10 +922,11 @@ final class MediaEditorScreenComponent: Component { transition.setAlpha(view: privacyButtonView, alpha: displayTopButtons && !component.isDismissing && !component.isInteractingWithEntities ? 1.0 : 0.0) } - let saveButtonSize = self.saveButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( + let saveContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + saveContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_storysave", @@ -877,9 +934,26 @@ final class MediaEditorScreenComponent: Component { range: nil ), colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) + size: CGSize(width: 30.0, height: 30.0) ).tagged(saveButtonTag) - ), + ) + ) + } else { + saveContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/SaveIcon", + tintColor: nil + ) + ) + ) + } + + let saveButtonSize = self.saveButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: saveContentComponent, action: { [weak self] in if let view = self?.saveButton.findTaggedView(tag: saveButtonTag) as? LottieAnimationComponent.View { view.playOnce() @@ -916,22 +990,42 @@ final class MediaEditorScreenComponent: Component { if let playerState = state.playerState, playerState.hasAudio { let isVideoMuted = component.mediaEditor?.values.videoIsMuted ?? false - let muteButtonSize = self.muteButton.update( - transition: transition, - component: AnyComponent(Button( - content: AnyComponent( + + let muteContentComponent: AnyComponentWithIdentity + if component.hasAppeared { + muteContentComponent = AnyComponentWithIdentity( + id: "animatedIcon", + component: AnyComponent( LottieAnimationComponent( animation: LottieAnimationComponent.AnimationItem( name: "anim_storymute", - mode: .animating(loop: false), + mode: state.muteDidChange ? .animating(loop: false) : .still(position: .begin), range: isVideoMuted ? (0.0, 0.5) : (0.5, 1.0) ), colors: ["__allcolors__": .white], - size: CGSize(width: 33.0, height: 33.0) + size: CGSize(width: 30.0, height: 30.0) ).tagged(muteButtonTag) - ), + ) + ) + } else { + muteContentComponent = AnyComponentWithIdentity( + id: "staticIcon", + component: AnyComponent( + BundleIconComponent( + name: "Media Editor/MuteIcon", + tintColor: nil + ) + ) + ) + } + + let muteButtonSize = self.muteButton.update( + transition: transition, + component: AnyComponent(CameraButton( + content: muteContentComponent, action: { [weak self, weak state] in if let self, let mediaEditor = self.component?.mediaEditor { + state?.muteDidChange = true let isMuted = !mediaEditor.values.videoIsMuted mediaEditor.setVideoIsMuted(isMuted) state?.updated() @@ -1106,7 +1200,7 @@ final class MediaEditorScreenComponent: Component { private let storyDimensions = CGSize(width: 1080.0, height: 1920.0) private let storyMaxVideoDuration: Double = 60.0 -public final class MediaEditorScreen: ViewController { +public final class MediaEditorScreen: ViewController, UIDropInteractionDelegate { public enum TransitionIn { public final class GalleryTransitionIn { public weak var sourceView: UIView? @@ -1166,6 +1260,7 @@ public final class MediaEditorScreen: ViewController { private var wasPlaying = false private let backgroundDimView: UIView + fileprivate let containerView: UIView fileprivate let componentHost: ComponentView fileprivate let storyPreview: ComponentView fileprivate let toolValue: ComponentView @@ -1190,6 +1285,8 @@ public final class MediaEditorScreen: ViewController { private var isDisplayingTool = false private var isInteractingWithEntities = false private var isEnhancing = false + + private var hasAppeared = false private var isDismissing = false private var dismissOffset: CGFloat = 0.0 private var isDismissed = false @@ -1207,6 +1304,9 @@ public final class MediaEditorScreen: ViewController { self.backgroundDimView.isHidden = true self.backgroundDimView.backgroundColor = .black + self.containerView = UIView() + self.containerView.clipsToBounds = true + self.componentHost = ComponentView() self.storyPreview = ComponentView() self.toolValue = ComponentView() @@ -1241,7 +1341,8 @@ public final class MediaEditorScreen: ViewController { self.backgroundColor = .clear self.view.addSubview(self.backgroundDimView) - self.view.addSubview(self.previewContainerView) + self.view.addSubview(self.containerView) + self.containerView.addSubview(self.previewContainerView) self.previewContainerView.addSubview(self.gradientView) self.previewContainerView.addSubview(self.entitiesContainerView) self.entitiesContainerView.addSubview(self.entitiesView) @@ -1276,7 +1377,7 @@ public final class MediaEditorScreen: ViewController { areUnicodeEmojiEnabled: true, areCustomEmojiEnabled: true, chatPeerId: controller.context.account.peerId, - hasSearch: false, + hasSearch: true, forceHasPremium: true ) @@ -1287,7 +1388,7 @@ public final class MediaEditorScreen: ViewController { stickerNamespaces: [Namespaces.ItemCollection.CloudStickerPacks], stickerOrderedItemListCollectionIds: [Namespaces.OrderedItemList.CloudSavedStickers, Namespaces.OrderedItemList.CloudRecentStickers, Namespaces.OrderedItemList.CloudAllPremiumStickers], chatPeerId: controller.context.account.peerId, - hasSearch: false, + hasSearch: true, hasTrending: true, forceHasPremium: true ) @@ -1370,8 +1471,8 @@ public final class MediaEditorScreen: ViewController { mediaEntity.scale = storyDimensions.width / fittedSize.width } self.entitiesView.add(mediaEntity, announce: false) - - if case let .image(_, _, additionalImage) = subject, let additionalImage { + + if case let .image(_, _, additionalImage, position) = subject, let additionalImage { let image = generateImage(CGSize(width: additionalImage.size.width, height: additionalImage.size.width), contextGenerator: { size, context in let bounds = CGRect(origin: .zero, size: size) context.clear(bounds) @@ -1386,8 +1487,15 @@ public final class MediaEditorScreen: ViewController { imageEntity.referenceDrawingSize = storyDimensions imageEntity.scale = 1.49 imageEntity.mirrored = true - imageEntity.position = CGPoint(x: storyDimensions.width - 224.0, y: storyDimensions.height - 403.0) + imageEntity.position = position.getPosition(storyDimensions) self.entitiesView.add(imageEntity, announce: false) + } else if case let .video(_, _, additionalVideoPath, additionalVideoImage, _, position) = subject, let additionalVideoPath { + let videoEntity = DrawingStickerEntity(content: .video(additionalVideoPath, additionalVideoImage)) + videoEntity.referenceDrawingSize = storyDimensions + videoEntity.scale = 1.49 + videoEntity.mirrored = true + videoEntity.position = position.getPosition(storyDimensions) + self.entitiesView.add(videoEntity, announce: false) } let initialPosition = mediaEntity.position @@ -1451,6 +1559,31 @@ public final class MediaEditorScreen: ViewController { } }) self.mediaEditor = mediaEditor + + mediaEditor.onPlaybackAction = { [weak self] action in + if let self { + switch action { + case .play: + self.entitiesView.eachView({ view in + if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content { + view.play() + } + }) + case .pause: + self.entitiesView.eachView({ view in + if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content { + view.pause() + } + }) + case let .seek(timestamp): + self.entitiesView.eachView({ view in + if let sticker = view.entity as? DrawingStickerEntity, case .video = sticker.content { + view.seek(to: timestamp) + } + }) + } + } + } } override func didLoad() { @@ -1679,13 +1812,19 @@ public final class MediaEditorScreen: ViewController { } func animateIn() { + let completion: () -> Void = { [weak self] in + Queue.mainQueue().after(0.1) { + self?.requestUpdate(hasAppeared: true, transition: .immediate) + } + } + if let transitionIn = self.controller?.transitionIn { switch transitionIn { case .camera: if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateIn(from: .camera) + view.animateIn(from: .camera, completion: completion) } - if let subject = self.subject, case let .video(_, transitionImage, _) = subject, let transitionImage { + if let subject = self.subject, case let .video(_, transitionImage, _, _, _, _) = subject, let transitionImage { self.setupTransitionImage(transitionImage) } case let .gallery(transitionIn): @@ -1703,7 +1842,9 @@ public final class MediaEditorScreen: ViewController { let duration: Double = 0.4 - self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring) + self.previewContainerView.layer.animatePosition(from: sourceLocalFrame.center, to: self.previewContainerView.center, duration: duration, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in + completion() + }) 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) @@ -1719,7 +1860,7 @@ public final class MediaEditorScreen: ViewController { } } else { if let view = self.componentHost.view as? MediaEditorScreenComponent.View { - view.animateIn(from: .camera) + view.animateIn(from: .camera, completion: completion) } } @@ -1778,19 +1919,12 @@ public final class MediaEditorScreen: ViewController { snapshotView.layer.anchorPoint = CGPoint(x: 0.0, y: 0.5) let snapshotScale = self.previewContainerView.bounds.width / snapshotView.frame.width snapshotView.center = CGPoint(x: 0.0, y: self.previewContainerView.bounds.height / 2.0) - - let snapshotTransform = CATransform3DMakeScale(0.001, snapshotScale, 1.0) - //snapshotTransform.m34 = 1.0 / -500 - //snapshotTransform = CATransform3DRotate(snapshotTransform, -90.0 * .pi / 180.0, 0.0, 1.0, 0.0) + snapshotView.layer.transform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0) - let targetTransform = CATransform3DMakeScale(snapshotScale, snapshotScale, 1.0) - //snapshotTransform - //targetTransform = CATransform3DRotate(targetTransform, 0.0, 0.0, 1.0, 0.0) - - snapshotView.layer.transform = snapshotTransform + snapshotView.alpha = 0.0 Queue.mainQueue().after(0.15) { - snapshotView.layer.transform = targetTransform - snapshotView.layer.animate(from: NSValue(caTransform3D: snapshotTransform), to: NSValue(caTransform3D: targetTransform), keyPath: "transform", timingFunction: kCAMediaTimingFunctionSpring, duration: 0.25) + snapshotView.alpha = 1.0 + snapshotView.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25) } self.previewContainerView.addSubview(snapshotView) @@ -2036,6 +2170,23 @@ public final class MediaEditorScreen: ViewController { self.controller?.present(tooltipController, in: .current) } + func updateModalTransitionFactor(_ value: CGFloat, transition: ContainedViewLayoutTransition) { + guard let layout = self.validLayout, case .compact = layout.metrics.widthClass else { + return + } + + let progress = 1.0 - value + let maxScale = (layout.size.width - 16.0 * 2.0) / layout.size.width + + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let targetTopInset = ceil((layout.statusBarHeight ?? 0.0) - (layout.size.height - layout.size.height * maxScale) / 2.0) + let deltaOffset = (targetTopInset - topInset) + + let scale = 1.0 * progress + (1.0 - progress) * maxScale + let offset = (1.0 - progress) * deltaOffset + transition.updateSublayerTransformScaleAndOffset(layer: self.containerView.layer, scale: scale, offset: CGPoint(x: 0.0, y: offset), beginWithCurrentState: true) + } + override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? { let result = super.hitTest(point, with: event) if result == self.componentHost.view { @@ -2045,24 +2196,44 @@ public final class MediaEditorScreen: ViewController { return result } - func requestUpdate(transition: Transition = .immediate) { + func requestUpdate(hasAppeared: Bool = false, transition: Transition = .immediate) { if let layout = self.validLayout { - self.containerLayoutUpdated(layout: layout, transition: transition) + self.containerLayoutUpdated(layout: layout, hasAppeared: hasAppeared, transition: transition) } } private var drawingScreen: DrawingScreen? - func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, animateOut: Bool = false, transition: Transition) { + private var stickerScreen: StickerPickerScreen? + + func containerLayoutUpdated(layout: ContainerViewLayout, forceUpdate: Bool = false, hasAppeared: Bool = false, transition: Transition) { guard let controller = self.controller, !self.isDismissed else { return } let isFirstTime = self.validLayout == nil self.validLayout = layout + + let isTablet: Bool + if case .regular = layout.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } - let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) - let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 //floorToScreenPixels(layout.size.height - previewSize.height) / 2.0 + let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + let previewSize: CGSize + if isTablet { + let previewHeight = layout.size.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + } let bottomInset = layout.size.height - previewSize.height - topInset + var inputHeight = layout.inputHeight ?? 0.0 + if self.stickerScreen != nil { + inputHeight = 0.0 + } + let environment = ViewControllerComponentContainer.Environment( statusBarHeight: layout.statusBarHeight ?? 0.0, navigationHeight: 0.0, @@ -2072,7 +2243,7 @@ public final class MediaEditorScreen: ViewController { bottom: bottomInset, right: layout.safeInsets.right ), - inputHeight: layout.inputHeight ?? 0.0, + inputHeight: inputHeight, metrics: layout.metrics, deviceMetrics: layout.deviceMetrics, orientation: nil, @@ -2084,6 +2255,10 @@ public final class MediaEditorScreen: ViewController { return self?.controller } ) + + if hasAppeared && !self.hasAppeared { + self.hasAppeared = hasAppeared + } let componentSize = self.componentHost.update( transition: transition, @@ -2093,6 +2268,7 @@ public final class MediaEditorScreen: ViewController { isDisplayingTool: self.isDisplayingTool, isInteractingWithEntities: self.isInteractingWithEntities, isSavingAvailable: controller.isSavingAvailable, + hasAppeared: self.hasAppeared, isDismissing: self.isDismissing, mediaEditor: self.mediaEditor, privacy: controller.state.privacy, @@ -2113,14 +2289,24 @@ public final class MediaEditorScreen: ViewController { 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.interaction?.insertEntity(stickerEntity) - - self.controller?.isSavingAvailable = true - self.controller?.requestLayout(transition: .immediate) + if let self { + if let file { + let stickerEntity = DrawingStickerEntity(content: .file(file)) + self.interaction?.insertEntity(stickerEntity) + + self.controller?.isSavingAvailable = true + self.controller?.requestLayout(transition: .immediate) + } + self.stickerScreen = nil } } + controller.customModalStyleOverlayTransitionFactorUpdated = { [weak self, weak controller] transition in + if let self, let controller { + let transitionFactor = controller.modalStyleOverlayTransitionFactor + self.updateModalTransitionFactor(transitionFactor, transition: transition) + } + } + self.stickerScreen = controller self.controller?.present(controller, in: .current) return case .text: @@ -2189,12 +2375,12 @@ public final class MediaEditorScreen: 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.addSubview(componentView) componentView.clipsToBounds = true } transition.setFrame(view: componentView, frame: CGRect(origin: CGPoint(x: 0.0, y: self.dismissOffset), size: componentSize)) @@ -2248,15 +2434,20 @@ public final class MediaEditorScreen: ViewController { transition.setAlpha(view: self.backgroundDimView, alpha: self.isDismissing ? 0.0 : 1.0) var bottomInputOffset: CGFloat = 0.0 - if let inputHeight = layout.inputHeight, inputHeight > 0.0 { - if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { - bottomInputOffset = inputHeight / 2.0 - } else { - bottomInputOffset = inputHeight - bottomInset - 17.0 + if inputHeight > 0.0 { + if self.stickerScreen == nil { + if self.entitiesView.selectedEntityView != nil || self.isDisplayingTool { + bottomInputOffset = inputHeight / 2.0 + } else { + bottomInputOffset = inputHeight - bottomInset - 17.0 + } } } - let previewFrame = CGRect(origin: CGPoint(x: 0.0, y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize) + 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)) + + let previewFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((layout.size.width - previewSize.width) / 2.0), y: topInset - bottomInputOffset + self.dismissOffset), size: previewSize) transition.setFrame(view: self.previewContainerView, frame: previewFrame) let entitiesViewScale = previewSize.width / storyDimensions.width self.entitiesContainerView.transform = CGAffineTransformMakeScale(entitiesViewScale, entitiesViewScale) @@ -2278,15 +2469,35 @@ public final class MediaEditorScreen: ViewController { return self.displayNode as! Node } + public enum PIPPosition { + case topLeft + case topRight + case bottomLeft + case bottomRight + + func getPosition(_ size: CGSize) -> CGPoint { + switch self { + case .topLeft: + return CGPoint(x: 224.0, y: 477.0) + case .topRight: + return CGPoint(x: size.width - 224.0, y: 477.0) + case .bottomLeft: + return CGPoint(x: 224.0, y: size.height - 477.0) + case .bottomRight: + return CGPoint(x: size.width - 224.0, y: size.height - 477.0) + } + } + } + public enum Subject { - case image(UIImage, PixelDimensions, UIImage?) - case video(String, UIImage?, PixelDimensions) + case image(UIImage, PixelDimensions, UIImage?, PIPPosition) + case video(String, UIImage?, String?, UIImage?, PixelDimensions, PIPPosition) case asset(PHAsset) case draft(MediaEditorDraft, Int64?) var dimensions: PixelDimensions { switch self { - case let .image(_, dimensions, _), let .video(_, _, dimensions): + case let .image(_, dimensions, _, _), let .video(_, _, _, _, dimensions, _): return dimensions case let .asset(asset): return PixelDimensions(width: Int32(asset.pixelWidth), height: Int32(asset.pixelHeight)) @@ -2297,9 +2508,9 @@ public final class MediaEditorScreen: ViewController { var editorSubject: MediaEditor.Subject { switch self { - case let .image(image, dimensions, _): + case let .image(image, dimensions, _, _): return .image(image, dimensions) - case let .video(videoPath, transitionImage, dimensions): + case let .video(videoPath, transitionImage, _, _, dimensions, _): return .video(videoPath, transitionImage, dimensions) case let .asset(asset): return .asset(asset) @@ -2310,9 +2521,9 @@ public final class MediaEditorScreen: ViewController { var mediaContent: DrawingMediaEntity.Content { switch self { - case let .image(image, dimensions, _): + case let .image(image, dimensions, _, _): return .image(image, dimensions) - case let .video(videoPath, _, dimensions): + case let .video(videoPath, _, _, _, dimensions, _): return .video(videoPath, dimensions) case let .asset(asset): return .asset(asset) @@ -2381,6 +2592,9 @@ public final class MediaEditorScreen: ViewController { self.displayNode = Node(controller: self) super.displayNodeDidLoad() + + let dropInteraction = UIDropInteraction(delegate: self) + self.displayNode.view.addInteraction(dropInteraction) } func openPrivacySettings() { @@ -2735,9 +2949,9 @@ public final class MediaEditorScreen: ViewController { } switch subject { - case let .image(image, dimensions, _): + case let .image(image, dimensions, _, _): saveImageDraft(image, dimensions) - case let .video(path, _, dimensions): + case let .video(path, _, _, _, dimensions, _): saveVideoDraft(path, dimensions) case let .asset(asset): if asset.mediaType == .video { @@ -2780,6 +2994,7 @@ public final class MediaEditorScreen: ViewController { self.dismissAllTooltips() + mediaEditor.seek(0.0, andPlay: false) mediaEditor.invalidate() self.node.entitiesView.invalidate() @@ -2802,14 +3017,14 @@ public final class MediaEditorScreen: ViewController { let videoResult: Result.VideoResult let duration: Double switch subject { - case let .image(image, _, _): + case let .image(image, _, _, _): let tempImagePath = NSTemporaryDirectory() + "\(Int64.random(in: Int64.min ... Int64.max)).jpg" if let data = image.jpegData(compressionQuality: 0.85) { try? data.write(to: URL(fileURLWithPath: tempImagePath)) } videoResult = .imageFile(path: tempImagePath) duration = 5.0 - case let .video(path, _, _): + case let .video(path, _, _, _, _, _): videoResult = .videoFile(path: path) if let videoTrimRange = mediaEditor.values.videoTrimRange { duration = videoTrimRange.upperBound - videoTrimRange.lowerBound @@ -2840,14 +3055,20 @@ public final class MediaEditorScreen: ViewController { duration = 5.0 } } - self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in - self?.node.animateOut(finished: true, completion: { [weak self] in - self?.dismiss() - Queue.mainQueue().justDispatch { - finished() - } - }) - }) + + +// makeEditorImageComposition(account: self.context.account, inputImage: image, dimensions: storyDimensions, values: mediaEditor.values, time: .zero, completion: { [weak self] coverImage in +// if let self { + self.completion(randomId, .video(video: videoResult, coverImage: nil, values: mediaEditor.values, duration: duration, dimensions: mediaEditor.values.resultDimensions, caption: caption), self.state.privacy, { [weak self] finished in + self?.node.animateOut(finished: true, completion: { [weak self] in + self?.dismiss() + Queue.mainQueue().justDispatch { + finished() + } + }) + }) +// } +// }) if case let .draft(draft, id) = subject, id == nil { removeStoryDraft(engine: self.context.engine, path: draft.path, delete: true) @@ -2925,10 +3146,10 @@ public final class MediaEditorScreen: ViewController { let exportSubject: Signal switch subject { - case let .video(path, _, _): + case let .video(path, _, _, _, _, _): let asset = AVURLAsset(url: NSURL(fileURLWithPath: path) as URL) exportSubject = .single(.video(asset)) - case let .image(image, _, _): + case let .image(image, _, _, _): exportSubject = .single(.image(image)) case let .asset(asset): exportSubject = Signal { subscriber in @@ -3061,21 +3282,60 @@ public final class MediaEditorScreen: ViewController { (self.displayNode as! Node).containerLayoutUpdated(layout: layout, transition: Transition(transition)) } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, canHandle session: UIDropSession) -> Bool { + return session.hasItemsConforming(toTypeIdentifiers: [kUTTypeImage as String]) + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidUpdate session: UIDropSession) -> UIDropProposal { + let operation: UIDropOperation + operation = .copy + return UIDropProposal(operation: operation) + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, performDrop session: UIDropSession) { + session.loadObjects(ofClass: UIImage.self) { [weak self] imageItems in + guard let self else { + return + } + let images = imageItems as! [UIImage] + if images.count == 1, let image = images.first, max(image.size.width, image.size.height) > 1.0 { + self.node.interaction?.insertEntity(DrawingStickerEntity(content: .image(image))) + } + } + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidExit session: UIDropSession) { + } + + @available(iOSApplicationExtension 11.0, iOS 11.0, *) + public func dropInteraction(_ interaction: UIDropInteraction, sessionDidEnd session: UIDropSession) { + } } final class PrivacyButtonComponent: CombinedComponent { + let backgroundColor: UIColor let icon: UIImage let text: String init( + backgroundColor: UIColor, icon: UIImage, text: String ) { + self.backgroundColor = backgroundColor self.icon = icon self.text = text } static func ==(lhs: PrivacyButtonComponent, rhs: PrivacyButtonComponent) -> Bool { + if lhs.backgroundColor != rhs.backgroundColor { + return false + } if lhs.text != rhs.text { return false } @@ -3106,7 +3366,7 @@ final class PrivacyButtonComponent: CombinedComponent { let backgroundSize = CGSize(width: text.size.width + 38.0, height: 30.0) let background = background.update( - component: BlurredBackgroundComponent(color: UIColor(white: 0.0, alpha: 0.5)), + component: BlurredBackgroundComponent(color: context.component.backgroundColor), availableSize: backgroundSize, transition: .immediate ) diff --git a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift index bf8070a374..ae5a6d0f1b 100644 --- a/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift +++ b/submodules/TelegramUI/Components/MediaEditorScreen/Sources/MediaToolsScreen.swift @@ -323,16 +323,33 @@ private final class MediaToolsScreenComponent: Component { self.component = component self.state = state + let isTablet: Bool + if case .regular = environment.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } + let mediaEditor = (environment.controller() as? MediaToolsScreen)?.mediaEditor let sectionUpdated = component.sectionUpdated - - let previewContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: environment.safeInsets.top), size: CGSize(width: availableSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom)) - let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom)) - - let buttonSideInset: CGFloat = 10.0 + + let buttonSideInset: CGFloat let buttonBottomInset: CGFloat = 8.0 - + let previewSize: CGSize + let topInset: CGFloat = environment.statusBarHeight + 12.0 + if isTablet { + let previewHeight = availableSize.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + buttonSideInset = 30.0 + } else { + previewSize = CGSize(width: availableSize.width, height: floorToScreenPixels(availableSize.width * 1.77778)) + buttonSideInset = 10.0 + } + + let previewContainerFrame = CGRect(origin: CGPoint(x: floorToScreenPixels((availableSize.width - previewSize.width) / 2.0), y: environment.safeInsets.top), size: CGSize(width: previewSize.width, height: availableSize.height - environment.safeInsets.top - environment.safeInsets.bottom)) + let buttonsContainerFrame = CGRect(origin: CGPoint(x: 0.0, y: availableSize.height - environment.safeInsets.bottom), size: CGSize(width: availableSize.width, height: environment.safeInsets.bottom)) + let cancelButtonSize = self.cancelButton.update( transition: transition, component: AnyComponent(Button( @@ -396,6 +413,16 @@ private final class MediaToolsScreenComponent: Component { transition.setFrame(view: doneButtonView, frame: doneButtonFrame) } + let buttonsAvailableWidth: CGFloat + let buttonsLeftOffset: CGFloat + if isTablet { + buttonsAvailableWidth = previewSize.width + 260.0 + buttonsLeftOffset = floorToScreenPixels((availableSize.width - buttonsAvailableWidth) / 2.0) + } else { + buttonsAvailableWidth = availableSize.width + buttonsLeftOffset = 0.0 + } + let adjustmentsButtonSize = self.adjustmentsButton.update( transition: transition, component: AnyComponent(Button( @@ -412,7 +439,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let adjustmentsButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 - 3.0 - adjustmentsButtonSize.width / 2.0), y: buttonBottomInset), size: adjustmentsButtonSize ) if let adjustmentsButtonView = self.adjustmentsButton.view { @@ -438,7 +465,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let tintButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 2.5 + 5.0 - tintButtonSize.width / 2.0), y: buttonBottomInset), size: tintButtonSize ) if let tintButtonView = self.tintButton.view { @@ -464,7 +491,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let blurButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width - availableSize.width / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: floorToScreenPixels(availableSize.width - buttonsLeftOffset - buttonsAvailableWidth / 2.5 - 5.0 - blurButtonSize.width / 2.0), y: buttonBottomInset), size: blurButtonSize ) if let blurButtonView = self.blurButton.view { @@ -490,7 +517,7 @@ private final class MediaToolsScreenComponent: Component { containerSize: CGSize(width: 40.0, height: 40.0) ) let curvesButtonFrame = CGRect( - origin: CGPoint(x: floorToScreenPixels(availableSize.width / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset), + origin: CGPoint(x: buttonsLeftOffset + floorToScreenPixels(buttonsAvailableWidth / 4.0 * 3.0 + 3.0 - curvesButtonSize.width / 2.0), y: buttonBottomInset), size: curvesButtonSize ) if let curvesButtonView = self.curvesButton.view { @@ -640,10 +667,31 @@ private final class MediaToolsScreenComponent: Component { } )), environment: {}, - containerSize: availableSize + containerSize: previewContainerFrame.size + ) + + let adjustmentsToolScreen: ComponentView + if let current = self.toolScreen, !sectionChanged { + adjustmentsToolScreen = current + } else { + adjustmentsToolScreen = ComponentView() + self.toolScreen = adjustmentsToolScreen + } + toolScreen = adjustmentsToolScreen + screenSize = adjustmentsToolScreen.update( + transition: optionsTransition, + component: AnyComponent( + AdjustmentsScreenComponent( + toggleUneditedPreview: { preview in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setPreviewUnedited(preview) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) ) - screenSize = previewContainerFrame.size - self.toolScreen = nil case .tint: self.curvesState = nil optionsSize = self.toolOptions.update( @@ -676,10 +724,31 @@ private final class MediaToolsScreenComponent: Component { } )), environment: {}, - containerSize: availableSize + containerSize: previewContainerFrame.size + ) + + let tintToolScreen: ComponentView + if let current = self.toolScreen, !sectionChanged { + tintToolScreen = current + } else { + tintToolScreen = ComponentView() + self.toolScreen = tintToolScreen + } + toolScreen = tintToolScreen + screenSize = tintToolScreen.update( + transition: optionsTransition, + component: AnyComponent( + AdjustmentsScreenComponent( + toggleUneditedPreview: { preview in + if let controller = environment.controller() as? MediaToolsScreen { + controller.mediaEditor.setPreviewUnedited(preview) + } + } + ) + ), + environment: {}, + containerSize: CGSize(width: previewContainerFrame.width, height: previewContainerFrame.height - optionsSize.height) ) - screenSize = previewContainerFrame.size - self.toolScreen = nil case .blur: self.curvesState = nil optionsSize = self.toolOptions.update( @@ -706,7 +775,7 @@ private final class MediaToolsScreenComponent: Component { } )), environment: {}, - containerSize: availableSize + containerSize: previewContainerFrame.size ) let blurToolScreen: ComponentView @@ -764,7 +833,7 @@ private final class MediaToolsScreenComponent: Component { internalState: internalState )), environment: {}, - containerSize: availableSize + containerSize: previewContainerFrame.size ) let curvesToolScreen: ComponentView @@ -917,9 +986,22 @@ public final class MediaToolsScreen: ViewController { } let isFirstTime = self.validLayout == nil self.validLayout = layout + + let isTablet: Bool + if case .regular = layout.metrics.widthClass { + isTablet = true + } else { + isTablet = false + } - let previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + let previewSize: CGSize let topInset: CGFloat = (layout.statusBarHeight ?? 0.0) + 12.0 + if isTablet { + let previewHeight = layout.size.height - topInset - 75.0 + previewSize = CGSize(width: floorToScreenPixels(previewHeight / 1.77778), height: previewHeight) + } else { + previewSize = CGSize(width: layout.size.width, height: floorToScreenPixels(layout.size.width * 1.77778)) + } let bottomInset = layout.size.height - previewSize.height - topInset let environment = ViewControllerComponentContainer.Environment( @@ -944,13 +1026,6 @@ public final class MediaToolsScreen: ViewController { } ) -// var transition = transition -// if isFirstTime { -// transition = transition.withUserData(CameraScreenTransition.animateIn) -// } else if animateOut { -// transition = transition.withUserData(CameraScreenTransition.animateOut) -// } - let componentSize = self.componentHost.update( transition: transition, component: AnyComponent( diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashIcon.imageset/flash.pdf b/submodules/TelegramUI/Images.xcassets/Camera/FlashIcon.imageset/flash.pdf deleted file mode 100644 index d945f8d547..0000000000 Binary files a/submodules/TelegramUI/Images.xcassets/Camera/FlashIcon.imageset/flash.pdf and /dev/null differ diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json index ac6650008b..68773a0944 100644 --- a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "off shadow.pdf", + "filename" : "off.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf deleted file mode 100644 index 297a8ab85d..0000000000 --- a/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off shadow.pdf +++ /dev/null @@ -1,1366 +0,0 @@ -%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/Camera/FlashOffIcon.imageset/off.pdf b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off.pdf new file mode 100644 index 0000000000..7b1ef47867 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlashOffIcon.imageset/off.pdf @@ -0,0 +1,378 @@ +%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.141342 cm +1.000000 1.000000 1.000000 scn +112.318077 230.817108 m +114.451881 236.621613 106.507004 239.951096 102.709404 235.070618 c +70.894165 194.178741 37.289165 151.643738 1.607707 108.848358 c +-1.328853 105.327118 -0.157611 101.418579 4.426768 101.506943 c +20.839167 101.823730 57.499165 100.883728 57.172985 100.331940 c +57.264164 100.648727 34.854565 40.328934 24.311525 11.312073 c +22.424946 6.120453 29.099884 2.524017 31.687706 5.647629 c +67.369164 48.713730 102.736664 92.306229 134.258621 130.929901 c +138.408722 136.015289 136.225098 141.255798 130.969559 141.268005 c +115.661659 141.303726 79.236656 141.303726 79.280838 141.150513 c +79.236656 141.303726 103.559158 206.986237 112.318077 230.817108 c +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 187.691406 134.211075 cm +1.000000 1.000000 1.000000 scn +112.318077 235.747375 m +108.788986 237.044708 l +108.788910 237.044510 l +112.318077 235.747375 l +h +102.709404 240.000885 m +99.741928 242.309921 l +99.741806 242.309769 l +102.709404 240.000885 l +h +1.607707 113.778625 m +4.495335 111.370468 l +4.495601 111.370789 l +1.607707 113.778625 l +h +4.426768 106.437210 m +4.499225 102.677917 l +4.499327 102.677917 l +4.426768 106.437210 l +h +57.172985 105.262207 m +53.559685 106.302246 l +53.036144 104.483353 53.946251 102.558838 55.684322 101.809464 c +57.422398 101.060089 59.446564 101.719498 60.409737 103.348831 c +57.172985 105.262207 l +h +24.311525 16.242340 m +27.845428 14.958160 l +27.845484 14.958298 l +24.311525 16.242340 l +h +31.687706 10.577896 m +28.792362 12.976761 l +28.792278 12.976669 l +31.687706 10.577896 l +h +134.258621 135.860168 m +137.171616 133.482773 l +137.171692 133.482849 l +134.258621 135.860168 l +h +130.969559 146.198273 m +130.960785 142.438293 l +130.960815 142.438293 l +130.969559 146.198273 l +h +75.668030 145.039047 m +76.243362 143.043747 78.327271 141.892639 80.322571 142.467957 c +82.317871 143.043304 83.468979 145.127197 82.893646 147.122498 c +75.668030 145.039047 l +h +115.847176 234.450043 m +116.701851 236.774994 116.619583 239.131042 115.641846 241.171738 c +114.692940 243.152237 113.039856 244.550476 111.229118 245.332687 c +107.622391 246.890762 102.796585 246.235611 99.741928 242.309921 c +105.676872 237.691833 l +106.419823 238.646622 107.465248 238.766968 108.246910 238.429306 c +108.630371 238.263657 108.800774 238.046204 108.860069 237.922424 c +108.890541 237.858841 109.001205 237.622009 108.788986 237.044708 c +115.847176 234.450043 l +h +99.741806 242.309769 m +67.937111 201.431442 34.361767 158.934448 -1.280186 116.186462 c +4.495601 111.370789 l +40.216564 154.213562 73.851219 196.786560 105.676994 237.691986 c +99.741806 242.309769 l +h +-1.279920 116.186783 m +-3.258407 113.814362 -4.442426 110.586304 -3.346803 107.497391 c +-2.147628 104.116531 1.122458 102.612823 4.499225 102.677917 c +4.354311 110.196518 l +3.731298 110.184509 3.509113 110.313995 3.518988 110.308289 c +3.525570 110.304489 3.570336 110.276184 3.624385 110.211868 c +3.678738 110.147186 3.717865 110.075272 3.740574 110.011246 c +3.787205 109.879776 3.727518 109.904007 3.795444 110.156387 c +3.863918 110.410812 4.051676 110.838470 4.495335 111.370468 c +-1.279920 116.186783 l +h +4.499327 102.677917 m +12.622606 102.834702 25.825699 102.680527 36.987919 102.416473 c +42.562939 102.284592 47.599640 102.125977 51.211418 101.966782 c +53.024452 101.886871 54.443344 101.808304 55.382313 101.735397 c +55.867409 101.697739 56.148544 101.667191 56.272243 101.649155 c +56.369865 101.634918 56.239033 101.647354 56.024120 101.711334 c +55.987492 101.722244 55.526577 101.847748 55.024200 102.194824 c +54.785992 102.359390 54.143162 102.837692 53.738750 103.746338 c +53.220856 104.909958 53.351353 106.186188 53.936230 107.175598 c +60.409737 103.348831 l +60.999710 104.346863 61.130787 105.631760 60.609016 106.804092 c +60.200726 107.721451 59.549557 108.208511 59.298603 108.381897 c +58.770741 108.746567 58.266712 108.889877 58.169785 108.918732 c +57.834274 109.018616 57.514095 109.067596 57.357121 109.090485 c +56.971634 109.146683 56.482571 109.192612 55.964401 109.232834 c +54.896851 109.315720 53.376972 109.398636 51.542557 109.479492 c +47.859436 109.641830 42.766521 109.801880 37.165764 109.934372 c +25.976433 110.199066 12.643328 110.356506 4.354208 110.196518 c +4.499327 102.677917 l +h +60.786282 104.222183 m +60.809654 104.307053 60.897423 104.745377 60.929855 105.103439 c +58.742435 108.680450 54.195972 107.559937 53.865387 107.051208 c +53.814571 106.953857 53.747608 106.813751 53.728859 106.771683 c +53.718666 106.748230 53.702507 106.710159 53.696033 106.694580 c +53.676624 106.647598 53.659908 106.604401 53.655502 106.593048 c +53.640240 106.553711 53.620911 106.502945 53.600471 106.448990 c +53.557949 106.336746 53.495045 106.169449 53.413906 105.952942 c +53.251022 105.518295 53.008900 104.869919 52.696381 104.031509 c +52.071083 102.353989 51.161480 99.908981 50.034843 96.876450 c +47.781475 90.811127 44.658810 82.392609 41.203846 73.057465 c +34.294983 54.390060 26.053322 32.046463 20.777569 17.526367 c +27.845484 14.958298 l +33.112766 29.455078 41.347427 51.779800 48.256329 70.447311 c +51.710243 79.779633 54.831825 88.195221 57.084080 94.257538 c +58.210258 97.288834 59.118813 99.731033 59.742771 101.404968 c +60.054882 102.242279 60.295158 102.885696 60.455673 103.314011 c +60.536228 103.528976 60.595272 103.685944 60.632847 103.785141 c +60.652458 103.836914 60.662716 103.863678 60.666313 103.872955 c +60.671341 103.885910 60.661373 103.859787 60.646000 103.822586 c +60.640533 103.809433 60.625214 103.773346 60.615692 103.751419 c +60.597614 103.710876 60.531147 103.571838 60.480659 103.475098 c +60.150402 102.966980 55.604095 101.846619 53.416664 105.423325 c +53.449085 105.781067 53.536671 106.218613 53.559685 106.302246 c +60.786282 104.222183 l +h +20.777622 17.526520 m +19.123594 12.974854 21.347857 9.066895 24.252548 7.152451 c +26.852655 5.438766 31.562777 4.533417 34.583130 8.179123 c +28.792278 12.976669 l +28.944626 13.160553 29.138533 13.175156 29.096033 13.174225 c +28.995413 13.172028 28.716915 13.216476 28.390881 13.431351 c +28.077776 13.637726 27.886385 13.893021 27.802916 14.105072 c +27.740448 14.263748 27.684525 14.515366 27.845428 14.958160 c +20.777622 17.526520 l +h +34.583050 8.179031 m +70.276260 51.259308 105.658882 94.870392 137.171616 133.482773 c +131.345612 138.237549 l +99.814445 99.602600 64.462067 56.028687 28.792362 12.976761 c +34.583050 8.179031 l +h +137.171692 133.482849 m +139.813660 136.720245 140.928268 140.649231 139.688675 144.177872 c +138.396896 147.855057 134.944427 149.949036 130.978302 149.958267 c +130.960815 142.438293 l +131.647614 142.436691 132.038910 142.268158 132.232162 142.142303 c +132.415222 142.023071 132.526093 141.877975 132.593735 141.685455 c +132.722305 141.319427 132.853668 140.085480 131.345535 138.237473 c +137.171692 133.482849 l +h +130.978333 149.958267 m +123.319649 149.976135 110.381828 149.985077 99.363159 149.970367 c +93.854500 149.963028 88.820534 149.949768 85.164253 149.928741 c +83.337914 149.918228 81.845848 149.905731 80.807587 149.890900 c +80.292084 149.883545 79.869400 149.875336 79.568008 149.865891 c +79.424759 149.861389 79.269722 149.855469 79.134247 149.846527 c +79.081985 149.843094 78.947746 149.834030 78.793877 149.812897 c +78.748192 149.806641 78.519775 149.776611 78.242943 149.696625 c +78.151802 149.670319 77.732826 149.552307 77.269096 149.258392 c +77.008621 149.077515 76.415733 148.516479 76.117638 148.114105 c +75.779449 147.451813 75.526001 145.882538 75.668030 145.039047 c +82.893646 147.122498 l +83.035507 146.279617 82.782158 144.710938 82.444359 144.049255 c +82.146645 143.647461 81.554413 143.087006 81.294876 142.906738 c +80.833023 142.614014 80.417023 142.497192 80.329964 142.472046 c +80.061295 142.394440 79.845428 142.366730 79.816643 142.362793 c +79.744957 142.352936 79.691727 142.348007 79.672020 142.346252 c +79.647842 142.344086 79.632370 142.343048 79.629318 142.342850 c +79.623993 142.342499 79.635231 142.343277 79.669975 142.344757 c +79.702553 142.346161 79.746620 142.347794 79.803734 142.349579 c +80.038528 142.356949 80.408836 142.364441 80.914993 142.371674 c +81.920067 142.386017 83.386047 142.398376 85.207527 142.408859 c +88.846886 142.429810 93.867622 142.443039 99.373184 142.450378 c +110.382942 142.465057 123.311569 142.456146 130.960785 142.438293 c +130.978333 149.958267 l +h +82.893646 147.122498 m +83.006897 146.584534 83.032082 145.826355 83.012100 145.618286 c +82.997322 145.512054 82.967239 145.342407 82.954086 145.279800 c +82.929825 145.169220 82.905975 145.085770 82.899849 145.064316 c +82.890221 145.030594 82.882523 145.005646 82.879196 144.994980 c +82.875359 144.982635 82.872734 144.974564 82.872002 144.972305 c +82.871216 144.969879 82.878197 144.991119 82.898605 145.049850 c +82.937004 145.160370 82.999245 145.335754 83.087128 145.580505 c +83.261513 146.066162 83.522041 146.783661 83.860504 147.710876 c +84.536697 149.563293 85.515999 152.230896 86.721222 155.506119 c +89.131294 162.055573 92.440521 171.023163 96.025543 180.732590 c +103.194412 200.148254 111.466515 222.531326 115.847252 234.450241 c +108.788910 237.044510 l +104.410728 225.132553 96.142120 202.758926 88.971054 183.337326 c +85.386108 173.628098 82.075554 164.656921 79.663872 158.103088 c +78.458214 154.826675 77.476135 152.151520 76.796440 150.289505 c +76.456955 149.359497 76.190903 148.626862 76.009583 148.121918 c +75.919609 147.871353 75.846710 147.666306 75.795128 147.517838 c +75.770538 147.447083 75.743820 147.369171 75.720924 147.298782 c +75.711311 147.269241 75.690361 147.204376 75.668648 147.128311 c +75.659912 147.097717 75.634186 147.007080 75.608788 146.891327 c +75.595062 146.826141 75.564598 146.654388 75.549614 146.546570 c +75.529434 146.336899 75.554611 145.577637 75.668030 145.039047 c +82.893646 147.122498 l +h +f +n +Q + +endstream +endobj + +2 0 obj + 9809 +endobj + +3 0 obj + << /Type /XObject + /Length 4 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 + +4 0 obj + 420 +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 +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 + +7 0 obj + 614 +endobj + +8 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 512.000000 512.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 +0000010069 00000 n +0000010092 00000 n +0000010762 00000 n +0000010784 00000 n +0000011082 00000 n +0000011752 00000 n +0000011774 00000 n +0000011949 00000 n +0000012023 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 10 0 R + /Size 11 +>> +startxref +12083 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlipIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/FlipIcon.imageset/Contents.json index a8b299efff..0a1dc78c0e 100644 --- a/submodules/TelegramUI/Images.xcassets/Camera/FlipIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Camera/FlipIcon.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/submodules/TelegramUI/Images.xcassets/Camera/LockedIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/LockedIcon.imageset/Contents.json index 1b2f743bbe..e0081c5fbc 100644 --- a/submodules/TelegramUI/Images.xcassets/Camera/LockedIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Camera/LockedIcon.imageset/Contents.json @@ -8,5 +8,8 @@ "info" : { "author" : "xcode", "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" } } diff --git a/submodules/TelegramUI/Images.xcassets/Camera/FlashIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/Contents.json similarity index 76% rename from submodules/TelegramUI/Images.xcassets/Camera/FlashIcon.imageset/Contents.json rename to submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/Contents.json index 4b8655a27a..fa549be002 100644 --- a/submodules/TelegramUI/Images.xcassets/Camera/FlashIcon.imageset/Contents.json +++ b/submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "flash.pdf", + "filename" : "magnifying.pdf", "idiom" : "universal" } ], diff --git a/submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/magnifying.pdf b/submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/magnifying.pdf new file mode 100644 index 0000000000..d73777bcdc --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Camera/ZoomIcon.imageset/magnifying.pdf @@ -0,0 +1,126 @@ +%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 13.500000 12.074233 cm +1.000000 1.000000 1.000000 scn +0.665000 7.425767 m +0.665000 7.793036 0.367269 8.090767 0.000000 8.090767 c +-0.367269 8.090767 -0.665000 7.793036 -0.665000 7.425767 c +0.665000 7.425767 l +h +-0.665000 1.330000 m +-0.665000 0.962730 -0.367269 0.665000 0.000000 0.665000 c +0.367269 0.665000 0.665000 0.962730 0.665000 1.330000 c +-0.665000 1.330000 l +h +-0.665000 7.425767 m +-0.665000 1.330000 l +0.665000 1.330000 l +0.665000 7.425767 l +-0.665000 7.425767 l +h +f +n +Q +q +0.000000 1.000000 -1.000000 0.000000 11.782148 16.452118 cm +1.000000 1.000000 1.000000 scn +0.665000 1.330000 m +0.665000 1.697269 0.367269 1.995000 0.000000 1.995000 c +-0.367269 1.995000 -0.665000 1.697269 -0.665000 1.330000 c +0.665000 1.330000 l +h +-0.665000 -4.765767 m +-0.665000 -5.133037 -0.367269 -5.430767 0.000000 -5.430767 c +0.367269 -5.430767 0.665000 -5.133037 0.665000 -4.765767 c +-0.665000 -4.765767 l +h +-0.665000 1.330000 m +-0.665000 -4.765767 l +0.665000 -4.765767 l +0.665000 1.330000 l +-0.665000 1.330000 l +h +f +n +Q +q +1.000000 0.000000 -0.000000 1.000000 5.334991 4.588974 cm +1.000000 1.000000 1.000000 scn +1.330000 11.911035 m +1.330000 15.685901 4.390134 18.746035 8.165000 18.746035 c +11.939866 18.746035 15.000000 15.685901 15.000000 11.911035 c +15.000000 8.136168 11.939866 5.076035 8.165000 5.076035 c +4.390134 5.076035 1.330000 8.136168 1.330000 11.911035 c +h +8.165000 20.076035 m +3.655595 20.076035 0.000000 16.420439 0.000000 11.911035 c +0.000000 7.401629 3.655595 3.746035 8.165000 3.746035 c +10.120762 3.746035 11.915919 4.433660 13.321901 5.580336 c +18.578102 0.324135 l +18.902237 0.000000 19.427763 0.000000 19.751900 0.324135 c +20.076035 0.648272 20.076035 1.173798 19.751900 1.497932 c +14.495699 6.754133 l +15.642375 8.160115 16.330000 9.955273 16.330000 11.911035 c +16.330000 16.420439 12.674405 20.076035 8.165000 20.076035 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1911 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000002001 00000 n +0000002024 00000 n +0000002197 00000 n +0000002271 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +2330 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/Contents.json new file mode 100644 index 0000000000..a1285d58f0 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "speaker_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/speaker_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/speaker_30.pdf new file mode 100644 index 0000000000..e4321f324d --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/MuteIcon.imageset/speaker_30.pdf @@ -0,0 +1,163 @@ +%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 2.669937 5.003574 cm +1.000000 1.000000 1.000000 scn +11.351625 18.250841 m +11.322918 18.234249 11.256240 18.192322 11.141315 18.096107 c +10.916164 17.907612 10.625579 17.618145 10.185589 17.178154 c +6.845894 13.838459 l +6.818897 13.811405 l +6.713208 13.705383 6.593981 13.585779 6.453676 13.489171 c +6.241572 13.343126 6.001034 13.243492 5.747785 13.196781 c +5.580265 13.165883 5.411390 13.166149 5.261689 13.166386 c +5.261677 13.166386 l +5.223461 13.166426 l +4.830062 13.166426 l +3.846654 13.166426 3.517771 13.159296 3.268425 13.092484 c +2.519576 12.891830 1.934657 12.306912 1.734004 11.558063 c +1.667192 11.308718 1.660063 10.979834 1.660063 9.996426 c +1.660063 9.013018 1.667192 8.684134 1.734004 8.434789 c +1.934657 7.685939 2.519576 7.101020 3.268425 6.900367 c +3.517771 6.833554 3.846654 6.826426 4.830062 6.826426 c +5.223460 6.826426 l +5.261672 6.826466 l +5.411377 6.826702 5.580258 6.826969 5.747785 6.796069 c +6.001034 6.749359 6.241573 6.649724 6.453676 6.503679 c +6.593977 6.407075 6.713201 6.287476 6.818888 6.181455 c +6.818899 6.181443 l +6.845895 6.154389 l +10.185591 2.814688 l +10.625580 2.374697 10.916165 2.085230 11.141315 1.896734 c +11.256239 1.800520 11.322917 1.758595 11.351624 1.742002 c +11.381638 1.745842 11.410119 1.757639 11.434055 1.776148 c +11.442623 1.808182 11.460124 1.884975 11.473353 2.034269 c +11.499272 2.326761 11.500063 2.736921 11.500063 3.359161 c +11.500063 16.633684 l +11.500063 17.255922 11.499272 17.666082 11.473353 17.958572 c +11.460124 18.107864 11.442620 18.184664 11.434053 18.216698 c +11.410115 18.235209 11.381640 18.247004 11.351625 18.250841 c +h +11.186482 19.906570 m +11.770469 19.952532 12.341165 19.716143 12.721605 19.270702 c +13.023456 18.917282 13.095314 18.461252 13.126874 18.105099 c +13.160094 17.730204 13.160080 17.245285 13.160063 16.673033 c +13.160063 16.672995 l +13.160063 16.633684 l +13.160063 3.359161 l +13.160063 3.319851 l +13.160063 3.319817 l +13.160080 2.747561 13.160094 2.262640 13.126874 1.887745 c +13.095314 1.531590 13.023456 1.075560 12.721605 0.722139 c +12.341164 0.276699 11.770468 0.040310 11.186481 0.086271 c +10.723136 0.122738 10.349862 0.394388 10.075707 0.623911 c +9.787114 0.865520 9.444218 1.208439 9.039564 1.613119 c +9.011792 1.640892 l +5.672097 4.980594 l +5.601249 5.051442 5.562954 5.089569 5.533292 5.117202 c +5.520135 5.129457 5.513000 5.135533 5.510193 5.137844 c +5.491690 5.150188 5.470943 5.158781 5.449131 5.163136 c +5.445509 5.163486 5.436167 5.164236 5.418206 5.164872 c +5.377693 5.166306 5.323654 5.166426 5.223460 5.166426 c +4.830062 5.166426 l +4.706686 5.166393 l +3.896474 5.166075 3.327978 5.165852 2.838786 5.296929 c +1.517083 5.651079 0.484716 6.683446 0.130567 8.005149 c +-0.000511 8.494340 -0.000288 9.062836 0.000031 9.873044 c +0.000063 9.996426 l +0.000031 10.119807 l +-0.000288 10.930016 -0.000511 11.498511 0.130567 11.987702 c +0.484716 13.309405 1.517083 14.341772 2.838786 14.695921 c +3.327976 14.827000 3.896471 14.826777 4.706679 14.826458 c +4.830062 14.826426 l +5.223461 14.826426 l +5.323655 14.826426 5.377693 14.826544 5.418207 14.827979 c +5.436173 14.828615 5.445515 14.829365 5.449134 14.829716 c +5.470947 14.834070 5.491693 14.842665 5.510196 14.855009 c +5.513005 14.857321 5.520141 14.863398 5.533292 14.875648 c +5.562954 14.903282 5.601249 14.941408 5.672096 15.012257 c +9.011791 18.351952 l +9.039572 18.379732 l +9.444222 18.784409 9.787117 19.127323 10.075708 19.368931 c +10.349863 19.598454 10.723137 19.870104 11.186482 19.906570 c +h +16.659830 15.076297 m +17.030680 15.345736 17.549738 15.263525 17.819178 14.892674 c +18.853163 13.469513 19.410063 11.755547 19.410063 9.996424 c +19.410063 8.237302 18.853161 6.523335 17.819174 5.100175 c +17.549736 4.729324 17.030680 4.647114 16.659830 4.916553 c +16.288979 5.185991 16.206768 5.705048 16.476206 6.075898 c +17.304140 7.215451 17.750063 8.587859 17.750063 9.996425 c +17.750063 11.404989 17.304140 12.777397 16.476208 13.916951 c +16.206770 14.287802 16.288980 14.806858 16.659830 15.076297 c +h +21.864262 17.831598 m +21.594824 18.202450 21.075768 18.284660 20.704916 18.015221 c +20.334066 17.745783 20.251856 17.226727 20.521294 16.855877 c +21.969868 14.862084 22.750063 12.460885 22.750063 9.996424 c +22.750063 7.531962 21.969866 5.130764 20.521292 3.136972 c +20.251852 2.766121 20.334063 2.247065 20.704914 1.977627 c +21.075764 1.708187 21.594822 1.790398 21.864260 2.161249 c +23.518887 4.438646 24.410063 7.181405 24.410063 9.996424 c +24.410063 12.811441 23.518888 15.554200 21.864262 17.831598 c +h +f* +n +Q + +endstream +endobj + +3 0 obj + 4546 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000004636 00000 n +0000004659 00000 n +0000004832 00000 n +0000004906 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +4965 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/Contents.json b/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/Contents.json new file mode 100644 index 0000000000..756ef35592 --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "squareandarrow_30.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf b/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf new file mode 100644 index 0000000000..0d14e5b8fb --- /dev/null +++ b/submodules/TelegramUI/Images.xcassets/Media Editor/SaveIcon.imageset/squareandarrow_30.pdf @@ -0,0 +1,96 @@ +%PDF-1.7 + +1 0 obj + << >> +endobj + +2 0 obj + << /Length 3 0 R >> +stream +/DeviceRGB CS +/DeviceRGB cs +q +1.000000 0.000000 -0.000000 1.000000 5.170449 5.169796 cm +1.000000 1.000000 1.000000 scn +9.830000 21.659878 m +10.288396 21.659878 10.660000 21.288275 10.660000 20.829878 c +10.660000 9.833675 l +14.743101 13.916777 l +15.067237 14.240911 15.592763 14.240911 15.916899 13.916777 c +16.241034 13.592642 16.241034 13.067114 15.916899 12.742979 c +10.416899 7.242979 l +10.092764 6.918844 9.567236 6.918844 9.243101 7.242979 c +3.743101 12.742979 l +3.418966 13.067114 3.418966 13.592642 3.743101 13.916777 c +4.067236 14.240911 4.592763 14.240911 4.916899 13.916777 c +9.000000 9.833675 l +9.000000 20.829878 l +9.000000 21.288275 9.371604 21.659878 9.830000 21.659878 c +h +1.660000 5.830000 m +1.660000 6.288396 1.288396 6.660000 0.830000 6.660000 c +0.371604 6.660000 0.000000 6.288396 0.000000 5.830000 c +0.000000 4.580000 l +0.000000 2.050535 2.050535 0.000000 4.579999 0.000000 c +15.080000 0.000000 l +17.609465 0.000000 19.660000 2.050535 19.660000 4.580000 c +19.660000 5.830000 l +19.660000 6.288396 19.288397 6.660000 18.830000 6.660000 c +18.371603 6.660000 18.000000 6.288396 18.000000 5.830000 c +18.000000 4.580000 l +18.000000 2.967329 16.692673 1.660000 15.080000 1.660000 c +4.579999 1.660000 l +2.967328 1.660000 1.660000 2.967329 1.660000 4.580000 c +1.660000 5.830000 l +h +f* +n +Q + +endstream +endobj + +3 0 obj + 1300 +endobj + +4 0 obj + << /Annots [] + /Type /Page + /MediaBox [ 0.000000 0.000000 30.000000 30.000000 ] + /Resources 1 0 R + /Contents 2 0 R + /Parent 5 0 R + >> +endobj + +5 0 obj + << /Kids [ 4 0 R ] + /Count 1 + /Type /Pages + >> +endobj + +6 0 obj + << /Pages 5 0 R + /Type /Catalog + >> +endobj + +xref +0 7 +0000000000 65535 f +0000000010 00000 n +0000000034 00000 n +0000001390 00000 n +0000001413 00000 n +0000001586 00000 n +0000001660 00000 n +trailer +<< /ID [ (some) (id) ] + /Root 6 0 R + /Size 7 +>> +startxref +1719 +%%EOF \ No newline at end of file diff --git a/submodules/TelegramUI/Sources/SharedAccountContext.swift b/submodules/TelegramUI/Sources/SharedAccountContext.swift index 9f96b37970..772e939e54 100644 --- a/submodules/TelegramUI/Sources/SharedAccountContext.swift +++ b/submodules/TelegramUI/Sources/SharedAccountContext.swift @@ -1829,8 +1829,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, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { - return storyMediaPickerController(context: context, completion: completion, dismissed: dismissed) + public func makeMediaPickerScreen(context: AccountContext, getSourceRect: @escaping () -> CGRect, completion: @escaping (Any, UIView, CGRect, UIImage?, @escaping () -> (UIView, CGRect)?, @escaping () -> Void) -> Void, dismissed: @escaping () -> Void) -> ViewController { + return storyMediaPickerController(context: context, getSourceRect: getSourceRect, 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 c2a37e3a55..417c55e302 100644 --- a/submodules/TelegramUI/Sources/TelegramRootController.swift +++ b/submodules/TelegramUI/Sources/TelegramRootController.swift @@ -27,6 +27,7 @@ import LocalMediaResources import ShareWithPeersScreen import ImageCompression import TextFormat +import UndoUI private class DetailsChatPlaceholderNode: ASDisplayNode, NavigationDetailsPlaceholderNode { private var presentationData: PresentationData @@ -254,6 +255,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon controller.view.endEditing(true) let context = self.context + let presentationData = context.sharedContext.currentPresentationData.with { $0 } var presentImpl: ((ViewController) -> Void)? var returnToCameraImpl: (() -> Void)? @@ -287,13 +289,25 @@ public final class TelegramRootController: NavigationController, TelegramRootCon completion: { result, resultTransition, dismissed in let subject: Signal = result |> map { value -> MediaEditorScreen.Subject? in + func editorPIPPosition(_ position: CameraScreen.PIPPosition) -> MediaEditorScreen.PIPPosition { + switch position { + case .topLeft: + return .topLeft + case .topRight: + return .topRight + case .bottomLeft: + return .bottomLeft + case .bottomRight: + return .bottomRight + } + } switch value { case .pendingImage: return nil - case let .image(image, additionalImage): - return .image(image, PixelDimensions(image.size), additionalImage) - case let .video(path, transitionImage, dimensions): - return .video(path, transitionImage, dimensions) + case let .image(image, additionalImage, pipPosition): + return .image(image, PixelDimensions(image.size), additionalImage, editorPIPPosition(pipPosition)) + case let .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, pipPosition): + return .video(path, transitionImage, additionalPath, additionalTransitionImage, dimensions, editorPIPPosition(pipPosition)) case let .asset(asset): return .asset(asset) case let .draft(draft): @@ -362,6 +376,17 @@ public final class TelegramRootController: NavigationController, TelegramRootCon Queue.mainQueue().after(0.2) { chatListController.updateStoryUploadProgress(nil) } + + let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image, title: nil, text: "Story successfully uploaded", round: false, undoText: "View"), elevatedLayout: false, action: { action in + switch action { + case .undo: + break + default: + break + } + return true + }) + chatListController.present(undoOverlayController, in: .current) } } }) @@ -420,7 +445,7 @@ public final class TelegramRootController: NavigationController, TelegramRootCon commit({}) } } - case let .video(content, _, values, duration, dimensions, caption): + case let .video(content, image, values, duration, dimensions, caption): let adjustments: VideoMediaResourceAdjustments if let valuesData = try? JSONEncoder().encode(values) { let data = MemoryBuffer(data: valuesData) @@ -451,6 +476,19 @@ public final class TelegramRootController: NavigationController, TelegramRootCon Queue.mainQueue().after(0.2) { chatListController.updateStoryUploadProgress(nil) } + + if let image { + let undoOverlayController = UndoOverlayController(presentationData: presentationData, content: .image(image: image, title: nil, text: "Story successfully uploaded", round: false, undoText: "View"), elevatedLayout: false, action: { action in + switch action { + case .undo: + break + default: + break + } + return true + }) + chatListController.present(undoOverlayController, in: .current) + } } } })