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 f7e1253..0a89415 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 @@ -228,11 +228,9 @@ fun ChatsListScreen( insetsController.isAppearanceLightStatusBars = !isDarkTheme window.statusBarColor = android.graphics.Color.TRANSPARENT - // Navigation bar — keep visible, match theme - insetsController.show( - androidx.core.view.WindowInsetsCompat.Type.navigationBars() - ) - insetsController.isAppearanceLightNavigationBars = !isDarkTheme + // Navigation bar: показываем только если есть нативные кнопки + com.rosetta.messenger.ui.utils.NavigationModeUtils + .applyNavigationBarVisibility(insetsController, context, isDarkTheme) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt index 0edb0bf..0602059 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/ChatsListViewModel.kt @@ -104,8 +104,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio ChatsUiState() ) - // Загрузка - private val _isLoading = MutableStateFlow(false) + // Загрузка (🔥 true по умолчанию — skeleton на первом кадре, чтобы не мигало empty→skeleton→empty) + private val _isLoading = MutableStateFlow(true) val isLoading: StateFlow = _isLoading.asStateFlow() private val TAG = "ChatsListVM" @@ -114,6 +114,8 @@ class ChatsListViewModel(application: Application) : AndroidViewModel(applicatio fun setAccount(publicKey: String, privateKey: String) { val setAccountStart = System.currentTimeMillis() if (currentAccount == publicKey) { + // 🔥 Сбрасываем skeleton если он ещё показан (при повторном заходе) + if (_isLoading.value) _isLoading.value = false return } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt index c090e72..b134331 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/ImageEditorScreen.kt @@ -212,9 +212,17 @@ fun ImageEditorScreen( onDispose { if (window == null || insetsController == null) return@onDispose window.statusBarColor = originalStatusBarColor - window.navigationBarColor = originalNavigationBarColor insetsController.isAppearanceLightStatusBars = originalLightStatusBars - insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + + // Navigation bar: восстанавливаем только если есть нативные кнопки + if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) { + window.navigationBarColor = originalNavigationBarColor + insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + } else { + insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = + androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } } } @@ -1573,9 +1581,17 @@ fun MultiImageEditorScreen( onDispose { if (window == null || insetsController == null) return@onDispose window.statusBarColor = originalStatusBarColor - window.navigationBarColor = originalNavigationBarColor insetsController.isAppearanceLightStatusBars = originalLightStatusBars - insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + + // Navigation bar: восстанавливаем только если есть нативные кнопки + if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) { + window.navigationBarColor = originalNavigationBarColor + insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + } else { + insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = + androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt index a4c1d3e..ee86540 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/components/InAppCameraScreen.kt @@ -139,9 +139,17 @@ fun InAppCameraScreen( onDispose { if (window == null || insetsController == null) return@onDispose window.statusBarColor = originalStatusBarColor - window.navigationBarColor = originalNavigationBarColor insetsController.isAppearanceLightStatusBars = originalLightStatusBars - insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + + // Navigation bar: восстанавливаем только если есть нативные кнопки + if (com.rosetta.messenger.ui.utils.NavigationModeUtils.hasNativeNavigationBar(context)) { + window.navigationBarColor = originalNavigationBarColor + insetsController.isAppearanceLightNavigationBars = originalLightNavigationBars + } else { + insetsController.hide(androidx.core.view.WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = + androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt index 9d29ce7..9b214d4 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/onboarding/OnboardingScreen.kt @@ -25,7 +25,9 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.scale +import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path @@ -43,9 +45,12 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat import com.airbnb.lottie.compose.* import com.rosetta.messenger.R import com.rosetta.messenger.ui.theme.* +import com.rosetta.messenger.ui.utils.NavigationModeUtils import kotlin.math.PI import kotlin.math.abs import kotlin.math.acos @@ -154,8 +159,12 @@ fun OnboardingScreen( val window = (view.context as android.app.Activity).window val insetsController = WindowCompat.getInsetsController(window, view) insetsController.isAppearanceLightStatusBars = !isDarkTheme - insetsController.isAppearanceLightNavigationBars = !isDarkTheme window.statusBarColor = android.graphics.Color.TRANSPARENT + + // Navigation bar: показываем только если есть нативные кнопки + NavigationModeUtils.applyNavigationBarVisibility( + insetsController, view.context, isDarkTheme + ) } } @@ -163,8 +172,16 @@ fun OnboardingScreen( LaunchedEffect(Unit) { if (!view.isInEditMode) { val window = (view.context as android.app.Activity).window - window.navigationBarColor = - if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() + val insetsController = WindowCompat.getInsetsController(window, view) + if (NavigationModeUtils.hasNativeNavigationBar(view.context)) { + window.navigationBarColor = + if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() + } else { + // Жестовая навигация — прячем бар + insetsController.hide(WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } } } @@ -685,7 +702,7 @@ fun GooeyPagerIndicator( modifier: Modifier = Modifier, dotRadius: Dp = 2.8.dp, dotSpacing: Dp = 12.dp, - indicatorHeight: Dp = 18.dp + indicatorHeight: Dp = 18.dp, ) { if (pageCount <= 0) return @@ -699,6 +716,17 @@ fun GooeyPagerIndicator( val trackWidth = if (pageCount > 1) spacing * (pageCount - 1) else 0f val startX = (size.width - trackWidth) / 2f + if (pageCount > 1) { + val trackInset = baseRadius * 0.75f + val trackHeight = baseRadius * 0.45f + drawRoundRect( + color = unselectedColor.copy(alpha = 0.14f), + topLeft = Offset(startX - trackInset, centerY - trackHeight / 2f), + size = Size(trackWidth + trackInset * 2f, trackHeight), + cornerRadius = CornerRadius(trackHeight, trackHeight) + ) + } + val rawPosition = (pagerState.currentPage + pagerState.currentPageOffsetFraction) .coerceIn(0f, (pageCount - 1).toFloat()) @@ -706,35 +734,77 @@ fun GooeyPagerIndicator( repeat(pageCount) { index -> val center = Offset(startX + index * spacing, centerY) - drawCircle(color = unselectedColor, radius = baseRadius, center = center) + val distanceToActive = abs(rawPosition - index) + val influence = (1f - distanceToActive / 1.25f).coerceIn(0f, 1f) + val dotScale = 0.88f + influence * 0.16f + val dotAlpha = 0.35f + influence * 0.35f + drawCircle( + color = unselectedColor.copy(alpha = unselectedColor.alpha * dotAlpha), + radius = baseRadius * dotScale, + center = center + ) } val from = floor(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1) val to = ceil(rawPosition.toDouble()).toInt().coerceIn(0, pageCount - 1) val transition = rawPosition - from val stretch = (1f - abs(transition - 0.5f) * 2f).coerceIn(0f, 1f) - val activeRadius = baseRadius * (1.08f + stretch * 0.34f) + val activeRadius = baseRadius * (1.05f + stretch * 0.3f) + + var metaballPath: Path? = null + var anchorCenter: Offset? = null + var anchorRadius = 0f if (from != to) { val anchorIndex = if (transition < 0.5f) from else to - val anchorCenter = Offset(startX + anchorIndex * spacing, centerY) - val anchorRadius = baseRadius * (1.0f - stretch * 0.1f) + anchorCenter = Offset(startX + anchorIndex * spacing, centerY) + anchorRadius = baseRadius * (0.86f + (1f - stretch) * 0.2f) - createMetaballPath( + metaballPath = + createMetaballPath( c1 = activeCenter, r1 = activeRadius, c2 = anchorCenter, r2 = anchorRadius, - maxDistance = spacing * 1.28f, - viscosity = 0.32f, - handleSize = 2.25f + maxDistance = spacing * 1.36f, + viscosity = 0.36f + stretch * 0.12f, + handleSize = 2.45f ) - ?.let { path -> - drawPath(path = path, color = selectedColor.copy(alpha = 0.92f)) - } } + drawCircle( + color = selectedColor.copy(alpha = 0.18f), + radius = activeRadius * 2.2f, + center = activeCenter + ) + if (anchorCenter != null) { + drawCircle( + color = selectedColor.copy(alpha = 0.1f + stretch * 0.1f), + radius = anchorRadius * 1.9f, + center = anchorCenter + ) + } + metaballPath?.let { path -> + drawPath(path = path, color = selectedColor.copy(alpha = 0.2f + stretch * 0.12f)) + } + if (anchorCenter != null) { + drawCircle( + color = selectedColor.copy(alpha = 0.78f), + radius = anchorRadius, + center = anchorCenter + ) + } + metaballPath?.let { path -> drawPath(path = path, color = selectedColor.copy(alpha = 0.92f)) } drawCircle(color = selectedColor, radius = activeRadius, center = activeCenter) + drawCircle( + color = Color.White.copy(alpha = 0.28f), + radius = activeRadius * 0.38f, + center = + Offset( + x = activeCenter.x - activeRadius * 0.28f, + y = activeCenter.y - activeRadius * 0.32f + ) + ) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/theme/Theme.kt b/app/src/main/java/com/rosetta/messenger/ui/theme/Theme.kt index 8e6a3bf..63456bc 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/theme/Theme.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/theme/Theme.kt @@ -13,6 +13,7 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView import androidx.core.view.WindowCompat +import com.rosetta.messenger.ui.utils.NavigationModeUtils import kotlinx.coroutines.delay private val DarkColorScheme = darkColorScheme( @@ -63,14 +64,17 @@ fun RosettaAndroidTheme( else -> LightColorScheme } val view = LocalView.current + val context = LocalContext.current if (!view.isInEditMode) { SideEffect { val window = (view.context as android.app.Activity).window + val insetsController = WindowCompat.getInsetsController(window, view) // Make status bar transparent for wave animation overlay window.statusBarColor = AndroidColor.TRANSPARENT - // Navigation bar color is managed by OnboardingScreen for smooth transition - // Don't change it here to avoid instant color change - WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme + insetsController.isAppearanceLightStatusBars = !darkTheme + + // Navigation bar: показываем только если есть нативные кнопки + NavigationModeUtils.applyNavigationBarVisibility(insetsController, context, darkTheme) } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt b/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt new file mode 100644 index 0000000..128b742 --- /dev/null +++ b/app/src/main/java/com/rosetta/messenger/ui/utils/NavigationModeUtils.kt @@ -0,0 +1,96 @@ +package com.rosetta.messenger.ui.utils + +import android.content.Context +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalContext +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat + +/** + * Утилита для определения типа системной навигации. + * + * Android поддерживает 3 режима: + * - 0 = 3-button navigation (нативные кнопки: Back, Home, Recents) + * - 1 = 2-button navigation (кнопка Home + жест назад) + * - 2 = Gesture navigation (полностью жестовая навигация, без кнопок) + * + * Если у устройства gesture navigation (2), нижний navigation bar прячем. + * Если у устройства кнопочная навигация (0 или 1), показываем navigation bar. + */ +object NavigationModeUtils { + + private const val NAV_MODE_THREE_BUTTON = 0 + private const val NAV_MODE_TWO_BUTTON = 1 + private const val NAV_MODE_GESTURE = 2 + + /** + * Возвращает текущий режим навигации. + * 0 = 3-button, 1 = 2-button, 2 = gesture + */ + fun getNavigationMode(context: Context): Int { + return try { + val resId = context.resources.getIdentifier( + "config_navBarInteractionMode", "integer", "android" + ) + if (resId > 0) context.resources.getInteger(resId) else NAV_MODE_THREE_BUTTON + } catch (_: Exception) { + NAV_MODE_THREE_BUTTON + } + } + + /** + * true если устройство использует жестовую навигацию (без нативных кнопок внизу) + */ + fun isGestureNavigation(context: Context): Boolean { + return getNavigationMode(context) == NAV_MODE_GESTURE + } + + /** + * true если у устройства есть нативная панель навигации (3 или 2 кнопки) + */ + fun hasNativeNavigationBar(context: Context): Boolean { + return !isGestureNavigation(context) + } + + /** + * Показывает или прячет navigation bar в зависимости от типа навигации. + * - Кнопочная навигация → показываем бар + * - Жестовая навигация → прячем бар, свайп снизу временно покажет + */ + fun applyNavigationBarVisibility( + insetsController: WindowInsetsControllerCompat, + context: Context, + isDarkTheme: Boolean + ) { + if (hasNativeNavigationBar(context)) { + // Есть нативные кнопки — показываем навигационный бар + insetsController.show(WindowInsetsCompat.Type.navigationBars()) + insetsController.isAppearanceLightNavigationBars = !isDarkTheme + } else { + // Жестовая навигация — прячем навигационный бар + insetsController.hide(WindowInsetsCompat.Type.navigationBars()) + insetsController.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + } +} + +/** + * Composable-хелпер: запоминает, использует ли устройство жестовую навигацию. + */ +@Composable +fun rememberIsGestureNavigation(): Boolean { + val context = LocalContext.current + return remember { NavigationModeUtils.isGestureNavigation(context) } +} + +/** + * Composable-хелпер: запоминает, есть ли нативная навигационная панель. + */ +@Composable +fun rememberHasNativeNavigationBar(): Boolean { + val context = LocalContext.current + return remember { NavigationModeUtils.hasNativeNavigationBar(context) } +}