diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a7ecc7..190061e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + () +// Dark theme colors (background darker, text lighter) +private val avatarColorsDark = listOf( + Color(0xFF7dd3fc) to Color(0xFF2d3548), // blue + Color(0xFF67e8f9) to Color(0xFF2d4248), // cyan + Color(0xFFd8b4fe) to Color(0xFF39334c), // grape + Color(0xFF86efac) to Color(0xFF2d3f32), // green + Color(0xFFa5b4fc) to Color(0xFF333448), // indigo + Color(0xFFbef264) to Color(0xFF383f2d), // lime + Color(0xFFfdba74) to Color(0xFF483529), // orange + Color(0xFFf9a8d4) to Color(0xFF482d3d), // pink + Color(0xFFfca5a5) to Color(0xFF482d2d), // red + Color(0xFF5eead4) to Color(0xFF2d4340), // teal + Color(0xFFc4b5fd) to Color(0xFF3a334c) // violet +) -fun getAvatarColor(name: String): Color { - return avatarColorCache.getOrPut(name) { - val index = name.hashCode().mod(avatarColors.size).let { - if (it < 0) it + avatarColors.size else it +// Cache для цветов аватаров +data class AvatarColors(val textColor: Color, val backgroundColor: Color) +private val avatarColorCache = mutableMapOf() + +fun getAvatarColor(name: String, isDarkTheme: Boolean): AvatarColors { + val cacheKey = "${name}_${if (isDarkTheme) "dark" else "light"}" + return avatarColorCache.getOrPut(cacheKey) { + val colors = if (isDarkTheme) avatarColorsDark else avatarColorsLight + val index = name.hashCode().mod(colors.size).let { + if (it < 0) it + colors.size else it } - avatarColors[index] + val (textColor, bgColor) = colors[index] + AvatarColors(textColor, bgColor) } } @@ -113,11 +138,97 @@ fun ChatsListScreen( onNewChat: () -> Unit, onLogout: () -> Unit ) { - val backgroundColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) + // Theme transition animation state + var isTransitioning by remember { mutableStateOf(false) } + var transitionProgress by remember { mutableStateOf(0f) } + var clickPosition by remember { mutableStateOf(Offset.Zero) } + var shouldUpdateStatusBar by remember { mutableStateOf(false) } + var hasInitialized by remember { mutableStateOf(false) } + var previousTheme by remember { mutableStateOf(isDarkTheme) } + var targetTheme by remember { mutableStateOf(isDarkTheme) } + + LaunchedEffect(Unit) { + hasInitialized = true + } + + // Theme transition animation + LaunchedEffect(isTransitioning) { + if (isTransitioning) { + shouldUpdateStatusBar = false + val duration = 800f + val startTime = System.currentTimeMillis() + while (transitionProgress < 1f) { + val elapsed = System.currentTimeMillis() - startTime + transitionProgress = (elapsed / duration).coerceAtMost(1f) + kotlinx.coroutines.delay(16) + } + shouldUpdateStatusBar = true + kotlinx.coroutines.delay(50) + isTransitioning = false + transitionProgress = 0f + shouldUpdateStatusBar = false + previousTheme = targetTheme + } + } + + val view = androidx.compose.ui.platform.LocalView.current + + // Animate navigation bar color starting at 80% of wave animation + LaunchedEffect(isTransitioning, transitionProgress) { + if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) { + val window = (view.context as android.app.Activity).window + val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f) + + val oldColor = if (previousTheme) 0xFF1A1A1A else 0xFFFFFFFF + val newColor = if (targetTheme) 0xFF1A1A1A else 0xFFFFFFFF + + val r1 = (oldColor shr 16 and 0xFF) + val g1 = (oldColor shr 8 and 0xFF) + val b1 = (oldColor and 0xFF) + val r2 = (newColor shr 16 and 0xFF) + val g2 = (newColor shr 8 and 0xFF) + val b2 = (newColor and 0xFF) + + val r = (r1 + (r2 - r1) * navProgress).toInt() + val g = (g1 + (g2 - g1) * 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() + } + } + + // Update status bar icons when animation finishes + LaunchedEffect(shouldUpdateStatusBar) { + if (shouldUpdateStatusBar && !view.isInEditMode) { + val window = (view.context as android.app.Activity).window + val insetsController = androidx.core.view.WindowCompat.getInsetsController(window, view) + insetsController.isAppearanceLightStatusBars = !isDarkTheme + insetsController.isAppearanceLightNavigationBars = !isDarkTheme + window.statusBarColor = android.graphics.Color.TRANSPARENT + } + } + + val backgroundColor by animateColorAsState( + targetValue = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF), + animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing), + label = "backgroundColor" + ) val drawerBackgroundColor = if (isDarkTheme) Color(0xFF212121) else Color(0xFFFFFFFF) - val textColor = if (isDarkTheme) Color.White else Color.Black - val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) - val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) + val textColor by animateColorAsState( + targetValue = if (isDarkTheme) Color.White else Color.Black, + animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing), + label = "textColor" + ) + val secondaryTextColor by animateColorAsState( + targetValue = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666), + animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing), + label = "secondaryTextColor" + ) + val dividerColor by animateColorAsState( + targetValue = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8), + animationSpec = if (!hasInitialized) snap() else tween(800, easing = FastOutSlowInEasing), + label = "dividerColor" + ) val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) val scope = rememberCoroutineScope() @@ -245,6 +356,31 @@ fun ChatsListScreen( ) ) + Box(modifier = Modifier.fillMaxSize()) { + // Base background - shows the OLD theme color during transition + Box( + modifier = Modifier + .fillMaxSize() + .background(if (isTransitioning) { + if (previousTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF) + } else backgroundColor) + ) + + // Circular reveal overlay - draws the NEW theme color expanding + if (isTransitioning) { + Canvas(modifier = Modifier.fillMaxSize()) { + val maxRadius = kotlin.math.hypot(size.width, size.height) + val radius = maxRadius * transitionProgress + + // Draw the NEW theme color expanding from click point + drawCircle( + color = if (targetTheme) Color(0xFF1A1A1A) else Color(0xFFFFFFFF), + radius = radius, + center = clickPosition + ) + } + } + ModalNavigationDrawer( drawerState = drawerState, drawerContent = { @@ -283,13 +419,35 @@ fun ChatsListScreen( // Theme toggle IconButton( - onClick = onToggleTheme + onClick = {}, + modifier = Modifier.onGloballyPositioned { coordinates -> + // This will be handled by clickable below + } ) { - Icon( - if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode, - contentDescription = "Toggle theme", - tint = textColor - ) + Box( + modifier = Modifier + .clickable { + if (!isTransitioning) { + previousTheme = isDarkTheme + targetTheme = !isDarkTheme + // Use center of icon as click position + val screenWidth = view.width.toFloat() + val screenHeight = view.height.toFloat() + clickPosition = Offset( + screenWidth - 48.dp.value * view.resources.displayMetrics.density, + 96.dp.value * view.resources.displayMetrics.density + ) + isTransitioning = true + onToggleTheme() + } + } + ) { + Icon( + if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode, + contentDescription = "Toggle theme", + tint = textColor + ) + } } } @@ -434,18 +592,19 @@ fun ChatsListScreen( .background(backgroundColor), contentAlignment = Alignment.Center ) { + val avatarColors = getAvatarColor(accountName, isDarkTheme) Box( modifier = Modifier .size(30.dp) .clip(CircleShape) - .background(getAvatarColor(accountName)), + .background(avatarColors.backgroundColor), contentAlignment = Alignment.Center ) { Text( text = getInitials(accountName), fontSize = 12.sp, fontWeight = FontWeight.Bold, - color = Color.White + color = avatarColors.textColor ) } } @@ -514,15 +673,50 @@ fun ChatsListScreen( }, containerColor = backgroundColor ) { paddingValues -> - // Empty state with Lottie animation - EmptyChatsState( - isDarkTheme = isDarkTheme, + // Dev Console Button in bottom left corner + Box( modifier = Modifier .fillMaxSize() .padding(paddingValues) - ) + ) { + // Console button + AnimatedVisibility( + visible = visible, + enter = fadeIn(tween(500, delayMillis = 400)) + slideInHorizontally( + initialOffsetX = { -it }, + animationSpec = tween(500, delayMillis = 400) + ), + modifier = Modifier + .align(Alignment.BottomStart) + .padding(16.dp) + ) { + FloatingActionButton( + onClick = { showDevConsole = true }, + containerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFF5F5F5), + contentColor = if (protocolState == ProtocolState.AUTHENTICATED) + Color(0xFF4CAF50) + else + Color(0xFFFF9800), + shape = CircleShape, + modifier = Modifier.size(48.dp) + ) { + Icon( + Icons.Default.Terminal, + contentDescription = "Dev Console", + modifier = Modifier.size(24.dp) + ) + } + } + + // Empty state with Lottie animation + EmptyChatsState( + isDarkTheme = isDarkTheme, + modifier = Modifier.fillMaxSize() + ) + } } } + } // Close Box for circular reveal } @Composable @@ -638,7 +832,7 @@ fun ChatItem( val secondaryTextColor = if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFF666666) val dividerColor = if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFE8E8E8) - val avatarColor = getAvatarColor(chat.name) + val avatarColors = getAvatarColor(chat.name, isDarkTheme) val initials = getInitials(chat.name) Column { @@ -654,14 +848,14 @@ fun ChatItem( modifier = Modifier .size(56.dp) .clip(CircleShape) - .background(avatarColor), + .background(avatarColors.backgroundColor), contentAlignment = Alignment.Center ) { Text( text = initials, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, - color = Color.White + color = avatarColors.textColor ) // Online indicator diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..449c01c --- /dev/null +++ b/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,6 @@ + + + + 46.28.71.12 + +