Initial commit: rosetta-android-prime

This commit is contained in:
k1ngsterr1
2026-01-08 19:06:37 +05:00
commit 42ddfe5b18
54 changed files with 68604 additions and 0 deletions

View File

@@ -0,0 +1,36 @@
package com.rosetta.messenger.ui.onboarding
data class OnboardingPage(
val title: String,
val description: String,
val highlightWords: List<String> = emptyList()
)
val onboardingPages = listOf(
OnboardingPage(
title = "Rosetta",
description = "A local-based messaging app.\nYour data stays on your device.",
highlightWords = listOf("local-based", "your device")
),
OnboardingPage(
title = "Fast",
description = "Rosetta delivers messages faster\nthan any other application.",
highlightWords = listOf("faster")
),
OnboardingPage(
title = "Free",
description = "Rosetta is free forever. No ads.\nNo subscription fees. Ever.",
highlightWords = listOf("free forever", "No ads", "Ever")
),
OnboardingPage(
title = "Secure",
description = "Rosetta keeps your messages safe\nwith local storage and encryption.",
highlightWords = listOf("safe", "local storage", "encryption")
),
OnboardingPage(
title = "Private",
description = "No servers. No tracking.\nEverything stays on your device.",
highlightWords = listOf("No servers", "No tracking", "your device")
)
)

View File

@@ -0,0 +1,634 @@
package com.rosetta.messenger.ui.onboarding
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.*
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.LightMode
import androidx.compose.material.icons.filled.DarkMode
import androidx.compose.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.draw.drawBehind
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.layout.boundsInWindow
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
import com.rosetta.messenger.ui.theme.*
import kotlinx.coroutines.delay
import kotlin.math.absoluteValue
import kotlin.math.hypot
import com.airbnb.lottie.compose.*
import androidx.compose.ui.res.painterResource
import com.rosetta.messenger.R
import androidx.compose.foundation.Image
// App colors (matching React Native)
val PrimaryBlue = Color(0xFF248AE6) // primary light theme
val PrimaryBlueDark = Color(0xFF1A72C2) // primary dark theme
val LightBlue = Color(0xFF74C0FC) // lightBlue
val OnboardingBackground = Color(0xFF1E1E1E) // dark background
val OnboardingBackgroundLight = Color(0xFFFFFFFF)
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun OnboardingScreen(
isDarkTheme: Boolean,
onThemeToggle: () -> Unit,
onStartMessaging: () -> Unit
) {
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"))
// 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(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)
// Update status bar when wave reaches top (around 15% progress)
if (transitionProgress >= 0.15f && !shouldUpdateStatusBar) {
shouldUpdateStatusBar = true
}
delay(16) // ~60fps
}
isTransitioning = false
transitionProgress = 0f
shouldUpdateStatusBar = false
previousTheme = targetTheme
}
}
// Update status bar and navigation bar icons when wave reaches the top
val view = LocalView.current
LaunchedEffect(shouldUpdateStatusBar, isDarkTheme) {
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
}
}
// Animate navigation bar color with theme transition
LaunchedEffect(isTransitioning, transitionProgress, isDarkTheme) {
if (!view.isInEditMode) {
val window = (view.context as android.app.Activity).window
if (isTransitioning) {
// Interpolate color during transition
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 r = (r1 + (r2 - r1) * transitionProgress).toInt()
val g = (g1 + (g2 - g1) * transitionProgress).toInt()
val b = (b1 + (b2 - b1) * transitionProgress).toInt()
window.navigationBarColor = (0xFF000000 or (r.toLong() shl 16) or (g.toLong() shl 8) or b.toLong()).toInt()
} else {
// Set final color when not transitioning
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()
) {
// 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 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()
)
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
HorizontalPager(
state = pagerState,
modifier = Modifier
.fillMaxWidth()
.height(150.dp)
) { 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
fun ThemeToggleButton(
isDarkTheme: Boolean,
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 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"
)
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)
)
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun AnimatedRosettaLogo(
pagerState: PagerState,
ideaComposition: Any?,
moneyComposition: Any?,
lockComposition: Any?,
bookComposition: Any?,
modifier: Modifier = Modifier
) {
val currentPage = pagerState.currentPage
val pageOffset = pagerState.currentPageOffsetFraction
// Animate scale and alpha based on swipe
val scale by animateFloatAsState(
targetValue = 1f - (pageOffset.absoluteValue * 0.08f),
animationSpec = tween(400, easing = FastOutSlowInEasing),
label = "scale"
)
val alpha by animateFloatAsState(
targetValue = 1f - (pageOffset.absoluteValue * 0.3f),
animationSpec = tween(400, easing = FastOutSlowInEasing),
label = "alpha"
)
Box(
modifier = modifier
.scale(scale)
.graphicsLayer { this.alpha = alpha },
contentAlignment = Alignment.Center
) {
// Pre-render all animations to avoid lag
Box(modifier = Modifier.fillMaxSize()) {
// Rosetta icon (page 0)
if (currentPage == 0) {
Image(
painter = painterResource(id = R.drawable.rosetta_icon),
contentDescription = "Rosetta Logo",
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
)
}
// Fast page - idea animation (page 1)
ideaComposition?.let { comp ->
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
val progress by animateLottieCompositionAsState(
composition = lottieComp,
iterations = 1,
isPlaying = currentPage == 1
)
if (currentPage == 1) {
LottieAnimation(
composition = lottieComp,
progress = { progress },
modifier = Modifier.fillMaxSize()
)
}
}
// Free page - money animation (page 2)
moneyComposition?.let { comp ->
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
val progress by animateLottieCompositionAsState(
composition = lottieComp,
iterations = 1,
isPlaying = currentPage == 2
)
if (currentPage == 2) {
LottieAnimation(
composition = lottieComp,
progress = { progress },
modifier = Modifier.fillMaxSize()
)
}
}
// Secure page - lock animation (page 3)
lockComposition?.let { comp ->
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
val progress by animateLottieCompositionAsState(
composition = lottieComp,
iterations = 1,
isPlaying = currentPage == 3
)
if (currentPage == 3) {
LottieAnimation(
composition = lottieComp,
progress = { progress },
modifier = Modifier.fillMaxSize()
)
}
}
// Private page - book animation (page 4)
bookComposition?.let { comp ->
val lottieComp = comp as? com.airbnb.lottie.LottieComposition
val progress by animateLottieCompositionAsState(
composition = lottieComp,
iterations = 1,
isPlaying = currentPage == 4
)
if (currentPage == 4) {
LottieAnimation(
composition = lottieComp,
progress = { progress },
modifier = Modifier.fillMaxSize()
)
}
}
}
}
}
@Composable
fun OnboardingPageContent(
page: OnboardingPage,
textColor: Color,
secondaryTextColor: Color,
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"
)
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))
// 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))
}
}
// 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))
}
}
}
Text(
text = annotatedDescription,
fontSize = 17.sp,
textAlign = TextAlign.Center,
lineHeight = 24.sp
)
}
}
@Composable
fun PagerIndicator(
pageCount: Int,
currentPage: Int,
selectedColor: Color,
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"
)
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"
)
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
) {
Text(
text = "Start Messaging",
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
}
}
}