From 0c4c636823df0bb5f7f1e677c6da4776515c347c Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Tue, 20 Jan 2026 03:06:06 +0500 Subject: [PATCH] refactor: Clean up OnboardingScreen code for improved readability and maintainability --- .../com/rosetta/messenger/MainActivity.kt | 59 +- .../messenger/data/PreferencesManager.kt | 180 ++- .../ui/onboarding/OnboardingScreen.kt | 1163 +++++++++-------- .../messenger/ui/settings/SettingsScreen.kt | 505 +++++++ 4 files changed, 1327 insertions(+), 580 deletions(-) create mode 100644 app/src/main/java/com/rosetta/messenger/ui/settings/SettingsScreen.kt diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index 044e40e..86a8c3b 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -39,6 +39,7 @@ import com.rosetta.messenger.ui.chats.ChatsListScreen import com.rosetta.messenger.ui.chats.SearchScreen import com.rosetta.messenger.ui.components.OptimizedEmojiCache import com.rosetta.messenger.ui.onboarding.OnboardingScreen +import com.rosetta.messenger.ui.settings.SettingsScreen import com.rosetta.messenger.ui.splash.SplashScreen import com.rosetta.messenger.ui.theme.RosettaAndroidTheme import java.text.SimpleDateFormat @@ -437,15 +438,18 @@ fun MainScreen( // Навигация между экранами var selectedUser by remember { mutableStateOf(null) } var showSearchScreen by remember { mutableStateOf(false) } + var showSettingsScreen by remember { mutableStateOf(false) } // 🔥 TELEGRAM-STYLE анимация - чистый slide БЕЗ прозрачности AnimatedContent( - targetState = Triple(selectedUser, showSearchScreen, Unit), + targetState = Triple(selectedUser, showSearchScreen, showSettingsScreen), transitionSpec = { val isEnteringChat = targetState.first != null && initialState.first == null val isExitingChat = targetState.first == null && initialState.first != null val isEnteringSearch = targetState.second && !initialState.second val isExitingSearch = !targetState.second && initialState.second + val isEnteringSettings = targetState.third && !initialState.third + val isExitingSettings = !targetState.third && initialState.third when { // 🚀 Вход в чат - плавный fade @@ -478,6 +482,18 @@ fun MainScreen( fadeOut(animationSpec = tween(150)) } + // ⚙️ Вход в Settings - плавный fade + isEnteringSettings -> { + fadeIn(animationSpec = tween(200)) togetherWith + fadeOut(animationSpec = tween(150)) + } + + // 🔙 Выход из Settings - плавный fade + isExitingSettings -> { + fadeIn(animationSpec = tween(200)) togetherWith + fadeOut(animationSpec = tween(150)) + } + // Default - мгновенный переход else -> { EnterTransition.None togetherWith ExitTransition.None @@ -485,7 +501,7 @@ fun MainScreen( } }, label = "screenNavigation" - ) { (currentUser, isSearchOpen, _) -> + ) { (currentUser, isSearchOpen, isSettingsOpen) -> when { currentUser != null -> { // Экран чата @@ -525,6 +541,41 @@ fun MainScreen( } ) } + isSettingsOpen -> { + // Экран настроек + SettingsScreen( + isDarkTheme = isDarkTheme, + accountName = accountName, + accountPhone = accountPhone, + accountPublicKey = accountPublicKey, + onBack = { showSettingsScreen = false }, + onToggleTheme = onToggleTheme, + onProfileClick = { + // TODO: Navigate to profile editor + }, + onPrivacySecurityClick = { + // TODO: Navigate to privacy settings + }, + onNotificationsClick = { + // TODO: Navigate to notifications settings + }, + onDataStorageClick = { + // TODO: Navigate to data storage settings + }, + onChatSettingsClick = { + // TODO: Navigate to chat settings + }, + onLanguageClick = { + // TODO: Navigate to language selection + }, + onHelpClick = { + // TODO: Navigate to help center + }, + onAboutClick = { + // TODO: Show about dialog + } + ) + } else -> { // Список чатов ChatsListScreen( @@ -558,9 +609,7 @@ fun MainScreen( online = 1 ) }, - onSettingsClick = { - // TODO: Navigate to settings - }, + onSettingsClick = { showSettingsScreen = true }, onInviteFriendsClick = { // TODO: Share invite link }, diff --git a/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt b/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt index 89492f7..6262855 100644 --- a/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt +++ b/app/src/main/java/com/rosetta/messenger/data/PreferencesManager.kt @@ -5,38 +5,178 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -private val Context.dataStore: DataStore by preferencesDataStore(name = "rosetta_preferences") +private val Context.dataStore: DataStore by + preferencesDataStore(name = "rosetta_preferences") class PreferencesManager(private val context: Context) { - + companion object { + // Onboarding & Theme val HAS_SEEN_ONBOARDING = booleanPreferencesKey("has_seen_onboarding") val IS_DARK_THEME = booleanPreferencesKey("is_dark_theme") + + // Notifications + val NOTIFICATIONS_ENABLED = booleanPreferencesKey("notifications_enabled") + val NOTIFICATION_SOUND_ENABLED = booleanPreferencesKey("notification_sound_enabled") + val NOTIFICATION_VIBRATE_ENABLED = booleanPreferencesKey("notification_vibrate_enabled") + val NOTIFICATION_PREVIEW_ENABLED = booleanPreferencesKey("notification_preview_enabled") + + // Chat Settings + val MESSAGE_TEXT_SIZE = intPreferencesKey("message_text_size") // 0=small, 1=medium, 2=large + val SEND_BY_ENTER = booleanPreferencesKey("send_by_enter") + val AUTO_DOWNLOAD_PHOTOS = booleanPreferencesKey("auto_download_photos") + val AUTO_DOWNLOAD_VIDEOS = booleanPreferencesKey("auto_download_videos") + val AUTO_DOWNLOAD_FILES = booleanPreferencesKey("auto_download_files") + + // Privacy + val SHOW_ONLINE_STATUS = booleanPreferencesKey("show_online_status") + val SHOW_READ_RECEIPTS = booleanPreferencesKey("show_read_receipts") + val SHOW_TYPING_INDICATOR = booleanPreferencesKey("show_typing_indicator") + + // Language + val APP_LANGUAGE = stringPreferencesKey("app_language") // "en", "ru", etc. } - - val hasSeenOnboarding: Flow = context.dataStore.data - .map { preferences -> - preferences[HAS_SEEN_ONBOARDING] ?: false - } - - val isDarkTheme: Flow = context.dataStore.data - .map { preferences -> - preferences[IS_DARK_THEME] ?: true // Default to dark theme like Telegram - } - + + // ═════════════════════════════════════════════════════════════ + // 🎨 ONBOARDING & THEME + // ═════════════════════════════════════════════════════════════ + + val hasSeenOnboarding: Flow = + context.dataStore.data.map { preferences -> preferences[HAS_SEEN_ONBOARDING] ?: false } + + val isDarkTheme: Flow = + context.dataStore.data.map { preferences -> + preferences[IS_DARK_THEME] ?: true // Default to dark theme like Telegram + } + suspend fun setHasSeenOnboarding(value: Boolean) { - context.dataStore.edit { preferences -> - preferences[HAS_SEEN_ONBOARDING] = value - } + context.dataStore.edit { preferences -> preferences[HAS_SEEN_ONBOARDING] = value } } - + suspend fun setDarkTheme(value: Boolean) { - context.dataStore.edit { preferences -> - preferences[IS_DARK_THEME] = value - } + context.dataStore.edit { preferences -> preferences[IS_DARK_THEME] = value } + } + + // ═════════════════════════════════════════════════════════════ + // 🔔 NOTIFICATIONS + // ═════════════════════════════════════════════════════════════ + + val notificationsEnabled: Flow = + context.dataStore.data.map { preferences -> preferences[NOTIFICATIONS_ENABLED] ?: true } + + val notificationSoundEnabled: Flow = + context.dataStore.data.map { preferences -> + preferences[NOTIFICATION_SOUND_ENABLED] ?: true + } + + val notificationVibrateEnabled: Flow = + context.dataStore.data.map { preferences -> + preferences[NOTIFICATION_VIBRATE_ENABLED] ?: true + } + + val notificationPreviewEnabled: Flow = + context.dataStore.data.map { preferences -> + preferences[NOTIFICATION_PREVIEW_ENABLED] ?: true + } + + suspend fun setNotificationsEnabled(value: Boolean) { + context.dataStore.edit { preferences -> preferences[NOTIFICATIONS_ENABLED] = value } + } + + suspend fun setNotificationSoundEnabled(value: Boolean) { + context.dataStore.edit { preferences -> preferences[NOTIFICATION_SOUND_ENABLED] = value } + } + + suspend fun setNotificationVibrateEnabled(value: Boolean) { + context.dataStore.edit { preferences -> preferences[NOTIFICATION_VIBRATE_ENABLED] = value } + } + + suspend fun setNotificationPreviewEnabled(value: Boolean) { + context.dataStore.edit { preferences -> preferences[NOTIFICATION_PREVIEW_ENABLED] = value } + } + + // ═════════════════════════════════════════════════════════════ + // 💬 CHAT SETTINGS + // ═════════════════════════════════════════════════════════════ + + val messageTextSize: Flow = + context.dataStore.data.map { preferences -> + preferences[MESSAGE_TEXT_SIZE] ?: 1 // Default medium + } + + val sendByEnter: Flow = + context.dataStore.data.map { preferences -> preferences[SEND_BY_ENTER] ?: false } + + val autoDownloadPhotos: Flow = + context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_PHOTOS] ?: true } + + val autoDownloadVideos: Flow = + context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_VIDEOS] ?: false } + + val autoDownloadFiles: Flow = + context.dataStore.data.map { preferences -> preferences[AUTO_DOWNLOAD_FILES] ?: false } + + suspend fun setMessageTextSize(value: Int) { + context.dataStore.edit { preferences -> preferences[MESSAGE_TEXT_SIZE] = value } + } + + suspend fun setSendByEnter(value: Boolean) { + context.dataStore.edit { preferences -> preferences[SEND_BY_ENTER] = value } + } + + suspend fun setAutoDownloadPhotos(value: Boolean) { + context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_PHOTOS] = value } + } + + suspend fun setAutoDownloadVideos(value: Boolean) { + context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_VIDEOS] = value } + } + + suspend fun setAutoDownloadFiles(value: Boolean) { + context.dataStore.edit { preferences -> preferences[AUTO_DOWNLOAD_FILES] = value } + } + + // ═════════════════════════════════════════════════════════════ + // 🔐 PRIVACY + // ═════════════════════════════════════════════════════════════ + + val showOnlineStatus: Flow = + context.dataStore.data.map { preferences -> preferences[SHOW_ONLINE_STATUS] ?: true } + + val showReadReceipts: Flow = + context.dataStore.data.map { preferences -> preferences[SHOW_READ_RECEIPTS] ?: true } + + val showTypingIndicator: Flow = + context.dataStore.data.map { preferences -> preferences[SHOW_TYPING_INDICATOR] ?: true } + + suspend fun setShowOnlineStatus(value: Boolean) { + context.dataStore.edit { preferences -> preferences[SHOW_ONLINE_STATUS] = value } + } + + suspend fun setShowReadReceipts(value: Boolean) { + context.dataStore.edit { preferences -> preferences[SHOW_READ_RECEIPTS] = value } + } + + suspend fun setShowTypingIndicator(value: Boolean) { + context.dataStore.edit { preferences -> preferences[SHOW_TYPING_INDICATOR] = value } + } + + // ═════════════════════════════════════════════════════════════ + // 🌐 LANGUAGE + // ═════════════════════════════════════════════════════════════ + + val appLanguage: Flow = + context.dataStore.data.map { preferences -> + preferences[APP_LANGUAGE] ?: "en" // Default English + } + + suspend fun setAppLanguage(value: String) { + context.dataStore.edit { preferences -> preferences[APP_LANGUAGE] = value } } } 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 d52c2b6..ee31acf 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 @@ -62,248 +62,264 @@ fun OnboardingScreen( onThemeToggle: () -> Unit, onStartMessaging: () -> Unit ) { - val pagerState = rememberPagerState(pageCount = { onboardingPages.size }) + val pagerState = rememberPagerState(pageCount = { onboardingPages.size }) - // Preload Lottie animations - val ideaComposition by - rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Idea.json")) - val moneyComposition by - rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Money.json")) - val lockComposition by - rememberLottieComposition(LottieCompositionSpec.Asset("lottie/lock.json")) - val bookComposition by - rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Book.json")) + // Preload Lottie animations + val ideaComposition by + rememberLottieComposition(LottieCompositionSpec.Asset("lottie/Idea.json")) + val moneyComposition by + 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 - var isTransitioning by remember { mutableStateOf(false) } - var transitionProgress by remember { mutableStateOf(0f) } - var clickPosition by remember { mutableStateOf(androidx.compose.ui.geometry.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) } + // Theme transition animation + var isTransitioning by remember { mutableStateOf(false) } + var transitionProgress by remember { mutableStateOf(0f) } + var clickPosition by remember { mutableStateOf(androidx.compose.ui.geometry.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 } + LaunchedEffect(Unit) { hasInitialized = true } - 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) + 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) - delay(16) // ~60fps - } - // Update status bar icons after animation is completely finished - shouldUpdateStatusBar = true - delay(50) // Small delay to ensure UI updates - isTransitioning = false - transitionProgress = 0f - shouldUpdateStatusBar = false - previousTheme = targetTheme + delay(16) // ~60fps + } + // Update status bar icons after animation is completely finished + shouldUpdateStatusBar = true + delay(50) // Small delay to ensure UI updates + isTransitioning = false + transitionProgress = 0f + shouldUpdateStatusBar = false + previousTheme = targetTheme + } } - } - // Animate navigation bar color starting at 80% of wave animation - val view = LocalView.current - LaunchedEffect(isTransitioning, transitionProgress) { - if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) { - val window = (view.context as android.app.Activity).window - // Map 0.8-1.0 to 0-1 for smooth interpolation - val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f) + // Animate navigation bar color starting at 80% of wave animation + val view = LocalView.current + LaunchedEffect(isTransitioning, transitionProgress) { + if (isTransitioning && transitionProgress >= 0.8f && !view.isInEditMode) { + val window = (view.context as android.app.Activity).window + // Map 0.8-1.0 to 0-1 for smooth interpolation + val navProgress = ((transitionProgress - 0.8f) / 0.2f).coerceIn(0f, 1f) - val oldColor = if (previousTheme) 0xFF1E1E1E else 0xFFFFFFFF - val newColor = if (targetTheme) 0xFF1E1E1E else 0xFFFFFFFF + val oldColor = if (previousTheme) 0xFF1E1E1E else 0xFFFFFFFF + val newColor = if (targetTheme) 0xFF1E1E1E 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 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() + 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() + 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 = WindowCompat.getInsetsController(window, view) - insetsController.isAppearanceLightStatusBars = !isDarkTheme - insetsController.isAppearanceLightNavigationBars = !isDarkTheme - window.statusBarColor = android.graphics.Color.TRANSPARENT + // Update status bar icons when animation finishes + LaunchedEffect(shouldUpdateStatusBar) { + if (shouldUpdateStatusBar && !view.isInEditMode) { + 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 + } } - } - // Set initial navigation bar color only on first launch - LaunchedEffect(Unit) { - if (!view.isInEditMode) { - val window = (view.context as android.app.Activity).window - window.navigationBarColor = if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() + // Set initial navigation bar color only on first launch + LaunchedEffect(Unit) { + if (!view.isInEditMode) { + val window = (view.context as android.app.Activity).window + window.navigationBarColor = + if (isDarkTheme) 0xFF1E1E1E.toInt() else 0xFFFFFFFF.toInt() + } } - } - val backgroundColor by - animateColorAsState( - targetValue = - if (isDarkTheme) OnboardingBackground else OnboardingBackgroundLight, - animationSpec = - if (!hasInitialized) snap() - else tween(800, easing = FastOutSlowInEasing), - label = "backgroundColor" - ) - 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 indicatorColor by - animateColorAsState( - targetValue = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0), - animationSpec = - if (!hasInitialized) snap() - else tween(800, easing = FastOutSlowInEasing), - label = "indicatorColor" - ) - - Box(modifier = Modifier.fillMaxSize().navigationBarsPadding()) { - // Base background - shows the OLD theme color during transition - Box( - modifier = - Modifier.fillMaxSize() - .background( - if (isTransitioning) { - if (previousTheme) OnboardingBackground - else OnboardingBackgroundLight - } else backgroundColor - ) - ) - - // Circular reveal overlay - draws the NEW theme color expanding - if (isTransitioning) { - Canvas(modifier = Modifier.fillMaxSize()) { - val maxRadius = hypot(size.width, size.height) - val radius = maxRadius * transitionProgress - - // Draw the NEW theme color expanding from click point - drawCircle( - color = - if (targetTheme) OnboardingBackground + val backgroundColor by + animateColorAsState( + targetValue = + if (isDarkTheme) OnboardingBackground else OnboardingBackgroundLight, - radius = radius, - center = clickPosition + animationSpec = + if (!hasInitialized) snap() + else tween(800, easing = FastOutSlowInEasing), + label = "backgroundColor" ) - } - } - // Theme toggle button in top right - ThemeToggleButton( - isDarkTheme = isDarkTheme, - onToggle = { position -> - if (!isTransitioning) { - previousTheme = isDarkTheme - targetTheme = !isDarkTheme - clickPosition = position - isTransitioning = true - onThemeToggle() - } - }, - modifier = Modifier.align(Alignment.TopEnd).padding(16.dp).statusBarsPadding() - ) - - Column( - modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Spacer(modifier = Modifier.weight(0.15f)) - - // Animated Logo - AnimatedRosettaLogo( - pagerState = pagerState, - ideaComposition = ideaComposition, - moneyComposition = moneyComposition, - lockComposition = lockComposition, - bookComposition = bookComposition, - modifier = Modifier.size(150.dp) - ) - - Spacer(modifier = Modifier.height(32.dp)) - - // Pager for text content with easier swipes - HorizontalPager( - state = pagerState, - modifier = - Modifier.fillMaxWidth().height(150.dp).graphicsLayer { - // Hardware acceleration for entire pager - compositingStrategy = - androidx.compose.ui.graphics.CompositingStrategy.Offscreen - }, - // Pre-load adjacent pages for smooth swiping - beyondBoundsPageCount = 2, - flingBehavior = - PagerDefaults.flingBehavior( - state = pagerState, - pagerSnapDistance = - PagerSnapDistance.atMost(0), // Snap to nearest page - lowVelocityAnimationSpec = snap(), // Instant! - snapAnimationSpec = snap(), // No animation! - positionalThreshold = - 0.4f // Увеличен порог с 0.5 до 0.4 (нужно больше - // свайпнуть) - ) - ) { page -> - OnboardingPageContent( - page = onboardingPages[page], - textColor = textColor, - secondaryTextColor = secondaryTextColor, - highlightColor = PrimaryBlue, - pageOffset = - ((pagerState.currentPage - page) + - pagerState.currentPageOffsetFraction) - .absoluteValue + 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 indicatorColor by + animateColorAsState( + targetValue = if (isDarkTheme) Color(0xFF4A4A4A) else Color(0xFFD0D0D0), + animationSpec = + if (!hasInitialized) snap() + else tween(800, easing = FastOutSlowInEasing), + label = "indicatorColor" ) - } - Spacer(modifier = Modifier.height(24.dp)) + Box(modifier = Modifier.fillMaxSize().navigationBarsPadding()) { + // Base background - shows the OLD theme color during transition + Box( + modifier = + Modifier.fillMaxSize() + .background( + if (isTransitioning) { + if (previousTheme) OnboardingBackground + else OnboardingBackgroundLight + } else backgroundColor + ) + ) - // Page indicators - PagerIndicator( - pageCount = onboardingPages.size, - currentPage = pagerState.currentPage, - selectedColor = PrimaryBlue, - unselectedColor = indicatorColor - ) + // Circular reveal overlay - draws the NEW theme color expanding + if (isTransitioning) { + Canvas(modifier = Modifier.fillMaxSize()) { + val maxRadius = hypot(size.width, size.height) + val radius = maxRadius * transitionProgress - Spacer(modifier = Modifier.weight(0.3f)) + // Draw the NEW theme color expanding from click point + drawCircle( + color = + if (targetTheme) OnboardingBackground + else OnboardingBackgroundLight, + radius = radius, + center = clickPosition + ) + } + } + // Theme toggle button in top right + ThemeToggleButton( + isDarkTheme = isDarkTheme, + onToggle = { position -> + if (!isTransitioning) { + previousTheme = isDarkTheme + targetTheme = !isDarkTheme + clickPosition = position + isTransitioning = true + onThemeToggle() + } + }, + modifier = + Modifier.align(Alignment.TopEnd).padding(16.dp).statusBarsPadding() + ) - // Start messaging button - StartMessagingButton( - onClick = onStartMessaging, - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) - ) + Column( + modifier = Modifier.fillMaxSize().padding(horizontal = 24.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.weight(0.15f)) - Spacer(modifier = Modifier.height(48.dp)) + // Animated Logo + AnimatedRosettaLogo( + pagerState = pagerState, + ideaComposition = ideaComposition, + moneyComposition = moneyComposition, + lockComposition = lockComposition, + bookComposition = bookComposition, + modifier = Modifier.size(150.dp) + ) + + Spacer(modifier = Modifier.height(32.dp)) + + // Pager for text content with easier swipes + HorizontalPager( + state = pagerState, + modifier = + Modifier.fillMaxWidth().height(150.dp).graphicsLayer { + // Hardware acceleration for entire pager + compositingStrategy = + androidx.compose.ui.graphics + .CompositingStrategy.Offscreen + }, + // Pre-load adjacent pages for smooth swiping + beyondBoundsPageCount = 2, + flingBehavior = + PagerDefaults.flingBehavior( + state = pagerState, + pagerSnapDistance = + PagerSnapDistance.atMost( + 1 + ), // Нужен более значительный свайп + lowVelocityAnimationSpec = + tween( + durationMillis = 400, + easing = FastOutSlowInEasing + ), + snapAnimationSpec = + spring( + dampingRatio = 0.8f, + stiffness = 380f + ) + ) + ) { page -> + OnboardingPageContent( + page = onboardingPages[page], + textColor = textColor, + secondaryTextColor = secondaryTextColor, + highlightColor = PrimaryBlue, + pageOffset = + ((pagerState.currentPage - page) + + pagerState + .currentPageOffsetFraction) + .absoluteValue + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + // Page indicators + PagerIndicator( + pageCount = onboardingPages.size, + currentPage = pagerState.currentPage, + selectedColor = PrimaryBlue, + unselectedColor = indicatorColor + ) + + Spacer(modifier = Modifier.weight(0.3f)) + + // Start messaging button + StartMessagingButton( + onClick = onStartMessaging, + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp) + ) + + Spacer(modifier = Modifier.height(48.dp)) + } } - } } @Composable @@ -312,63 +328,67 @@ fun ThemeToggleButton( onToggle: (androidx.compose.ui.geometry.Offset) -> Unit, modifier: Modifier = Modifier ) { - val rotation by - animateFloatAsState( - targetValue = if (isDarkTheme) 360f else 0f, - animationSpec = spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow), - label = "rotation" - ) + val rotation by + animateFloatAsState( + targetValue = if (isDarkTheme) 360f else 0f, + animationSpec = + spring(dampingRatio = 0.6f, stiffness = Spring.StiffnessLow), + label = "rotation" + ) - val scale by - animateFloatAsState( - targetValue = 1f, - animationSpec = spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium), - label = "scale" - ) + val scale by + animateFloatAsState( + targetValue = 1f, + animationSpec = + spring(dampingRatio = 0.4f, stiffness = Spring.StiffnessMedium), + label = "scale" + ) - val iconColor by - animateColorAsState( - targetValue = if (isDarkTheme) Color.White else Color(0xFF2A2A2A), - animationSpec = tween(800, easing = FastOutSlowInEasing), - label = "iconColor" - ) + val iconColor by + animateColorAsState( + targetValue = if (isDarkTheme) Color.White else Color(0xFF2A2A2A), + animationSpec = tween(800, easing = FastOutSlowInEasing), + label = "iconColor" + ) - var buttonPosition by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) } - var isClickable by remember { mutableStateOf(true) } + var buttonPosition by remember { mutableStateOf(androidx.compose.ui.geometry.Offset.Zero) } + var isClickable by remember { mutableStateOf(true) } - LaunchedEffect(isDarkTheme) { - isClickable = false - delay(800) - isClickable = true - } - - IconButton( - onClick = { if (isClickable) onToggle(buttonPosition) }, - enabled = isClickable, - modifier = - modifier.size(48.dp).onGloballyPositioned { coordinates -> - val bounds = coordinates.boundsInWindow() - buttonPosition = - androidx.compose.ui.geometry.Offset( - x = bounds.center.x, - y = bounds.center.y - ) - } - ) { - Box( - modifier = Modifier.size(24.dp).scale(scale).rotate(rotation), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = - if (isDarkTheme) Icons.Default.LightMode else Icons.Default.DarkMode, - contentDescription = - if (isDarkTheme) "Switch to Light Mode" else "Switch to Dark Mode", - tint = iconColor, - modifier = Modifier.size(24.dp) - ) + LaunchedEffect(isDarkTheme) { + isClickable = false + delay(800) + isClickable = true + } + + IconButton( + onClick = { if (isClickable) onToggle(buttonPosition) }, + enabled = isClickable, + modifier = + modifier.size(48.dp).onGloballyPositioned { coordinates -> + val bounds = coordinates.boundsInWindow() + buttonPosition = + androidx.compose.ui.geometry.Offset( + x = bounds.center.x, + y = bounds.center.y + ) + } + ) { + Box( + modifier = Modifier.size(24.dp).scale(scale).rotate(rotation), + contentAlignment = Alignment.Center + ) { + Icon( + imageVector = + if (isDarkTheme) Icons.Default.LightMode + else Icons.Default.DarkMode, + contentDescription = + if (isDarkTheme) "Switch to Light Mode" + else "Switch to Dark Mode", + tint = iconColor, + modifier = Modifier.size(24.dp) + ) + } } - } } @OptIn(ExperimentalFoundationApi::class) @@ -381,163 +401,170 @@ fun AnimatedRosettaLogo( bookComposition: Any?, modifier: Modifier = Modifier ) { - // Use derivedStateOf for optimized reads - prevents unnecessary recompositions - val currentPage by remember { derivedStateOf { pagerState.currentPage } } + // Use derivedStateOf for optimized reads - prevents unnecessary recompositions + val currentPage by remember { derivedStateOf { pagerState.currentPage } } - // Pre-calculate all animation progress states ONCE (they stay in memory) - val ideaLottieComp = ideaComposition as? com.airbnb.lottie.LottieComposition - val moneyLottieComp = moneyComposition as? com.airbnb.lottie.LottieComposition - val lockLottieComp = lockComposition as? com.airbnb.lottie.LottieComposition - val bookLottieComp = bookComposition as? com.airbnb.lottie.LottieComposition + // Pre-calculate all animation progress states ONCE (they stay in memory) + val ideaLottieComp = ideaComposition as? com.airbnb.lottie.LottieComposition + val moneyLottieComp = moneyComposition as? com.airbnb.lottie.LottieComposition + val lockLottieComp = lockComposition as? com.airbnb.lottie.LottieComposition + val bookLottieComp = bookComposition as? com.airbnb.lottie.LottieComposition - // All animations are always "playing" but only visible one shows - val ideaProgress by - animateLottieCompositionAsState( - composition = ideaLottieComp, - iterations = 1, - isPlaying = currentPage == 1, - speed = 1.5f, - restartOnPlay = true - ) - val moneyProgress by - animateLottieCompositionAsState( - composition = moneyLottieComp, - iterations = 1, - isPlaying = currentPage == 2, - speed = 1.5f, - restartOnPlay = true - ) - val lockProgress by - animateLottieCompositionAsState( - composition = lockLottieComp, - iterations = 1, - isPlaying = currentPage == 3, - speed = 1.5f, - restartOnPlay = true - ) - val bookProgress by - animateLottieCompositionAsState( - composition = bookLottieComp, - iterations = 1, - isPlaying = currentPage == 4, - speed = 1.5f, - restartOnPlay = true - ) + // All animations are always "playing" but only visible one shows + val ideaProgress by + animateLottieCompositionAsState( + composition = ideaLottieComp, + iterations = 1, + isPlaying = currentPage == 1, + speed = 1.5f, + restartOnPlay = true + ) + val moneyProgress by + animateLottieCompositionAsState( + composition = moneyLottieComp, + iterations = 1, + isPlaying = currentPage == 2, + speed = 1.5f, + restartOnPlay = true + ) + val lockProgress by + animateLottieCompositionAsState( + composition = lockLottieComp, + iterations = 1, + isPlaying = currentPage == 3, + speed = 1.5f, + restartOnPlay = true + ) + val bookProgress by + animateLottieCompositionAsState( + composition = bookLottieComp, + iterations = 1, + isPlaying = currentPage == 4, + speed = 1.5f, + restartOnPlay = true + ) - // Pulse animation for logo (always running, cheap) - val pulseScale by - rememberInfiniteTransition(label = "pulse") - .animateFloat( - initialValue = 1f, - targetValue = 1.1f, - animationSpec = - infiniteRepeatable( - animation = tween(800, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "pulseScale" - ) + // Pulse animation for logo (always running, cheap) + val pulseScale by + rememberInfiniteTransition(label = "pulse") + .animateFloat( + initialValue = 1f, + targetValue = 1.1f, + animationSpec = + infiniteRepeatable( + animation = + tween(800, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "pulseScale" + ) - Box(modifier = modifier, contentAlignment = Alignment.Center) { - // === PRE-RENDERED LAYERS - All always exist, just alpha changes === + Box(modifier = modifier, contentAlignment = Alignment.Center) { + // === PRE-RENDERED LAYERS - All always exist, just alpha changes === - // Page 0: Rosetta Logo - Box( - modifier = - Modifier.fillMaxSize().graphicsLayer { - alpha = if (currentPage == 0) 1f else 0f - }, - contentAlignment = Alignment.Center - ) { - // Glow effect - Box( - modifier = - Modifier.size(180.dp) - .scale(if (currentPage == 0) pulseScale else 1f) - .background( - color = Color(0xFF54A9EB).copy(alpha = 0.2f), - shape = CircleShape - ) - ) - // Main logo - Image( - painter = painterResource(id = R.drawable.rosetta_icon), - contentDescription = "Rosetta Logo", - modifier = Modifier.size(150.dp).clip(CircleShape) - ) + // Page 0: Rosetta Logo + Box( + modifier = + Modifier.fillMaxSize().graphicsLayer { + alpha = if (currentPage == 0) 1f else 0f + }, + contentAlignment = Alignment.Center + ) { + // Glow effect + Box( + modifier = + Modifier.size(180.dp) + .scale(if (currentPage == 0) pulseScale else 1f) + .background( + color = + Color(0xFF54A9EB) + .copy(alpha = 0.2f), + shape = CircleShape + ) + ) + // Main logo + Image( + painter = painterResource(id = R.drawable.rosetta_icon), + contentDescription = "Rosetta Logo", + modifier = Modifier.size(150.dp).clip(CircleShape) + ) + } + + // Page 1: Idea animation (always in memory!) + if (ideaLottieComp != null) { + LottieAnimation( + composition = ideaLottieComp, + progress = { ideaProgress }, + modifier = + Modifier.fillMaxSize().graphicsLayer { + alpha = if (currentPage == 1) 1f else 0f + // Hardware layer optimization + compositingStrategy = + androidx.compose.ui.graphics + .CompositingStrategy.Offscreen + // Disable clipping for performance + clip = false + }, + maintainOriginalImageBounds = true, + // Disable dynamic properties for max performance + enableMergePaths = false + ) + } + + // Page 2: Money animation + if (moneyLottieComp != null) { + LottieAnimation( + composition = moneyLottieComp, + progress = { moneyProgress }, + modifier = + Modifier.fillMaxSize().graphicsLayer { + alpha = if (currentPage == 2) 1f else 0f + compositingStrategy = + androidx.compose.ui.graphics + .CompositingStrategy.Offscreen + clip = false + }, + maintainOriginalImageBounds = true, + enableMergePaths = false + ) + } + + // Page 3: Lock animation + if (lockLottieComp != null) { + LottieAnimation( + composition = lockLottieComp, + progress = { lockProgress }, + modifier = + Modifier.fillMaxSize().graphicsLayer { + alpha = if (currentPage == 3) 1f else 0f + compositingStrategy = + androidx.compose.ui.graphics + .CompositingStrategy.Offscreen + clip = false + }, + maintainOriginalImageBounds = true, + enableMergePaths = false + ) + } + + // Page 4: Book animation + if (bookLottieComp != null) { + LottieAnimation( + composition = bookLottieComp, + progress = { bookProgress }, + modifier = + Modifier.fillMaxSize().graphicsLayer { + alpha = if (currentPage == 4) 1f else 0f + compositingStrategy = + androidx.compose.ui.graphics + .CompositingStrategy.Offscreen + clip = false + }, + maintainOriginalImageBounds = true, + enableMergePaths = false + ) + } } - - // Page 1: Idea animation (always in memory!) - if (ideaLottieComp != null) { - LottieAnimation( - composition = ideaLottieComp, - progress = { ideaProgress }, - modifier = - Modifier.fillMaxSize().graphicsLayer { - alpha = if (currentPage == 1) 1f else 0f - // Hardware layer optimization - compositingStrategy = - androidx.compose.ui.graphics.CompositingStrategy.Offscreen - // Disable clipping for performance - clip = false - }, - maintainOriginalImageBounds = true, - // Disable dynamic properties for max performance - enableMergePaths = false - ) - } - - // Page 2: Money animation - if (moneyLottieComp != null) { - LottieAnimation( - composition = moneyLottieComp, - progress = { moneyProgress }, - modifier = - Modifier.fillMaxSize().graphicsLayer { - alpha = if (currentPage == 2) 1f else 0f - compositingStrategy = - androidx.compose.ui.graphics.CompositingStrategy.Offscreen - clip = false - }, - maintainOriginalImageBounds = true, - enableMergePaths = false - ) - } - - // Page 3: Lock animation - if (lockLottieComp != null) { - LottieAnimation( - composition = lockLottieComp, - progress = { lockProgress }, - modifier = - Modifier.fillMaxSize().graphicsLayer { - alpha = if (currentPage == 3) 1f else 0f - compositingStrategy = - androidx.compose.ui.graphics.CompositingStrategy.Offscreen - clip = false - }, - maintainOriginalImageBounds = true, - enableMergePaths = false - ) - } - - // Page 4: Book animation - if (bookLottieComp != null) { - LottieAnimation( - composition = bookLottieComp, - progress = { bookProgress }, - modifier = - Modifier.fillMaxSize().graphicsLayer { - alpha = if (currentPage == 4) 1f else 0f - compositingStrategy = - androidx.compose.ui.graphics.CompositingStrategy.Offscreen - clip = false - }, - maintainOriginalImageBounds = true, - enableMergePaths = false - ) - } - } } @Composable @@ -548,77 +575,93 @@ fun OnboardingPageContent( highlightColor: Color, pageOffset: Float ) { - val alpha by - animateFloatAsState( - targetValue = 1f - (pageOffset * 0.7f).coerceIn(0f, 1f), - animationSpec = tween(400, easing = FastOutSlowInEasing), - label = "alpha" - ) - val scale by - animateFloatAsState( - targetValue = 1f - (pageOffset * 0.1f).coerceIn(0f, 1f), - animationSpec = tween(400, easing = FastOutSlowInEasing), - label = "scale" - ) + val alpha by + animateFloatAsState( + targetValue = 1f - (pageOffset * 0.7f).coerceIn(0f, 1f), + animationSpec = tween(400, easing = FastOutSlowInEasing), + label = "alpha" + ) + val scale by + animateFloatAsState( + targetValue = 1f - (pageOffset * 0.1f).coerceIn(0f, 1f), + animationSpec = tween(400, easing = FastOutSlowInEasing), + label = "scale" + ) - Column( - modifier = - Modifier.fillMaxWidth().graphicsLayer { - this.alpha = alpha - scaleX = scale - scaleY = scale - }, - horizontalAlignment = Alignment.CenterHorizontally - ) { - // Title - Text( - text = page.title, - fontSize = 32.sp, - fontWeight = FontWeight.Bold, - color = textColor, - textAlign = TextAlign.Center - ) + Column( + modifier = + Modifier.fillMaxWidth().graphicsLayer { + this.alpha = alpha + scaleX = scale + scaleY = scale + }, + horizontalAlignment = Alignment.CenterHorizontally + ) { + // Title + Text( + text = page.title, + fontSize = 32.sp, + fontWeight = FontWeight.Bold, + color = textColor, + textAlign = TextAlign.Center + ) - Spacer(modifier = Modifier.height(12.dp)) + Spacer(modifier = Modifier.height(12.dp)) - // Description with highlighted words - val annotatedDescription = buildAnnotatedString { - var currentIndex = 0 - val description = page.description + // Description with highlighted words + val annotatedDescription = buildAnnotatedString { + var currentIndex = 0 + val description = page.description - // Find and highlight words - page.highlightWords.forEach { word -> - val startIndex = description.indexOf(word, currentIndex, ignoreCase = true) - if (startIndex >= 0) { - // Add text before the word - if (startIndex > currentIndex) { - withStyle(SpanStyle(color = secondaryTextColor)) { - append(description.substring(currentIndex, startIndex)) + // Find and highlight words + page.highlightWords.forEach { word -> + val startIndex = + description.indexOf(word, currentIndex, ignoreCase = true) + if (startIndex >= 0) { + // Add text before the word + if (startIndex > currentIndex) { + withStyle(SpanStyle(color = secondaryTextColor)) { + append( + description.substring( + currentIndex, + startIndex + ) + ) + } + } + // Add highlighted word + withStyle( + SpanStyle( + color = highlightColor, + fontWeight = FontWeight.SemiBold + ) + ) { + append( + description.substring( + startIndex, + startIndex + word.length + ) + ) + } + currentIndex = startIndex + word.length + } } - } - // Add highlighted word - withStyle(SpanStyle(color = highlightColor, fontWeight = FontWeight.SemiBold)) { - append(description.substring(startIndex, startIndex + word.length)) - } - currentIndex = startIndex + word.length - } - } - // Add remaining text - if (currentIndex < description.length) { - withStyle(SpanStyle(color = secondaryTextColor)) { - append(description.substring(currentIndex)) + // Add remaining text + if (currentIndex < description.length) { + withStyle(SpanStyle(color = secondaryTextColor)) { + append(description.substring(currentIndex)) + } + } } - } + + Text( + text = annotatedDescription, + fontSize = 17.sp, + textAlign = TextAlign.Center, + lineHeight = 24.sp + ) } - - Text( - text = annotatedDescription, - fontSize = 17.sp, - textAlign = TextAlign.Center, - lineHeight = 24.sp - ) - } } @Composable @@ -629,86 +672,96 @@ fun PagerIndicator( unselectedColor: Color, modifier: Modifier = Modifier ) { - Row( - modifier = modifier, - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - repeat(pageCount) { index -> - val isSelected = index == currentPage - val width by - animateDpAsState( - targetValue = if (isSelected) 20.dp else 8.dp, - animationSpec = spring(dampingRatio = 0.8f), - label = "indicatorWidth" - ) + Row( + modifier = modifier, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(pageCount) { index -> + val isSelected = index == currentPage + val width by + animateDpAsState( + targetValue = if (isSelected) 20.dp else 8.dp, + animationSpec = spring(dampingRatio = 0.8f), + label = "indicatorWidth" + ) - Box( - modifier = - Modifier.height(8.dp) - .width(width) - .clip(CircleShape) - .background(if (isSelected) selectedColor else unselectedColor) - ) + Box( + modifier = + Modifier.height(8.dp) + .width(width) + .clip(CircleShape) + .background( + if (isSelected) selectedColor + else unselectedColor + ) + ) + } } - } } @Composable fun StartMessagingButton(onClick: () -> Unit, modifier: Modifier = Modifier) { - // Shining effect animation - val infiniteTransition = rememberInfiniteTransition(label = "shine") - val shimmerTranslate by - infiniteTransition.animateFloat( - initialValue = -0.5f, - targetValue = 1.5f, - animationSpec = - infiniteRepeatable( - animation = tween(2000, easing = LinearEasing), - repeatMode = RepeatMode.Restart - ), - label = "shimmerTranslate" - ) + // Shining effect animation + val infiniteTransition = rememberInfiniteTransition(label = "shine") + val shimmerTranslate by + infiniteTransition.animateFloat( + initialValue = -0.5f, + targetValue = 1.5f, + animationSpec = + infiniteRepeatable( + animation = tween(2000, easing = LinearEasing), + repeatMode = RepeatMode.Restart + ), + label = "shimmerTranslate" + ) - Surface( - onClick = onClick, - modifier = modifier.fillMaxWidth().height(54.dp), - shape = RoundedCornerShape(12.dp), - color = PrimaryBlue - ) { - Box( - modifier = - Modifier.fillMaxSize().drawWithContent { - drawContent() - // Draw shimmer on top - val shimmerWidth = size.width * 0.6f - val shimmerStart = shimmerTranslate * size.width - drawRect( - brush = - Brush.linearGradient( - colors = - listOf( - Color.White.copy(alpha = 0f), - Color.White.copy(alpha = 0.3f), - Color.White.copy(alpha = 0f) - ), - start = Offset(shimmerStart, 0f), - end = - Offset( - shimmerStart + shimmerWidth, - size.height - ) - ) - ) - }, - contentAlignment = Alignment.Center + Surface( + onClick = onClick, + modifier = modifier.fillMaxWidth().height(54.dp), + shape = RoundedCornerShape(12.dp), + color = PrimaryBlue ) { - Text( - text = "Start Messaging", - fontSize = 17.sp, - fontWeight = FontWeight.SemiBold, - color = Color.White - ) + Box( + modifier = + Modifier.fillMaxSize().drawWithContent { + drawContent() + // Draw shimmer on top + val shimmerWidth = size.width * 0.6f + val shimmerStart = shimmerTranslate * size.width + drawRect( + brush = + Brush.linearGradient( + colors = + listOf( + Color.White.copy( + alpha = 0f + ), + Color.White.copy( + alpha = 0.3f + ), + Color.White.copy( + alpha = 0f + ) + ), + start = Offset(shimmerStart, 0f), + end = + Offset( + shimmerStart + + shimmerWidth, + size.height + ) + ) + ) + }, + contentAlignment = Alignment.Center + ) { + Text( + text = "Start Messaging", + fontSize = 17.sp, + fontWeight = FontWeight.SemiBold, + color = Color.White + ) + } } - } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/SettingsScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..e2f4ceb --- /dev/null +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/SettingsScreen.kt @@ -0,0 +1,505 @@ +package com.rosetta.messenger.ui.settings + +import androidx.compose.animation.* +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.outlined.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.rosetta.messenger.ui.onboarding.PrimaryBlue + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SettingsScreen( + isDarkTheme: Boolean, + accountName: String, + accountPhone: String, + accountPublicKey: String, + onBack: () -> Unit, + onToggleTheme: () -> Unit, + onProfileClick: () -> Unit = {}, + onPrivacySecurityClick: () -> Unit = {}, + onNotificationsClick: () -> Unit = {}, + onDataStorageClick: () -> Unit = {}, + onChatSettingsClick: () -> Unit = {}, + onLanguageClick: () -> Unit = {}, + onHelpClick: () -> Unit = {}, + onAboutClick: () -> Unit = {} +) { + // Цвета в зависимости от темы + val backgroundColor = if (isDarkTheme) Color(0xFF0F0F0F) else Color.White + val surfaceColor = if (isDarkTheme) Color(0xFF1A1A1A) else Color(0xFFF5F5F5) + val textColor = if (isDarkTheme) Color.White else Color(0xFF1A1A1A) + val secondaryTextColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666) + val dividerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8) + val iconTintColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666) + + Column(modifier = Modifier.fillMaxSize().background(backgroundColor)) { + // ═════════════════════════════════════════════════════════════ + // 🎨 TOP BAR + // ═════════════════════════════════════════════════════════════ + TopAppBar( + title = { + Text( + text = "Settings", + color = textColor, + fontSize = 20.sp, + fontWeight = FontWeight.SemiBold + ) + }, + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + imageVector = Icons.Filled.ArrowBack, + contentDescription = "Back", + tint = textColor + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors(containerColor = backgroundColor) + ) + + // ═════════════════════════════════════════════════════════════ + // 📱 CONTENT + // ═════════════════════════════════════════════════════════════ + Column(modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState())) { + // ═════════════════════════════════════════════════════════════ + // 👤 PROFILE SECTION + // ═════════════════════════════════════════════════════════════ + Surface( + modifier = + Modifier.fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp) + .clip(RoundedCornerShape(16.dp)) + .clickable(onClick = onProfileClick), + color = surfaceColor + ) { + Row( + modifier = Modifier.fillMaxWidth().padding(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Avatar + Box( + modifier = + Modifier.size(64.dp).clip(CircleShape).background(PrimaryBlue), + contentAlignment = Alignment.Center + ) { + Text( + text = getInitials(accountName), + fontSize = 24.sp, + fontWeight = FontWeight.Bold, + color = Color.White + ) + } + + Spacer(modifier = Modifier.width(16.dp)) + + // Name and Phone + Column(modifier = Modifier.weight(1f)) { + Text( + text = accountName, + fontSize = 18.sp, + fontWeight = FontWeight.SemiBold, + color = textColor + ) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = accountPhone, fontSize = 14.sp, color = secondaryTextColor) + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = accountPublicKey.take(12) + "...", + fontSize = 12.sp, + color = secondaryTextColor.copy(alpha = 0.7f) + ) + } + + // Arrow + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = iconTintColor + ) + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + // ═════════════════════════════════════════════════════════════ + // 🎨 APPEARANCE SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "Appearance", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsToggleItem( + icon = + if (isDarkTheme) Icons.Outlined.DarkMode + else Icons.Outlined.LightMode, + title = "Dark Mode", + subtitle = if (isDarkTheme) "Enabled" else "Disabled", + isChecked = isDarkTheme, + onToggle = onToggleTheme, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ═════════════════════════════════════════════════════════════ + // 🔐 PRIVACY & SECURITY SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "Privacy & Security", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsNavigationItem( + icon = Icons.Outlined.Lock, + title = "Privacy Settings", + subtitle = "Control who can see your info", + onClick = onPrivacySecurityClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.Security, + title = "Security", + subtitle = "Passcode, 2FA, sessions", + onClick = onPrivacySecurityClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.Block, + title = "Blocked Users", + subtitle = "Manage blocked contacts", + onClick = onPrivacySecurityClick, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ═════════════════════════════════════════════════════════════ + // 🔔 NOTIFICATIONS SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "Notifications & Sounds", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsNavigationItem( + icon = Icons.Outlined.Notifications, + title = "Notifications", + subtitle = "Messages, groups, channels", + onClick = onNotificationsClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.VolumeUp, + title = "Sounds", + subtitle = "Notification sounds", + onClick = onNotificationsClick, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ═════════════════════════════════════════════════════════════ + // 💾 DATA & STORAGE SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "Data & Storage", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsNavigationItem( + icon = Icons.Outlined.Storage, + title = "Storage Usage", + subtitle = "Clear cache, manage data", + onClick = onDataStorageClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.DataUsage, + title = "Network Usage", + subtitle = "Mobile and Wi-Fi", + onClick = onDataStorageClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.DownloadForOffline, + title = "Auto-Download", + subtitle = "Photos, videos, files", + onClick = onDataStorageClick, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ═════════════════════════════════════════════════════════════ + // 💬 CHAT SETTINGS SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "Chat Settings", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsNavigationItem( + icon = Icons.Outlined.ChatBubbleOutline, + title = "Chat Background", + subtitle = "Set wallpaper for chats", + onClick = onChatSettingsClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.TextFields, + title = "Message Text Size", + subtitle = "Adjust text size in chats", + onClick = onChatSettingsClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.EmojiEmotions, + title = "Stickers & Emojis", + subtitle = "Manage sticker packs", + onClick = onChatSettingsClick, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ═════════════════════════════════════════════════════════════ + // 🌐 GENERAL SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "General", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsNavigationItem( + icon = Icons.Outlined.Language, + title = "Language", + subtitle = "English", + onClick = onLanguageClick, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(24.dp)) + + // ═════════════════════════════════════════════════════════════ + // ℹ️ SUPPORT SECTION + // ═════════════════════════════════════════════════════════════ + SettingsSectionTitle(title = "Support", isDarkTheme = isDarkTheme) + + Surface( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + color = surfaceColor, + shape = RoundedCornerShape(16.dp) + ) { + Column { + SettingsNavigationItem( + icon = Icons.Outlined.HelpOutline, + title = "Help Center", + subtitle = "FAQ and support", + onClick = onHelpClick, + isDarkTheme = isDarkTheme, + showDivider = true + ) + SettingsNavigationItem( + icon = Icons.Outlined.Info, + title = "About", + subtitle = "Version 1.0.0", + onClick = onAboutClick, + isDarkTheme = isDarkTheme + ) + } + } + + Spacer(modifier = Modifier.height(32.dp)) + } + } +} + +// ═════════════════════════════════════════════════════════════ +// 📦 HELPER COMPONENTS +// ═════════════════════════════════════════════════════════════ + +@Composable +private fun SettingsSectionTitle(title: String, isDarkTheme: Boolean) { + val textColor = if (isDarkTheme) Color(0xFF666666) else Color(0xFF999999) + + Text( + text = title.uppercase(), + fontSize = 13.sp, + fontWeight = FontWeight.SemiBold, + color = textColor, + modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp) + ) +} + +@Composable +private fun SettingsNavigationItem( + icon: ImageVector, + title: String, + subtitle: String, + onClick: () -> Unit, + isDarkTheme: Boolean, + showDivider: Boolean = false +) { + val textColor = if (isDarkTheme) Color.White else Color(0xFF1A1A1A) + val secondaryTextColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666) + val iconTintColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666) + val dividerColor = if (isDarkTheme) Color(0xFF2A2A2A) else Color(0xFFE8E8E8) + + Column { + Row( + modifier = + Modifier.fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon + Icon( + imageVector = icon, + contentDescription = null, + tint = iconTintColor, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + // Title and Subtitle + Column(modifier = Modifier.weight(1f)) { + Text(text = title, fontSize = 16.sp, color = textColor) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor) + } + + // Arrow + Icon( + imageVector = Icons.Default.ChevronRight, + contentDescription = null, + tint = iconTintColor.copy(alpha = 0.5f), + modifier = Modifier.size(20.dp) + ) + } + + if (showDivider) { + Divider( + color = dividerColor, + thickness = 0.5.dp, + modifier = Modifier.padding(start = 56.dp) + ) + } + } +} + +@Composable +private fun SettingsToggleItem( + icon: ImageVector, + title: String, + subtitle: String, + isChecked: Boolean, + onToggle: () -> Unit, + isDarkTheme: Boolean +) { + val textColor = if (isDarkTheme) Color.White else Color(0xFF1A1A1A) + val secondaryTextColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666) + val iconTintColor = if (isDarkTheme) Color(0xFF999999) else Color(0xFF666666) + + Row( + modifier = + Modifier.fillMaxWidth() + .clickable(onClick = onToggle) + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + // Icon + Icon( + imageVector = icon, + contentDescription = null, + tint = iconTintColor, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(16.dp)) + + // Title and Subtitle + Column(modifier = Modifier.weight(1f)) { + Text(text = title, fontSize = 16.sp, color = textColor) + Spacer(modifier = Modifier.height(2.dp)) + Text(text = subtitle, fontSize = 13.sp, color = secondaryTextColor) + } + + // Toggle Switch + Switch( + checked = isChecked, + onCheckedChange = { onToggle() }, + colors = + SwitchDefaults.colors( + checkedThumbColor = Color.White, + checkedTrackColor = PrimaryBlue, + uncheckedThumbColor = Color.White, + uncheckedTrackColor = + if (isDarkTheme) Color(0xFF3A3A3A) else Color(0xFFCCCCCC) + ) + ) + } +} + +private fun getInitials(name: String): String { + if (name.isBlank()) return "?" + val parts = name.trim().split(" ").filter { it.isNotEmpty() } + return when { + parts.isEmpty() -> "?" + parts.size == 1 -> parts[0].take(2).uppercase() + else -> (parts[0].first().toString() + parts[1].first().toString()).uppercase() + } +}