From 8c8a6515003f9383bdd7103c458a0cc135ce7e76 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 11 Feb 2026 05:50:08 +0500 Subject: [PATCH] feat: enhance chat and requests screens with avatar handling, pinning, and user blocking functionalities --- .../com/rosetta/messenger/MainActivity.kt | 23 +- .../rosetta/messenger/crypto/MessageCrypto.kt | 70 +- .../rosetta/messenger/data/ForwardManager.kt | 82 ++- .../messenger/ui/chats/ChatDetailScreen.kt | 59 +- .../messenger/ui/chats/ChatViewModel.kt | 267 +++++++- .../messenger/ui/chats/ChatsListScreen.kt | 601 ++++++++++++------ .../ui/chats/ForwardChatPickerBottomSheet.kt | 123 +++- .../messenger/ui/chats/RequestsListScreen.kt | 31 +- .../chats/components/AttachmentComponents.kt | 91 ++- .../chats/components/ChatDetailComponents.kt | 298 ++++++--- .../ui/chats/components/ImageViewerScreen.kt | 48 +- .../ui/chats/models/ChatDetailModels.kt | 1 + app/src/main/res/raw/no_requests.json | 1 + 13 files changed, 1244 insertions(+), 451 deletions(-) create mode 100644 app/src/main/res/raw/no_requests.json diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 41d09f3..44fc54d 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -710,15 +710,22 @@ fun MainScreen( isDarkTheme = isDarkTheme ) { RequestsListScreen( - isDarkTheme = isDarkTheme, - chatsViewModel = chatsListViewModel, - onBack = { navStack = navStack.filterNot { it is Screen.Requests } }, - onUserSelect = { selectedRequestUser -> - navStack = - navStack.filterNot { - it is Screen.Requests || it is Screen.ChatDetail - } + Screen.ChatDetail(selectedRequestUser) + isDarkTheme = isDarkTheme, + chatsViewModel = chatsListViewModel, + pinnedChats = pinnedChats, + onTogglePin = { opponentKey -> + mainScreenScope.launch { + prefsManager.togglePinChat(opponentKey) } + }, + onBack = { navStack = navStack.filterNot { it is Screen.Requests } }, + onUserSelect = { selectedRequestUser -> + navStack = + navStack.filterNot { + it is Screen.ChatDetail || it is Screen.OtherProfile + } + Screen.ChatDetail(selectedRequestUser) + }, + avatarRepository = avatarRepository ) } diff --git a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt index 5f3afc4..f96887e 100644 --- a/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt +++ b/app/src/main/java/com/rosetta/messenger/crypto/MessageCrypto.kt @@ -811,16 +811,8 @@ object MessageCrypto { val compressed = compressedBuffer.copyOf(compressedSize) // PBKDF2 key derivation (matching crypto-js: crypto.PBKDF2(password, 'rosetta', {keySize: 256/32, iterations: 1000})) - // CRITICAL: crypto-js PBKDF2 uses SHA256 by default (NOT SHA1!) - val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256") - val spec = javax.crypto.spec.PBEKeySpec( - password.toCharArray(), - "rosetta".toByteArray(Charsets.UTF_8), - 1000, - 256 - ) - val secretKey = factory.generateSecret(spec) - val keyBytes = secretKey.encoded + // Используем generatePBKDF2Key() для совместимости с crypto-js (UTF-8 encoding) + val keyBytes = generatePBKDF2Key(password) // Generate random IV (16 bytes) val iv = ByteArray(16) @@ -1030,28 +1022,17 @@ object MessageCrypto { // Password from plainKeyAndNonce - use same JS-like UTF-8 conversion val password = bytesToJsUtf8String(plainKeyAndNonce) - - // PBKDF2 key derivation - // CRITICAL: Must use SHA1 to match Desktop crypto-js (not SHA256!) - val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") - val spec = javax.crypto.spec.PBEKeySpec( - password.toCharArray(), - "rosetta".toByteArray(Charsets.UTF_8), - 1000, - 256 - ) - val secretKey = factory.generateSecret(spec) - val keyBytes = secretKey.encoded - - + + // PBKDF2 key derivation — SHA256 (совместимо с crypto-js и encryptReplyBlob) + val keyBytes = generatePBKDF2Key(password) + // AES-CBC decryption val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") val keySpec = SecretKeySpec(keyBytes, "AES") val ivSpec = IvParameterSpec(iv) cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec) val decompressed = cipher.doFinal(ciphertext) - - + // Decompress with inflate val inflater = java.util.zip.Inflater() inflater.setInput(decompressed) @@ -1059,12 +1040,41 @@ object MessageCrypto { val outputSize = inflater.inflate(outputBuffer) inflater.end() val plaintext = String(outputBuffer, 0, outputSize, Charsets.UTF_8) - - + plaintext } catch (e: Exception) { - // Return as-is, might be plain JSON - encryptedBlob + // Fallback: пробуем SHA1 для обратной совместимости со старыми сообщениями + try { + val parts = encryptedBlob.split(':') + if (parts.size != 2) return encryptedBlob + + val iv = Base64.decode(parts[0], Base64.DEFAULT) + val ciphertext = Base64.decode(parts[1], Base64.DEFAULT) + val password = bytesToJsUtf8String(plainKeyAndNonce) + + val factory = javax.crypto.SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") + val spec = javax.crypto.spec.PBEKeySpec( + password.toCharArray(), + "rosetta".toByteArray(Charsets.UTF_8), + 1000, + 256 + ) + val keyBytesSha1 = factory.generateSecret(spec).encoded + + val cipher = Cipher.getInstance("AES/CBC/PKCS5Padding") + cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(keyBytesSha1, "AES"), IvParameterSpec(iv)) + val decompressed = cipher.doFinal(ciphertext) + + val inflater = java.util.zip.Inflater() + inflater.setInput(decompressed) + val outputBuffer = ByteArray(decompressed.size * 10) + val outputSize = inflater.inflate(outputBuffer) + inflater.end() + String(outputBuffer, 0, outputSize, Charsets.UTF_8) + } catch (e2: Exception) { + // Return as-is, might be plain JSON + encryptedBlob + } } } } diff --git a/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt b/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt index 89bb2a8..14bdf24 100644 --- a/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt +++ b/app/src/main/java/com/rosetta/messenger/data/ForwardManager.kt @@ -7,18 +7,18 @@ import kotlinx.coroutines.flow.asStateFlow /** * 📨 Менеджер для пересылки сообщений (Forward) - * + * * Логика как в десктопе: * 1. Пользователь выбирает сообщения в чате * 2. Нажимает Forward - * 3. Открывается список чатов - * 4. Выбирает чат куда переслать - * 5. Переходит в выбранный чат с сообщениями в Reply панели (как Forward) - * + * 3. Открывается список чатов (мультивыбор) + * 4. Выбирает один или несколько чатов + * 5. Первый чат — навигация с reply панелью, остальные — прямая отправка + * * Singleton для передачи данных между экранами */ object ForwardManager { - + /** * Сообщение для пересылки */ @@ -29,25 +29,31 @@ object ForwardManager { val isOutgoing: Boolean, val senderPublicKey: String, // publicKey отправителя сообщения val originalChatPublicKey: String, // publicKey чата откуда пересылается + val senderName: String = "", // Имя отправителя для атрибуции val attachments: List = emptyList() ) - + // Сообщения для пересылки private val _forwardMessages = MutableStateFlow>(emptyList()) val forwardMessages: StateFlow> = _forwardMessages.asStateFlow() - + // Флаг показа выбора чата private val _showChatPicker = MutableStateFlow(false) val showChatPicker: StateFlow = _showChatPicker.asStateFlow() - - // Выбранный чат (publicKey собеседника) - private val _selectedChatPublicKey = MutableStateFlow(null) - val selectedChatPublicKey: StateFlow = _selectedChatPublicKey.asStateFlow() - + + // Выбранные чаты (publicKeys собеседников) — поддержка мультивыбора + private val _selectedChatPublicKeys = MutableStateFlow>(emptyList()) + val selectedChatPublicKeys: StateFlow> = _selectedChatPublicKeys.asStateFlow() + + // Обратная совместимость: первый выбранный чат + val selectedChatPublicKey: StateFlow get() = MutableStateFlow( + _selectedChatPublicKeys.value.firstOrNull() + ) + // 🔥 Счётчик для триггера перезагрузки диалога при forward private val _forwardTrigger = MutableStateFlow(0) val forwardTrigger: StateFlow = _forwardTrigger.asStateFlow() - + /** * Установить сообщения для пересылки и показать выбор чата */ @@ -60,24 +66,31 @@ object ForwardManager { _showChatPicker.value = true } } - + /** - * Выбрать чат для пересылки + * Выбрать один чат для пересылки (обратная совместимость) */ fun selectChat(publicKey: String) { - _selectedChatPublicKey.value = publicKey + selectChats(listOf(publicKey)) + } + + /** + * Выбрать несколько чатов для пересылки + */ + fun selectChats(publicKeys: List) { + _selectedChatPublicKeys.value = publicKeys _showChatPicker.value = false // 🔥 Увеличиваем триггер чтобы ChatDetailScreen перезагрузил диалог _forwardTrigger.value++ } - + /** * Скрыть выбор чата (отмена) */ fun hideChatPicker() { _showChatPicker.value = false } - + /** * Получить сообщения и очистить состояние * Вызывается при открытии выбранного чата @@ -86,57 +99,64 @@ object ForwardManager { val messages = _forwardMessages.value return messages } - + /** * Очистить все данные (после применения или отмены) */ fun clear() { _forwardMessages.value = emptyList() _showChatPicker.value = false - _selectedChatPublicKey.value = null + _selectedChatPublicKeys.value = emptyList() } - + /** * Проверить есть ли сообщения для пересылки */ fun hasForwardMessages(): Boolean = _forwardMessages.value.isNotEmpty() - + /** * Проверить есть ли сообщения для конкретного чата */ fun hasForwardMessagesForChat(publicKey: String): Boolean { - val selectedKey = _selectedChatPublicKey.value + val selectedKeys = _selectedChatPublicKeys.value val hasMessages = _forwardMessages.value.isNotEmpty() - return selectedKey == publicKey && hasMessages + return publicKey in selectedKeys && hasMessages } - + /** * Установить выбранный чат и вернуть сообщения для него * Комбинированный метод для атомарного получения данных */ fun getForwardMessagesForChat(publicKey: String): List { - val selectedKey = _selectedChatPublicKey.value - return if (selectedKey == publicKey && _forwardMessages.value.isNotEmpty()) { + val selectedKeys = _selectedChatPublicKeys.value + return if (publicKey in selectedKeys && _forwardMessages.value.isNotEmpty()) { _forwardMessages.value } else { emptyList() } } + /** + * Получить список дополнительных чатов (кроме основного, куда навигируемся) + */ + fun getAdditionalChatKeys(primaryKey: String): List { + return _selectedChatPublicKeys.value.filter { it != primaryKey } + } + /** * Атомарно получить forward-сообщения для конкретного чата и очистить pending state. * Это повторяет desktop-подход "consume once" после перехода в целевой диалог. */ @Synchronized fun consumeForwardMessagesForChat(publicKey: String): List { - val selectedKey = _selectedChatPublicKey.value + val selectedKeys = _selectedChatPublicKeys.value val pending = _forwardMessages.value - if (selectedKey != publicKey || pending.isEmpty()) { + if (publicKey !in selectedKeys || pending.isEmpty()) { return emptyList() } _forwardMessages.value = emptyList() - _selectedChatPublicKey.value = null + _selectedChatPublicKeys.value = emptyList() _showChatPicker.value = false return pending } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt index 5eafca2..061bb8e 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatDetailScreen.kt @@ -189,6 +189,7 @@ fun ChatDetailScreen( var showImageViewer by remember { mutableStateOf(false) } var imageViewerInitialIndex by remember { mutableStateOf(0) } var imageViewerSourceBounds by remember { mutableStateOf(null) } + var imageViewerImages by remember { mutableStateOf>(emptyList()) } // 🎨 Управление статус баром DisposableEffect(isDarkTheme, showImageViewer, window, view) { @@ -1390,6 +1391,9 @@ fun ChatDetailScreen( user.publicKey, originalChatPublicKey = user.publicKey, + senderName = + if (msg.isOutgoing) "You" + else user.title.ifEmpty { user.username.ifEmpty { "User" } }, attachments = msg.attachments .filter { @@ -1936,6 +1940,7 @@ fun ChatDetailScreen( bounds -> // 📸 Открыть просмотрщик фото с shared element animation + // Фиксируем список на момент клика (защита от краша при новых сообщениях) val allImages = extractImagesFromMessages( messages, @@ -1946,6 +1951,8 @@ fun ChatDetailScreen( "User" } ) + imageViewerImages = + allImages imageViewerInitialIndex = findImageIndex( allImages, @@ -2010,21 +2017,15 @@ fun ChatDetailScreen( } // Закрытие Box // 📸 Image Viewer Overlay with Telegram-style shared element animation - if (showImageViewer) { - val allImages = - extractImagesFromMessages( - messages, - currentUserPublicKey, - user.publicKey, - user.title.ifEmpty { "User" } - ) + if (showImageViewer && imageViewerImages.isNotEmpty()) { ImageViewerScreen( - images = allImages, + images = imageViewerImages, initialIndex = imageViewerInitialIndex, privateKey = currentUserPrivateKey, onDismiss = { showImageViewer = false imageViewerSourceBounds = null + imageViewerImages = emptyList() onImageViewerChanged(false) }, onClosingStart = { @@ -2237,18 +2238,36 @@ fun ChatDetailScreen( showForwardPicker = false ForwardManager.clear() }, - onChatSelected = { selectedDialog -> + onChatsSelected = { selectedDialogs -> showForwardPicker = false - ForwardManager.selectChat(selectedDialog.opponentKey) - val searchUser = - SearchUser( - title = selectedDialog.opponentTitle, - username = selectedDialog.opponentUsername, - publicKey = selectedDialog.opponentKey, - verified = selectedDialog.verified, - online = selectedDialog.isOnline - ) - onNavigateToChat(searchUser) + if (selectedDialogs.isNotEmpty()) { + val primaryDialog = selectedDialogs.first() + val additionalDialogs = selectedDialogs.drop(1) + + // Отправляем forward напрямую в дополнительные чаты + val fwdMessages = ForwardManager.consumeForwardMessages() + if (additionalDialogs.isNotEmpty() && fwdMessages.isNotEmpty()) { + additionalDialogs.forEach { dialog -> + viewModel.sendForwardDirectly( + dialog.opponentKey, + fwdMessages + ) + } + } + + // Навигируемся в первый выбранный чат с forward + ForwardManager.setForwardMessages(fwdMessages, showPicker = false) + ForwardManager.selectChat(primaryDialog.opponentKey) + val searchUser = + SearchUser( + title = primaryDialog.opponentTitle, + username = primaryDialog.opponentUsername, + publicKey = primaryDialog.opponentKey, + verified = primaryDialog.verified, + online = primaryDialog.isOnline + ) + onNavigateToChat(searchUser) + } } ) } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt index 016c882..549792e 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatViewModel.kt @@ -165,6 +165,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val timestamp: Long, val isOutgoing: Boolean, val publicKey: String = "", // publicKey отправителя цитируемого сообщения + val senderName: String = "", // Имя отправителя для атрибуции forward val attachments: List = emptyList() // Для показа превью ) private val _replyMessages = MutableStateFlow>(emptyList()) @@ -544,6 +545,7 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { timestamp = fm.timestamp, isOutgoing = fm.isOutgoing, publicKey = fm.senderPublicKey, + senderName = fm.senderName, attachments = fm.attachments ) } @@ -1248,6 +1250,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val replyText = replyMessage.optString("message", "") val replyMessageIdFromJson = replyMessage.optString("message_id", "") val replyTimestamp = replyMessage.optLong("timestamp", 0L) + val isForwarded = replyMessage.optBoolean("forwarded", false) + val senderNameFromJson = replyMessage.optString("senderName", "") // 📸 Парсим attachments из JSON reply (как в Desktop) val replyAttachmentsFromJson = mutableListOf() @@ -1343,6 +1347,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { }, text = replyText, isFromMe = isReplyFromMe, + isForwarded = isForwarded, + forwardedFromName = if (isForwarded) senderNameFromJson.ifEmpty { + if (isReplyFromMe) "You" + else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } } + } else "", attachments = originalAttachments, senderPublicKey = if (isReplyFromMe) myPublicKey ?: "" @@ -1397,7 +1406,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { timestamp = msg.timestamp.time, isOutgoing = msg.isOutgoing, // Если сообщение от меня - мой publicKey, иначе - собеседника - publicKey = if (msg.isOutgoing) sender else opponent + publicKey = if (msg.isOutgoing) sender else opponent, + attachments = + msg.attachments + .filter { it.type != AttachmentType.MESSAGES } ) } _isForwardMode.value = false @@ -1419,7 +1431,6 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { attachments = msg.attachments .filter { it.type != AttachmentType.MESSAGES } - .map { it.copy(localUri = "") } ) } _isForwardMode.value = true @@ -1504,9 +1515,10 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { if (replyMsgs.isNotEmpty()) { val firstReply = replyMsgs.first() // 🖼️ Получаем attachments из текущих сообщений для превью + // Fallback на firstReply.attachments для forward из другого чата val replyAttachments = _messages.value.find { it.id == firstReply.messageId }?.attachments - ?: emptyList() + ?: firstReply.attachments.filter { it.type != AttachmentType.MESSAGES } ReplyData( messageId = firstReply.messageId, senderName = @@ -1518,6 +1530,11 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { text = firstReply.text, isFromMe = firstReply.isOutgoing, isForwarded = isForward, + forwardedFromName = + if (isForward) firstReply.senderName.ifEmpty { + if (firstReply.isOutgoing) "You" + else opponentTitle.ifEmpty { opponentUsername.ifEmpty { "User" } } + } else "", attachments = replyAttachments, senderPublicKey = if (firstReply.isOutgoing) myPublicKey ?: "" @@ -1566,23 +1583,73 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { val messageAttachments = mutableListOf() var replyBlobForDatabase = "" // Зашифрованный blob для БД (приватным ключом) + // 📸 Forward: сначала загружаем IMAGE на CDN, чтобы обновить ссылки в MESSAGES blob + // Map: originalAttId → (newAttId, newPreview) — для подстановки в reply JSON + val forwardedAttMap = mutableMapOf>() + if (isForwardToSend && replyMsgsToSend.isNotEmpty()) { + val context = getApplication() + val isSaved = (sender == recipient) + var fwdIdx = 0 + + for (msg in replyMsgsToSend) { + for (att in msg.attachments) { + if (att.type == AttachmentType.IMAGE) { + try { + val imageBlob = AttachmentFileManager.readAttachment( + context = context, + attachmentId = att.id, + publicKey = msg.publicKey, + privateKey = privateKey + ) + if (imageBlob != null) { + val encryptedBlob = MessageCrypto.encryptReplyBlob(imageBlob, plainKeyAndNonce) + val newAttId = "fwd_${timestamp}_${fwdIdx++}" + + var uploadTag = "" + if (!isSaved) { + uploadTag = TransportManager.uploadFile(newAttId, encryptedBlob) + } + + val blurhash = att.preview.substringAfter("::", "") + val newPreview = if (uploadTag.isNotEmpty()) "$uploadTag::$blurhash" else blurhash + + forwardedAttMap[att.id] = Pair(newAttId, newPreview) + + // Сохраняем локально с новым ID + // publicKey = msg.publicKey чтобы совпадал с JSON для parseReplyFromAttachments + AttachmentFileManager.saveAttachment( + context = context, + blob = imageBlob, + attachmentId = newAttId, + publicKey = msg.publicKey, + privateKey = privateKey + ) + } + } catch (e: Exception) { } + } + } + } + } + if (replyMsgsToSend.isNotEmpty()) { // Формируем JSON массив с цитируемыми сообщениями (как в Desktop) val replyJsonArray = JSONArray() replyMsgsToSend.forEach { msg -> - // Формируем attachments JSON (как в Desktop) val attachmentsArray = JSONArray() msg.attachments.forEach { att -> + // Для forward IMAGE: подставляем НОВЫЙ id и preview (CDN tag) + val fwdInfo = forwardedAttMap[att.id] + val attId = fwdInfo?.first ?: att.id + val attPreview = fwdInfo?.second ?: att.preview + attachmentsArray.put( JSONObject().apply { - put("id", att.id) + put("id", attId) put("type", att.type.value) - put("preview", att.preview) + put("preview", attPreview) put("width", att.width) put("height", att.height) - // Для IMAGE/FILE - blob не включаем (слишком большой) - // Для MESSAGES - включаем blob put( "blob", if (att.type == AttachmentType.MESSAGES) att.blob @@ -1599,17 +1666,19 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { put("message", msg.text) put("timestamp", msg.timestamp) put("attachments", attachmentsArray) + if (isForwardToSend) { + put("forwarded", true) + put("senderName", msg.senderName) + } } replyJsonArray.put(replyJson) } val replyBlobPlaintext = replyJsonArray.toString() - // 🔥 Шифруем reply blob (для network transmission) с ChaCha ключом val encryptedReplyBlob = MessageCrypto.encryptReplyBlob(replyBlobPlaintext, plainKeyAndNonce) - // 🔥 Re-encrypt с приватным ключом для хранения в БД (как в Desktop Архиве) replyBlobForDatabase = CryptoManager.encryptWithPassword(replyBlobPlaintext, privateKey) @@ -1667,6 +1736,8 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { put("id", att.id) put("type", att.type.value) put("preview", att.preview) + put("width", att.width) + put("height", att.height) // Только для MESSAGES сохраняем blob (reply // data небольшие) // Для IMAGE/FILE - пустой blob @@ -1710,6 +1781,182 @@ class ChatViewModel(application: Application) : AndroidViewModel(application) { } } + /** + * 📨 Прямая отправка forward-сообщений в указанный чат (без навигации) + * Используется при мультивыборе чатов для отправки в дополнительные чаты + */ + fun sendForwardDirectly( + recipientPublicKey: String, + forwardMessages: List + ) { + val sender = myPublicKey ?: return + val privateKey = myPrivateKey ?: return + if (forwardMessages.isEmpty()) return + + viewModelScope.launch(Dispatchers.IO) { + try { + val context = getApplication() + val messageId = UUID.randomUUID().toString().replace("-", "").take(32) + val timestamp = System.currentTimeMillis() + val isSavedMessages = (sender == recipientPublicKey) + + // Шифрование (пустой текст для forward) + val encryptResult = MessageCrypto.encryptForSending("", recipientPublicKey) + val encryptedContent = encryptResult.ciphertext + val encryptedKey = encryptResult.encryptedKey + val plainKeyAndNonce = encryptResult.plainKeyAndNonce + val privateKeyHash = CryptoManager.generatePrivateKeyHash(privateKey) + + val messageAttachments = mutableListOf() + var replyBlobForDatabase = "" + + // 📸 Forward: сначала загружаем IMAGE на CDN, чтобы обновить ссылки в MESSAGES blob + // Map: originalAttId → (newAttId, newPreview) + val forwardedAttMap = mutableMapOf>() + var fwdIdx = 0 + for (fm in forwardMessages) { + for (att in fm.attachments) { + if (att.type == AttachmentType.IMAGE) { + try { + val imageBlob = AttachmentFileManager.readAttachment( + context = context, + attachmentId = att.id, + publicKey = fm.senderPublicKey, + privateKey = privateKey + ) + if (imageBlob != null) { + val encryptedBlob = MessageCrypto.encryptReplyBlob(imageBlob, plainKeyAndNonce) + val newAttId = "fwd_${timestamp}_${fwdIdx++}" + + var uploadTag = "" + if (!isSavedMessages) { + uploadTag = TransportManager.uploadFile(newAttId, encryptedBlob) + } + + val blurhash = att.preview.substringAfter("::", "") + val newPreview = if (uploadTag.isNotEmpty()) "$uploadTag::$blurhash" else blurhash + + forwardedAttMap[att.id] = Pair(newAttId, newPreview) + + // Сохраняем локально с новым ID + // publicKey = fm.senderPublicKey чтобы совпадал с JSON для parseReplyFromAttachments + AttachmentFileManager.saveAttachment( + context = context, + blob = imageBlob, + attachmentId = newAttId, + publicKey = fm.senderPublicKey, + privateKey = privateKey + ) + } + } catch (e: Exception) { } + } + } + } + + // Формируем MESSAGES attachment (reply/forward JSON) с обновлёнными ссылками + val replyJsonArray = JSONArray() + forwardMessages.forEach { fm -> + val attachmentsArray = JSONArray() + fm.attachments.forEach { att -> + // Для forward IMAGE: подставляем НОВЫЙ id и preview (CDN tag) + val fwdInfo = forwardedAttMap[att.id] + val attId = fwdInfo?.first ?: att.id + val attPreview = fwdInfo?.second ?: att.preview + + attachmentsArray.put(JSONObject().apply { + put("id", attId) + put("type", att.type.value) + put("preview", attPreview) + put("width", att.width) + put("height", att.height) + put("blob", if (att.type == AttachmentType.MESSAGES) att.blob else "") + }) + } + replyJsonArray.put(JSONObject().apply { + put("message_id", fm.messageId) + put("publicKey", fm.senderPublicKey) + put("message", fm.text) + put("timestamp", fm.timestamp) + put("attachments", attachmentsArray) + put("forwarded", true) + put("senderName", fm.senderName) + }) + } + + val replyBlobPlaintext = replyJsonArray.toString() + val encryptedReplyBlob = MessageCrypto.encryptReplyBlob(replyBlobPlaintext, plainKeyAndNonce) + replyBlobForDatabase = CryptoManager.encryptWithPassword(replyBlobPlaintext, privateKey) + + val replyAttachmentId = "reply_${timestamp}" + messageAttachments.add(MessageAttachment( + id = replyAttachmentId, + blob = encryptedReplyBlob, + type = AttachmentType.MESSAGES, + preview = "" + )) + + // Отправляем пакет + val packet = PacketMessage().apply { + fromPublicKey = sender + toPublicKey = recipientPublicKey + content = encryptedContent + chachaKey = encryptedKey + this.timestamp = timestamp + this.privateKey = privateKeyHash + this.messageId = messageId + attachments = messageAttachments + } + if (!isSavedMessages) { + ProtocolManager.send(packet) + } + + // Сохраняем в БД + val attachmentsJson = JSONArray().apply { + messageAttachments.forEach { att -> + put(JSONObject().apply { + put("id", att.id) + put("type", att.type.value) + put("preview", att.preview) + put("width", att.width) + put("height", att.height) + put("blob", when (att.type) { + AttachmentType.MESSAGES -> replyBlobForDatabase + else -> "" + }) + }) + } + }.toString() + + saveMessageToDatabase( + messageId = messageId, + text = "", + encryptedContent = encryptedContent, + encryptedKey = encryptedKey, + timestamp = timestamp, + isFromMe = true, + delivered = if (isSavedMessages) 2 else 0, + attachmentsJson = attachmentsJson + ) + + // Сохраняем диалог (для списка чатов) + val db = RosettaDatabase.getDatabase(context) + val dialogDao = db.dialogDao() + val dialogKey = if (sender < recipientPublicKey) "$sender:$recipientPublicKey" + else "$recipientPublicKey:$sender" + val existingDialog = dialogDao.getDialog(sender, recipientPublicKey) + val encryptedLastMsg = CryptoManager.encryptWithPassword("Forwarded message", privateKey) + if (existingDialog != null) { + dialogDao.updateLastMessage( + sender, + recipientPublicKey, + encryptedLastMsg, + timestamp + ) + } + } catch (e: Exception) { } + } + } + /** * 📸🚀 Отправка изображения по URI с МГНОВЕННЫМ optimistic UI Фото появляется в чате СРАЗУ, * конвертация и отправка происходят в фоне diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt index 44f4ee0..db2f5bc 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListScreen.kt @@ -290,6 +290,10 @@ fun ChatsListScreen( // Реактивный set заблокированных пользователей из ViewModel (Room Flow) val blockedUsers by chatsViewModel.blockedUsers.collectAsState() + // Requests count for badge on hamburger & sidebar + val topLevelChatsState by chatsViewModel.chatsState.collectAsState() + val topLevelRequestsCount = topLevelChatsState.requestsCount + // Dev console dialog - commented out for now /* if (showDevConsole) { @@ -515,15 +519,12 @@ fun ChatsListScreen( ) { ModalNavigationDrawer( drawerState = drawerState, - gesturesEnabled = true, // 🔥 Явно включаем свайп для открытия drawer + gesturesEnabled = !showRequestsScreen, // Disable drawer swipe when requests are open drawerContent = { ModalDrawerSheet( drawerContainerColor = Color.Transparent, windowInsets = - WindowInsets( - 0 - ), // 🎨 Убираем системные отступы - drawer идет до - // верха + WindowInsets(0), modifier = Modifier.width(300.dp) ) { Column( @@ -532,7 +533,7 @@ fun ChatsListScreen( .background(drawerBackgroundColor) ) { // ═══════════════════════════════════════════════════════════ - // 🎨 DRAWER HEADER - Avatar and status + // 🎨 DRAWER HEADER // ═══════════════════════════════════════════════════════════ val avatarColors = getAvatarColor( @@ -543,10 +544,6 @@ fun ChatsListScreen( // Header с размытым фоном аватарки Box(modifier = Modifier.fillMaxWidth()) { - // ═══════════════════════════════════════════════════════════ - // 🎨 BLURRED AVATAR BACKGROUND (на всю - // область header) - // ═══════════════════════════════════════════════════════════ BlurredAvatarBackground( publicKey = accountPublicKey, avatarRepository = avatarRepository, @@ -567,99 +564,106 @@ fun ChatsListScreen( .statusBarsPadding() .padding( top = 16.dp, - start = - 20.dp, + start = 20.dp, end = 20.dp, - bottom = - 20.dp + bottom = 20.dp ) ) { - // Avatar - используем AvatarImage - Box( - modifier = - Modifier.size(72.dp) - .clip( - CircleShape - ) - .background( - Color.White - .copy( - alpha = - 0.2f - ) - ) - .padding( - 3.dp - ), - contentAlignment = - Alignment.Center + // Avatar row with theme toggle + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.Top ) { - AvatarImage( - publicKey = - accountPublicKey, - avatarRepository = - avatarRepository, - size = 66.dp, - isDarkTheme = - isDarkTheme, - displayName = - accountName - .ifEmpty { - accountUsername - } // 🔥 Для инициалов - ) + // Avatar + Box( + modifier = + Modifier.size(72.dp) + .clip(CircleShape) + .background( + Color.White + .copy(alpha = 0.2f) + ) + .padding(3.dp), + contentAlignment = + Alignment.Center + ) { + AvatarImage( + publicKey = + accountPublicKey, + avatarRepository = + avatarRepository, + size = 66.dp, + isDarkTheme = + isDarkTheme, + displayName = + accountName + .ifEmpty { + accountUsername + } + ) + } + + // Theme toggle icon + IconButton( + onClick = { onToggleTheme() }, + modifier = Modifier.size(40.dp) + ) { + Icon( + imageVector = + if (isDarkTheme) + TablerIcons.Sun + else TablerIcons.Moon, + contentDescription = + if (isDarkTheme) "Light Mode" + else "Dark Mode", + tint = + if (isDarkTheme) + Color.White.copy(alpha = 0.8f) + else + Color.Black.copy(alpha = 0.7f), + modifier = Modifier.size(22.dp) + ) + } } Spacer( modifier = - Modifier.height( - 14.dp - ) + Modifier.height(14.dp) ) - // Display name (above username) + // Display name if (accountName.isNotEmpty()) { Text( text = accountName, fontSize = 16.sp, fontWeight = - FontWeight - .SemiBold, + FontWeight.SemiBold, color = - if (isDarkTheme - ) + if (isDarkTheme) Color.White else Color.Black ) } - // Username display (below name) + // Username if (accountUsername.isNotEmpty()) { Spacer( modifier = - Modifier.height( - 4.dp - ) + Modifier.height(4.dp) ) Text( text = "@$accountUsername", fontSize = 13.sp, color = - if (isDarkTheme - ) + if (isDarkTheme) Color.White - .copy( - alpha = - 0.7f - ) + .copy(alpha = 0.7f) else Color.Black - .copy( - alpha = - 0.7f - ) + .copy(alpha = 0.7f) ) } } @@ -680,7 +684,7 @@ fun ChatsListScreen( val menuIconColor = textColor.copy(alpha = 0.6f) - // 👤 Profile Section + // 👤 Profile DrawerMenuItemEnhanced( icon = TablerIcons.User, text = "My Profile", @@ -696,7 +700,24 @@ fun ChatsListScreen( } ) - // 📖 Saved Messages + // � Requests + DrawerMenuItemEnhanced( + icon = TablerIcons.MessageCircle2, + text = "Requests", + iconColor = menuIconColor, + textColor = textColor, + badge = if (topLevelRequestsCount > 0) topLevelRequestsCount.toString() else null, + onClick = { + scope.launch { + drawerState.close() + kotlinx.coroutines + .delay(100) + showRequestsScreen = true + } + } + ) + + // �📖 Saved Messages DrawerMenuItemEnhanced( icon = TablerIcons.Bookmark, text = "Saved Messages", @@ -705,9 +726,6 @@ fun ChatsListScreen( onClick = { scope.launch { drawerState.close() - // Ждём завершения - // анимации закрытия - // drawer kotlinx.coroutines .delay(250) onSavedMessagesClick() @@ -717,7 +735,7 @@ fun ChatsListScreen( DrawerDivider(isDarkTheme) - // ⚙️ Settings + // ⚙️ Settings DrawerMenuItemEnhanced( icon = TablerIcons.Settings, text = "Settings", @@ -733,20 +751,6 @@ fun ChatsListScreen( } ) - // 🌓 Theme Toggle - DrawerMenuItemEnhanced( - icon = - if (isDarkTheme) - TablerIcons.Sun - else TablerIcons.Moon, - text = - if (isDarkTheme) - "Light Mode" - else "Dark Mode", - iconColor = menuIconColor, - textColor = textColor, - onClick = { onToggleTheme() } - ) } // ═══════════════════════════════════════════════════════════ @@ -782,10 +786,8 @@ fun ChatsListScreen( modifier = Modifier.fillMaxWidth() .padding( - horizontal = - 20.dp, - vertical = - 12.dp + horizontal = 20.dp, + vertical = 12.dp ), contentAlignment = Alignment.CenterStart @@ -795,13 +797,9 @@ fun ChatsListScreen( fontSize = 12.sp, color = if (isDarkTheme) - Color( - 0xFF666666 - ) + Color(0xFF666666) else - Color( - 0xFF999999 - ) + Color(0xFF999999) ) } @@ -827,11 +825,11 @@ fun ChatsListScreen( ) { Icon( TablerIcons - .ArrowLeft, + .ChevronLeft, contentDescription = "Back", tint = - PrimaryBlue + Color.White ) } } else { @@ -846,18 +844,31 @@ fun ChatsListScreen( } } ) { - Icon( - TablerIcons - .Menu2, - contentDescription = - "Menu", - tint = - textColor - .copy( - alpha = - 0.6f - ) - ) + Box { + Icon( + TablerIcons + .Menu2, + contentDescription = + "Menu", + tint = + textColor + .copy( + alpha = + 0.6f + ) + ) + if (topLevelRequestsCount > 0) { + Box( + modifier = + Modifier + .align(Alignment.TopEnd) + .offset(x = 2.dp, y = (-2).dp) + .size(8.dp) + .clip(CircleShape) + .background(PrimaryBlue) + ) + } + } } } }, @@ -873,13 +884,8 @@ fun ChatsListScreen( color = textColor ) } else { - // Rosetta title - // with status - Row( - verticalAlignment = - Alignment - .CenterVertically - ) { + // Rosetta title or Connecting animation + if (protocolState == ProtocolState.AUTHENTICATED) { Text( "Rosetta", fontWeight = @@ -890,48 +896,12 @@ fun ChatsListScreen( color = textColor ) - Spacer( - modifier = - Modifier.width( - 8.dp - ) - ) - Box( - modifier = - Modifier.size( - 10.dp - ) - .clip( - CircleShape - ) - .background( - when (protocolState - ) { - ProtocolState - .AUTHENTICATED -> - Color( - 0xFF4CAF50 - ) - ProtocolState - .CONNECTING, - ProtocolState - .CONNECTED, - ProtocolState - .HANDSHAKING -> - Color( - 0xFFFFC107 - ) - ProtocolState - .DISCONNECTED -> - Color( - 0xFFF44336 - ) - } - ) - .clickable { - showStatusDialog = - true - } + } else { + AnimatedDotsText( + baseText = "Connecting", + color = textColor, + fontSize = 20.sp, + fontWeight = FontWeight.Bold ) } } @@ -1092,7 +1062,55 @@ fun ChatsListScreen( label = "RequestsTransition" ) { isRequestsScreen -> if (isRequestsScreen) { - // 📬 Show Requests Screen + // 📬 Show Requests Screen with swipe-back + Box( + modifier = Modifier + .fillMaxSize() + .pointerInput(Unit) { + val velocityTracker = androidx.compose.ui.input.pointer.util.VelocityTracker() + awaitEachGesture { + val down = awaitFirstDown(requireUnconsumed = false) + velocityTracker.resetTracking() + velocityTracker.addPosition(down.uptimeMillis, down.position) + var totalDragX = 0f + var totalDragY = 0f + var claimed = false + val touchSlop = viewConfiguration.touchSlop * 0.6f + + while (true) { + val event = awaitPointerEvent() + val change = event.changes.firstOrNull { it.id == down.id } ?: break + if (change.changedToUpIgnoreConsumed()) break + + val delta = change.positionChange() + totalDragX += delta.x + totalDragY += delta.y + velocityTracker.addPosition(change.uptimeMillis, change.position) + + if (!claimed) { + val distance = kotlin.math.sqrt(totalDragX * totalDragX + totalDragY * totalDragY) + if (distance < touchSlop) continue + if (totalDragX > 0 && kotlin.math.abs(totalDragX) > kotlin.math.abs(totalDragY) * 1.2f) { + claimed = true + change.consume() + } else { + break + } + } else { + change.consume() + } + } + + if (claimed) { + val velocityX = velocityTracker.calculateVelocity().x + val screenWidth = size.width.toFloat() + if (totalDragX > screenWidth * 0.08f || velocityX > 200f) { + showRequestsScreen = false + } + } + } + } + ) { RequestsScreen( requests = requests, isDarkTheme = isDarkTheme, @@ -1100,15 +1118,50 @@ fun ChatsListScreen( showRequestsScreen = false }, onRequestClick = { request -> - showRequestsScreen = false val user = chatsViewModel .dialogToSearchUser( request ) onUserSelect(user) + }, + avatarRepository = + avatarRepository, + blockedUsers = blockedUsers, + pinnedChats = pinnedChats, + isDrawerOpen = + drawerState.isOpen || + drawerState + .isAnimationRunning, + onTogglePin = { opponentKey -> + onTogglePin(opponentKey) + }, + onDeleteDialog = { opponentKey -> + scope.launch { + chatsViewModel + .deleteDialog( + opponentKey + ) + } + }, + onBlockUser = { opponentKey -> + scope.launch { + chatsViewModel + .blockUser( + opponentKey + ) + } + }, + onUnblockUser = { opponentKey -> + scope.launch { + chatsViewModel + .unblockUser( + opponentKey + ) + } } ) + } // Close Box wrapper } else if (isLoading) { // 🚀 Shimmer skeleton пока данные грузятся ChatsListSkeleton(isDarkTheme = isDarkTheme) @@ -1586,6 +1639,49 @@ private fun EmptyChatsState(isDarkTheme: Boolean, modifier: Modifier = Modifier) } } +@Composable +private fun EmptyRequestsState(isDarkTheme: Boolean, modifier: Modifier = Modifier) { + val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) + val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) + + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.no_requests)) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever + ) + + Column( + modifier = modifier.fillMaxSize().background(backgroundColor), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier.size(160.dp) + ) + + Spacer(modifier = Modifier.height(20.dp)) + + Text( + text = "No Requests", + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + color = secondaryTextColor, + textAlign = TextAlign.Center + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Text( + text = "New message requests will appear here", + fontSize = 15.sp, + color = secondaryTextColor.copy(alpha = 0.7f), + textAlign = TextAlign.Center + ) + } +} + // Chat item for list @Composable fun ChatItem( @@ -2784,7 +2880,7 @@ fun RequestsSection( Modifier .defaultMinSize(minWidth = 22.dp, minHeight = 22.dp) .clip(RoundedCornerShape(11.dp)) - .background(iconBgColor), + .background(PrimaryBlue), contentAlignment = Alignment.Center ) { Text( @@ -2809,7 +2905,7 @@ fun RequestsSection( Modifier .defaultMinSize(minWidth = 22.dp, minHeight = 22.dp) .clip(RoundedCornerShape(11.dp)) - .background(iconBgColor), + .background(PrimaryBlue), contentAlignment = Alignment.Center ) { Text( @@ -2832,27 +2928,37 @@ fun RequestsScreen( requests: List, isDarkTheme: Boolean, onBack: () -> Unit, - onRequestClick: (DialogUiModel) -> Unit + onRequestClick: (DialogUiModel) -> Unit, + avatarRepository: com.rosetta.messenger.repository.AvatarRepository? = null, + blockedUsers: Set = emptySet(), + pinnedChats: Set = emptySet(), + isDrawerOpen: Boolean = false, + onTogglePin: (String) -> Unit = {}, + onDeleteDialog: (String) -> Unit = {}, + onBlockUser: (String) -> Unit = {}, + onUnblockUser: (String) -> Unit = {} ) { val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) + val textColor = if (isDarkTheme) Color.White else Color.Black + val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) + + var swipedItemKey by remember { mutableStateOf(null) } + + LaunchedEffect(isDrawerOpen) { + if (isDrawerOpen) { + swipedItemKey = null + } + } + + var dialogToDelete by remember { mutableStateOf(null) } + var dialogToBlock by remember { mutableStateOf(null) } + var dialogToUnblock by remember { mutableStateOf(null) } Column(modifier = Modifier.fillMaxSize().background(backgroundColor)) { if (requests.isEmpty()) { - // Empty state - Box( - modifier = Modifier.fillMaxSize().padding(32.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = "No requests", - fontSize = 16.sp, - color = - if (isDarkTheme) Color(0xFF8E8E8E) - else Color(0xFF8E8E93), - textAlign = TextAlign.Center - ) - } + // Empty state with Lottie animation + EmptyRequestsState(isDarkTheme = isDarkTheme) } else { // Requests list LazyColumn(modifier = Modifier.fillMaxSize()) { @@ -2861,11 +2967,35 @@ fun RequestsScreen( key = { it.opponentKey }, contentType = { "request" } ) { request -> - DialogItemContent( + val isBlocked = blockedUsers.contains(request.opponentKey) + val isPinned = pinnedChats.contains(request.opponentKey) + + SwipeableDialogItem( dialog = request, isDarkTheme = isDarkTheme, isTyping = false, - onClick = { onRequestClick(request) } + isBlocked = isBlocked, + isSavedMessages = false, + avatarRepository = avatarRepository, + isDrawerOpen = isDrawerOpen, + isSwipedOpen = + swipedItemKey == request.opponentKey, + onSwipeStarted = { + swipedItemKey = request.opponentKey + }, + onSwipeClosed = { + if (swipedItemKey == request.opponentKey) + swipedItemKey = null + }, + onClick = { + swipedItemKey = null + onRequestClick(request) + }, + onDelete = { dialogToDelete = request }, + onBlock = { dialogToBlock = request }, + onUnblock = { dialogToUnblock = request }, + isPinned = isPinned, + onPin = { onTogglePin(request.opponentKey) } ) Divider( @@ -2877,6 +3007,111 @@ fun RequestsScreen( } } } + + dialogToDelete?.let { dialog -> + AlertDialog( + onDismissRequest = { dialogToDelete = null }, + containerColor = + if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Delete Chat", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "Are you sure you want to delete this chat? This action cannot be undone.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + val opponentKey = dialog.opponentKey + dialogToDelete = null + onDeleteDialog(opponentKey) + } + ) { Text("Delete", color = Color(0xFFFF3B30)) } + }, + dismissButton = { + TextButton(onClick = { dialogToDelete = null }) { + Text("Cancel", color = PrimaryBlue) + } + } + ) + } + + dialogToBlock?.let { dialog -> + AlertDialog( + onDismissRequest = { dialogToBlock = null }, + containerColor = + if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Block ${dialog.opponentTitle.ifEmpty { "User" }}", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "Are you sure you want to block this user? They won't be able to send you messages.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + val opponentKey = dialog.opponentKey + dialogToBlock = null + onBlockUser(opponentKey) + } + ) { Text("Block", color = Color(0xFFFF3B30)) } + }, + dismissButton = { + TextButton(onClick = { dialogToBlock = null }) { + Text("Cancel", color = PrimaryBlue) + } + } + ) + } + + dialogToUnblock?.let { dialog -> + AlertDialog( + onDismissRequest = { dialogToUnblock = null }, + containerColor = + if (isDarkTheme) Color(0xFF2C2C2E) else Color.White, + title = { + Text( + "Unblock ${dialog.opponentTitle.ifEmpty { "User" }}", + fontWeight = FontWeight.Bold, + color = textColor + ) + }, + text = { + Text( + "Are you sure you want to unblock this user? They will be able to send you messages again.", + color = secondaryTextColor + ) + }, + confirmButton = { + TextButton( + onClick = { + val opponentKey = dialog.opponentKey + dialogToUnblock = null + onUnblockUser(opponentKey) + } + ) { Text("Unblock", color = Color(0xFF34C759)) } + }, + dismissButton = { + TextButton(onClick = { dialogToUnblock = null }) { + Text("Cancel", color = PrimaryBlue) + } + } + ) + } } /** 🎨 Enhanced Drawer Menu Item - красивый пункт меню с hover эффектом */ diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt index 0289aca..b3b5b8b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ForwardChatPickerBottomSheet.kt @@ -1,6 +1,7 @@ package com.rosetta.messenger.ui.chats import android.view.HapticFeedbackConstants +import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -12,6 +13,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowForward import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Check import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -33,11 +35,12 @@ import java.util.* import kotlinx.coroutines.launch /** - * 📨 BottomSheet для выбора чата при Forward сообщений + * 📨 BottomSheet для выбора чатов при Forward сообщений * - * Логика как в десктопной версии: - * 1. Показывает список диалогов - * 2. При выборе диалога - переходит в чат с сообщениями в Reply панели + * Поддержка мультивыбора: + * 1. Показывает список диалогов с чекбоксами + * 2. Можно выбрать один или несколько чатов + * 3. Кнопка "Forward" внизу подтверждает выбор */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -47,9 +50,10 @@ fun ForwardChatPickerBottomSheet( currentUserPublicKey: String, avatarRepository: AvatarRepository? = null, onDismiss: () -> Unit, - onChatSelected: (DialogUiModel) -> Unit + onChatSelected: (DialogUiModel) -> Unit = {}, + onChatsSelected: (List) -> Unit = { list -> if (list.size == 1) onChatSelected(list.first()) } ) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) val scope = rememberCoroutineScope() val view = LocalView.current @@ -61,6 +65,9 @@ fun ForwardChatPickerBottomSheet( val forwardMessages by ForwardManager.forwardMessages.collectAsState() val messagesCount = forwardMessages.size + // Мультивыбор чатов + var selectedChats by remember { mutableStateOf>(emptySet()) } + // 🔥 Haptic feedback при открытии LaunchedEffect(Unit) { view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS) @@ -72,7 +79,7 @@ fun ForwardChatPickerBottomSheet( val window = (view.context as? android.app.Activity)?.window val originalStatusBarColor = window?.statusBarColor ?: 0 val scrimColor = android.graphics.Color.argb(153, 0, 0, 0) // 60% черный - + // Плавное затемнение val fadeInAnimator = android.animation.ValueAnimator.ofArgb(originalStatusBarColor, scrimColor).apply { duration = 200 @@ -81,7 +88,7 @@ fun ForwardChatPickerBottomSheet( } } fadeInAnimator.start() - + onDispose { // Плавное восстановление val fadeOutAnimator = android.animation.ValueAnimator.ofArgb(scrimColor, originalStatusBarColor).apply { @@ -109,7 +116,7 @@ fun ForwardChatPickerBottomSheet( onDismissRequest = { dismissWithAnimation() }, sheetState = sheetState, containerColor = backgroundColor, - scrimColor = Color.Black.copy(alpha = 0.6f), // 🔥 Более тёмный overlay - перекрывает status bar + scrimColor = Color.Black.copy(alpha = 0.6f), dragHandle = { // Кастомный handle Column( @@ -131,7 +138,7 @@ fun ForwardChatPickerBottomSheet( } }, shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), - modifier = Modifier.statusBarsPadding() // 🔥 Учитываем status bar + modifier = Modifier.statusBarsPadding() ) { Column(modifier = Modifier.fillMaxWidth().navigationBarsPadding()) { // Header @@ -142,7 +149,6 @@ fun ForwardChatPickerBottomSheet( ) { // Иконка и заголовок Row(verticalAlignment = Alignment.CenterVertically) { - // 🔥 Красивая иконка Forward Icon( Icons.Filled.ArrowForward, contentDescription = null, @@ -206,18 +212,24 @@ fun ForwardChatPickerBottomSheet( LazyColumn( modifier = Modifier.fillMaxWidth() - .heightIn( - min = 300.dp, - max = 400.dp - ) // 🔥 Минимальная высота для лучшего UX + .weight(1f) ) { items(dialogs, key = { it.opponentKey }) { dialog -> + val isSelected = dialog.opponentKey in selectedChats ForwardDialogItem( dialog = dialog, isDarkTheme = isDarkTheme, isSavedMessages = dialog.opponentKey == currentUserPublicKey, + isSelected = isSelected, avatarRepository = avatarRepository, - onClick = { onChatSelected(dialog) } + onClick = { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) + selectedChats = if (isSelected) { + selectedChats - dialog.opponentKey + } else { + selectedChats + dialog.opponentKey + } + } ) // Сепаратор между диалогами @@ -232,6 +244,45 @@ fun ForwardChatPickerBottomSheet( } } + // Кнопка Forward (всегда видна, disabled когда ничего не выбрано) + Spacer(modifier = Modifier.height(8.dp)) + Divider(color = dividerColor, thickness = 0.5.dp) + Spacer(modifier = Modifier.height(8.dp)) + + val hasSelection = selectedChats.isNotEmpty() + Button( + onClick = { + val selectedDialogs = dialogs.filter { it.opponentKey in selectedChats } + onChatsSelected(selectedDialogs) + }, + enabled = hasSelection, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(48.dp), + shape = RoundedCornerShape(12.dp), + colors = ButtonDefaults.buttonColors( + containerColor = PrimaryBlue, + disabledContainerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFD1D1D6), + disabledContentColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF999999) + ) + ) { + Icon( + Icons.Filled.ArrowForward, + contentDescription = null, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = if (hasSelection) + "Forward to ${selectedChats.size} chat${if (selectedChats.size > 1) "s" else ""}" + else + "Select a chat", + fontSize = 16.sp, + fontWeight = FontWeight.SemiBold + ) + } + // Нижний padding Spacer(modifier = Modifier.height(16.dp)) } @@ -244,6 +295,7 @@ private fun ForwardDialogItem( dialog: DialogUiModel, isDarkTheme: Boolean, isSavedMessages: Boolean = false, + isSelected: Boolean = false, avatarRepository: AvatarRepository? = null, onClick: () -> Unit ) { @@ -269,19 +321,22 @@ private fun ForwardDialogItem( } } + val selectedBg by animateColorAsState( + targetValue = if (isSelected) PrimaryBlue.copy(alpha = 0.08f) else Color.Transparent, + animationSpec = tween(150), + label = "selectedBg" + ) + Row( modifier = Modifier.fillMaxWidth() + .background(selectedBg) .clickable(onClick = onClick) .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { // Avatar with real image support if (isSavedMessages) { - // Saved Messages - special icon - val avatarColors = remember(dialog.opponentKey, isDarkTheme) { - getAvatarColor(dialog.opponentKey, isDarkTheme) - } Box( modifier = Modifier .size(48.dp) @@ -324,7 +379,7 @@ private fun ForwardDialogItem( overflow = TextOverflow.Ellipsis, modifier = Modifier.weight(1f, fill = false) ) - + // ✅ Verified badge if (!isSavedMessages && dialog.verified > 0) { VerifiedBadge( @@ -347,7 +402,7 @@ private fun ForwardDialogItem( dialog.lastMessage.isNotEmpty() -> dialog.lastMessage else -> "No messages" } - + AppleEmojiText( text = previewText, fontSize = 14.sp, @@ -357,9 +412,27 @@ private fun ForwardDialogItem( ) } - // Online indicator - if (!isSavedMessages && dialog.isOnline == 1) { - Box(modifier = Modifier.size(10.dp).clip(CircleShape).background(Color(0xFF34C759))) + // Чекбокс выбора (справа) + Spacer(modifier = Modifier.width(8.dp)) + Box( + modifier = Modifier + .size(24.dp) + .clip(CircleShape) + .background( + if (isSelected) Color(0xFF4CD964) + else if (isDarkTheme) Color(0xFF3A3A3A) + else Color(0xFFE0E0E0) + ), + contentAlignment = Alignment.Center + ) { + if (isSelected) { + Icon( + Icons.Default.Check, + contentDescription = "Selected", + tint = Color.White, + modifier = Modifier.size(16.dp) + ) + } } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt index 269a62d..cd1a111 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/RequestsListScreen.kt @@ -14,25 +14,33 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import com.rosetta.messenger.network.SearchUser +import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.ui.onboarding.PrimaryBlue import compose.icons.TablerIcons -import compose.icons.tablericons.ArrowLeft +import compose.icons.tablericons.ChevronLeft +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3Api::class) @Composable fun RequestsListScreen( isDarkTheme: Boolean, chatsViewModel: ChatsListViewModel, + pinnedChats: Set, + onTogglePin: (String) -> Unit, onBack: () -> Unit, - onUserSelect: (SearchUser) -> Unit + onUserSelect: (SearchUser) -> Unit, + avatarRepository: AvatarRepository? = null ) { val chatsState by chatsViewModel.chatsState.collectAsState() val requests = chatsState.requests + val blockedUsers by chatsViewModel.blockedUsers.collectAsState() + val scope = rememberCoroutineScope() val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF2F2F7) val textColor = if (isDarkTheme) Color.White else Color.Black @@ -42,9 +50,9 @@ fun RequestsListScreen( navigationIcon = { IconButton(onClick = onBack) { Icon( - imageVector = TablerIcons.ArrowLeft, + imageVector = TablerIcons.ChevronLeft, contentDescription = "Back", - tint = PrimaryBlue + tint = Color.White ) } }, @@ -79,6 +87,21 @@ fun RequestsListScreen( onBack = onBack, onRequestClick = { request -> onUserSelect(chatsViewModel.dialogToSearchUser(request)) + }, + avatarRepository = avatarRepository, + blockedUsers = blockedUsers, + pinnedChats = pinnedChats, + onTogglePin = onTogglePin, + onDeleteDialog = { opponentKey -> + scope.launch { + chatsViewModel.deleteDialog(opponentKey) + } + }, + onBlockUser = { opponentKey -> + scope.launch { chatsViewModel.blockUser(opponentKey) } + }, + onUnblockUser = { opponentKey -> + scope.launch { chatsViewModel.unblockUser(opponentKey) } } ) } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt index d07fbae..d34316f 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/AttachmentComponents.kt @@ -11,7 +11,10 @@ import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background +import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape @@ -204,6 +207,8 @@ fun MessageAttachments( currentUserPublicKey: String = "", hasCaption: Boolean = false, // Если есть caption - время показывается под фото, не на фото showTail: Boolean = true, // Показывать хвостик пузырька + isSelectionMode: Boolean = false, // Блокировать клик на фото в selection mode + onLongClick: () -> Unit = {}, // Long press на фото — запускает selection mode onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> }, modifier: Modifier = Modifier ) { @@ -230,6 +235,8 @@ fun MessageAttachments( messageStatus = messageStatus, hasCaption = hasCaption, showTail = showTail, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -290,6 +297,8 @@ fun ImageCollage( messageStatus: MessageStatus = MessageStatus.READ, hasCaption: Boolean = false, // Если есть caption - время показывается под фото showTail: Boolean = true, // Показывать хвостик пузырька + isSelectionMode: Boolean = false, // Блокировать клик на фото в selection mode + onLongClick: () -> Unit = {}, // Long press на фото — запускает selection mode onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> }, modifier: Modifier = Modifier ) { @@ -335,6 +344,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = showOverlayOnLast, hasCaption = hasCaption, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -357,6 +368,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = showOverlayOnLast && index == count - 1, aspectRatio = 1f, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -382,6 +395,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -402,6 +417,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -417,6 +434,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = showOverlayOnLast, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -445,6 +464,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -460,6 +481,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -480,6 +503,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -495,6 +520,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = showOverlayOnLast, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -524,6 +551,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -539,6 +568,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = false, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -565,6 +596,8 @@ fun ImageCollage( messageStatus = messageStatus, showTimeOverlay = showOverlayOnLast && isLastItem, fillMaxSize = true, + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, onImageClick = onImageClick ) } @@ -588,6 +621,7 @@ fun ImageCollage( * 3. Если файл есть локально → DOWNLOADED (загружаем из файла) * 4. Иначе → NOT_DOWNLOADED (нужно скачать с CDN) */ +@OptIn(ExperimentalFoundationApi::class) @Composable fun ImageAttachment( attachment: MessageAttachment, @@ -602,6 +636,8 @@ fun ImageAttachment( aspectRatio: Float? = null, fillMaxSize: Boolean = false, hasCaption: Boolean = false, + isSelectionMode: Boolean = false, + onLongClick: () -> Unit = {}, onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> } ) { val context = LocalContext.current @@ -964,27 +1000,36 @@ fun ImageAttachment( // Capture bounds for shared element transition imageBounds = coordinates.boundsInWindow() } - .clickable { - when (downloadStatus) { - DownloadStatus.NOT_DOWNLOADED -> download() - DownloadStatus.DOWNLOADED -> { - // 📸 Open image viewer with bounds for animation - val bounds = imageBounds?.let { - ImageSourceBounds( - left = it.left, - top = it.top, - width = it.width, - height = it.height, - cornerRadius = cornerRadius, - thumbnailBitmap = imageBitmap - ) + .combinedClickable( + indication = null, + interactionSource = remember { MutableInteractionSource() }, + onLongClick = onLongClick, + onClick = { + if (!isSelectionMode) { + when (downloadStatus) { + DownloadStatus.NOT_DOWNLOADED -> download() + DownloadStatus.DOWNLOADED -> { + // 📸 Open image viewer with bounds for animation + val bounds = imageBounds?.let { + ImageSourceBounds( + left = it.left, + top = it.top, + width = it.width, + height = it.height, + cornerRadius = cornerRadius, + thumbnailBitmap = imageBitmap + ) + } + onImageClick(attachment.id, bounds) + } + DownloadStatus.ERROR -> download() + else -> {} + } + } else { + onLongClick() } - onImageClick(attachment.id, bounds) } - DownloadStatus.ERROR -> download() - else -> {} - } - }, + ), contentAlignment = Alignment.Center ) { // Фоновый слой - blurhash или placeholder @@ -1884,13 +1929,13 @@ private val uuidRegex = * Проверка является ли preview UUID тегом для скачивания Как в desktop: * attachment.preview.split("::")[0].match(uuidRegex) */ -private fun isDownloadTag(preview: String): Boolean { +internal fun isDownloadTag(preview: String): Boolean { val firstPart = preview.split("::").firstOrNull() ?: return false return uuidRegex.matches(firstPart) } /** Получить download tag из preview */ -private fun getDownloadTag(preview: String): String { +internal fun getDownloadTag(preview: String): String { val parts = preview.split("::") if (parts.isNotEmpty() && uuidRegex.matches(parts[0])) { return parts[0] @@ -1899,7 +1944,7 @@ private fun getDownloadTag(preview: String): String { } /** Получить preview без download tag Как в desktop: preview.split("::").splice(1).join("::") */ -private fun getPreview(preview: String): String { +internal fun getPreview(preview: String): String { val parts = preview.split("::") if (parts.isNotEmpty() && uuidRegex.matches(parts[0])) { return parts.drop(1).joinToString("::") @@ -1931,7 +1976,7 @@ private fun parseFilePreview(preview: String): Pair { } /** Декодирование base64 в Bitmap */ -private fun base64ToBitmap(base64: String): Bitmap? { +internal fun base64ToBitmap(base64: String): Bitmap? { return try { val cleanBase64 = if (base64.contains(",")) { diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt index 991b6ff..49b8cfd 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ChatDetailComponents.kt @@ -29,6 +29,8 @@ import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.font.FontWeight @@ -44,6 +46,8 @@ import com.rosetta.messenger.ui.chats.models.* import com.rosetta.messenger.ui.chats.utils.* import com.rosetta.messenger.ui.components.AppleEmojiText import com.rosetta.messenger.ui.onboarding.PrimaryBlue +import com.rosetta.messenger.crypto.MessageCrypto +import com.rosetta.messenger.network.TransportManager import com.rosetta.messenger.utils.AttachmentFileManager import com.vanniktech.blurhash.BlurHash import compose.icons.TablerIcons @@ -640,7 +644,10 @@ fun MessageBubble( replyData = reply, isOutgoing = message.isOutgoing, isDarkTheme = isDarkTheme, - onClick = { onReplyClick(reply.messageId) } + chachaKey = message.chachaKey, + privateKey = privateKey, + onClick = { onReplyClick(reply.messageId) }, + onImageClick = onImageClick ) Spacer(modifier = Modifier.height(4.dp)) } @@ -664,7 +671,10 @@ fun MessageBubble( hasCaption = hasImageWithCaption, showTail = showTail, // Передаём для формы // пузырька - onImageClick = onImageClick + isSelectionMode = isSelectionMode, + onLongClick = onLongClick, + // В selection mode блокируем открытие фото + onImageClick = if (isSelectionMode) { _, _ -> } else onImageClick ) } @@ -1067,7 +1077,10 @@ fun ReplyBubble( replyData: ReplyData, isOutgoing: Boolean, isDarkTheme: Boolean, - onClick: () -> Unit = {} + chachaKey: String = "", + privateKey: String = "", + onClick: () -> Unit = {}, + onImageClick: (attachmentId: String, bounds: ImageSourceBounds?) -> Unit = { _, _ -> } ) { val context = androidx.compose.ui.platform.LocalContext.current val backgroundColor = @@ -1095,13 +1108,11 @@ fun ReplyBubble( // Blurhash preview для fallback var blurPreviewBitmap by remember { mutableStateOf(null) } - // Сначала загружаем blurhash preview + // Сначала загружаем blurhash preview (более высокое разрешение для чёткости) LaunchedEffect(imageAttachment?.preview) { if (imageAttachment != null && imageAttachment.preview.isNotEmpty()) { withContext(Dispatchers.IO) { try { - // Получаем blurhash из preview (может быть в формате - // "tag::blurhash") val blurhash = if (imageAttachment.preview.contains("::")) { imageAttachment.preview.substringAfter("::") @@ -1115,57 +1126,49 @@ fun ReplyBubble( } if (blurhash.isNotEmpty()) { blurPreviewBitmap = - BlurHash.decode(blurhash, 36, 36) + BlurHash.decode(blurhash, 64, 64) } - } catch (e: Exception) { - // Ignore blurhash decode errors - } + } catch (e: Exception) {} } } } - // Потом пробуем загрузить полноценную картинку + // Загружаем полноценную картинку LaunchedEffect(imageAttachment?.id) { if (imageAttachment != null) { + // 1. Проверяем in-memory кэш (фото уже загружено в чате) + val cached = ImageBitmapCache.get("img_${imageAttachment.id}") + if (cached != null) { + imageBitmap = cached + return@LaunchedEffect + } + withContext(Dispatchers.IO) { try { - // Пробуем сначала из blob - if (imageAttachment.blob.isNotEmpty()) { - val decoded = - try { - val cleanBase64 = - if (imageAttachment.blob - .contains( - "," - ) - ) { - imageAttachment.blob - .substringAfter( - "," - ) - } else { - imageAttachment.blob - } - val decodedBytes = - Base64.decode( - cleanBase64, - Base64.DEFAULT - ) - BitmapFactory.decodeByteArray( - decodedBytes, - 0, - decodedBytes.size - ) - } catch (e: Exception) { - null + // 2. Загружаем из localUri (для недавно отправленных/полученных фото) + if (imageAttachment.localUri.isNotEmpty()) { + try { + val uri = android.net.Uri.parse(imageAttachment.localUri) + context.contentResolver.openInputStream(uri)?.use { stream -> + val bitmap = BitmapFactory.decodeStream(stream) + if (bitmap != null) { + imageBitmap = bitmap + return@withContext + } } + } catch (_: Exception) {} + } + + // 3. Пробуем из blob + if (imageAttachment.blob.isNotEmpty()) { + val decoded = base64ToBitmap(imageAttachment.blob) if (decoded != null) { imageBitmap = decoded return@withContext } } - // Если blob нет - загружаем из локального файла + // 4. Загружаем из AttachmentFileManager val localBlob = AttachmentFileManager.readAttachment( context, @@ -1173,37 +1176,84 @@ fun ReplyBubble( replyData.senderPublicKey, replyData.recipientPrivateKey ) - if (localBlob != null) { - val decoded = - try { - val cleanBase64 = - if (localBlob.contains(",") - ) { - localBlob - .substringAfter( - "," - ) - } else { - localBlob - } - val decodedBytes = - Base64.decode( - cleanBase64, - Base64.DEFAULT - ) - BitmapFactory.decodeByteArray( - decodedBytes, - 0, - decodedBytes.size - ) - } catch (e: Exception) { - null - } - imageBitmap = decoded + imageBitmap = base64ToBitmap(localBlob) + if (imageBitmap != null) return@withContext } } catch (e: Exception) {} } + + // 5. Retry: фото может загрузиться в кэш параллельно + if (imageBitmap == null) { + repeat(6) { + kotlinx.coroutines.delay(500) + val retry = ImageBitmapCache.get("img_${imageAttachment.id}") + if (retry != null) { + imageBitmap = retry + return@LaunchedEffect + } + } + } + + // 6. CDN download — для форвардов, где фото загружено на CDN + if (imageBitmap == null && imageAttachment.preview.isNotEmpty()) { + val downloadTag = getDownloadTag(imageAttachment.preview) + if (downloadTag.isNotEmpty()) { + try { + withContext(Dispatchers.IO) { + val encryptedContent = TransportManager.downloadFile( + imageAttachment.id, downloadTag + ) + if (encryptedContent.isNotEmpty()) { + // Расшифровываем: нужен chachaKey сообщения-контейнера + val keyToUse = chachaKey.ifEmpty { replyData.recipientPrivateKey } + val privKey = privateKey.ifEmpty { replyData.recipientPrivateKey } + var decrypted: String? = null + + // Способ 1: chachaKey + privateKey → ECDH → decrypt + if (chachaKey.isNotEmpty() && privKey.isNotEmpty()) { + try { + val plainKeyAndNonce = MessageCrypto.decryptKeyFromSender( + chachaKey, privKey + ) + decrypted = MessageCrypto.decryptAttachmentBlobWithPlainKey( + encryptedContent, plainKeyAndNonce + ) + } catch (_: Exception) {} + } + + // Способ 2: senderPublicKey + recipientPrivateKey + if (decrypted == null && replyData.senderPublicKey.isNotEmpty() && replyData.recipientPrivateKey.isNotEmpty()) { + try { + val plainKeyAndNonce = MessageCrypto.decryptKeyFromSender( + replyData.senderPublicKey, replyData.recipientPrivateKey + ) + decrypted = MessageCrypto.decryptAttachmentBlobWithPlainKey( + encryptedContent, plainKeyAndNonce + ) + } catch (_: Exception) {} + } + + if (decrypted != null) { + val bitmap = base64ToBitmap(decrypted) + if (bitmap != null) { + imageBitmap = bitmap + ImageBitmapCache.put("img_${imageAttachment.id}", bitmap) + // Сохраняем на диск + AttachmentFileManager.saveAttachment( + context = context, + blob = decrypted, + attachmentId = imageAttachment.id, + publicKey = replyData.senderPublicKey, + privateKey = replyData.recipientPrivateKey + ) + } + } + } + } + } catch (_: Exception) {} + } + } } } @@ -1219,19 +1269,36 @@ fun ReplyBubble( Box(modifier = Modifier.width(3.dp).fillMaxHeight().background(borderColor)) // Контент reply - Row( + Column( modifier = Modifier.weight(1f) .padding( start = 8.dp, - end = if (hasImage) 4.dp else 10.dp, + end = 10.dp, top = 4.dp, bottom = 4.dp - ), - verticalAlignment = Alignment.CenterVertically + ) ) { - // Текстовая часть - Column(modifier = Modifier.weight(1f)) { + // Заголовок (имя отправителя / Forwarded from) + if (replyData.isForwarded && replyData.forwardedFromName.isNotEmpty()) { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "Forwarded from ", + color = nameColor, + fontSize = 14.sp, + fontWeight = FontWeight.Normal, + maxLines = 1 + ) + Text( + text = replyData.forwardedFromName, + color = nameColor, + fontSize = 14.sp, + fontWeight = FontWeight.Bold, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } else { Text( text = if (replyData.isForwarded) "Forwarded message" @@ -1242,18 +1309,23 @@ fun ReplyBubble( maxLines = 1, overflow = TextOverflow.Ellipsis ) + } - // Текст или "Photo" - val displayText = - when { - replyData.text.isNotEmpty() -> replyData.text - hasImage -> "Photo" - replyData.attachments.any { - it.type == AttachmentType.FILE - } -> "File" - else -> "..." - } - + // Текст сообщения + if (replyData.text.isNotEmpty()) { + AppleEmojiText( + text = replyData.text, + color = replyTextColor, + fontSize = 14.sp, + maxLines = 2, + overflow = android.text.TextUtils.TruncateAt.END, + enableLinks = false + ) + } else if (!hasImage) { + val displayText = when { + replyData.attachments.any { it.type == AttachmentType.FILE } -> "File" + else -> "..." + } AppleEmojiText( text = displayText, color = replyTextColor, @@ -1264,33 +1336,60 @@ fun ReplyBubble( ) } - // 🖼️ Превью изображения справа (как в Telegram) + // 🖼️ Большое фото внизу пузырька if (hasImage) { - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.height(4.dp)) + + // Aspect ratio из attachment + val imgWidth = imageAttachment?.width ?: 0 + val imgHeight = imageAttachment?.height ?: 0 + val aspectRatio = if (imgWidth > 0 && imgHeight > 0) { + imgWidth.toFloat() / imgHeight.toFloat() + } else { + 1.5f + } + + var photoBoxBounds by remember { mutableStateOf(null) } + Box( modifier = - Modifier.size(36.dp) - .clip(RoundedCornerShape(4.dp)) - .background(Color.Gray.copy(alpha = 0.3f)) + Modifier.fillMaxWidth() + .aspectRatio(aspectRatio.coerceIn(0.5f, 2.5f)) + .heightIn(max = 180.dp) + .clip(RoundedCornerShape(6.dp)) + .background(Color.Gray.copy(alpha = 0.2f)) + .onGloballyPositioned { coords -> + val rect = coords.boundsInWindow() + photoBoxBounds = ImageSourceBounds( + left = rect.left, + top = rect.top, + width = rect.width, + height = rect.height, + cornerRadius = 6f, + thumbnailBitmap = imageBitmap ?: blurPreviewBitmap + ) + } + .clickable { + if (imageAttachment != null) { + onImageClick(imageAttachment.id, photoBoxBounds) + } + } ) { if (imageBitmap != null) { Image( bitmap = imageBitmap!!.asImageBitmap(), - contentDescription = "Photo preview", + contentDescription = "Photo", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) } else if (blurPreviewBitmap != null) { - // Blurhash preview если картинка не загружена Image( - bitmap = - blurPreviewBitmap!!.asImageBitmap(), - contentDescription = "Photo preview", + bitmap = blurPreviewBitmap!!.asImageBitmap(), + contentDescription = "Photo", modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop ) } else { - // Placeholder с иконкой только если нет blurhash Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center @@ -1298,11 +1397,8 @@ fun ReplyBubble( Icon( TablerIcons.Photo, contentDescription = null, - tint = - Color.White.copy( - alpha = 0.7f - ), - modifier = Modifier.size(20.dp) + tint = Color.White.copy(alpha = 0.7f), + modifier = Modifier.size(28.dp) ) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt index ebbd6c7..70d0f66 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageViewerScreen.kt @@ -395,7 +395,7 @@ fun ImageViewerScreen( } ), pageSpacing = 30.dp, // Telegram: dp(30) между фото - key = { images[it].attachmentId }, + key = { images.getOrNull(it)?.attachmentId ?: "page_$it" }, userScrollEnabled = !isAnimating, // Отключаем скролл во время анимации beyondBoundsPageCount = 1, flingBehavior = PagerDefaults.flingBehavior( @@ -403,7 +403,7 @@ fun ImageViewerScreen( snapPositionalThreshold = 0.5f ) ) { page -> - val image = images[page] + val image = images.getOrNull(page) ?: return@HorizontalPager // 🎬 Telegram-style наслаивание: правая страница уменьшается и затемняется val pageOffset = (pagerState.currentPage - page) + pagerState.currentPageOffsetFraction @@ -901,6 +901,10 @@ private suspend fun loadBitmapForViewerImage( privateKey: String ): Bitmap? { return try { + // 0. Проверяем in-memory кэш (ReplyBubble / основной чат уже загрузили) + val cached = ImageBitmapCache.get("img_${image.attachmentId}") + if (cached != null) return cached + // 1. Если blob уже есть в сообщении if (image.blob.isNotEmpty()) { base64ToBitmapSafe(image.blob)?.let { return it } @@ -959,19 +963,6 @@ private fun base64ToBitmapSafe(base64String: String): Bitmap? { } } -/** - * Извлечение download tag из preview - */ -private fun getDownloadTag(preview: String): String { - return if (preview.contains("::")) { - preview.split("::").firstOrNull() ?: "" - } else if (isUUID(preview)) { - preview - } else { - "" - } -} - /** * Проверка является ли строка UUID */ @@ -988,9 +979,10 @@ fun extractImagesFromMessages( opponentPublicKey: String, opponentName: String ): List { + val seenIds = mutableSetOf() return messages .flatMap { message -> - message.attachments + val mainImages = message.attachments .filter { it.type == AttachmentType.IMAGE } .map { attachment -> ViewableImage( @@ -1006,7 +998,31 @@ fun extractImagesFromMessages( caption = message.text ) } + + // Also include images from reply/forward bubbles + val replyImages = message.replyData?.attachments + ?.filter { it.type == AttachmentType.IMAGE } + ?.map { attachment -> + val replySenderKey = message.replyData.senderPublicKey.ifEmpty { + if (message.replyData.isFromMe) currentPublicKey else opponentPublicKey + } + ViewableImage( + attachmentId = attachment.id, + preview = attachment.preview, + blob = attachment.blob, + chachaKey = message.chachaKey, + senderPublicKey = replySenderKey, + senderName = message.replyData.senderName, + timestamp = message.timestamp, + width = attachment.width, + height = attachment.height, + caption = message.replyData.text + ) + } ?: emptyList() + + mainImages + replyImages } + .filter { seenIds.add(it.attachmentId) } .sortedBy { it.timestamp } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/models/ChatDetailModels.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/models/ChatDetailModels.kt index 5062715..6f68079 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/models/ChatDetailModels.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/models/ChatDetailModels.kt @@ -23,6 +23,7 @@ data class ReplyData( val text: String, val isFromMe: Boolean, val isForwarded: Boolean = false, + val forwardedFromName: String = "", // Имя оригинального отправителя для атрибуции val attachments: List = emptyList(), // 🖼️ Для превью фото в reply val senderPublicKey: String = "", // Для расшифровки attachments в reply val recipientPrivateKey: String = "" // Для расшифровки attachments в reply diff --git a/app/src/main/res/raw/no_requests.json b/app/src/main/res/raw/no_requests.json new file mode 100644 index 0000000..f201434 --- /dev/null +++ b/app/src/main/res/raw/no_requests.json @@ -0,0 +1 @@ +{"tgs":1,"v":"5.5.2","fr":60,"ip":0,"op":180,"w":512,"h":512,"nm":"13_FOLDER_empty","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":0,"k":[316.961,264.141,0]},"a":{"a":0,"k":[60,60,0]},"s":{"a":0,"k":[120,120,100]}},"ao":0,"ip":-100,"op":207,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 1","parent":1,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.875],"y":[1]},"o":{"x":[0.374],"y":[0]},"t":30.346,"s":[63.714]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":76,"s":[83.516]},{"t":92.306640625,"s":[87.874]}]},"p":{"a":1,"k":[{"i":{"x":0.875,"y":1},"o":{"x":0.374,"y":0},"t":30.346,"s":[37.083,76.667,0],"to":[83.536,-21.492,0],"ti":[2.144,11.401,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":76,"s":[142.279,9.884,0],"to":[-0.818,-4.352,0],"ti":[26.172,-3.566,0]},{"t":92.306640625,"s":[113.634,1.863,0]}]},"a":{"a":0,"k":[96.211,4.641,0]},"s":{"a":1,"k":[{"i":{"x":[0.875,0.875,0.875],"y":[1,1,1]},"o":{"x":[0.374,0.374,0.374],"y":[0,0,0]},"t":30.346,"s":[80,100,100]},{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":76,"s":[75,60,100]},{"t":92.306640625,"s":[60,75,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.351,-13.492],[-3.816,0.687],[2.039,8.098]],"o":[[0.19,7.331],[2.545,-0.458],[-3.698,-14.689]],"v":[[89.215,2.765],[98.703,18.842],[104.1,3.393]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.260436118141,0.20539522956,0.144827829251,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.200053184852,0.160982271017,0.117987982302,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":12},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-73,"op":207,"st":27,"bm":0},{"ddd":0,"ind":3,"ty":3,"nm":"NULL CONTROL","parent":2,"sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":-77.774},"p":{"a":0,"k":[96.683,4.941,0]},"a":{"a":0,"k":[60,60,0]},"s":{"a":0,"k":[181.743,215.51,100]}},"ao":0,"ip":-54,"op":207,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 10","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[67.568,56.963,0],"to":[-0.51,0.229,0],"ti":[0.51,-0.229,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[64.505,58.337,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[64.505,58.337,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[-90.673,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-3.469,7.401],[-2.28,6.628],[-2.641,-2.205]],"o":[[4.355,-9.287],[4.757,-2.601],[4.724,-0.882]],"v":[[123.357,-12.459],[130.769,-33.153],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":34.906,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47.447,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":54.525,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.833,"y":0.847},"o":{"x":0.333,"y":0},"t":67.066,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":76,"s":[{"i":[[-3.648,7.326],[-2.023,6.488],[-2.641,-2.205]],"o":[[4.584,-9.205],[4.757,-1.917],[4.788,0.053]],"v":[[121.068,0.734],[131.522,-30.039],[143.788,-22.368]],"c":false}]},{"t":88.859375,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.201807598039,0.162009145699,0.118456859215,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-54,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Shape Layer 9","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[67.568,56.963,0],"to":[-0.51,0.229,0],"ti":[0.51,-0.229,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[64.505,58.337,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[64.505,58.337,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[90.673,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-2.842,7.661],[-3.179,7.119],[-2.641,-2.205]],"o":[[3.555,-9.574],[4.757,-4.994],[4.502,-4.151]],"v":[[122.042,-0.723],[129.454,-21.417],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":38.83,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":43.734,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":56.275,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":63.354,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":73.826,"s":[{"i":[[-2.549,4.917],[-3.6,4.642],[-2.641,-1.393]],"o":[[3.18,-6.133],[4.757,-3.863],[4.398,-3.591]],"v":[[121.426,-7.312],[128.838,-20.385],[143.788,-24.462]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":82.973,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":97.6875,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-72,"op":189,"st":9,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"Shape Layer 8","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[60.46,60.035,0],"to":[-0.076,0.086,0],"ti":[0.076,-0.086,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[60.004,60.553,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[60.004,60.553,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[-90.673,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-6.635,4.759],[-6.521,4.493],[-2.641,-2.205]],"o":[[1.356,-0.972],[4.757,-4.242],[4.572,-3.124]],"v":[[114.323,-22.714],[130.095,-35.368],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41.771,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":59.217,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.833,"y":0.847},"o":{"x":0.333,"y":0},"t":66.299,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":76,"s":[{"i":[[-2.864,5.623],[-3.15,5.22],[-2.641,-1.621]],"o":[[3.58,-7.03],[4.757,-3.615],[4.509,-2.975]],"v":[[122.084,-7.882],[129.496,-23.09],[143.788,-23.518]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":81.012,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":85.918,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":100.630859375,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-73,"op":192,"st":12,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"Shape Layer 7","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[60.46,60.035,0],"to":[-0.076,0.086,0],"ti":[0.076,-0.086,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[60.004,60.553,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[60.004,60.553,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[90.673,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-3.367,7.443],[-2.426,6.708],[-2.641,-2.205]],"o":[[4.225,-9.334],[4.757,-2.989],[4.688,-1.412]],"v":[[123.143,-10.553],[130.555,-31.247],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":35.887,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":48,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":56,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":68.047,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":76,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.726,-14.564],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":89.842,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":94.746,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":109.4609375,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":3},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-33,"op":201,"st":21,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"Shape Layer 12","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[64.337,59.083,0],"to":[-0.262,0.386,0],"ti":[0.262,-0.386,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[62.768,61.396,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[62.768,61.396,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[-60.449,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-7.34,3.579],[-2.728,2.948],[-2.641,-2.205]],"o":[[5.682,-2.771],[3.079,-0.869],[4.565,-3.236]],"v":[[108.28,-12.624],[129.549,-27.338],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":38.83,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":51.371,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":58.449,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":70.99,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":78.068,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":92.783203125,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4.5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-67,"op":184,"st":4,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"Shape Layer 11","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[64.337,59.083,0],"to":[-0.262,0.386,0],"ti":[0.262,-0.386,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[62.768,61.396,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[62.768,61.396,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[60.449,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-3.109,7.55],[-2.796,6.91],[-2.641,-2.205]],"o":[[3.896,-9.452],[4.757,-3.973],[4.597,-2.757]],"v":[[122.603,-5.727],[130.015,-26.421],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41.666,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":47.66,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":61.285,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.833,"y":0.847},"o":{"x":0.333,"y":0},"t":67.279,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":76,"s":[{"i":[[-2.971,7.607],[-2.994,7.018],[-2.641,-2.205]],"o":[[3.719,-9.515],[4.757,-4.502],[4.548,-3.48]],"v":[[122.856,-9.354],[129.724,-23.827],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":81.992,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":86.898,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":101.611328125,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4.5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-41,"op":193,"st":13,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"Shape Layer 6","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[52.613,62.957,0],"to":[0.196,-0.091,0],"ti":[-0.196,0.091,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[53.79,62.41,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[53.79,62.41,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[-60.449,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":30.346,"s":[{"i":[[-7.053,4.129],[-3.218,4.759],[-2.641,-2.205]],"o":[[6.694,-3.918],[4.757,-2.656],[4.719,-0.956]],"v":[[106.483,-18.526],[125.062,-34.696],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":46.572,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":52.564,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":67.279,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.833,"y":0.847},"o":{"x":0.333,"y":0},"t":72.184,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":76,"s":[{"i":[[-3.519,7.38],[-2.208,6.589],[-2.641,-2.205]],"o":[[4.42,-9.264],[4.757,-2.408],[4.742,-0.617]],"v":[[123.137,-17.139],[130.875,-34.101],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":86.898,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":91.803,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":106.517578125,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4.5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-36,"op":198,"st":18,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"Shape Layer 5","parent":3,"sr":1,"ks":{"r":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.167],"y":[0.167]},"t":30.346,"s":[67.601]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.333],"y":[0]},"t":70.566,"s":[79.868]},{"t":92.306640625,"s":[67.601]}]},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30.346,"s":[52.613,62.957,0],"to":[0.196,-0.091,0],"ti":[-0.196,0.091,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":76,"s":[53.79,62.41,0],"to":[0,0,0],"ti":[0,0,0]},{"t":92.306640625,"s":[53.79,62.41,0]}]},"a":{"a":0,"k":[143.788,-23.152,0]},"s":{"a":0,"k":[60.449,71.61,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":30.346,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[5.699,-1.164],[4.398,-5.684]],"v":[[121.426,4.778],[123.42,-23.014],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":36.867,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":41.771,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":56.486,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.833,"y":0.847},"o":{"x":0.333,"y":0},"t":61.393,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.153},"t":76,"s":[{"i":[[-2.549,1.897],[-3.599,1.791],[-2.641,-0.537]],"o":[[3.18,-2.366],[4.757,-1.49],[4.399,-1.385]],"v":[[121.427,-20.052],[128.839,-25.095],[143.788,-26.666]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":76.107,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":81.012,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":95.727,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":100.631,"s":[{"i":[[-3.714,7.299],[-1.929,6.436],[-2.641,-2.205]],"o":[[4.668,-9.175],[4.757,-1.664],[4.811,0.398]],"v":[[123.871,-17.052],[131.283,-37.746],[143.788,-22.368]],"c":false}]},{"t":115.345703125,"s":[{"i":[[-2.549,7.782],[-3.6,7.349],[-2.641,-2.205]],"o":[[3.18,-9.709],[4.757,-6.116],[4.398,-5.684]],"v":[[121.426,4.778],[128.838,-15.916],[143.788,-22.368]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.2,0.160784313725,0.117647058824,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":4.5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":-27,"op":207,"st":27,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Layer 48","parent":2,"sr":1,"ks":{"p":{"a":0,"k":[-0.691,0.492,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-24.961,37.768],[7.741,-1.702],[0,0],[4.469,-9.108],[6.375,-39.883],[14.688,-5.293],[0,0],[-0.782,7.932]],"o":[[4.37,-6.613],[0,0],[-9.908,2.181],[-10.401,21.201],[-11.337,70.931],[0,0],[7.725,-1.963],[6.775,-68.707]],"v":[[468.123,222.836],[458.903,209.357],[166.165,273.788],[143.374,291.616],[115.511,390.419],[98.758,502.748],[407.551,424.311],[421.711,407.822]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[-23.907,28.05],[7.689,-1.517],[0,0],[5.636,-6.162],[8.362,-20.219],[14.533,-4.358],[0,0],[-0.564,5.721]],"o":[[4.186,-4.911],[0,0],[-9.841,1.943],[-12.184,13.321],[-20.014,48.392],[0,0],[7.665,-1.703],[4.887,-49.554]],"v":[[504.103,294.062],[496.52,279.492],[194.103,351.391],[171.817,365.052],[133.888,425.969],[98.805,502.909],[404.914,426.737],[418.611,414.366]],"c":true}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[1.26,40.797],[4.413,-1.098],[0,0],[1.086,-8.319],[-4.321,-69.16],[6.933,-2.99],[0,0],[-0.201,7.968]],"o":[[-0.245,-7.922],[0,0],[-9.908,2.181],[-1.684,12.892],[4.392,70.299],[0,0],[7.725,-1.963],[1.98,-78.314]],"v":[[413.431,180.211],[404.278,171.482],[114.665,235.288],[91.874,253.116],[99.261,385.419],[98.758,502.748],[409.301,423.561],[417.21,401.322]],"c":true}]},{"t":180,"s":[{"i":[[-24.961,37.768],[7.741,-1.702],[0,0],[4.469,-9.108],[6.375,-39.883],[14.688,-5.293],[0,0],[-0.782,7.932]],"o":[[4.37,-6.613],[0,0],[-9.908,2.181],[-10.401,21.201],[-11.337,70.931],[0,0],[7.725,-1.963],[6.775,-68.707]],"v":[[468.123,222.836],[458.903,209.357],[166.165,273.788],[143.374,291.616],[115.511,390.419],[98.758,502.748],[407.551,424.311],[421.711,407.822]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":-17,"op":180,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Layer 47","parent":13,"sr":1,"ks":{"p":{"a":0,"k":[42.742,39.17,0]},"a":{"a":0,"k":[284.188,355.943,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0]],"o":[[0,0]],"v":[[436.736,278.703]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.666666666667,0.486274539723,0.294117647059,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-146.369,32.216],[0,0],[146.369,-32.216]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[-5.218,2.102],[0,0],[0,0]],"o":[[5.218,-2.102],[0,0],[0,0]],"v":[[-124.954,109.986],[-104.744,105.383],[184.145,36.215]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[-2.587,8.5],[0,0],[0,0]],"o":[[3.545,-11.647],[0,0],[0,0]],"v":[[-216.369,7.216],[-197.497,-6.366],[94.869,-70.716]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-146.369,32.216],[0,0],[146.369,-32.216]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[313.824,247.432]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[156.65,-39.791],[-156.65,39.791]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[153.754,-35.229],[-151.877,40.53]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[151.65,-39.291],[-159.4,40.291]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[156.65,-39.791],[-156.65,39.791]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.933333337307,0.945098042488,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[262.449,454.978]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":50},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[-0.117,2.624],[-4.814,30.119],[-10.636,21.678],[-8.212,1.808],[0,0]],"o":[[0.118,-2.391],[0.781,-17.567],[5.964,-37.312],[3.669,-7.479],[0,0],[0,0]],"v":[[-172.883,127.709],[-172.537,120.187],[-165.873,48.441],[-138.548,-48.666],[-119.854,-63.277],[172.883,-127.709]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[-0.578,1.835],[-9.331,20.193],[-14.275,13.007],[-8.156,1.61],[0,0]],"o":[[0.052,-1.72],[3.774,-11.988],[8.166,-17.673],[5.221,-4.757],[0,0],[0,0]],"v":[[-166.234,136.059],[-166.094,130.649],[-147.725,81.748],[-110.037,23.817],[-91.756,12.621],[210.66,-59.278]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[0,0],[-0.117,2.624],[1.314,24.693],[-0.511,15.491],[-8.212,1.808],[0,0]],"o":[[0.118,-2.391],[0.781,-17.567],[-2.009,-37.733],[0.275,-8.325],[0,0],[0,0]],"v":[[-179.633,122.646],[-179.287,115.125],[-182.873,43.383],[-190.048,-87.166],[-171.354,-101.777],[121.383,-166.209]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[-0.117,2.624],[-4.814,30.119],[-10.636,21.678],[-8.212,1.808],[0,0]],"o":[[0.118,-2.391],[0.781,-17.567],[5.964,-37.312],[3.669,-7.479],[0,0],[0,0]],"v":[[-172.883,127.709],[-172.537,120.187],[-165.873,48.441],[-138.548,-48.666],[-119.854,-63.277],[172.883,-127.709]],"c":false}]}]},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.900359509038,0.900359509038,0.904794730392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[287.309,342.925]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[4.321,-1.557],[0,0],[-1.467,7.009],[0,0],[0,0]],"o":[[-1.309,6.993],[0,0],[7.055,-1.793],[0,0],[0,0],[0,0]],"v":[[-153.798,36.65],[-161.674,49.427],[147.119,-29.01],[160.954,-43.472],[161.674,-49.427],[-152.905,30.234]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[4.275,-1.282],[0,0],[-1.273,5.085],[0,0],[0,0]],"o":[[-1.116,5.067],[0,0],[7,-1.556],[0,0],[0,0],[0,0]],"v":[[-150.609,39.987],[-161.627,49.588],[144.482,-26.585],[157.911,-37.489],[158.466,-41.789],[-147.925,34.462]],"c":true}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[0,0],[4.321,-1.557],[0,0],[-1.467,7.009],[0,0],[0,0]],"o":[[-1.309,6.993],[0,0],[7.055,-1.793],[0,0],[0,0],[0,0]],"v":[[-157.548,34.15],[-161.674,49.427],[149.494,-30.51],[156.454,-49.972],[156.674,-50.677],[-157.155,29.234]],"c":true}]},{"t":180,"s":[{"i":[[0,0],[4.321,-1.557],[0,0],[-1.467,7.009],[0,0],[0,0]],"o":[[-1.309,6.993],[0,0],[7.055,-1.793],[0,0],[0,0],[0,0]],"v":[[-153.798,36.65],[-161.674,49.427],[147.119,-29.01],[160.954,-43.472],[161.674,-49.427],[-152.905,30.234]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[260.432,453.321]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":20},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-24.961,37.768],[7.741,-1.702],[0,0],[4.469,-9.108],[6.375,-39.883],[14.688,-5.293],[0,0],[-0.782,7.932]],"o":[[4.37,-6.613],[0,0],[-9.908,2.181],[-10.401,21.201],[-11.337,70.931],[0,0],[7.725,-1.963],[6.775,-68.707]],"v":[[183.935,-133.107],[174.714,-146.586],[-118.024,-82.155],[-140.814,-64.327],[-168.677,34.476],[-185.431,146.805],[123.363,68.368],[137.522,51.88]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[-23.907,28.05],[7.689,-1.517],[0,0],[5.636,-6.162],[8.362,-20.219],[14.533,-4.358],[0,0],[-0.564,5.721]],"o":[[4.186,-4.911],[0,0],[-9.841,1.943],[-12.184,13.321],[-20.014,48.392],[0,0],[7.665,-1.703],[4.887,-49.554]],"v":[[219.914,-61.881],[212.331,-76.45],[-90.086,-4.552],[-112.371,9.109],[-150.301,70.026],[-185.384,146.966],[120.725,70.794],[134.422,58.423]],"c":true}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[1.26,40.797],[4.413,-1.098],[0,0],[1.086,-8.319],[-4.321,-69.16],[6.933,-2.99],[0,0],[-0.201,7.968]],"o":[[-0.245,-7.922],[0,0],[-9.908,2.181],[-1.684,12.892],[4.392,70.299],[0,0],[7.725,-1.963],[1.98,-78.314]],"v":[[129.242,-175.732],[120.089,-184.461],[-169.524,-120.655],[-192.314,-102.827],[-184.927,29.476],[-185.431,146.805],[125.113,67.618],[133.022,45.38]],"c":true}]},{"t":180,"s":[{"i":[[-24.961,37.768],[7.741,-1.702],[0,0],[4.469,-9.108],[6.375,-39.883],[14.688,-5.293],[0,0],[-0.782,7.932]],"o":[[4.37,-6.613],[0,0],[-9.908,2.181],[-10.401,21.201],[-11.337,70.931],[0,0],[7.725,-1.963],[6.775,-68.707]],"v":[[183.935,-133.107],[174.714,-146.586],[-118.024,-82.155],[-140.814,-64.327],[-168.677,34.476],[-185.431,146.805],[123.363,68.368],[137.522,51.88]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"gf","o":{"a":0,"k":100},"r":1,"bm":0,"g":{"p":3,"k":{"a":0,"k":[0,0.808,0.808,0.82,0.5,0.869,0.871,0.882,1,0.929,0.933,0.945]}},"s":{"a":0,"k":[-6.189,-17.943]},"e":{"a":0,"k":[23.15,93.055]},"t":1,"nm":"gr1","hd":false},{"ty":"tr","p":{"a":0,"k":[284.189,355.943]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[284.189,355.943]},"a":{"a":0,"k":[284.189,355.943]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false}],"ip":-17,"op":180,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"SPIDER","parent":13,"refId":"comp_0","sr":1,"ks":{"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[7.553,-16.773,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":1},"o":{"x":1,"y":0},"t":60,"s":[17.553,-46.773,0],"to":[0,0,0],"ti":[0,0,0]},{"t":120,"s":[7.553,-16.773,0]}]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":0,"op":76,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"pautina 2","parent":13,"sr":1,"ks":{"p":{"a":0,"k":[58.553,-83.17,0]},"a":{"a":0,"k":[41,-18.397,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.163,0.163,0.163],"y":[1,1,1]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":60,"s":[100,80,100]},{"i":{"x":[0.301,0.301,0.301],"y":[1,1,1]},"o":{"x":[0.951,0.951,0.951],"y":[0,0,0]},"t":120,"s":[100,80,100]},{"t":180,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[13.78,-11.274],[10,11.5],[12.5,9.5],[15,6.103],[17,6.25],[12.25,-1],[14,-0.5]],"o":[[-11,9],[-5,14],[0,21.5],[2,14.103],[4.5,13.25],[7.5,13.25],[-14,0.5]],"v":[[136,4.103],[98,-4.897],[56.5,1.603],[13,22],[-44.75,47],[-80.75,82],[-106.25,117]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[7.784,-5.753],[10,10.388],[12.5,8.581],[15,5.513],[19,7.771],[20.12,-2.505],[29.068,-5.357]],"o":[[-11,8.13],[-5,12.646],[0,19.421],[2,12.74],[2.75,11.272],[8.62,1.165],[-55.933,10.308]],"v":[[135.625,10.147],[101.274,-2.56],[59.5,12.76],[16.75,31.298],[-44.125,52.257],[-83.865,81.124],[-114.136,130.046]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[7.784,-5.753],[10,10.388],[12.5,8.581],[15,5.513],[19,7.771],[20.12,-2.505],[29.068,-5.357]],"o":[[-11,8.13],[-5,12.646],[0,19.421],[2,12.74],[2.75,11.272],[8.62,1.165],[-55.933,10.308]],"v":[[135.125,9.835],[101.399,-3.029],[59.5,12.76],[16.75,31.298],[-45,52.414],[-83.74,81.436],[-114.136,130.046]],"c":false}]},{"t":180,"s":[{"i":[[13.78,-11.274],[10,11.5],[12.5,9.5],[15,6.103],[17,6.25],[12.25,-1],[14,-0.5]],"o":[[-11,9],[-5,14],[0,21.5],[2,14.103],[4.5,13.25],[7.5,13.25],[-14,0.5]],"v":[[136,4.103],[98,-4.897],[56.5,1.603],[13,22],[-44.75,47],[-80.75,82],[-106.25,117]],"c":false}]}]},"nm":"Path 8","hd":false},{"ind":1,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[5.564,-4.162],[7.619,8.011],[9.524,6.618],[10.449,4.252],[13.236,5.993],[15.161,-6.425],[13.631,-5]],"o":[[-8.381,6.27],[-3.81,9.753],[0,14.978],[1.393,9.825],[-1.547,11.64],[-1.839,10.825],[-39.55,14.507]],"v":[[126.501,23.79],[97.796,21.419],[65.856,26.888],[31.231,40.257],[-13.703,60.11],[-40.411,88.675],[-63.131,118]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[10.499,-7.095],[7.619,7.237],[9.524,5.978],[10.449,3.841],[22.453,2.478],[23.75,-3.387],[21,-1.411]],"o":[[-8.381,5.663],[-3.81,8.81],[0,13.529],[1.393,8.875],[3.203,10.382],[4,9.88],[-25.552,1.717]],"v":[[134.001,24.682],[108.946,19.491],[75.336,29.935],[38.461,40.91],[-0.906,60.426],[-42,81.761],[-69.5,121.032]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[10.499,-7.095],[7.619,7.237],[9.524,5.978],[10.449,3.841],[22.453,2.478],[23.75,-3.387],[21,-1.411]],"o":[[-8.381,5.663],[-3.81,8.81],[0,13.529],[1.393,8.875],[3.203,10.382],[4,9.88],[-25.552,1.717]],"v":[[134.001,24.682],[108.446,18.71],[75.461,29.622],[38.461,40.91],[-0.906,60.426],[-42,81.761],[-69.5,121.032]],"c":false}]},{"t":180,"s":[{"i":[[5.564,-4.162],[7.619,8.011],[9.524,6.618],[10.449,4.252],[13.236,5.993],[15.161,-6.425],[13.631,-5]],"o":[[-8.381,6.27],[-3.81,9.753],[0,14.978],[1.393,9.825],[-1.547,11.64],[-1.839,10.825],[-39.55,14.507]],"v":[[126.501,23.79],[97.796,21.419],[65.856,26.888],[31.231,40.257],[-13.703,60.11],[-40.411,88.675],[-63.131,118]],"c":false}]}]},"nm":"Path 7","hd":false},{"ind":2,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[6.775,-5.674],[4.917,5.787],[6.146,4.781],[6.744,3.071],[8.542,4.329],[10.5,-1],[13.25,-0.75]],"o":[[-5.409,4.529],[-2.458,7.045],[0,10.82],[0.899,7.097],[1.506,7.976],[1,10.5],[-13.25,0.75]],"v":[[114.975,52.037],[98.45,50.324],[75.087,54.524],[53.492,63.933],[26.494,78.274],[8.25,98],[-7,118.25]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[4.116,-3.113],[4.917,5.228],[6.146,4.318],[6.743,2.774],[10.012,1.51],[26.858,-1.417],[22.195,-5.055]],"o":[[-5.409,4.091],[-2.458,6.364],[0,9.773],[0.899,6.411],[-0.494,7.242],[1.608,3.946],[-28.717,6.54]],"v":[[130.975,47.77],[116.674,46.899],[93.099,48.846],[70.984,54.796],[43.238,67.464],[8.034,82.284],[-13.89,109.733]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[4.116,-3.113],[4.917,5.228],[6.146,4.318],[6.743,2.774],[8.542,3.911],[26.858,-1.417],[22.195,-5.055]],"o":[[-5.409,4.091],[-2.458,6.364],[0,9.773],[0.899,6.411],[-0.494,7.242],[1.608,3.946],[-28.717,6.54]],"v":[[130.975,47.77],[115.799,46.899],[92.599,49.003],[70.984,54.796],[41.988,68.089],[7.784,83.69],[-13.89,109.733]],"c":false}]},{"t":180,"s":[{"i":[[6.775,-5.674],[4.917,5.787],[6.146,4.781],[6.744,3.071],[8.542,4.329],[10.5,-1],[13.25,-0.75]],"o":[[-5.409,4.529],[-2.458,7.045],[0,10.82],[0.899,7.097],[1.506,7.976],[1,10.5],[-13.25,0.75]],"v":[[114.975,52.037],[98.45,50.324],[75.087,54.524],[53.492,63.933],[26.494,78.274],[8.25,98],[-7,118.25]],"c":false}]}]},"nm":"Path 9","hd":false},{"ind":3,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[6,-40.397],[17,46.103]],"o":[[-11.5,-42.397],[-11.611,-31.487]],"v":[[95,119],[51.5,-11]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[-0.5,-37.168],[17,20.755]],"o":[[-24,-30.599],[-19.677,-24.023]],"v":[[125,85.751],[51.5,3.292]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[-0.5,-37.168],[17,20.755]],"o":[[-24,-30.599],[-19.677,-24.023]],"v":[[125,85.751],[51.5,3.292]],"c":false}]},{"t":180,"s":[{"i":[[6,-40.397],[17,46.103]],"o":[[-11.5,-42.397],[-11.611,-31.487]],"v":[[95,119],[51.5,-11]],"c":false}]}]},"nm":"Path 3","hd":false},{"ind":4,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-45.5,-10.5],[25,24.603]],"o":[[-23,-40.397],[-23.919,-23.54]],"v":[[95,119],[3.5,13]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[-3.5,-38.975],[28.5,19.063]],"o":[[-43,-33.103],[-26.971,-18.04]],"v":[[125,85.751],[3.5,23.619]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[-3.5,-38.975],[28.5,19.063]],"o":[[-43,-33.103],[-26.971,-18.04]],"v":[[125,85.751],[3.5,23.619]],"c":false}]},{"t":180,"s":[{"i":[[-45.5,-10.5],[25,24.603]],"o":[[-23,-40.397],[-23.919,-23.54]],"v":[[95,119],[3.5,13]],"c":false}]}]},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[3.5,-41.635],[0,30.855]],"o":[[4,-43.135],[0,-31.709]],"v":[[94.75,118.635],[98,-12.855]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[-3.5,-38.975],[8.75,19.023]],"o":[[-6.25,-36.346],[-14.127,-30.713]],"v":[[125,85.751],[98,-10.529]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[-3.5,-38.975],[8.75,19.023]],"o":[[-6.25,-36.346],[-14.127,-30.713]],"v":[[125,85.751],[98,-10.529]],"c":false}]},{"t":180,"s":[{"i":[[3.5,-41.635],[0,30.855]],"o":[[4,-43.135],[0,-31.709]],"v":[[94.75,118.635],[98,-12.855]],"c":false}]}]},"nm":"Path 12","hd":false},{"ind":6,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[-73,0.5],[50.5,4.853]],"o":[[0,0],[-62,-18],[-33.406,-3.21]],"v":[[-135,116.603],[94,119],[-123,76.75]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[-99,14.207],[50.5,4.384]],"o":[[0,0],[-58.5,-4.988],[-33.406,-2.9]],"v":[[-135,134.785],[125,85.751],[-123,79.176]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[0,0],[-99,14.207],[50.5,4.384]],"o":[[0,0],[-58.5,-4.988],[-33.406,-2.9]],"v":[[-135,134.785],[125,85.751],[-123,79.176]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[-73,0.5],[50.5,4.853]],"o":[[0,0],[-62,-18],[-33.406,-3.21]],"v":[[-135,116.603],[94,119],[-123,76.75]],"c":false}]}]},"nm":"Path 11","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[6,-40.397],[32,9.603]],"o":[[0,0],[-32,-25.897],[-32.143,-9.646]],"v":[[137.5,0.103],[95,119],[-55.5,43.5]],"c":false}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[6,-36.491],[32,8.675]],"o":[[0,0],[-44,-14.811],[-32.143,-8.714]],"v":[[137.5,-3.584],[125,85.751],[-55.5,49.817]],"c":false}]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[{"i":[[0,0],[6,-36.491],[32,8.675]],"o":[[0,0],[-44,-14.811],[-32.143,-8.714]],"v":[[137.5,-3.584],[125,85.751],[-55.5,49.817]],"c":false}]},{"t":180,"s":[{"i":[[0,0],[6,-40.397],[32,9.603]],"o":[[0,0],[-32,-25.897],[-32.143,-9.646]],"v":[[137.5,0.103],[95,119],[-55.5,43.5]],"c":false}]}]},"nm":"Path 4","hd":false},{"ty":"tr","p":{"a":0,"k":[41,49.552]},"a":{"a":0,"k":[41,49.552]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"st","c":{"a":0,"k":[0.705882352941,0.721568627451,0.741176470588,1]},"o":{"a":0,"k":100},"w":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[2]},{"i":{"x":[0.163],"y":[1]},"o":{"x":[1],"y":[0]},"t":60,"s":[2.5]},{"i":{"x":[0.301],"y":[1]},"o":{"x":[0.951],"y":[0]},"t":120,"s":[2.5]},{"t":180,"s":[2]}]},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"p_str","parent":13,"sr":1,"ks":{},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-0.258,6.055],[0,0],[6.31,-1.446],[0,0],[-0.073,5.112],[0,0],[6.333,-1.453],[0,0],[-0.058,-6.533],[0,0],[-7.065,-16.479],[-5.052,1.289]],"o":[[6.051,-1.378],[0,0],[0.28,-6.551],[0,0],[-4.812,1.103],[0,0],[0.093,-6.574],[0,0],[-6.323,1.45],[0,0],[0.043,4.885],[7.129,16.628],[0,0]],"v":[[144.859,112.603],[156.283,99.152],[166.7,-176.707],[155.78,-185.961],[-9.063,-148.178],[-18.293,-155.977],[-18.097,-169.807],[-29.394,-179.09],[-155.367,-149.84],[-166.711,-135.375],[-164.411,125.639],[-158.249,172.856],[-139.624,185.173]],"c":true}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"p_bl4","parent":13,"sr":1,"ks":{"p":{"a":0,"k":[-94.374,-154.336,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-3.595,0.824],[0,0]],"o":[[-0.033,-3.738],[0,0],[0,0]],"v":[[-66.337,18.909],[-59.652,10.344],[66.338,-18.909]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.933333337307,0.945098042488,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"p_sh2","parent":13,"sr":1,"ks":{"o":{"a":0,"k":33},"p":{"a":0,"k":[-140.348,177.818,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-4.572,7.663]],"o":[[10.337,14.81],[0,0]],"v":[[-14.261,0.419],[14.261,-6.841]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"p_shad","parent":13,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[90]},{"i":{"x":[0.163],"y":[1]},"o":{"x":[1],"y":[0]},"t":60,"s":[90]},{"i":{"x":[0.301],"y":[1]},"o":{"x":[0.951],"y":[0]},"t":120,"s":[90]},{"t":180,"s":[90]}]},"p":{"a":1,"k":[{"i":{"x":0,"y":0},"o":{"x":0.333,"y":0.333},"t":0,"s":[3.569,12.387,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[3.569,12.387,0],"to":[-0.25,-0.583,0],"ti":[0,0,0]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":120,"s":[2.069,8.887,0],"to":[0,0,0],"ti":[-0.25,-0.583,0]},{"t":180,"s":[3.569,12.387,0]}]},"a":{"a":0,"k":[245.016,323.16,0]},"s":{"a":1,"k":[{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,97,100]},{"i":{"x":[0.163,0.163,0.163],"y":[1,1,1]},"o":{"x":[1,1,1],"y":[0,0,0]},"t":60,"s":[100,97,100]},{"i":{"x":[0.301,0.301,0.301],"y":[1,1,1]},"o":{"x":[0.951,0.951,0.951],"y":[0,0,0]},"t":120,"s":[100,100,100]},{"t":180,"s":[100,97,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.262,-1.435],[0,0],[1.345,-0.897],[-0.044,-4.985],[0,0],[-9.197,-14.632],[-1.207,0.307]],"o":[[0,0],[0.278,-6.501],[0,0],[-1.557,0.357],[-4.148,2.766],[0,0],[0.045,5.059],[1.147,0.153],[0,0]],"v":[[135.781,91.782],[145.245,-150.133],[134.41,-159.315],[-134.447,-97.21],[-138.828,-95.291],[-145.256,-82.737],[-143.551,110.732],[-135.398,159.515],[-131.847,159.315]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.262,-1.435],[0,0],[1.345,-0.897],[-0.044,-4.985],[0,0],[-9.197,-14.632],[-1.207,0.307]],"o":[[0,0],[0.278,-6.501],[0,0],[-1.557,0.357],[-4.148,2.766],[0,0],[0.045,5.059],[1.147,0.153],[0,0]],"v":[[135.781,91.782],[145.245,-119.205],[134.41,-128.387],[-134.447,-66.282],[-138.828,-64.363],[-145.256,-51.809],[-143.551,110.732],[-135.398,159.515],[-131.847,159.315]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.262,-1.435],[0,0],[1.345,-0.897],[-0.044,-4.985],[0,0],[-9.197,-14.632],[-1.207,0.307]],"o":[[0,0],[0.278,-6.501],[0,0],[-1.557,0.357],[-4.148,2.766],[0,0],[0.045,5.059],[1.147,0.153],[0,0]],"v":[[135.781,91.782],[145.245,-150.133],[134.41,-159.315],[-134.447,-97.21],[-138.828,-95.291],[-145.256,-82.737],[-143.551,110.732],[-135.398,159.515],[-131.847,159.315]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[249.77,335.347]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":80},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.351,-1.456],[0,0],[1.364,-0.909],[-0.045,-5.056],[0,0],[-9.328,-14.84],[-1.224,0.311]],"o":[[0,0],[0.282,-6.594],[0,0],[-1.579,0.362],[-4.207,2.805],[0,0],[0.045,5.131],[1.163,0.155],[0,0]],"v":[[137.715,93.09],[147.314,-152.271],[136.324,-161.584],[-136.362,-98.595],[-140.805,-96.649],[-147.325,-83.915],[-145.596,112.309],[-137.327,161.787],[-133.725,161.584]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.351,-1.456],[0,0],[1.364,-0.909],[-0.045,-5.056],[0,0],[-9.328,-14.84],[-1.224,0.311]],"o":[[0,0],[0.282,-6.594],[0,0],[-1.579,0.362],[-4.207,2.805],[0,0],[0.045,5.131],[1.163,0.155],[0,0]],"v":[[137.715,93.09],[147.314,-121.343],[136.324,-130.656],[-136.362,-67.667],[-140.805,-65.721],[-147.325,-52.987],[-145.596,112.309],[-137.327,161.787],[-133.725,161.584]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.351,-1.456],[0,0],[1.364,-0.909],[-0.045,-5.056],[0,0],[-9.328,-14.84],[-1.224,0.311]],"o":[[0,0],[0.282,-6.594],[0,0],[-1.579,0.362],[-4.207,2.805],[0,0],[0.045,5.131],[1.163,0.155],[0,0]],"v":[[137.715,93.09],[147.314,-152.271],[136.324,-161.584],[-136.362,-98.595],[-140.805,-96.649],[-147.325,-83.915],[-145.596,112.309],[-137.327,161.787],[-133.725,161.584]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[249.175,333.823]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":70},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 2","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.44,-1.476],[0,0],[1.383,-0.922],[-0.045,-5.127],[0,0],[-9.459,-15.049],[-1.241,0.316]],"o":[[0,0],[0.286,-6.686],[0,0],[-1.601,0.367],[-4.266,2.845],[0,0],[0.046,5.203],[1.179,0.158],[0,0]],"v":[[139.648,94.397],[149.383,-154.41],[138.239,-163.853],[-138.277,-99.979],[-142.783,-98.006],[-149.394,-85.094],[-147.64,113.886],[-139.255,164.059],[-135.603,163.853]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.44,-1.476],[0,0],[1.383,-0.922],[-0.045,-5.127],[0,0],[-9.459,-15.049],[-1.241,0.316]],"o":[[0,0],[0.286,-6.686],[0,0],[-1.601,0.367],[-4.266,2.845],[0,0],[0.046,5.203],[1.179,0.157],[0,0]],"v":[[139.648,94.397],[149.383,-123.482],[138.239,-132.925],[-138.277,-69.051],[-142.783,-67.078],[-149.394,-54.166],[-147.64,113.886],[-139.255,164.059],[-135.603,163.853]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.44,-1.476],[0,0],[1.383,-0.922],[-0.045,-5.127],[0,0],[-9.459,-15.049],[-1.241,0.316]],"o":[[0,0],[0.286,-6.686],[0,0],[-1.601,0.367],[-4.266,2.845],[0,0],[0.046,5.203],[1.179,0.158],[0,0]],"v":[[139.648,94.397],[149.383,-154.41],[138.239,-163.853],[-138.277,-99.979],[-142.783,-98.006],[-149.394,-85.094],[-147.64,113.886],[-139.255,164.059],[-135.603,163.853]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[248.581,332.3]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":60},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 3","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.529,-1.497],[0,0],[1.402,-0.935],[-0.046,-5.198],[0,0],[-9.59,-15.257],[-1.259,0.32]],"o":[[0,0],[0.289,-6.779],[0,0],[-1.623,0.372],[-4.325,2.884],[0,0],[0.046,5.275],[1.196,0.16],[0,0]],"v":[[141.583,95.704],[151.452,-156.548],[140.153,-166.122],[-140.192,-101.364],[-144.76,-99.363],[-151.463,-86.272],[-149.685,115.463],[-141.184,166.331],[-137.481,166.122]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.529,-1.497],[0,0],[1.402,-0.935],[-0.046,-5.198],[0,0],[-9.59,-15.257],[-1.259,0.32]],"o":[[0,0],[0.289,-6.779],[0,0],[-1.623,0.372],[-4.325,2.884],[0,0],[0.046,5.275],[1.196,0.16],[0,0]],"v":[[141.583,95.704],[151.452,-125.62],[140.153,-135.194],[-140.192,-70.436],[-144.76,-68.435],[-151.463,-55.344],[-149.685,115.463],[-141.184,166.331],[-137.481,166.122]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.529,-1.497],[0,0],[1.402,-0.935],[-0.046,-5.198],[0,0],[-9.59,-15.257],[-1.259,0.32]],"o":[[0,0],[0.289,-6.779],[0,0],[-1.623,0.372],[-4.325,2.884],[0,0],[0.046,5.275],[1.196,0.16],[0,0]],"v":[[141.583,95.704],[151.452,-156.548],[140.153,-166.122],[-140.192,-101.364],[-144.76,-99.363],[-151.463,-86.272],[-149.685,115.463],[-141.184,166.331],[-137.481,166.122]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[247.987,330.777]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":50},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 4","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.618,-1.517],[0,0],[1.421,-0.948],[-0.046,-5.269],[0,0],[-9.721,-15.465],[-1.276,0.324]],"o":[[0,0],[0.293,-6.871],[0,0],[-1.646,0.378],[-4.384,2.924],[0,0],[0.047,5.348],[1.212,0.162],[0,0]],"v":[[143.517,97.012],[153.521,-158.686],[142.068,-168.391],[-142.107,-102.748],[-146.738,-100.72],[-153.532,-87.451],[-151.73,117.041],[-143.113,168.604],[-139.359,168.392]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.618,-1.517],[0,0],[1.421,-0.948],[-0.046,-5.269],[0,0],[-9.721,-15.466],[-1.276,0.324]],"o":[[0,0],[0.293,-6.871],[0,0],[-1.646,0.378],[-4.384,2.924],[0,0],[0.047,5.348],[1.212,0.162],[0,0]],"v":[[143.517,97.012],[153.521,-127.759],[142.068,-137.464],[-142.107,-71.821],[-146.738,-69.793],[-153.532,-56.523],[-151.73,117.041],[-143.113,168.604],[-139.359,168.392]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.618,-1.517],[0,0],[1.421,-0.948],[-0.046,-5.269],[0,0],[-9.721,-15.465],[-1.276,0.324]],"o":[[0,0],[0.293,-6.871],[0,0],[-1.646,0.378],[-4.384,2.924],[0,0],[0.047,5.348],[1.212,0.162],[0,0]],"v":[[143.517,97.012],[153.521,-158.686],[142.068,-168.391],[-142.107,-102.748],[-146.738,-100.72],[-153.532,-87.451],[-151.73,117.041],[-143.113,168.604],[-139.359,168.392]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[247.393,329.253]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":40},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 5","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.708,-1.537],[0,0],[1.44,-0.961],[-0.047,-5.34],[0,0],[-9.852,-15.674],[-1.293,0.329]],"o":[[0,0],[0.297,-6.964],[0,0],[-1.668,0.383],[-4.443,2.963],[0,0],[0.048,5.42],[1.228,0.164],[0,0]],"v":[[145.451,98.319],[155.589,-160.825],[143.982,-170.661],[-144.022,-104.133],[-148.715,-102.078],[-155.601,-88.629],[-153.774,118.618],[-145.041,170.876],[-141.237,170.661]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.708,-1.537],[0,0],[1.44,-0.961],[-0.047,-5.34],[0,0],[-9.852,-15.674],[-1.293,0.329]],"o":[[0,0],[0.297,-6.964],[0,0],[-1.668,0.383],[-4.443,2.963],[0,0],[0.048,5.42],[1.228,0.164],[0,0]],"v":[[145.451,98.319],[155.589,-129.897],[143.982,-139.733],[-144.022,-73.205],[-148.715,-71.15],[-155.601,-57.701],[-153.774,118.618],[-145.041,170.876],[-141.237,170.661]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.708,-1.537],[0,0],[1.44,-0.961],[-0.047,-5.34],[0,0],[-9.852,-15.674],[-1.293,0.329]],"o":[[0,0],[0.297,-6.964],[0,0],[-1.668,0.383],[-4.443,2.963],[0,0],[0.048,5.42],[1.228,0.164],[0,0]],"v":[[145.451,98.319],[155.589,-160.825],[143.982,-170.661],[-144.022,-104.133],[-148.715,-102.078],[-155.601,-88.629],[-153.774,118.618],[-145.041,170.876],[-141.237,170.661]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[246.799,327.73]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":30},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 6","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.797,-1.558],[0,0],[1.46,-0.973],[-0.048,-5.411],[0,0],[-9.983,-15.882],[-1.31,0.333]],"o":[[0,0],[0.301,-7.056],[0,0],[-1.69,0.388],[-4.502,3.002],[0,0],[0.048,5.492],[1.245,0.166],[0,0]],"v":[[147.385,99.626],[157.658,-162.963],[145.897,-172.93],[-145.937,-105.518],[-150.693,-103.435],[-157.67,-89.807],[-155.819,120.195],[-146.97,173.148],[-143.115,172.93]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.797,-1.558],[0,0],[1.46,-0.973],[-0.048,-5.411],[0,0],[-9.983,-15.882],[-1.31,0.333]],"o":[[0,0],[0.301,-7.056],[0,0],[-1.69,0.388],[-4.502,3.002],[0,0],[0.048,5.492],[1.245,0.166],[0,0]],"v":[[147.385,99.626],[157.658,-132.035],[145.897,-142.002],[-145.937,-74.59],[-150.693,-72.507],[-157.67,-58.88],[-155.819,120.195],[-146.97,173.148],[-143.115,172.93]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.797,-1.558],[0,0],[1.46,-0.973],[-0.048,-5.411],[0,0],[-9.983,-15.882],[-1.31,0.333]],"o":[[0,0],[0.301,-7.056],[0,0],[-1.69,0.388],[-4.502,3.002],[0,0],[0.048,5.492],[1.245,0.166],[0,0]],"v":[[147.385,99.626],[157.658,-162.963],[145.897,-172.93],[-145.937,-105.518],[-150.693,-103.435],[-157.67,-89.807],[-155.819,120.195],[-146.97,173.148],[-143.115,172.93]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[246.204,326.207]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":20},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 7","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.886,-1.578],[0,0],[1.479,-0.986],[-0.048,-5.482],[0,0],[-10.114,-16.091],[-1.327,0.337]],"o":[[0,0],[0.305,-7.149],[0,0],[-1.712,0.393],[-4.561,3.042],[0,0],[0.049,5.564],[1.261,0.168],[0,0]],"v":[[149.319,100.934],[159.727,-165.102],[147.811,-175.199],[-147.852,-106.902],[-152.67,-104.792],[-159.739,-90.986],[-157.864,121.772],[-148.898,175.42],[-144.993,175.199]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.886,-1.578],[0,0],[1.479,-0.986],[-0.048,-5.482],[0,0],[-10.114,-16.091],[-1.327,0.337]],"o":[[0,0],[0.305,-7.149],[0,0],[-1.712,0.393],[-4.561,3.042],[0,0],[0.049,5.564],[1.261,0.168],[0,0]],"v":[[149.319,100.934],[159.727,-134.174],[147.811,-144.271],[-147.852,-75.974],[-152.67,-73.865],[-159.739,-60.058],[-157.864,121.772],[-148.898,175.42],[-144.993,175.199]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.886,-1.578],[0,0],[1.479,-0.986],[-0.048,-5.482],[0,0],[-10.114,-16.091],[-1.327,0.337]],"o":[[0,0],[0.305,-7.149],[0,0],[-1.712,0.393],[-4.561,3.042],[0,0],[0.049,5.564],[1.261,0.168],[0,0]],"v":[[149.319,100.934],[159.727,-165.102],[147.811,-175.199],[-147.852,-106.902],[-152.67,-104.792],[-159.739,-90.986],[-157.864,121.772],[-148.898,175.42],[-144.993,175.199]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[245.61,324.684]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":10},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 8","bm":0,"hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[0,0],[0,0],[6.975,-1.599],[0,0],[1.498,-0.999],[-0.049,-5.553],[0,0],[-10.245,-16.299],[-1.345,0.342]],"o":[[0,0],[0.309,-7.242],[0,0],[-1.734,0.398],[-4.62,3.081],[0,0],[0.05,5.636],[1.277,0.171],[0,0]],"v":[[151.253,102.241],[161.796,-167.24],[149.726,-177.468],[-149.767,-108.287],[-154.647,-106.15],[-161.808,-92.164],[-159.909,123.349],[-150.827,177.692],[-146.871,177.469]],"c":true}]},{"i":{"x":0.163,"y":1},"o":{"x":1,"y":0},"t":60,"s":[{"i":[[0,0],[0,0],[6.975,-1.599],[0,0],[1.498,-0.999],[-0.049,-5.553],[0,0],[-10.245,-16.299],[-1.345,0.342]],"o":[[0,0],[0.309,-7.242],[0,0],[-1.734,0.398],[-4.62,3.081],[0,0],[0.05,5.636],[1.277,0.171],[0,0]],"v":[[151.253,102.241],[161.796,-136.312],[149.726,-146.54],[-149.767,-77.359],[-154.647,-75.222],[-161.808,-61.237],[-159.909,123.349],[-150.827,177.692],[-146.871,177.469]],"c":true}]},{"t":120,"s":[{"i":[[0,0],[0,0],[6.975,-1.599],[0,0],[1.498,-0.999],[-0.049,-5.553],[0,0],[-10.245,-16.299],[-1.345,0.342]],"o":[[0,0],[0.309,-7.242],[0,0],[-1.734,0.398],[-4.62,3.081],[0,0],[0.05,5.636],[1.277,0.171],[0,0]],"v":[[151.253,102.241],[161.796,-167.24],[149.726,-177.468],[-149.767,-108.287],[-154.647,-106.15],[-161.808,-92.164],[-159.909,123.349],[-150.827,177.692],[-146.871,177.469]],"c":true}]}]},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.396078437567,0.458823531866,0.509803950787,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[245.016,323.16]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":0},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 9","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"p_bl3","parent":13,"sr":1,"ks":{"p":{"a":0,"k":[74.699,-161.22,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-82.422,18.892],[82.422,-18.892]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.933333337307,0.945098042488,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"p_bl2","parent":13,"sr":1,"ks":{"o":{"a":0,"k":40},"p":{"a":0,"k":[-158.75,9.661,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.076,8.644],[0,0]],"o":[[-1.278,-9.446],[0,0],[0,0]],"v":[[1.961,145.088],[0.34,115.925],[-1.961,-145.088]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.933333337307,0.945098042488,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":6},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"p_bl1","parent":13,"sr":1,"ks":{"o":{"a":0,"k":25},"p":{"a":0,"k":[-10.378,135.807,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[145.485,-37.034],[-145.485,37.034]],"c":false}},"nm":"Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.929411768913,0.933333337307,0.945098042488,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":10},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":3,"nm":"NULL CONTROL","sr":1,"ks":{"o":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.163,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[238.447,457.021,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.301,"y":1},"o":{"x":0.951,"y":0},"t":60,"s":[238.447,447.021,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.223,"y":1},"o":{"x":0.476,"y":0},"t":120,"s":[238.447,465.021,0],"to":[0,0,0],"ti":[0,0,0]},{"t":179,"s":[238.447,457.021,0]}]},"a":{"a":0,"k":[60,60,0]}},"ao":0,"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"papk1","parent":12,"sr":1,"ks":{"p":{"a":0,"k":[60,60,0]},"a":{"a":0,"k":[0,144.248,0]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-0.258,6.055],[0,0],[6.31,-1.446],[0,0],[-0.073,5.112],[0,0],[6.333,-1.453],[0,0],[-0.058,-6.533],[0,0],[-7.065,-16.479],[-5.052,1.289]],"o":[[6.051,-1.378],[0,0],[0.28,-6.551],[0,0],[-4.812,1.103],[0,0],[0.093,-6.574],[0,0],[-6.323,1.45],[0,0],[0.043,4.885],[7.129,16.628],[0,0]],"v":[[144.859,112.603],[156.283,99.152],[166.7,-176.707],[155.78,-185.961],[-9.063,-148.178],[-18.293,-155.977],[-18.097,-169.807],[-29.394,-179.09],[-155.367,-149.84],[-166.711,-135.375],[-164.411,125.639],[-158.249,172.856],[-139.624,185.173]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.705882370472,0.721568644047,0.741176486015,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Group 1","bm":0,"hd":false}],"ip":0,"op":180,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":0,"nm":"SPIDER","parent":3,"refId":"comp_0","sr":1,"ks":{"p":{"a":0,"k":[256,256,0]},"a":{"a":0,"k":[256,256,0]}},"ao":0,"w":512,"h":512,"ip":76,"op":108,"st":0,"bm":0}]} \ No newline at end of file