From ae78e4a16287379632ed5d0317c39e86b39af65b Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Wed, 8 Apr 2026 20:06:56 +0500 Subject: [PATCH] =?UTF-8?q?QR=20=D1=8D=D0=BA=D1=80=D0=B0=D0=BD:=20=D0=BE?= =?UTF-8?q?=D0=B1=D0=BE=D0=B8=20=D0=B2=D0=BC=D0=B5=D1=81=D1=82=D0=BE=20?= =?UTF-8?q?=D0=B3=D1=80=D0=B0=D0=B4=D0=B8=D0=B5=D0=BD=D1=82=D0=BE=D0=B2,?= =?UTF-8?q?=20circular=20reveal=20=D0=B0=D0=BD=D0=B8=D0=BC=D0=B0=D1=86?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=20=D1=81=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=B5=20=D0=BE=D0=B1=D0=BE=D0=B5=D0=B2=20=D0=B8=20=D1=82=D0=B5?= =?UTF-8?q?=D0=BC=D1=8B,=20=D0=BA=D0=BD=D0=BE=D0=BF=D0=BA=D0=B0=20sun/moon?= =?UTF-8?q?=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D0=B5?= =?UTF-8?q?=D1=82=20=D1=82=D0=B5=D0=BC=D1=83=20=D0=BF=D1=80=D0=B8=D0=BB?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F,=20cooldown=20600ms.=20?= =?UTF-8?q?=D0=A3=D0=B1=D1=80=D0=B0=D0=BD=D0=B0=20=D0=BA=D0=BD=D0=BE=D0=BF?= =?UTF-8?q?=D0=BA=D0=B0=20Start=20New=20Call=20=D1=81=20=D1=8D=D0=BA=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=B0=20=D0=B7=D0=B2=D0=BE=D0=BD=D0=BA=D0=BE=D0=B2?= =?UTF-8?q?.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/rosetta/messenger/MainActivity.kt | 3 +- .../ui/chats/calls/CallsHistoryScreen.kt | 5 - .../rosetta/messenger/ui/qr/MyQrCodeScreen.kt | 343 +++++++++++++----- 3 files changed, 255 insertions(+), 96 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/MainActivity.kt b/app/src/main/java/com/rosetta/messenger/MainActivity.kt index a92c340..0fcc30e 100644 --- a/app/src/main/java/com/rosetta/messenger/MainActivity.kt +++ b/app/src/main/java/com/rosetta/messenger/MainActivity.kt @@ -1863,7 +1863,8 @@ fun MainScreen( onScanQr = { navStack = navStack.filterNot { it is Screen.MyQr } pushScreen(Screen.QrScanner) - } + }, + onToggleTheme = onToggleTheme ) } diff --git a/app/src/main/java/com/rosetta/messenger/ui/chats/calls/CallsHistoryScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/chats/calls/CallsHistoryScreen.kt index ec77c66..35a2578 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/chats/calls/CallsHistoryScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/chats/calls/CallsHistoryScreen.kt @@ -109,11 +109,6 @@ fun CallsHistoryScreen( contentPadding = PaddingValues(bottom = 16.dp) ) { item(key = "start_new_call") { - StartNewCallRow( - isDarkTheme = isDarkTheme, - onClick = onStartNewCall - ) - Divider(color = dividerColor, thickness = 0.5.dp) Text( text = "You can add up to 200 participants to a call.", color = secondaryTextColor, diff --git a/app/src/main/java/com/rosetta/messenger/ui/qr/MyQrCodeScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/qr/MyQrCodeScreen.kt index fd9c9dd..122809c 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/qr/MyQrCodeScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/qr/MyQrCodeScreen.kt @@ -1,6 +1,9 @@ package com.rosetta.messenger.ui.qr import android.content.Intent +import androidx.compose.animation.core.Animatable +import androidx.compose.animation.core.CubicBezierEasing +import androidx.compose.animation.core.tween import androidx.compose.foundation.* import androidx.compose.foundation.layout.* import androidx.compose.foundation.shape.CircleShape @@ -10,55 +13,69 @@ 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.Brush +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.CompositingStrategy +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.drawscope.clipPath +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.core.view.drawToBitmap +import com.rosetta.messenger.R import com.rosetta.messenger.repository.AvatarRepository import com.rosetta.messenger.ui.components.AvatarImage +import com.rosetta.messenger.ui.settings.ThemeWallpapers import com.rosetta.messenger.utils.QrCodeGenerator import compose.icons.TablerIcons import compose.icons.tablericons.* +import androidx.compose.ui.layout.onSizeChanged +import kotlinx.coroutines.launch +import kotlin.math.hypot -// Theme presets — gradient background + QR foreground color +// QR theme = wallpaper + QR color private data class QrTheme( - val gradientColors: List, + val wallpaperId: String, val qrColor: Int, - val name: String + val tintColor: Color, + val isDark: Boolean ) private val qrThemes = listOf( - QrTheme( - listOf(Color(0xFF667EEA), Color(0xFF764BA2)), - 0xFF4A3F9F.toInt(), - "Purple" - ), - QrTheme( - listOf(Color(0xFF43E97B), Color(0xFF38F9D7)), - 0xFF2D8F5E.toInt(), - "Green" - ), - QrTheme( - listOf(Color(0xFFF78CA0), Color(0xFFF9748F), Color(0xFFFD868C)), - 0xFFBF3A5A.toInt(), - "Pink" - ), - QrTheme( - listOf(Color(0xFF4FACFE), Color(0xFF00F2FE)), - 0xFF2171B5.toInt(), - "Blue" - ), - QrTheme( - listOf(Color(0xFFFFA751), Color(0xFFFFE259)), - 0xFFC67B1C.toInt(), - "Orange" - ), + QrTheme("dark_01", 0xFF5AA5FF.toInt(), Color(0xFF5AA5FF), true), + QrTheme("dark_02", 0xFF9575CD.toInt(), Color(0xFF9575CD), true), + QrTheme("dark_03", 0xFF4DB6AC.toInt(), Color(0xFF4DB6AC), true), + QrTheme("light_01", 0xFF228BE6.toInt(), Color(0xFF228BE6), false), + QrTheme("light_02", 0xFF43A047.toInt(), Color(0xFF43A047), false), + QrTheme("light_03", 0xFFE64980.toInt(), Color(0xFFE64980), false), ) +private fun maxRevealRadius(center: Offset, bounds: IntSize): Float { + if (bounds.width <= 0 || bounds.height <= 0) return 0f + val w = bounds.width.toFloat() + val h = bounds.height.toFloat() + return maxOf( + hypot(center.x, center.y), + hypot(w - center.x, center.y), + hypot(center.x, h - center.y), + hypot(w - center.x, h - center.y) + ) +} + @Composable fun MyQrCodeScreen( isDarkTheme: Boolean, @@ -67,12 +84,31 @@ fun MyQrCodeScreen( username: String, avatarRepository: AvatarRepository? = null, onBack: () -> Unit, - onScanQr: () -> Unit = {} + onScanQr: () -> Unit = {}, + onToggleTheme: () -> Unit = {} ) { val context = LocalContext.current - var selectedThemeIndex by remember { mutableIntStateOf(0) } + val view = LocalView.current + val scope = rememberCoroutineScope() + + var selectedThemeIndex by remember { mutableIntStateOf(if (isDarkTheme) 0 else 3) } + + // Auto-switch to matching theme group when app theme changes + LaunchedEffect(isDarkTheme) { + val currentTheme = qrThemes.getOrNull(selectedThemeIndex) + if (currentTheme != null && currentTheme.isDark != isDarkTheme) { + // Map to same position in the other group + val posInGroup = if (currentTheme.isDark) selectedThemeIndex else selectedThemeIndex - 3 + selectedThemeIndex = if (isDarkTheme) posInGroup.coerceIn(0, 2) else (posInGroup + 3).coerceIn(3, 5) + } + } + val theme = qrThemes[selectedThemeIndex] + val wallpaperResId = remember(theme.wallpaperId) { + ThemeWallpapers.drawableResOrNull(theme.wallpaperId) + } + val qrBitmap = remember(publicKey, selectedThemeIndex) { QrCodeGenerator.generateQrBitmap( content = QrCodeGenerator.profilePayload(publicKey), @@ -88,27 +124,90 @@ fun MyQrCodeScreen( else QrCodeGenerator.profileShareUrlByKey(publicKey) } + // Circular reveal state + val revealRadius = remember { Animatable(0f) } + var revealActive by remember { mutableStateOf(false) } + var revealSnapshot by remember { mutableStateOf(null) } + var revealCenter by remember { mutableStateOf(Offset.Zero) } + var rootSize by remember { mutableStateOf(IntSize.Zero) } + var lastRevealTime by remember { mutableLongStateOf(0L) } + val revealCooldownMs = 600L + + fun startReveal(newIndex: Int, center: Offset) { + val now = System.currentTimeMillis() + if (revealActive || newIndex == selectedThemeIndex || now - lastRevealTime < revealCooldownMs) return + lastRevealTime = now + if (rootSize.width <= 0 || rootSize.height <= 0) { + selectedThemeIndex = newIndex + return + } + + val snapshot = runCatching { view.drawToBitmap() }.getOrNull() + if (snapshot == null) { + selectedThemeIndex = newIndex + return + } + + val maxR = maxRevealRadius(center, rootSize) + if (maxR <= 0f) { + selectedThemeIndex = newIndex + return + } + + revealActive = true + revealCenter = center + revealSnapshot = snapshot.asImageBitmap() + + scope.launch { + try { + revealRadius.snapTo(0f) + selectedThemeIndex = newIndex + kotlinx.coroutines.delay(16) // wait one frame for recomposition + revealRadius.animateTo( + targetValue = maxR, + animationSpec = tween( + durationMillis = 400, + easing = CubicBezierEasing(0.45f, 0.05f, 0.55f, 0.95f) + ) + ) + } finally { + revealSnapshot = null + revealActive = false + } + } + } + Box( modifier = Modifier .fillMaxSize() - .background(Brush.verticalGradient(theme.gradientColors)) + .onSizeChanged { rootSize = it } ) { + // Wallpaper background + if (wallpaperResId != null) { + Image( + painter = painterResource(id = wallpaperResId), + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.Crop + ) + } else { + Box(modifier = Modifier.fillMaxSize().background(if (theme.isDark) Color(0xFF1A1A1A) else Color(0xFFF0F0F5))) + } + + // Main content Column( modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally ) { Spacer(modifier = Modifier.statusBarsPadding().height(40.dp)) - // QR Card with avatar overlapping + // QR Card with overlapping avatar Box( modifier = Modifier.padding(horizontal = 32.dp), contentAlignment = Alignment.TopCenter ) { - // White card Card( - modifier = Modifier - .fillMaxWidth() - .padding(top = 40.dp), + modifier = Modifier.fillMaxWidth().padding(top = 40.dp), shape = RoundedCornerShape(24.dp), colors = CardDefaults.cardColors(containerColor = Color.White), elevation = CardDefaults.cardElevation(defaultElevation = 8.dp) @@ -119,7 +218,6 @@ fun MyQrCodeScreen( .padding(top = 52.dp, bottom = 24.dp, start = 24.dp, end = 24.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // QR code if (qrBitmap != null) { Image( bitmap = qrBitmap.asImageBitmap(), @@ -130,7 +228,6 @@ fun MyQrCodeScreen( Spacer(modifier = Modifier.height(16.dp)) - // Username Text( text = if (username.isNotBlank()) "@${username.uppercase()}" else displayName.uppercase(), @@ -142,7 +239,7 @@ fun MyQrCodeScreen( } } - // Avatar overlapping the card top + // Avatar overlapping Box( modifier = Modifier .size(80.dp) @@ -163,12 +260,11 @@ fun MyQrCodeScreen( Spacer(modifier = Modifier.weight(1f)) - // Bottom sheet area + // Bottom sheet Surface( modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp), color = if (isDarkTheme) Color(0xFF1C1C1E) else Color.White, - tonalElevation = 0.dp, shadowElevation = 16.dp ) { Column( @@ -178,71 +274,120 @@ fun MyQrCodeScreen( .padding(top = 16.dp, bottom = 16.dp), horizontalAlignment = Alignment.CenterHorizontally ) { - // Handle bar + // Handle Box( modifier = Modifier - .width(36.dp) - .height(4.dp) + .width(36.dp).height(4.dp) .clip(RoundedCornerShape(2.dp)) .background(Color.Gray.copy(alpha = 0.3f)) ) Spacer(modifier = Modifier.height(12.dp)) - // Header: Close + title + // Header: X + "QR Code" + theme toggle Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically ) { IconButton(onClick = onBack) { + Icon(TablerIcons.X, contentDescription = "Close", + tint = if (isDarkTheme) Color.White else Color.Black) + } + Spacer(modifier = Modifier.weight(1f)) + Text("QR Code", fontSize = 17.sp, fontWeight = FontWeight.SemiBold, + color = if (isDarkTheme) Color.White else Color.Black) + Spacer(modifier = Modifier.weight(1f)) + var themeButtonPos by remember { mutableStateOf(Offset.Zero) } + IconButton( + onClick = { + // Snapshot → toggle theme → circular reveal + val now = System.currentTimeMillis() + if (!revealActive && rootSize.width > 0 && now - lastRevealTime >= revealCooldownMs) { + lastRevealTime = now + val snapshot = runCatching { view.drawToBitmap() }.getOrNull() + if (snapshot != null) { + val maxR = maxRevealRadius(themeButtonPos, rootSize) + revealActive = true + revealCenter = themeButtonPos + revealSnapshot = snapshot.asImageBitmap() + // Switch to matching wallpaper in new theme + val posInGroup = if (isDarkTheme) selectedThemeIndex else selectedThemeIndex - 3 + val newIndex = if (isDarkTheme) (posInGroup + 3).coerceIn(3, 5) else posInGroup.coerceIn(0, 2) + selectedThemeIndex = newIndex + onToggleTheme() + scope.launch { + try { + revealRadius.snapTo(0f) + kotlinx.coroutines.delay(16) + revealRadius.animateTo( + targetValue = maxR, + animationSpec = tween(400, easing = CubicBezierEasing(0.45f, 0.05f, 0.55f, 0.95f)) + ) + } finally { + revealSnapshot = null + revealActive = false + } + } + } else { + onToggleTheme() + } + } else { + onToggleTheme() + } + }, + modifier = Modifier.onGloballyPositioned { coords -> + val pos = coords.positionInRoot() + val sz = coords.size + themeButtonPos = Offset(pos.x + sz.width / 2f, pos.y + sz.height / 2f) + } + ) { Icon( - TablerIcons.X, - contentDescription = "Close", + imageVector = if (isDarkTheme) TablerIcons.Sun else TablerIcons.MoonStars, + contentDescription = "Toggle theme", tint = if (isDarkTheme) Color.White else Color.Black ) } - Spacer(modifier = Modifier.weight(1f)) - Text( - "QR Code", - fontSize = 17.sp, - fontWeight = FontWeight.SemiBold, - color = if (isDarkTheme) Color.White else Color.Black - ) - Spacer(modifier = Modifier.weight(1f)) - Spacer(modifier = Modifier.width(48.dp)) } Spacer(modifier = Modifier.height(12.dp)) - // Theme selector + // Wallpaper selector — show current theme's wallpapers + val currentThemes = qrThemes.filter { it.isDark == isDarkTheme } + Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - qrThemes.forEachIndexed { index, t -> + currentThemes.forEach { t -> + val index = qrThemes.indexOf(t) val isSelected = index == selectedThemeIndex + val wpRes = ThemeWallpapers.drawableResOrNull(t.wallpaperId) + var itemPosition by remember { mutableStateOf(Offset.Zero) } + Box( modifier = Modifier - .weight(1f) - .aspectRatio(0.85f) + .weight(1f).aspectRatio(0.85f) .clip(RoundedCornerShape(12.dp)) .border( width = if (isSelected) 2.dp else 0.dp, color = if (isSelected) Color(0xFF3390EC) else Color.Transparent, shape = RoundedCornerShape(12.dp) ) - .background(Brush.verticalGradient(t.gradientColors)) - .clickable { selectedThemeIndex = index }, + .onGloballyPositioned { coords -> + val pos = coords.positionInRoot() + val sz = coords.size + itemPosition = Offset(pos.x + sz.width / 2f, pos.y + sz.height / 2f) + } + .clickable { if (index != selectedThemeIndex) startReveal(index, itemPosition) }, contentAlignment = Alignment.Center ) { - Icon( - TablerIcons.Scan, - contentDescription = t.name, - tint = Color.White.copy(alpha = 0.8f), - modifier = Modifier.size(24.dp) - ) + if (wpRes != null) { + Image(painter = painterResource(id = wpRes), contentDescription = null, + modifier = Modifier.fillMaxSize(), contentScale = ContentScale.Crop) + } + Icon(TablerIcons.Scan, contentDescription = null, + tint = if (isDarkTheme) Color.White.copy(alpha = 0.6f) else Color.Black.copy(alpha = 0.35f), + modifier = Modifier.size(22.dp)) } } } @@ -258,10 +403,7 @@ fun MyQrCodeScreen( } context.startActivity(Intent.createChooser(intent, "Share Profile")) }, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .height(50.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).height(50.dp), colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF3390EC)), shape = RoundedCornerShape(12.dp) ) { @@ -270,24 +412,45 @@ fun MyQrCodeScreen( Spacer(modifier = Modifier.height(12.dp)) - // Scan QR button + // Scan QR TextButton(onClick = onScanQr) { - Icon( - TablerIcons.Scan, - contentDescription = null, - tint = Color(0xFF3390EC), - modifier = Modifier.size(20.dp) - ) + Icon(TablerIcons.Scan, contentDescription = null, + tint = Color(0xFF3390EC), modifier = Modifier.size(20.dp)) Spacer(modifier = Modifier.width(8.dp)) - Text( - "Scan QR Code", - color = Color(0xFF3390EC), - fontSize = 15.sp, - fontWeight = FontWeight.Medium - ) + Text("Scan QR Code", color = Color(0xFF3390EC), + fontSize = 15.sp, fontWeight = FontWeight.Medium) } } } } + + // Circular reveal overlay + if (revealActive) { + val snapshot = revealSnapshot + if (snapshot != null) { + Canvas( + modifier = Modifier + .fillMaxSize() + .graphicsLayer(compositingStrategy = CompositingStrategy.Offscreen) + ) { + val dstSize = IntSize(size.width.toInt(), size.height.toInt()) + // Draw old screenshot + drawImage( + image = snapshot, + srcOffset = IntOffset.Zero, + srcSize = IntSize(snapshot.width, snapshot.height), + dstOffset = IntOffset.Zero, + dstSize = dstSize + ) + // Cut expanding circle to reveal new theme underneath + drawCircle( + color = Color.Transparent, + radius = revealRadius.value, + center = revealCenter, + blendMode = BlendMode.Clear + ) + } + } + } } }