Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,7 +1,6 @@
|
|||||||
package com.rosetta.messenger
|
package com.rosetta.messenger
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
import android.content.Context
|
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -11,59 +10,42 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.compose.animation.*
|
import androidx.compose.animation.*
|
||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.FastOutLinearInEasing
|
|
||||||
import androidx.compose.animation.core.LinearOutSlowInEasing
|
|
||||||
import androidx.compose.animation.core.LinearEasing
|
|
||||||
import androidx.compose.animation.core.Spring
|
|
||||||
import androidx.compose.animation.core.spring
|
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import com.google.firebase.FirebaseApp
|
import com.google.firebase.FirebaseApp
|
||||||
import com.google.firebase.messaging.FirebaseMessaging
|
import com.google.firebase.messaging.FirebaseMessaging
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
|
||||||
import com.rosetta.messenger.data.AccountManager
|
import com.rosetta.messenger.data.AccountManager
|
||||||
import com.rosetta.messenger.data.DecryptedAccount
|
import com.rosetta.messenger.data.DecryptedAccount
|
||||||
import com.rosetta.messenger.data.PreferencesManager
|
import com.rosetta.messenger.data.PreferencesManager
|
||||||
import com.rosetta.messenger.data.RecentSearchesManager
|
import com.rosetta.messenger.data.RecentSearchesManager
|
||||||
import com.rosetta.messenger.network.PacketPushNotification
|
import com.rosetta.messenger.network.PacketPushNotification
|
||||||
import com.rosetta.messenger.network.PushNotificationAction
|
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
import com.rosetta.messenger.network.ProtocolManager
|
||||||
import com.rosetta.messenger.network.ProtocolState
|
import com.rosetta.messenger.network.ProtocolState
|
||||||
|
import com.rosetta.messenger.network.PushNotificationAction
|
||||||
|
import com.rosetta.messenger.network.SearchUser
|
||||||
import com.rosetta.messenger.ui.auth.AccountInfo
|
import com.rosetta.messenger.ui.auth.AccountInfo
|
||||||
import com.rosetta.messenger.ui.auth.AuthFlow
|
import com.rosetta.messenger.ui.auth.AuthFlow
|
||||||
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
|
||||||
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
import com.rosetta.messenger.ui.chats.ChatDetailScreen
|
||||||
|
import com.rosetta.messenger.ui.chats.ChatsListScreen
|
||||||
import com.rosetta.messenger.ui.chats.SearchScreen
|
import com.rosetta.messenger.ui.chats.SearchScreen
|
||||||
import com.rosetta.messenger.network.SearchUser
|
|
||||||
import com.rosetta.messenger.ui.components.EmojiCache
|
|
||||||
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
import com.rosetta.messenger.ui.components.OptimizedEmojiCache
|
||||||
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
import com.rosetta.messenger.ui.onboarding.OnboardingScreen
|
||||||
import com.rosetta.messenger.ui.splash.SplashScreen
|
import com.rosetta.messenger.ui.splash.SplashScreen
|
||||||
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
import com.rosetta.messenger.ui.theme.RosettaAndroidTheme
|
||||||
import kotlinx.coroutines.launch
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.Locale
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
private lateinit var preferencesManager: PreferencesManager
|
private lateinit var preferencesManager: PreferencesManager
|
||||||
@@ -71,6 +53,25 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "MainActivity"
|
private const val TAG = "MainActivity"
|
||||||
|
|
||||||
|
// 🔔 FCM Логи для отображения в UI
|
||||||
|
private val _fcmLogs = mutableStateListOf<String>()
|
||||||
|
val fcmLogs: List<String>
|
||||||
|
get() = _fcmLogs
|
||||||
|
|
||||||
|
fun addFcmLog(message: String) {
|
||||||
|
val timestamp = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
|
||||||
|
_fcmLogs.add(0, "[$timestamp] $message") // Добавляем в начало списка
|
||||||
|
// Ограничиваем количество логов
|
||||||
|
if (_fcmLogs.size > 20) {
|
||||||
|
_fcmLogs.removeAt(_fcmLogs.size - 1)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "FCM: $message")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearFcmLogs() {
|
||||||
|
_fcmLogs.clear()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
@@ -95,22 +96,25 @@ class MainActivity : ComponentActivity() {
|
|||||||
OptimizedEmojiCache.preload(this)
|
OptimizedEmojiCache.preload(this)
|
||||||
|
|
||||||
setContent { // 🔔 Запрос разрешения на уведомления для Android 13+
|
setContent { // 🔔 Запрос разрешения на уведомления для Android 13+
|
||||||
val notificationPermissionLauncher = rememberLauncherForActivityResult(
|
val notificationPermissionLauncher =
|
||||||
|
rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.RequestPermission(),
|
contract = ActivityResultContracts.RequestPermission(),
|
||||||
onResult = { isGranted ->
|
onResult = { isGranted -> }
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Запрашиваем разрешение при первом запуске (Android 13+)
|
// Запрашиваем разрешение при первом запуске (Android 13+)
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
val hasPermission = ContextCompat.checkSelfPermission(
|
val hasPermission =
|
||||||
|
ContextCompat.checkSelfPermission(
|
||||||
this@MainActivity,
|
this@MainActivity,
|
||||||
Manifest.permission.POST_NOTIFICATIONS
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
) == PackageManager.PERMISSION_GRANTED
|
) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
if (!hasPermission) {
|
if (!hasPermission) {
|
||||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
notificationPermissionLauncher.launch(
|
||||||
|
Manifest.permission.POST_NOTIFICATIONS
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,16 +134,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
accountManager.logout() // Always start logged out
|
accountManager.logout() // Always start logged out
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
hasExistingAccount = accounts.isNotEmpty()
|
hasExistingAccount = accounts.isNotEmpty()
|
||||||
accountInfoList = accounts.map { account ->
|
accountInfoList =
|
||||||
|
accounts.map { account ->
|
||||||
val shortKey = account.publicKey.take(7)
|
val shortKey = account.publicKey.take(7)
|
||||||
val displayName = account.name ?: shortKey
|
val displayName = account.name ?: shortKey
|
||||||
val initials = displayName.trim().split(Regex("\\s+"))
|
val initials =
|
||||||
|
displayName
|
||||||
|
.trim()
|
||||||
|
.split(Regex("\\s+"))
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
.let { words ->
|
.let { words ->
|
||||||
when {
|
when {
|
||||||
words.isEmpty() -> "??"
|
words.isEmpty() -> "??"
|
||||||
words.size == 1 -> words[0].take(2).uppercase()
|
words.size == 1 -> words[0].take(2).uppercase()
|
||||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
else ->
|
||||||
|
"${words[0].first()}${words[1].first()}".uppercase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AccountInfo(
|
AccountInfo(
|
||||||
@@ -154,27 +163,30 @@ class MainActivity : ComponentActivity() {
|
|||||||
// Wait for initial load
|
// Wait for initial load
|
||||||
if (hasExistingAccount == null) {
|
if (hasExistingAccount == null) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.background(if (isDarkTheme) Color(0xFF1B1B1B) else Color.White)
|
.background(
|
||||||
|
if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
||||||
|
)
|
||||||
)
|
)
|
||||||
return@setContent
|
return@setContent
|
||||||
}
|
}
|
||||||
|
|
||||||
RosettaAndroidTheme(
|
RosettaAndroidTheme(darkTheme = isDarkTheme, animated = true) {
|
||||||
darkTheme = isDarkTheme,
|
|
||||||
animated = true
|
|
||||||
) {
|
|
||||||
Surface(
|
Surface(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
color = if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
color = if (isDarkTheme) Color(0xFF1B1B1B) else Color.White
|
||||||
) {
|
) {
|
||||||
AnimatedContent(
|
AnimatedContent(
|
||||||
targetState = when {
|
targetState =
|
||||||
|
when {
|
||||||
showSplash -> "splash"
|
showSplash -> "splash"
|
||||||
showOnboarding && hasExistingAccount == false -> "onboarding"
|
showOnboarding && hasExistingAccount == false ->
|
||||||
isLoggedIn != true && hasExistingAccount == false -> "auth_new"
|
"onboarding"
|
||||||
isLoggedIn != true && hasExistingAccount == true -> "auth_unlock"
|
isLoggedIn != true && hasExistingAccount == false ->
|
||||||
|
"auth_new"
|
||||||
|
isLoggedIn != true && hasExistingAccount == true ->
|
||||||
|
"auth_unlock"
|
||||||
else -> "main"
|
else -> "main"
|
||||||
},
|
},
|
||||||
transitionSpec = {
|
transitionSpec = {
|
||||||
@@ -198,9 +210,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
preferencesManager.setDarkTheme(!isDarkTheme)
|
preferencesManager.setDarkTheme(!isDarkTheme)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onStartMessaging = {
|
onStartMessaging = { showOnboarding = false }
|
||||||
showOnboarding = false
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
"auth_new", "auth_unlock" -> {
|
"auth_new", "auth_unlock" -> {
|
||||||
@@ -213,24 +223,41 @@ class MainActivity : ComponentActivity() {
|
|||||||
currentAccount = account
|
currentAccount = account
|
||||||
hasExistingAccount = true
|
hasExistingAccount = true
|
||||||
// Save as last logged account
|
// Save as last logged account
|
||||||
account?.let { accountManager.setLastLoggedPublicKey(it.publicKey) }
|
account?.let {
|
||||||
|
accountManager.setLastLoggedPublicKey(it.publicKey)
|
||||||
|
}
|
||||||
|
|
||||||
// 📤 Отправляем FCM токен на сервер после успешной аутентификации
|
// 📤 Отправляем FCM токен на сервер после успешной
|
||||||
|
// аутентификации
|
||||||
account?.let { sendFcmTokenToServer(it) }
|
account?.let { sendFcmTokenToServer(it) }
|
||||||
|
|
||||||
// Reload accounts list
|
// Reload accounts list
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val accounts = accountManager.getAllAccounts()
|
val accounts = accountManager.getAllAccounts()
|
||||||
accountInfoList = accounts.map { acc ->
|
accountInfoList =
|
||||||
|
accounts.map { acc ->
|
||||||
val shortKey = acc.publicKey.take(7)
|
val shortKey = acc.publicKey.take(7)
|
||||||
val displayName = acc.name ?: shortKey
|
val displayName = acc.name ?: shortKey
|
||||||
val initials = displayName.trim().split(Regex("\\s+"))
|
val initials =
|
||||||
.filter { it.isNotEmpty() }
|
displayName
|
||||||
|
.trim()
|
||||||
|
.split(Regex("\\s+"))
|
||||||
|
.filter {
|
||||||
|
it.isNotEmpty()
|
||||||
|
}
|
||||||
.let { words ->
|
.let { words ->
|
||||||
when {
|
when {
|
||||||
words.isEmpty() -> "??"
|
words.isEmpty() ->
|
||||||
words.size == 1 -> words[0].take(2).uppercase()
|
"??"
|
||||||
else -> "${words[0].first()}${words[1].first()}".uppercase()
|
words.size ==
|
||||||
|
1 ->
|
||||||
|
words[0]
|
||||||
|
.take(
|
||||||
|
2
|
||||||
|
)
|
||||||
|
.uppercase()
|
||||||
|
else ->
|
||||||
|
"${words[0].first()}${words[1].first()}".uppercase()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
AccountInfo(
|
AccountInfo(
|
||||||
@@ -243,10 +270,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
// Set currentAccount to null immediately to prevent UI lag
|
// Set currentAccount to null immediately to prevent UI
|
||||||
|
// lag
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
com.rosetta.messenger.network.ProtocolManager
|
||||||
|
.disconnect()
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -262,10 +291,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onLogout = {
|
onLogout = {
|
||||||
// Set currentAccount to null immediately to prevent UI lag
|
// Set currentAccount to null immediately to prevent UI
|
||||||
|
// lag
|
||||||
currentAccount = null
|
currentAccount = null
|
||||||
scope.launch {
|
scope.launch {
|
||||||
com.rosetta.messenger.network.ProtocolManager.disconnect()
|
com.rosetta.messenger.network.ProtocolManager
|
||||||
|
.disconnect()
|
||||||
accountManager.logout()
|
accountManager.logout()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -290,44 +321,51 @@ class MainActivity : ComponentActivity() {
|
|||||||
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = false
|
com.rosetta.messenger.push.RosettaFirebaseMessagingService.isAppInForeground = false
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** 🔔 Инициализация Firebase Cloud Messaging */
|
||||||
* 🔔 Инициализация Firebase Cloud Messaging
|
|
||||||
*/
|
|
||||||
private fun initializeFirebase() {
|
private fun initializeFirebase() {
|
||||||
try {
|
try {
|
||||||
|
addFcmLog("🔔 Инициализация Firebase...")
|
||||||
// Инициализируем Firebase
|
// Инициализируем Firebase
|
||||||
FirebaseApp.initializeApp(this)
|
FirebaseApp.initializeApp(this)
|
||||||
|
addFcmLog("✅ Firebase инициализирован")
|
||||||
|
|
||||||
// Получаем FCM токен
|
// Получаем FCM токен
|
||||||
|
addFcmLog("📲 Запрос FCM токена...")
|
||||||
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
FirebaseMessaging.getInstance().token.addOnCompleteListener { task ->
|
||||||
if (!task.isSuccessful) {
|
if (!task.isSuccessful) {
|
||||||
|
addFcmLog("❌ Ошибка получения токена: ${task.exception?.message}")
|
||||||
return@addOnCompleteListener
|
return@addOnCompleteListener
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = task.result
|
val token = task.result
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
val shortToken = "${token.take(12)}...${token.takeLast(8)}"
|
||||||
|
addFcmLog("✅ FCM токен получен: $shortToken")
|
||||||
// Сохраняем токен локально
|
// Сохраняем токен локально
|
||||||
token?.let { saveFcmToken(it) }
|
saveFcmToken(token)
|
||||||
|
addFcmLog("💾 Токен сохранен локально")
|
||||||
|
} else {
|
||||||
|
addFcmLog("⚠️ Токен пустой")
|
||||||
|
}
|
||||||
|
|
||||||
// Токен будет отправлен на сервер после успешной аутентификации
|
// Токен будет отправлен на сервер после успешной аутентификации
|
||||||
// (см. вызов sendFcmTokenToServer в onAccountLogin)
|
// (см. вызов sendFcmTokenToServer в onAccountLogin)
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
addFcmLog("❌ Ошибка Firebase: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Сохранить FCM токен в SharedPreferences */
|
||||||
* Сохранить FCM токен в SharedPreferences
|
|
||||||
*/
|
|
||||||
private fun saveFcmToken(token: String) {
|
private fun saveFcmToken(token: String) {
|
||||||
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
val prefs = getSharedPreferences("rosetta_prefs", MODE_PRIVATE)
|
||||||
prefs.edit().putString("fcm_token", token).apply()
|
prefs.edit().putString("fcm_token", token).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Отправить FCM токен на сервер
|
* Отправить FCM токен на сервер Вызывается после успешной аутентификации, когда аккаунт уже
|
||||||
* Вызывается после успешной аутентификации, когда аккаунт уже расшифрован
|
* расшифрован
|
||||||
*/
|
*/
|
||||||
private fun sendFcmTokenToServer(account: DecryptedAccount) {
|
private fun sendFcmTokenToServer(account: DecryptedAccount) {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
@@ -336,27 +374,41 @@ class MainActivity : ComponentActivity() {
|
|||||||
val token = prefs.getString("fcm_token", null)
|
val token = prefs.getString("fcm_token", null)
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
|
addFcmLog("⚠️ Нет сохраненного токена для отправки")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val shortToken = "${token.take(12)}...${token.takeLast(8)}"
|
||||||
|
addFcmLog("📤 Подготовка к отправке токена на сервер")
|
||||||
|
addFcmLog("⏳ Ожидание аутентификации...")
|
||||||
|
|
||||||
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
|
// 🔥 КРИТИЧНО: Ждем пока протокол станет AUTHENTICATED
|
||||||
var waitAttempts = 0
|
var waitAttempts = 0
|
||||||
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED && waitAttempts < 50) {
|
while (ProtocolManager.state.value != ProtocolState.AUTHENTICATED &&
|
||||||
|
waitAttempts < 50) {
|
||||||
delay(100) // Ждем 100ms
|
delay(100) // Ждем 100ms
|
||||||
waitAttempts++
|
waitAttempts++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
if (ProtocolManager.state.value != ProtocolState.AUTHENTICATED) {
|
||||||
|
addFcmLog("❌ Таймаут аутентификации (${waitAttempts * 100}ms)")
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
val packet = PacketPushNotification().apply {
|
addFcmLog("✅ Аутентификация успешна")
|
||||||
|
addFcmLog("📨 Отправка токена: $shortToken")
|
||||||
|
|
||||||
|
val packet =
|
||||||
|
PacketPushNotification().apply {
|
||||||
this.notificationsToken = token
|
this.notificationsToken = token
|
||||||
this.action = PushNotificationAction.SUBSCRIBE
|
this.action = PushNotificationAction.SUBSCRIBE
|
||||||
}
|
}
|
||||||
|
|
||||||
ProtocolManager.send(packet)
|
ProtocolManager.send(packet)
|
||||||
|
addFcmLog("✅ Пакет отправлен на сервер (ID: 0x10)")
|
||||||
|
addFcmLog("🎉 FCM токен успешно зарегистрирован!")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
addFcmLog("❌ Ошибка отправки: ${e.message}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,9 +422,11 @@ fun MainScreen(
|
|||||||
onLogout: () -> Unit = {}
|
onLogout: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val accountName = account?.name ?: "Account"
|
val accountName = account?.name ?: "Account"
|
||||||
val accountPhone = account?.publicKey?.take(16)?.let {
|
val accountPhone =
|
||||||
|
account?.publicKey?.take(16)?.let {
|
||||||
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
|
"+${it.take(1)} ${it.substring(1, 4)} ${it.substring(4, 7)}${it.substring(7)}"
|
||||||
} ?: "+7 775 9932587"
|
}
|
||||||
|
?: "+7 775 9932587"
|
||||||
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
|
val accountPublicKey = account?.publicKey ?: "04c266b98ae5"
|
||||||
val accountPrivateKey = account?.privateKey ?: ""
|
val accountPrivateKey = account?.privateKey ?: ""
|
||||||
val privateKeyHash = account?.privateKeyHash ?: ""
|
val privateKeyHash = account?.privateKeyHash ?: ""
|
||||||
@@ -396,47 +450,32 @@ fun MainScreen(
|
|||||||
when {
|
when {
|
||||||
// 🚀 Вход в чат - плавный fade
|
// 🚀 Вход в чат - плавный fade
|
||||||
isEnteringChat -> {
|
isEnteringChat -> {
|
||||||
fadeIn(
|
fadeIn(animationSpec = tween(200)) togetherWith
|
||||||
animationSpec = tween(200)
|
fadeOut(animationSpec = tween(150))
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(150)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔙 Выход из чата - плавный fade
|
// 🔙 Выход из чата - плавный fade
|
||||||
isExitingChat -> {
|
isExitingChat -> {
|
||||||
fadeIn(
|
fadeIn(animationSpec = tween(200)) togetherWith
|
||||||
animationSpec = tween(200)
|
fadeOut(animationSpec = tween(150))
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(150)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔍 Вход в Search - плавный fade
|
// 🔍 Вход в Search - плавный fade
|
||||||
isEnteringSearch -> {
|
isEnteringSearch -> {
|
||||||
fadeIn(
|
fadeIn(animationSpec = tween(200)) togetherWith
|
||||||
animationSpec = tween(200)
|
fadeOut(animationSpec = tween(150))
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(150)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔙 Выход из Search - плавный fade
|
// 🔙 Выход из Search - плавный fade
|
||||||
isEnteringSearch -> {
|
isEnteringSearch -> {
|
||||||
fadeIn(
|
fadeIn(animationSpec = tween(200)) togetherWith
|
||||||
animationSpec = tween(200)
|
fadeOut(animationSpec = tween(150))
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(150)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔙 Выход из Search - плавный fade
|
// 🔙 Выход из Search - плавный fade
|
||||||
isExitingSearch -> {
|
isExitingSearch -> {
|
||||||
fadeIn(
|
fadeIn(animationSpec = tween(200)) togetherWith
|
||||||
animationSpec = tween(200)
|
fadeOut(animationSpec = tween(150))
|
||||||
) togetherWith fadeOut(
|
|
||||||
animationSpec = tween(150)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default - мгновенный переход
|
// Default - мгновенный переход
|
||||||
@@ -459,8 +498,10 @@ fun MainScreen(
|
|||||||
onNavigateToChat = { publicKey ->
|
onNavigateToChat = { publicKey ->
|
||||||
// 📨 Forward: переход в выбранный чат
|
// 📨 Forward: переход в выбранный чат
|
||||||
// Нужно получить SearchUser из публичного ключа
|
// Нужно получить SearchUser из публичного ключа
|
||||||
// Используем минимальные данные - остальное подгрузится в ChatDetailScreen
|
// Используем минимальные данные - остальное подгрузится в
|
||||||
selectedUser = SearchUser(
|
// ChatDetailScreen
|
||||||
|
selectedUser =
|
||||||
|
SearchUser(
|
||||||
title = "",
|
title = "",
|
||||||
username = "",
|
username = "",
|
||||||
publicKey = publicKey,
|
publicKey = publicKey,
|
||||||
@@ -508,7 +549,8 @@ fun MainScreen(
|
|||||||
},
|
},
|
||||||
onSavedMessagesClick = {
|
onSavedMessagesClick = {
|
||||||
// Открываем чат с самим собой (Saved Messages)
|
// Открываем чат с самим собой (Saved Messages)
|
||||||
selectedUser = SearchUser(
|
selectedUser =
|
||||||
|
SearchUser(
|
||||||
title = "Saved Messages",
|
title = "Saved Messages",
|
||||||
username = "",
|
username = "",
|
||||||
publicKey = accountPublicKey,
|
publicKey = accountPublicKey,
|
||||||
@@ -522,19 +564,14 @@ fun MainScreen(
|
|||||||
onInviteFriendsClick = {
|
onInviteFriendsClick = {
|
||||||
// TODO: Share invite link
|
// TODO: Share invite link
|
||||||
},
|
},
|
||||||
onSearchClick = {
|
onSearchClick = { showSearchScreen = true },
|
||||||
showSearchScreen = true
|
|
||||||
},
|
|
||||||
onNewChat = {
|
onNewChat = {
|
||||||
// TODO: Show new chat screen
|
// TODO: Show new chat screen
|
||||||
},
|
},
|
||||||
onUserSelect = { selectedChatUser ->
|
onUserSelect = { selectedChatUser -> selectedUser = selectedChatUser },
|
||||||
selectedUser = selectedChatUser
|
|
||||||
},
|
|
||||||
onLogout = onLogout
|
onLogout = onLogout
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,19 +6,14 @@ import android.app.PendingIntent
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.app.NotificationCompat
|
import androidx.core.app.NotificationCompat
|
||||||
import com.google.firebase.messaging.FirebaseMessagingService
|
import com.google.firebase.messaging.FirebaseMessagingService
|
||||||
import com.google.firebase.messaging.RemoteMessage
|
import com.google.firebase.messaging.RemoteMessage
|
||||||
import com.rosetta.messenger.MainActivity
|
import com.rosetta.messenger.MainActivity
|
||||||
import com.rosetta.messenger.R
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.crypto.CryptoManager
|
|
||||||
import com.rosetta.messenger.data.AccountManager
|
|
||||||
import com.rosetta.messenger.network.ProtocolManager
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Firebase Cloud Messaging Service для обработки push-уведомлений
|
* Firebase Cloud Messaging Service для обработки push-уведомлений
|
||||||
@@ -39,14 +34,10 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
private const val NOTIFICATION_ID = 1
|
private const val NOTIFICATION_ID = 1
|
||||||
|
|
||||||
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
// 🔥 Флаг - приложение в foreground (видимо пользователю)
|
||||||
@Volatile
|
@Volatile var isAppInForeground = false
|
||||||
var isAppInForeground = false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Вызывается когда получен новый FCM токен Отправляем его на сервер через протокол */
|
||||||
* Вызывается когда получен новый FCM токен
|
|
||||||
* Отправляем его на сервер через протокол
|
|
||||||
*/
|
|
||||||
override fun onNewToken(token: String) {
|
override fun onNewToken(token: String) {
|
||||||
super.onNewToken(token)
|
super.onNewToken(token)
|
||||||
|
|
||||||
@@ -56,18 +47,16 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
|
// 📤 Токен будет отправлен на сервер после успешного логина в MainActivity
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Вызывается когда получено push-уведомление */
|
||||||
* Вызывается когда получено push-уведомление
|
|
||||||
*/
|
|
||||||
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
override fun onMessageReceived(remoteMessage: RemoteMessage) {
|
||||||
super.onMessageReceived(remoteMessage)
|
super.onMessageReceived(remoteMessage)
|
||||||
|
|
||||||
// Обрабатываем data payload
|
// Обрабатываем data payload
|
||||||
remoteMessage.data.isNotEmpty().let {
|
remoteMessage.data.isNotEmpty().let {
|
||||||
|
|
||||||
val type = remoteMessage.data["type"]
|
val type = remoteMessage.data["type"]
|
||||||
val senderPublicKey = remoteMessage.data["sender_public_key"]
|
val senderPublicKey = remoteMessage.data["sender_public_key"]
|
||||||
val senderName = remoteMessage.data["sender_name"] ?: senderPublicKey?.take(10) ?: "Unknown"
|
val senderName =
|
||||||
|
remoteMessage.data["sender_name"] ?: senderPublicKey?.take(10) ?: "Unknown"
|
||||||
val messagePreview = remoteMessage.data["message_preview"] ?: "New message"
|
val messagePreview = remoteMessage.data["message_preview"] ?: "New message"
|
||||||
|
|
||||||
when (type) {
|
when (type) {
|
||||||
@@ -78,8 +67,7 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
"message_read" -> {
|
"message_read" -> {
|
||||||
// Сообщение прочитано - можно обновить UI если приложение открыто
|
// Сообщение прочитано - можно обновить UI если приложение открыто
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,10 +77,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Показать уведомление о новом сообщении */
|
||||||
* Показать уведомление о новом сообщении
|
private fun showMessageNotification(
|
||||||
*/
|
senderPublicKey: String?,
|
||||||
private fun showMessageNotification(senderPublicKey: String?, senderName: String, messagePreview: String) {
|
senderName: String,
|
||||||
|
messagePreview: String
|
||||||
|
) {
|
||||||
// 🔥 Не показываем уведомление если приложение открыто
|
// 🔥 Не показываем уведомление если приложение открыто
|
||||||
if (isAppInForeground) {
|
if (isAppInForeground) {
|
||||||
return
|
return
|
||||||
@@ -101,20 +91,23 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
|
||||||
// Intent для открытия чата
|
// Intent для открытия чата
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
val intent =
|
||||||
|
Intent(this, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
putExtra("open_chat", senderPublicKey)
|
putExtra("open_chat", senderPublicKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
val notification =
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(senderName)
|
.setContentTitle(senderName)
|
||||||
.setContentText(messagePreview)
|
.setContentText(messagePreview)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
@@ -123,13 +116,12 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Показать простое уведомление */
|
||||||
* Показать простое уведомление
|
|
||||||
*/
|
|
||||||
private fun showSimpleNotification(title: String, body: String) {
|
private fun showSimpleNotification(title: String, body: String) {
|
||||||
// 🔥 Не показываем уведомление если приложение открыто
|
// 🔥 Не показываем уведомление если приложение открыто
|
||||||
if (isAppInForeground) {
|
if (isAppInForeground) {
|
||||||
@@ -138,19 +130,22 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
|
|
||||||
createNotificationChannel()
|
createNotificationChannel()
|
||||||
|
|
||||||
val intent = Intent(this, MainActivity::class.java).apply {
|
val intent =
|
||||||
|
Intent(this, MainActivity::class.java).apply {
|
||||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
}
|
}
|
||||||
|
|
||||||
val pendingIntent = PendingIntent.getActivity(
|
val pendingIntent =
|
||||||
|
PendingIntent.getActivity(
|
||||||
this,
|
this,
|
||||||
0,
|
0,
|
||||||
intent,
|
intent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||||
)
|
)
|
||||||
|
|
||||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
val notification =
|
||||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
NotificationCompat.Builder(this, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setContentText(body)
|
.setContentText(body)
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
@@ -158,32 +153,32 @@ class RosettaFirebaseMessagingService : FirebaseMessagingService() {
|
|||||||
.setContentIntent(pendingIntent)
|
.setContentIntent(pendingIntent)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.notify(NOTIFICATION_ID, notification)
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Создать notification channel для Android 8+ */
|
||||||
* Создать notification channel для Android 8+
|
|
||||||
*/
|
|
||||||
private fun createNotificationChannel() {
|
private fun createNotificationChannel() {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
val channel = NotificationChannel(
|
val channel =
|
||||||
|
NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
CHANNEL_NAME,
|
CHANNEL_NAME,
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
).apply {
|
)
|
||||||
|
.apply {
|
||||||
description = "Notifications for new messages"
|
description = "Notifications for new messages"
|
||||||
enableVibration(true)
|
enableVibration(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager =
|
||||||
|
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Сохранить FCM токен в SharedPreferences */
|
||||||
* Сохранить FCM токен в SharedPreferences
|
|
||||||
*/
|
|
||||||
private fun saveFcmToken(token: String) {
|
private fun saveFcmToken(token: String) {
|
||||||
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
val prefs = getSharedPreferences("rosetta_prefs", Context.MODE_PRIVATE)
|
||||||
prefs.edit().putString("fcm_token", token).apply()
|
prefs.edit().putString("fcm_token", token).apply()
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,32 +1,27 @@
|
|||||||
package com.rosetta.messenger.ui.onboarding
|
package com.rosetta.messenger.ui.onboarding
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.core.*
|
import androidx.compose.animation.core.*
|
||||||
import androidx.compose.animation.fadeIn
|
|
||||||
import androidx.compose.animation.fadeOut
|
|
||||||
import androidx.compose.animation.scaleIn
|
|
||||||
import androidx.compose.animation.scaleOut
|
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.PagerDefaults
|
import androidx.compose.foundation.pager.PagerDefaults
|
||||||
|
import androidx.compose.foundation.pager.PagerSnapDistance
|
||||||
import androidx.compose.foundation.pager.PagerState
|
import androidx.compose.foundation.pager.PagerState
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Send
|
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
|
||||||
import androidx.compose.material.icons.filled.DarkMode
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawBehind
|
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
@@ -34,9 +29,10 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.layout.onGloballyPositioned
|
|
||||||
import androidx.compose.ui.layout.boundsInWindow
|
import androidx.compose.ui.layout.boundsInWindow
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.buildAnnotatedString
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -45,14 +41,12 @@ import androidx.compose.ui.text.withStyle
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import com.airbnb.lottie.compose.*
|
||||||
|
import com.rosetta.messenger.R
|
||||||
import com.rosetta.messenger.ui.theme.*
|
import com.rosetta.messenger.ui.theme.*
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
import kotlin.math.hypot
|
import kotlin.math.hypot
|
||||||
import com.airbnb.lottie.compose.*
|
import kotlinx.coroutines.delay
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import com.rosetta.messenger.R
|
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
|
|
||||||
// App colors (matching React Native)
|
// App colors (matching React Native)
|
||||||
val PrimaryBlue = Color(0xFF248AE6) // primary light theme
|
val PrimaryBlue = Color(0xFF248AE6) // primary light theme
|
||||||
@@ -71,10 +65,14 @@ fun OnboardingScreen(
|
|||||||
val pagerState = rememberPagerState(pageCount = { onboardingPages.size })
|
val pagerState = rememberPagerState(pageCount = { onboardingPages.size })
|
||||||
|
|
||||||
// Preload Lottie animations
|
// Preload Lottie animations
|
||||||
val ideaComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Idea.json"))
|
val ideaComposition by
|
||||||
val moneyComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Money.json"))
|
rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Idea.json"))
|
||||||
val lockComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
val moneyComposition by
|
||||||
val bookComposition by rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Book.json"))
|
rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Money.json"))
|
||||||
|
val lockComposition by
|
||||||
|
rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json"))
|
||||||
|
val bookComposition by
|
||||||
|
rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Book.json"))
|
||||||
|
|
||||||
// Theme transition animation
|
// Theme transition animation
|
||||||
var isTransitioning by remember { mutableStateOf(false) }
|
var isTransitioning by remember { mutableStateOf(false) }
|
||||||
@@ -85,9 +83,7 @@ fun OnboardingScreen(
|
|||||||
var previousTheme by remember { mutableStateOf(isDarkTheme) }
|
var previousTheme by remember { mutableStateOf(isDarkTheme) }
|
||||||
var targetTheme by remember { mutableStateOf(isDarkTheme) }
|
var targetTheme by remember { mutableStateOf(isDarkTheme) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) { hasInitialized = true }
|
||||||
hasInitialized = true
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(isTransitioning) {
|
LaunchedEffect(isTransitioning) {
|
||||||
if (isTransitioning) {
|
if (isTransitioning) {
|
||||||
@@ -132,7 +128,8 @@ fun OnboardingScreen(
|
|||||||
val g = (g1 + (g2 - g1) * navProgress).toInt()
|
val g = (g1 + (g2 - g1) * navProgress).toInt()
|
||||||
val b = (b1 + (b2 - b1) * navProgress).toInt()
|
val b = (b1 + (b2 - b1) * navProgress).toInt()
|
||||||
|
|
||||||
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
|
window.navigationBarColor =
|
||||||
|
(0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,39 +152,51 @@ fun OnboardingScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val backgroundColor by animateColorAsState(
|
val backgroundColor by
|
||||||
targetValue = if (isDarkTheme) OnboardingBackground else OnboardingBackgroundLight,
|
animateColorAsState(
|
||||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
targetValue =
|
||||||
|
if (isDarkTheme) OnboardingBackground else OnboardingBackgroundLight,
|
||||||
|
animationSpec =
|
||||||
|
if (!hasInitialized) snap()
|
||||||
|
else tween(800, easing = FastOutSlowInEasing),
|
||||||
label = "backgroundColor"
|
label = "backgroundColor"
|
||||||
)
|
)
|
||||||
val textColor by animateColorAsState(
|
val textColor by
|
||||||
|
animateColorAsState(
|
||||||
targetValue = if (isDarkTheme) Color.White else Color.Black,
|
targetValue = if (isDarkTheme) Color.White else Color.Black,
|
||||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
animationSpec =
|
||||||
|
if (!hasInitialized) snap()
|
||||||
|
else tween(800, easing = FastOutSlowInEasing),
|
||||||
label = "textColor"
|
label = "textColor"
|
||||||
)
|
)
|
||||||
val secondaryTextColor by animateColorAsState(
|
val secondaryTextColor by
|
||||||
|
animateColorAsState(
|
||||||
targetValue = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
|
targetValue = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666),
|
||||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
animationSpec =
|
||||||
|
if (!hasInitialized) snap()
|
||||||
|
else tween(800, easing = FastOutSlowInEasing),
|
||||||
label = "secondaryTextColor"
|
label = "secondaryTextColor"
|
||||||
)
|
)
|
||||||
val indicatorColor by animateColorAsState(
|
val indicatorColor by
|
||||||
|
animateColorAsState(
|
||||||
targetValue = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
|
targetValue = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0),
|
||||||
animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing),
|
animationSpec =
|
||||||
|
if (!hasInitialized) snap()
|
||||||
|
else tween(800, easing = FastOutSlowInEasing),
|
||||||
label = "indicatorColor"
|
label = "indicatorColor"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(modifier = Modifier.fillMaxSize().navigationBarsPadding()) {
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.navigationBarsPadding()
|
|
||||||
) {
|
|
||||||
// Base background - shows the OLD theme color during transition
|
// Base background - shows the OLD theme color during transition
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.background(if (isTransitioning) {
|
.background(
|
||||||
if (previousTheme) OnboardingBackground else OnboardingBackgroundLight
|
if (isTransitioning) {
|
||||||
} else backgroundColor)
|
if (previousTheme) OnboardingBackground
|
||||||
|
else OnboardingBackgroundLight
|
||||||
|
} else backgroundColor
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// Circular reveal overlay - draws the NEW theme color expanding
|
// Circular reveal overlay - draws the NEW theme color expanding
|
||||||
@@ -198,7 +207,9 @@ fun OnboardingScreen(
|
|||||||
|
|
||||||
// Draw the NEW theme color expanding from click point
|
// Draw the NEW theme color expanding from click point
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = if (targetTheme) OnboardingBackground else OnboardingBackgroundLight,
|
color =
|
||||||
|
if (targetTheme) OnboardingBackground
|
||||||
|
else OnboardingBackgroundLight,
|
||||||
radius = radius,
|
radius = radius,
|
||||||
center = clickPosition
|
center = clickPosition
|
||||||
)
|
)
|
||||||
@@ -216,16 +227,11 @@ fun OnboardingScreen(
|
|||||||
onThemeToggle()
|
onThemeToggle()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier.align(Alignment.TopEnd).padding(16.dp).statusBarsPadding()
|
||||||
.align(Alignment.TopEnd)
|
|
||||||
.padding(16.dp)
|
|
||||||
.statusBarsPadding()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp),
|
||||||
.fillMaxSize()
|
|
||||||
.padding(horizontal = 24.dp),
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
) {
|
) {
|
||||||
Spacer(modifier = Modifier.weight(0.15f))
|
Spacer(modifier = Modifier.weight(0.15f))
|
||||||
@@ -245,19 +251,24 @@ fun OnboardingScreen(
|
|||||||
// Pager for text content with easier swipes
|
// Pager for text content with easier swipes
|
||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth().height(150.dp).graphicsLayer {
|
||||||
.height(150.dp)
|
|
||||||
.graphicsLayer {
|
|
||||||
// Hardware acceleration for entire pager
|
// Hardware acceleration for entire pager
|
||||||
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
compositingStrategy =
|
||||||
|
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
||||||
},
|
},
|
||||||
// Pre-load adjacent pages for smooth swiping
|
// Pre-load adjacent pages for smooth swiping
|
||||||
beyondBoundsPageCount = 2,
|
beyondBoundsPageCount = 2,
|
||||||
flingBehavior = PagerDefaults.flingBehavior(
|
flingBehavior =
|
||||||
|
PagerDefaults.flingBehavior(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
|
pagerSnapDistance =
|
||||||
|
PagerSnapDistance.atMost(0), // Snap to nearest page
|
||||||
lowVelocityAnimationSpec = snap(), // Instant!
|
lowVelocityAnimationSpec = snap(), // Instant!
|
||||||
snapAnimationSpec = snap() // No animation!
|
snapAnimationSpec = snap(), // No animation!
|
||||||
|
positionalThreshold =
|
||||||
|
0.4f // Увеличен порог с 0.5 до 0.4 (нужно больше
|
||||||
|
// свайпнуть)
|
||||||
)
|
)
|
||||||
) { page ->
|
) { page ->
|
||||||
OnboardingPageContent(
|
OnboardingPageContent(
|
||||||
@@ -265,7 +276,10 @@ fun OnboardingScreen(
|
|||||||
textColor = textColor,
|
textColor = textColor,
|
||||||
secondaryTextColor = secondaryTextColor,
|
secondaryTextColor = secondaryTextColor,
|
||||||
highlightColor = PrimaryBlue,
|
highlightColor = PrimaryBlue,
|
||||||
pageOffset = ((pagerState.currentPage - page) + pagerState.currentPageOffsetFraction).absoluteValue
|
pageOffset =
|
||||||
|
((pagerState.currentPage - page) +
|
||||||
|
pagerState.currentPageOffsetFraction)
|
||||||
|
.absoluteValue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,9 +298,7 @@ fun OnboardingScreen(
|
|||||||
// Start messaging button
|
// Start messaging button
|
||||||
StartMessagingButton(
|
StartMessagingButton(
|
||||||
onClick = onStartMessaging,
|
onClick = onStartMessaging,
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(48.dp))
|
Spacer(modifier = Modifier.height(48.dp))
|
||||||
@@ -300,25 +312,22 @@ fun ThemeToggleButton(
|
|||||||
onToggle: (androidx.compose.ui.geometry.Offset) -> Unit,
|
onToggle: (androidx.compose.ui.geometry.Offset) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val rotation by animateFloatAsState(
|
val rotation by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = if (isDarkTheme) 360f else 0f,
|
targetValue = if (isDarkTheme) 360f else 0f,
|
||||||
animationSpec = spring(
|
animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow),
|
||||||
dampingRatio = 0.6f,
|
|
||||||
stiffness = Spring.StiffnessLow
|
|
||||||
),
|
|
||||||
label = "rotation"
|
label = "rotation"
|
||||||
)
|
)
|
||||||
|
|
||||||
val scale by animateFloatAsState(
|
val scale by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = 1f,
|
targetValue = 1f,
|
||||||
animationSpec = spring(
|
animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium),
|
||||||
dampingRatio = 0.4f,
|
|
||||||
stiffness = Spring.StiffnessMedium
|
|
||||||
),
|
|
||||||
label = "scale"
|
label = "scale"
|
||||||
)
|
)
|
||||||
|
|
||||||
val iconColor by animateColorAsState(
|
val iconColor by
|
||||||
|
animateColorAsState(
|
||||||
targetValue = if (isDarkTheme) Color.White else Color(0xFF2A2A2A),
|
targetValue = if (isDarkTheme) Color.White else Color(0xFF2A2A2A),
|
||||||
animationSpec = tween(800, easing = FastOutSlowInEasing),
|
animationSpec = tween(800, easing = FastOutSlowInEasing),
|
||||||
label = "iconColor"
|
label = "iconColor"
|
||||||
@@ -336,26 +345,25 @@ fun ThemeToggleButton(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = { if (isClickable) onToggle(buttonPosition) },
|
onClick = { if (isClickable) onToggle(buttonPosition) },
|
||||||
enabled = isClickable,
|
enabled = isClickable,
|
||||||
modifier = modifier
|
modifier =
|
||||||
.size(48.dp)
|
modifier.size(48.dp).onGloballyPositioned { coordinates ->
|
||||||
.onGloballyPositioned { coordinates ->
|
|
||||||
val bounds = coordinates.boundsInWindow()
|
val bounds = coordinates.boundsInWindow()
|
||||||
buttonPosition = androidx.compose.ui.geometry.Offset(
|
buttonPosition =
|
||||||
|
androidx.compose.ui.geometry.Offset(
|
||||||
x = bounds.center.x,
|
x = bounds.center.x,
|
||||||
y = bounds.center.y
|
y = bounds.center.y
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.size(24.dp).scale(scale).rotate(rotation),
|
||||||
.size(24.dp)
|
|
||||||
.scale(scale)
|
|
||||||
.rotate(rotation),
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
|
imageVector =
|
||||||
contentDescription = if (isDarkTheme) "Switch to Light Mode" else "Switch to Dark Mode",
|
if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode,
|
||||||
|
contentDescription =
|
||||||
|
if (isDarkTheme) "Switch to Light Mode" else "Switch to Dark Mode",
|
||||||
tint = iconColor,
|
tint = iconColor,
|
||||||
modifier = Modifier.size(24.dp)
|
modifier = Modifier.size(24.dp)
|
||||||
)
|
)
|
||||||
@@ -383,28 +391,32 @@ fun AnimatedRosettaLogo(
|
|||||||
val bookLottieComp = bookComposition as? com.airbnb.lottie.LottieComposition
|
val bookLottieComp = bookComposition as? com.airbnb.lottie.LottieComposition
|
||||||
|
|
||||||
// All animations are always "playing" but only visible one shows
|
// All animations are always "playing" but only visible one shows
|
||||||
val ideaProgress by animateLottieCompositionAsState(
|
val ideaProgress by
|
||||||
|
animateLottieCompositionAsState(
|
||||||
composition = ideaLottieComp,
|
composition = ideaLottieComp,
|
||||||
iterations = 1,
|
iterations = 1,
|
||||||
isPlaying = currentPage == 1,
|
isPlaying = currentPage == 1,
|
||||||
speed = 1.5f,
|
speed = 1.5f,
|
||||||
restartOnPlay = true
|
restartOnPlay = true
|
||||||
)
|
)
|
||||||
val moneyProgress by animateLottieCompositionAsState(
|
val moneyProgress by
|
||||||
|
animateLottieCompositionAsState(
|
||||||
composition = moneyLottieComp,
|
composition = moneyLottieComp,
|
||||||
iterations = 1,
|
iterations = 1,
|
||||||
isPlaying = currentPage == 2,
|
isPlaying = currentPage == 2,
|
||||||
speed = 1.5f,
|
speed = 1.5f,
|
||||||
restartOnPlay = true
|
restartOnPlay = true
|
||||||
)
|
)
|
||||||
val lockProgress by animateLottieCompositionAsState(
|
val lockProgress by
|
||||||
|
animateLottieCompositionAsState(
|
||||||
composition = lockLottieComp,
|
composition = lockLottieComp,
|
||||||
iterations = 1,
|
iterations = 1,
|
||||||
isPlaying = currentPage == 3,
|
isPlaying = currentPage == 3,
|
||||||
speed = 1.5f,
|
speed = 1.5f,
|
||||||
restartOnPlay = true
|
restartOnPlay = true
|
||||||
)
|
)
|
||||||
val bookProgress by animateLottieCompositionAsState(
|
val bookProgress by
|
||||||
|
animateLottieCompositionAsState(
|
||||||
composition = bookLottieComp,
|
composition = bookLottieComp,
|
||||||
iterations = 1,
|
iterations = 1,
|
||||||
isPlaying = currentPage == 4,
|
isPlaying = currentPage == 4,
|
||||||
@@ -413,35 +425,34 @@ fun AnimatedRosettaLogo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Pulse animation for logo (always running, cheap)
|
// Pulse animation for logo (always running, cheap)
|
||||||
val pulseScale by rememberInfiniteTransition(label = "pulse").animateFloat(
|
val pulseScale by
|
||||||
|
rememberInfiniteTransition(label = "pulse")
|
||||||
|
.animateFloat(
|
||||||
initialValue = 1f,
|
initialValue = 1f,
|
||||||
targetValue = 1.1f,
|
targetValue = 1.1f,
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec =
|
||||||
|
infiniteRepeatable(
|
||||||
animation = tween(800, easing = FastOutSlowInEasing),
|
animation = tween(800, easing = FastOutSlowInEasing),
|
||||||
repeatMode = RepeatMode.Reverse
|
repeatMode = RepeatMode.Reverse
|
||||||
),
|
),
|
||||||
label = "pulseScale"
|
label = "pulseScale"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||||
modifier = modifier,
|
|
||||||
contentAlignment = Alignment.Center
|
|
||||||
) {
|
|
||||||
// === PRE-RENDERED LAYERS - All always exist, just alpha changes ===
|
// === PRE-RENDERED LAYERS - All always exist, just alpha changes ===
|
||||||
|
|
||||||
// Page 0: Rosetta Logo
|
// Page 0: Rosetta Logo
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().graphicsLayer {
|
||||||
.graphicsLayer {
|
|
||||||
alpha = if (currentPage == 0) 1f else 0f
|
alpha = if (currentPage == 0) 1f else 0f
|
||||||
},
|
},
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// Glow effect
|
// Glow effect
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.size(180.dp)
|
Modifier.size(180.dp)
|
||||||
.scale(if (currentPage == 0) pulseScale else 1f)
|
.scale(if (currentPage == 0) pulseScale else 1f)
|
||||||
.background(
|
.background(
|
||||||
color = Color(0xFF54A9EB).copy(alpha = 0.2f),
|
color = Color(0xFF54A9EB).copy(alpha = 0.2f),
|
||||||
@@ -452,9 +463,7 @@ fun AnimatedRosettaLogo(
|
|||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = R.drawable.rosetta_icon),
|
painter = painterResource(id = R.drawable.rosetta_icon),
|
||||||
contentDescription = "Rosetta Logo",
|
contentDescription = "Rosetta Logo",
|
||||||
modifier = Modifier
|
modifier = Modifier.size(150.dp).clip(CircleShape)
|
||||||
.size(150.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -463,12 +472,12 @@ fun AnimatedRosettaLogo(
|
|||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
composition = ideaLottieComp,
|
composition = ideaLottieComp,
|
||||||
progress = { ideaProgress },
|
progress = { ideaProgress },
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().graphicsLayer {
|
||||||
.graphicsLayer {
|
|
||||||
alpha = if (currentPage == 1) 1f else 0f
|
alpha = if (currentPage == 1) 1f else 0f
|
||||||
// Hardware layer optimization
|
// Hardware layer optimization
|
||||||
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
compositingStrategy =
|
||||||
|
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
||||||
// Disable clipping for performance
|
// Disable clipping for performance
|
||||||
clip = false
|
clip = false
|
||||||
},
|
},
|
||||||
@@ -483,11 +492,11 @@ fun AnimatedRosettaLogo(
|
|||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
composition = moneyLottieComp,
|
composition = moneyLottieComp,
|
||||||
progress = { moneyProgress },
|
progress = { moneyProgress },
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().graphicsLayer {
|
||||||
.graphicsLayer {
|
|
||||||
alpha = if (currentPage == 2) 1f else 0f
|
alpha = if (currentPage == 2) 1f else 0f
|
||||||
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
compositingStrategy =
|
||||||
|
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
||||||
clip = false
|
clip = false
|
||||||
},
|
},
|
||||||
maintainOriginalImageBounds = true,
|
maintainOriginalImageBounds = true,
|
||||||
@@ -500,11 +509,11 @@ fun AnimatedRosettaLogo(
|
|||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
composition = lockLottieComp,
|
composition = lockLottieComp,
|
||||||
progress = { lockProgress },
|
progress = { lockProgress },
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().graphicsLayer {
|
||||||
.graphicsLayer {
|
|
||||||
alpha = if (currentPage == 3) 1f else 0f
|
alpha = if (currentPage == 3) 1f else 0f
|
||||||
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
compositingStrategy =
|
||||||
|
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
||||||
clip = false
|
clip = false
|
||||||
},
|
},
|
||||||
maintainOriginalImageBounds = true,
|
maintainOriginalImageBounds = true,
|
||||||
@@ -517,11 +526,11 @@ fun AnimatedRosettaLogo(
|
|||||||
LottieAnimation(
|
LottieAnimation(
|
||||||
composition = bookLottieComp,
|
composition = bookLottieComp,
|
||||||
progress = { bookProgress },
|
progress = { bookProgress },
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().graphicsLayer {
|
||||||
.graphicsLayer {
|
|
||||||
alpha = if (currentPage == 4) 1f else 0f
|
alpha = if (currentPage == 4) 1f else 0f
|
||||||
compositingStrategy = androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
compositingStrategy =
|
||||||
|
androidx.compose.ui.graphics.CompositingStrategy.Offscreen
|
||||||
clip = false
|
clip = false
|
||||||
},
|
},
|
||||||
maintainOriginalImageBounds = true,
|
maintainOriginalImageBounds = true,
|
||||||
@@ -539,21 +548,22 @@ fun OnboardingPageContent(
|
|||||||
highlightColor: Color,
|
highlightColor: Color,
|
||||||
pageOffset: Float
|
pageOffset: Float
|
||||||
) {
|
) {
|
||||||
val alpha by animateFloatAsState(
|
val alpha by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = 1f - (pageOffset * 0.7f).coerceIn(0f, 1f),
|
targetValue = 1f - (pageOffset * 0.7f).coerceIn(0f, 1f),
|
||||||
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
||||||
label = "alpha"
|
label = "alpha"
|
||||||
)
|
)
|
||||||
val scale by animateFloatAsState(
|
val scale by
|
||||||
|
animateFloatAsState(
|
||||||
targetValue = 1f - (pageOffset * 0.1f).coerceIn(0f, 1f),
|
targetValue = 1f - (pageOffset * 0.1f).coerceIn(0f, 1f),
|
||||||
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
animationSpec = tween(400, easing = FastOutSlowInEasing),
|
||||||
label = "scale"
|
label = "scale"
|
||||||
)
|
)
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxWidth()
|
Modifier.fillMaxWidth().graphicsLayer {
|
||||||
.graphicsLayer {
|
|
||||||
this.alpha = alpha
|
this.alpha = alpha
|
||||||
scaleX = scale
|
scaleX = scale
|
||||||
scaleY = scale
|
scaleY = scale
|
||||||
@@ -626,15 +636,16 @@ fun PagerIndicator(
|
|||||||
) {
|
) {
|
||||||
repeat(pageCount) { index ->
|
repeat(pageCount) { index ->
|
||||||
val isSelected = index == currentPage
|
val isSelected = index == currentPage
|
||||||
val width by animateDpAsState(
|
val width by
|
||||||
|
animateDpAsState(
|
||||||
targetValue = if (isSelected) 20.dp else 8.dp,
|
targetValue = if (isSelected) 20.dp else 8.dp,
|
||||||
animationSpec = spring(dampingRatio = 0.8f),
|
animationSpec = spring(dampingRatio = 0.8f),
|
||||||
label = "indicatorWidth"
|
label = "indicatorWidth"
|
||||||
)
|
)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.height(8.dp)
|
Modifier.height(8.dp)
|
||||||
.width(width)
|
.width(width)
|
||||||
.clip(CircleShape)
|
.clip(CircleShape)
|
||||||
.background(if (isSelected) selectedColor else unselectedColor)
|
.background(if (isSelected) selectedColor else unselectedColor)
|
||||||
@@ -644,16 +655,15 @@ fun PagerIndicator(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StartMessagingButton(
|
fun StartMessagingButton(onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||||
onClick: () -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
// Shining effect animation
|
// Shining effect animation
|
||||||
val infiniteTransition = rememberInfiniteTransition(label = "shine")
|
val infiniteTransition = rememberInfiniteTransition(label = "shine")
|
||||||
val shimmerTranslate by infiniteTransition.animateFloat(
|
val shimmerTranslate by
|
||||||
|
infiniteTransition.animateFloat(
|
||||||
initialValue = -0.5f,
|
initialValue = -0.5f,
|
||||||
targetValue = 1.5f,
|
targetValue = 1.5f,
|
||||||
animationSpec = infiniteRepeatable(
|
animationSpec =
|
||||||
|
infiniteRepeatable(
|
||||||
animation = tween(2000, easing = LinearEasing),
|
animation = tween(2000, easing = LinearEasing),
|
||||||
repeatMode = RepeatMode.Restart
|
repeatMode = RepeatMode.Restart
|
||||||
),
|
),
|
||||||
@@ -662,29 +672,32 @@ fun StartMessagingButton(
|
|||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier
|
modifier = modifier.fillMaxWidth().height(54.dp),
|
||||||
.fillMaxWidth()
|
|
||||||
.height(54.dp),
|
|
||||||
shape = RoundedCornerShape(12.dp),
|
shape = RoundedCornerShape(12.dp),
|
||||||
color = PrimaryBlue
|
color = PrimaryBlue
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier.fillMaxSize().drawWithContent {
|
||||||
.drawWithContent {
|
|
||||||
drawContent()
|
drawContent()
|
||||||
// Draw shimmer on top
|
// Draw shimmer on top
|
||||||
val shimmerWidth = size.width * 0.6f
|
val shimmerWidth = size.width * 0.6f
|
||||||
val shimmerStart = shimmerTranslate * size.width
|
val shimmerStart = shimmerTranslate * size.width
|
||||||
drawRect(
|
drawRect(
|
||||||
brush = Brush.linearGradient(
|
brush =
|
||||||
colors = listOf(
|
Brush.linearGradient(
|
||||||
|
colors =
|
||||||
|
listOf(
|
||||||
Color.White.copy(alpha = 0f),
|
Color.White.copy(alpha = 0f),
|
||||||
Color.White.copy(alpha = 0.3f),
|
Color.White.copy(alpha = 0.3f),
|
||||||
Color.White.copy(alpha = 0f)
|
Color.White.copy(alpha = 0f)
|
||||||
),
|
),
|
||||||
start = Offset(shimmerStart, 0f),
|
start = Offset(shimmerStart, 0f),
|
||||||
end = Offset(shimmerStart + shimmerWidth, size.height)
|
end =
|
||||||
|
Offset(
|
||||||
|
shimmerStart + shimmerWidth,
|
||||||
|
size.height
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
9
app/src/main/res/drawable/ic_notification.xml
Normal file
9
app/src/main/res/drawable/ic_notification.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFFFF"
|
||||||
|
android:pathData="M20,2H4c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2V4c0,-1.1 -0.9,-2 -2,-2zM18,14H6v-2h12v2zM18,11H6V9h12v2zM18,8H6V6h12v2z"/>
|
||||||
|
</vector>
|
||||||
Reference in New Issue
Block a user