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 d0e0700..d267bcc 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 @@ -416,24 +416,56 @@ fun ChatsListScreen( ) ) { key(isDarkTheme) { - TopAppBar( - navigationIcon = { - // Burger menu - скрывается при поиске - if (!isSearchExpanded) { - IconButton( - onClick = { - scope.launch { drawerState.open() } - } - ) { - Icon( - Icons.Default.Menu, - contentDescription = "Menu", - tint = textColor - ) + // Custom header с фиксированной структурой для плавной анимации без скачков + Surface( + modifier = Modifier + .fillMaxWidth() + .statusBarsPadding(), + color = backgroundColor + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .height(64.dp) + ) { + // Burger menu - всегда на месте, только меняет alpha + val burgerAlpha by animateFloatAsState( + targetValue = if (isSearchExpanded) 0f else 1f, + animationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing + ), + label = "burgerAlpha" + ) + + IconButton( + onClick = { + if (!isSearchExpanded) { + scope.launch { drawerState.open() } } - } - }, - title = { + }, + enabled = !isSearchExpanded, + modifier = Modifier + .align(Alignment.CenterStart) + .padding(start = 4.dp) + .graphicsLayer { + alpha = burgerAlpha + } + ) { + Icon( + Icons.Default.Menu, + contentDescription = "Menu", + tint = textColor + ) + } + + // Center content - Title и Search в одном месте + Box( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth() + .padding(horizontal = 60.dp) + ) { val focusRequester = remember { FocusRequester() } // Auto-focus when search opens @@ -444,82 +476,21 @@ fun ChatsListScreen( } } - // Animate alpha for smooth fade without position changes - val titleAlpha by animateFloatAsState( - targetValue = if (isSearchExpanded) 0f else 1f, - animationSpec = tween(durationMillis = 200, easing = FastOutSlowInEasing), - label = "titleAlpha" - ) - val searchAlpha by animateFloatAsState( - targetValue = if (isSearchExpanded) 1f else 0f, - animationSpec = tween(durationMillis = 250, easing = FastOutSlowInEasing), - label = "searchAlpha" - ) - - Box(modifier = Modifier.fillMaxWidth()) { - // Title - Triple click to open dev console (stays in place, just fades) - if (titleAlpha > 0.01f) { + // Crossfade for smooth transition without position jumps + Crossfade( + targetState = isSearchExpanded, + animationSpec = tween( + durationMillis = 300, + easing = FastOutSlowInEasing + ), + modifier = Modifier.fillMaxWidth(), + label = "searchCrossfade" + ) { expanded -> + if (expanded) { + // Search Mode Row( - modifier = Modifier - .clickable { - val currentTime = - System.currentTimeMillis() - if (currentTime - - lastClickTime < - 500 - ) { - titleClickCount++ - if (titleClickCount >= - 3 - ) { - showDevConsole = - true - titleClickCount = - 0 - } - } else { - titleClickCount = - 1 - } - lastClickTime = - currentTime - } - .graphicsLayer { alpha = titleAlpha }, - verticalAlignment = Alignment.CenterVertically - ) { - Text( - "Rosetta", - fontWeight = FontWeight.Bold, - fontSize = 20.sp, - color = textColor - ) - Spacer(modifier = Modifier.width(8.dp)) - // Status indicator dot - Box( - modifier = Modifier - .size(10.dp) - .clip(CircleShape) - .background( - when (protocolState) { - ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) // Green - ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) // Yellow - ProtocolState.DISCONNECTED -> Color(0xFFF44336) // Red - } - ) - .clickable { - showStatusDialog = true - } - ) - } - } - - // Search TextField (appears on top with fade) - if (searchAlpha > 0.01f) { - Row( - verticalAlignment = - Alignment.CenterVertically, + verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() - .graphicsLayer { alpha = searchAlpha } ) { // Back arrow IconButton( @@ -638,80 +609,106 @@ fun ChatsListScreen( } } } + } else { + // Title Mode - Triple click to open dev console + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + val currentTime = + System.currentTimeMillis() + if (currentTime - + lastClickTime < + 500 + ) { + titleClickCount++ + if (titleClickCount >= + 3 + ) { + showDevConsole = + true + titleClickCount = + 0 + } + } else { + titleClickCount = + 1 + } + lastClickTime = + currentTime + }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + "Rosetta", + fontWeight = FontWeight.Bold, + fontSize = 20.sp, + color = textColor + ) + Spacer(modifier = Modifier.width(8.dp)) + // Status indicator dot + Box( + modifier = Modifier + .size(10.dp) + .clip(CircleShape) + .background( + when (protocolState) { + ProtocolState.AUTHENTICATED -> Color(0xFF4CAF50) // Green + ProtocolState.CONNECTING, ProtocolState.CONNECTED, ProtocolState.HANDSHAKING -> Color(0xFFFFC107) // Yellow + ProtocolState.DISCONNECTED -> Color(0xFFF44336) // Red + } + ) + .clickable { + showStatusDialog = true + } + ) + } } } - }, - actions = { - // Animated search button with fade - val searchButtonScale by - animateFloatAsState( - targetValue = - if (isSearchExpanded) 0f - else 1f, - animationSpec = - tween( - durationMillis = 200, - easing = FastOutSlowInEasing - ), - label = "searchButtonScale" - ) - val searchButtonAlpha by - animateFloatAsState( - targetValue = - if (isSearchExpanded) 0f - else 1f, - animationSpec = - tween( - durationMillis = 200, - easing = FastOutSlowInEasing - ), - label = "searchButtonAlpha" - ) - - if (searchButtonScale > 0.01f) { - IconButton( - onClick = { - if (protocolState == - ProtocolState - .AUTHENTICATED - ) { - searchViewModel.expandSearch() - } - }, - enabled = - protocolState == - ProtocolState.AUTHENTICATED, - modifier = - Modifier.graphicsLayer { - scaleX = searchButtonScale - scaleY = searchButtonScale - alpha = - searchButtonAlpha * - if (protocolState == - ProtocolState - .AUTHENTICATED - ) - 1f - else 0.5f - } - ) { - Icon( - Icons.Default.Search, - contentDescription = "Search", - tint = textColor - ) + } + + // Search button - справа, всегда на месте + val searchButtonScale by animateFloatAsState( + targetValue = if (isSearchExpanded) 0f else 1f, + animationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing + ), + label = "searchButtonScale" + ) + val searchButtonAlpha by animateFloatAsState( + targetValue = if (isSearchExpanded) 0f else 1f, + animationSpec = tween( + durationMillis = 250, + easing = FastOutSlowInEasing + ), + label = "searchButtonAlpha" + ) + + IconButton( + onClick = { + if (protocolState == ProtocolState.AUTHENTICATED) { + searchViewModel.expandSearch() } - } - }, - colors = - TopAppBarDefaults.topAppBarColors( - containerColor = backgroundColor, - scrolledContainerColor = backgroundColor, - navigationIconContentColor = textColor, - titleContentColor = textColor, - actionIconContentColor = textColor - ) - ) + }, + enabled = protocolState == ProtocolState.AUTHENTICATED && !isSearchExpanded, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(end = 4.dp) + .graphicsLayer { + scaleX = searchButtonScale + scaleY = searchButtonScale + alpha = searchButtonAlpha * if (protocolState == ProtocolState.AUTHENTICATED) 1f else 0.5f + } + ) { + Icon( + Icons.Default.Search, + contentDescription = "Search", + tint = textColor + ) + } + } + } } } },