QR экран: обои вместо градиентов, circular reveal анимация при смене обоев и темы, кнопка sun/moon переключает тему приложения, cooldown 600ms. Убрана кнопка Start New Call с экрана звонков.

This commit is contained in:
2026-04-08 20:06:56 +05:00
parent 325073fc09
commit ae78e4a162
3 changed files with 255 additions and 96 deletions

View File

@@ -1863,7 +1863,8 @@ fun MainScreen(
onScanQr = {
navStack = navStack.filterNot { it is Screen.MyQr }
pushScreen(Screen.QrScanner)
}
},
onToggleTheme = onToggleTheme
)
}

View File

@@ -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,

View File

@@ -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<Color>,
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<ImageBitmap?>(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
)
}
}
}
}
}