From 06dc9a2b5db36fae22c0d512ca892a10d9b05538 Mon Sep 17 00:00:00 2001 From: k1ngsterr1 Date: Fri, 6 Mar 2026 19:19:01 +0500 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80=D0=BE?= =?UTF-8?q?=D0=B2:=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B0=20=D0=BF=D0=BE=D0=B4=D0=B4=D0=B5=D1=80=D0=B6=D0=BA=D0=B0?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=D0=B0=20=D1=81=20=D1=8D=D0=BC?= =?UTF-8?q?=D0=BE=D0=B4=D0=B7=D0=B8=20=D0=B8=20=D1=83=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D1=88=D0=B5=D0=BD=D0=B0=20=D0=BB=D0=BE=D0=B3=D0=B8=D0=BA=D0=B0?= =?UTF-8?q?=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8?= =?UTF-8?q?=D1=8F=20=D0=B2=20AvatarImage.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2?= =?UTF-8?q?=D0=BB=D0=B5=D0=BD=20SharedMediaFastScrollOverlay=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D0=BA=D0=BE=D1=80=D1=80=D0=B5=D0=BA=D1=82=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D1=80=D0=B8=20=D0=B8=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B8=20=D1=80=D0=B0=D0=B7?= =?UTF-8?q?=D0=BC=D0=B5=D1=80=D0=B0.=20=D0=98=D1=81=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=BE=20=D1=81=D0=BE=D0=BE=D0=B1=D1=89?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D0=BE=D0=B4=D1=81=D0=BA=D0=B0?= =?UTF-8?q?=D0=B7=D0=BA=D0=B8=20=D0=B2=20=D1=81=D1=82=D1=80=D0=BE=D0=BA?= =?UTF-8?q?=D0=B0=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../messenger/ui/components/AvatarImage.kt | 38 ++- .../SharedMediaFastScrollOverlay.kt | 251 +++++++++++------- .../ui/settings/OtherProfileScreen.kt | 67 +++-- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 240 insertions(+), 118 deletions(-) diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt b/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt index 5c27ae1..7617a55 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/AvatarImage.kt @@ -199,6 +199,7 @@ fun AvatarPlaceholder( } else { getAvatarText(publicKey) } + val resolvedFontSize = fontSize ?: (size.value / 2.5).sp Box( modifier = Modifier @@ -207,15 +208,34 @@ fun AvatarPlaceholder( .background(avatarColors.backgroundColor), contentAlignment = Alignment.Center ) { - AppleEmojiText( - text = avatarText, - color = avatarColors.textColor, - fontSize = fontSize ?: (size.value / 2.5).sp, - fontWeight = FontWeight.Medium, - maxLines = 1, - overflow = android.text.TextUtils.TruncateAt.END, - enableLinks = false - ) + if (containsEmojiAvatarText(avatarText)) { + AppleEmojiText( + text = avatarText, + color = avatarColors.textColor, + fontSize = resolvedFontSize, + fontWeight = FontWeight.Medium, + maxLines = 1, + overflow = android.text.TextUtils.TruncateAt.END, + enableLinks = false + ) + } else { + Text( + text = avatarText, + color = avatarColors.textColor, + fontSize = resolvedFontSize, + fontWeight = FontWeight.Medium, + maxLines = 1 + ) + } + } +} + +private fun containsEmojiAvatarText(text: String): Boolean { + if (text.contains(":emoji_", ignoreCase = true)) return true + return text.any { ch -> + val type = java.lang.Character.getType(ch.code) + type == java.lang.Character.SURROGATE.toInt() || + type == java.lang.Character.OTHER_SYMBOL.toInt() } } diff --git a/app/src/main/java/com/rosetta/messenger/ui/components/SharedMediaFastScrollOverlay.kt b/app/src/main/java/com/rosetta/messenger/ui/components/SharedMediaFastScrollOverlay.kt index 7ea06d3..83cd36d 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/components/SharedMediaFastScrollOverlay.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/components/SharedMediaFastScrollOverlay.kt @@ -11,11 +11,13 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.UnfoldMore @@ -28,15 +30,21 @@ import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.drawscope.DrawScope import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -57,12 +65,14 @@ fun SharedMediaFastScrollOverlay( ) { if (!visible) return - val thumbWidth = 24.dp - val thumbHeight = 44.dp - val thumbHeightPx = with(androidx.compose.ui.platform.LocalDensity.current) { thumbHeight.toPx() } - val monthBubbleOffsetXPx = with(androidx.compose.ui.platform.LocalDensity.current) { (-90).dp.roundToPx() } + val density = androidx.compose.ui.platform.LocalDensity.current + val handleSize = 48.dp + val handleSizePx = with(density) { handleSize.toPx() } + val bubbleOffsetX = with(density) { (-96).dp.roundToPx() } + var rootHeightPx by remember { mutableIntStateOf(0) } var trackHeightPx by remember { mutableIntStateOf(0) } + var monthBubbleHeightPx by remember { mutableIntStateOf(0) } var isDragging by remember { mutableStateOf(false) } var dragProgress by remember { mutableFloatStateOf(progress.coerceIn(0f, 1f)) } var hintVisible by remember(showHint) { mutableStateOf(showHint) } @@ -87,11 +97,18 @@ fun SharedMediaFastScrollOverlay( } val shownProgress = if (isDragging) dragProgress else normalizedProgress - val trackTravelPx = (trackHeightPx - thumbHeightPx).coerceAtLeast(1f) - val thumbOffsetY = (trackTravelPx * shownProgress).coerceIn(0f, trackTravelPx) + val trackTravelPx = (trackHeightPx - handleSizePx).coerceAtLeast(1f) + val handleOffsetYPx = (trackTravelPx * shownProgress).coerceIn(0f, trackTravelPx) + val latestShownProgress by rememberUpdatedState(shownProgress) + val latestHandleOffsetYPx by rememberUpdatedState(handleOffsetYPx) + val handleCenterYPx = handleOffsetYPx + handleSizePx / 2f + val trackTopPx = ((rootHeightPx - trackHeightPx) / 2f).coerceAtLeast(0f) + val bubbleY = (trackTopPx + handleCenterYPx - monthBubbleHeightPx / 2f).roundToInt() Box( - modifier = modifier.fillMaxSize() + modifier = modifier + .fillMaxSize() + .onSizeChanged { rootHeightPx = it.height } ) { AnimatedVisibility( visible = hintVisible && !isDragging, @@ -99,7 +116,7 @@ fun SharedMediaFastScrollOverlay( exit = fadeOut(), modifier = Modifier .align(Alignment.CenterEnd) - .padding(end = 44.dp) + .padding(end = 60.dp) ) { SharedMediaFastScrollHint(isDarkTheme = isDarkTheme) } @@ -107,105 +124,172 @@ fun SharedMediaFastScrollOverlay( Box( modifier = Modifier .align(Alignment.CenterEnd) - .padding(end = 8.dp) + .padding(end = 2.dp) .fillMaxHeight(0.86f) - .width(40.dp) + .width(56.dp) .onSizeChanged { trackHeightPx = it.height } - .pointerInput(trackHeightPx, thumbHeightPx) { - if (trackHeightPx <= 0) return@pointerInput - fun updateProgress(y: Float) { - val fraction = ((y - thumbHeightPx / 2f) / trackTravelPx).coerceIn(0f, 1f) - dragProgress = fraction - onDragProgressChanged(fraction) - } + .pointerInput(trackTravelPx, handleSizePx) { + if (trackTravelPx <= 0f) return@pointerInput + var dragFromHandle = false detectDragGestures( onDragStart = { offset -> + val handleLeft = size.width - handleSizePx + val handleRight = size.width.toFloat() + val handleTop = latestHandleOffsetYPx + val handleBottom = handleTop + handleSizePx + dragFromHandle = + offset.x in handleLeft..handleRight && + offset.y in handleTop..handleBottom + if (!dragFromHandle) return@detectDragGestures + isDragging = true + dragProgress = latestShownProgress if (hintVisible) { hintVisible = false onHintDismissed() } - updateProgress(offset.y) }, - onDragEnd = { isDragging = false }, - onDragCancel = { isDragging = false }, - onDrag = { change, _ -> - updateProgress(change.position.y) + onDragEnd = { + if (dragFromHandle) { + isDragging = false + } + dragFromHandle = false + }, + onDragCancel = { + if (dragFromHandle) { + isDragging = false + } + dragFromHandle = false + }, + onDrag = { change, dragAmount -> + if (!dragFromHandle) return@detectDragGestures + change.consume() + val nextProgress = + (dragProgress + dragAmount.y / trackTravelPx).coerceIn(0f, 1f) + if (nextProgress != dragProgress) { + dragProgress = nextProgress + onDragProgressChanged(nextProgress) + } } ) } ) { - val trackColor = if (isDarkTheme) Color(0x5A7C8798) else Color(0x663F4F64) - val thumbColor = if (isDarkTheme) Color(0xFF29364A) else Color(0xFF2B4E73) - val thumbBorderColor = if (isDarkTheme) Color(0x6688A7CC) else Color(0x554B7DB0) - - Box( + TelegramDateHandle( + isDarkTheme = isDarkTheme, modifier = Modifier - .align(Alignment.Center) - .width(3.dp) - .fillMaxHeight() - .clip(RoundedCornerShape(2.dp)) - .background(trackColor) + .align(Alignment.TopEnd) + .offset { IntOffset(0, handleOffsetYPx.roundToInt()) } + .size(handleSize) ) + } - Box( - modifier = Modifier - .align(Alignment.TopCenter) - .offset { IntOffset(0, thumbOffsetY.roundToInt()) } - .size(width = thumbWidth, height = thumbHeight) - .clip(RoundedCornerShape(12.dp)) - .background(thumbColor) - .border(1.dp, thumbBorderColor, RoundedCornerShape(12.dp)), - contentAlignment = Alignment.Center - ) { - Icon( - imageVector = Icons.Default.UnfoldMore, - contentDescription = "Fast scroll handle", - tint = Color.White.copy(alpha = 0.92f), - modifier = Modifier.size(18.dp) - ) - } - - AnimatedVisibility( - visible = isDragging && monthLabel.isNotBlank(), - enter = fadeIn(), - exit = fadeOut(), - modifier = Modifier - .align(Alignment.TopCenter) - .offset { - IntOffset( - monthBubbleOffsetXPx, - (thumbOffsetY + 6f).roundToInt() - ) - } - ) { - SharedMediaMonthPill( - monthLabel = monthLabel, - isDarkTheme = isDarkTheme - ) - } + AnimatedVisibility( + visible = isDragging && monthLabel.isNotBlank(), + enter = fadeIn(), + exit = fadeOut(), + modifier = Modifier + .align(Alignment.TopEnd) + .offset { IntOffset(bubbleOffsetX, bubbleY) } + ) { + SharedMediaMonthBubble( + monthLabel = monthLabel, + isDarkTheme = isDarkTheme, + onMeasured = { monthBubbleHeightPx = it } + ) } } } +@Composable +private fun TelegramDateHandle(isDarkTheme: Boolean, modifier: Modifier = Modifier) { + val handleBackground = if (isDarkTheme) Color(0xFF3D4C63) else Color(0xFFEAF0F8) + val borderColor = if (isDarkTheme) Color(0x668EA0BA) else Color(0x33405673) + val arrowColor = if (isDarkTheme) Color(0xFFF0F4FB) else Color(0xFF4A5A73) + + Box( + modifier = modifier + .shadow(8.dp, CircleShape, clip = false) + .clip(CircleShape) + .background(handleBackground) + .border(1.dp, borderColor, CircleShape) + ) { + androidx.compose.foundation.Canvas(modifier = Modifier.fillMaxSize()) { + drawTelegramArrowPair(color = arrowColor) + } + } +} + +private fun DrawScope.drawTelegramArrowPair(color: Color) { + val centerX = size.width / 2f + val centerY = size.height / 2f + val halfWidth = size.minDimension * 0.12f + val halfHeight = size.minDimension * 0.08f + val gap = size.minDimension * 0.14f + + val up = Path().apply { + moveTo(centerX - halfWidth, centerY - gap + halfHeight) + lineTo(centerX + halfWidth, centerY - gap + halfHeight) + lineTo(centerX, centerY - gap - halfHeight) + close() + } + val down = Path().apply { + moveTo(centerX - halfWidth, centerY + gap - halfHeight) + lineTo(centerX + halfWidth, centerY + gap - halfHeight) + lineTo(centerX, centerY + gap + halfHeight) + close() + } + drawPath(path = up, color = color) + drawPath(path = down, color = color) +} + +@Composable +private fun SharedMediaMonthBubble( + monthLabel: String, + isDarkTheme: Boolean, + onMeasured: (Int) -> Unit +) { + val bubbleBackground = if (isDarkTheme) Color(0xFF3C4655) else Color(0xFFF1F5FB) + val bubbleBorder = if (isDarkTheme) Color(0x55424F62) else Color(0x223D4D66) + val textColor = if (isDarkTheme) Color(0xFFF2F5FB) else Color(0xFF283548) + + Text( + text = monthLabel, + color = textColor, + maxLines = 1, + overflow = TextOverflow.Clip, + style = TextStyle( + fontSize = 14.sp, + fontWeight = FontWeight.SemiBold + ), + modifier = Modifier + .onSizeChanged { onMeasured(it.height) } + .clip(RoundedCornerShape(18.dp)) + .background(bubbleBackground) + .border(1.dp, bubbleBorder, RoundedCornerShape(18.dp)) + .padding(horizontal = 12.dp, vertical = 6.dp) + .widthIn(min = 94.dp) + ) +} + @Composable private fun SharedMediaFastScrollHint(isDarkTheme: Boolean) { - val background = if (isDarkTheme) Color(0xE6212934) else Color(0xE8263F63) - val iconBackground = if (isDarkTheme) Color(0x553A4A60) else Color(0x553A5A84) + val background = if (isDarkTheme) Color(0xEA2A323D) else Color(0xEA26374E) + val iconBackground = if (isDarkTheme) Color(0x553C4656) else Color(0x55324A67) Row( modifier = Modifier - .clip(RoundedCornerShape(10.dp)) + .clip(RoundedCornerShape(8.dp)) .background(background) .padding(horizontal = 10.dp, vertical = 8.dp) - .widthIn(max = 250.dp), + .widthIn(max = 300.dp) + .heightIn(min = 34.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Box( modifier = Modifier - .size(22.dp) - .clip(RoundedCornerShape(6.dp)) + .size(20.dp) + .clip(RoundedCornerShape(5.dp)) .background(iconBackground), contentAlignment = Alignment.Center ) { @@ -213,7 +297,7 @@ private fun SharedMediaFastScrollHint(isDarkTheme: Boolean) { imageVector = Icons.Default.UnfoldMore, contentDescription = null, tint = Color.White.copy(alpha = 0.92f), - modifier = Modifier.size(14.dp) + modifier = Modifier.size(12.dp) ) } Text( @@ -224,18 +308,3 @@ private fun SharedMediaFastScrollHint(isDarkTheme: Boolean) { ) } } - -@Composable -private fun SharedMediaMonthPill(monthLabel: String, isDarkTheme: Boolean) { - val background = if (isDarkTheme) Color(0xEE2A3445) else Color(0xEE2B4E73) - Text( - text = monthLabel, - color = Color.White.copy(alpha = 0.95f), - fontSize = 14.sp, - fontWeight = FontWeight.SemiBold, - modifier = Modifier - .clip(RoundedCornerShape(16.dp)) - .background(background) - .padding(horizontal = 12.dp, vertical = 6.dp) - ) -} diff --git a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt index 9deb12f..192798b 100644 --- a/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt +++ b/app/src/main/java/com/rosetta/messenger/ui/settings/OtherProfileScreen.kt @@ -52,6 +52,9 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.layout.positionInRoot import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity @@ -67,6 +70,7 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.Velocity +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel @@ -561,6 +565,8 @@ fun OtherProfileScreen( val mediaScreenWidth = LocalConfiguration.current.screenWidthDp.dp val mediaCellSize = (mediaScreenWidth - mediaSpacing * (mediaColumns - 1)) / mediaColumns val mediaDecodeSemaphore = remember { Semaphore(4) } + var rootHeightPx by remember { mutableIntStateOf(0) } + var sharedTabsBottomPx by remember { mutableIntStateOf(0) } val profileListState = rememberLazyListState() var mediaFastScrollHintDismissed by rememberSaveable(user.publicKey) { mutableStateOf(false) } // Use stable key for bitmap cache - don't recreate on size change @@ -622,9 +628,20 @@ fun OtherProfileScreen( formatMediaMonthLabel(sharedContent.mediaPhotos[itemIndex].timestamp) } } - val mediaFastScrollVisible by remember(selectedTab, sharedContent.mediaPhotos.size, mediaMaxScrollPx) { + val overlayTopPx = sharedTabsBottomPx.coerceIn(0, rootHeightPx) + val overlayHeightPx = (rootHeightPx - overlayTopPx).coerceAtLeast(0) + val mediaFastScrollVisible by remember( + selectedTab, + sharedContent.mediaPhotos.size, + mediaMaxScrollPx, + overlayHeightPx + ) { derivedStateOf { - selectedTab == OtherProfileTab.MEDIA && sharedContent.mediaPhotos.isNotEmpty() && mediaMaxScrollPx > 24f + selectedTab == OtherProfileTab.MEDIA && + sharedContent.mediaPhotos.isNotEmpty() && + mediaMaxScrollPx > 24f && + profileListState.firstVisibleItemIndex >= 1 && + overlayHeightPx > 0 } } @@ -632,6 +649,7 @@ fun OtherProfileScreen( modifier = Modifier.fillMaxSize() .background(backgroundColor) + .onSizeChanged { rootHeightPx = it.height } .nestedScroll(nestedScrollConnection) ) { // Scrollable content @@ -796,6 +814,10 @@ fun OtherProfileScreen( modifier = Modifier .fillMaxWidth() .background(backgroundColor) + .onGloballyPositioned { coords -> + sharedTabsBottomPx = + (coords.positionInRoot().y + coords.size.height).roundToInt() + } ) { OtherProfileSharedTabs( selectedTab = selectedTab, @@ -1030,21 +1052,32 @@ fun OtherProfileScreen( } } - SharedMediaFastScrollOverlay( - visible = mediaFastScrollVisible, - progress = mediaFastScrollProgress, - monthLabel = mediaFastScrollMonthLabel, - isDarkTheme = isDarkTheme, - showHint = mediaFastScrollVisible && !mediaFastScrollHintDismissed, - onHintDismissed = { mediaFastScrollHintDismissed = true }, - onDragProgressChanged = { fraction -> - if (!mediaFastScrollVisible) return@SharedMediaFastScrollOverlay - val targetOffset = (mediaMaxScrollPx * fraction).roundToInt() - coroutineScope.launch { - profileListState.scrollToItem(index = 2, scrollOffset = targetOffset) - } - } - ) + if (overlayHeightPx > 0) { + Box( + modifier = Modifier + .align(Alignment.TopEnd) + .offset { IntOffset(0, overlayTopPx) } + .fillMaxWidth() + .height(with(density) { overlayHeightPx.toDp() }) + .clipToBounds() + ) { + SharedMediaFastScrollOverlay( + visible = mediaFastScrollVisible, + progress = mediaFastScrollProgress, + monthLabel = mediaFastScrollMonthLabel, + isDarkTheme = isDarkTheme, + showHint = mediaFastScrollVisible && !mediaFastScrollHintDismissed, + onHintDismissed = { mediaFastScrollHintDismissed = true }, + onDragProgressChanged = { fraction -> + if (!mediaFastScrollVisible) return@SharedMediaFastScrollOverlay + val targetOffset = (mediaMaxScrollPx * fraction).roundToInt() + coroutineScope.launch { + profileListState.scrollToItem(index = 2, scrollOffset = targetOffset) + } + } + ) + } + } // ═══════════════════════════════════════════════════════════ // 🎨 COLLAPSING HEADER with METABALL EFFECT diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ab8542..e7bbe62 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ Rosetta - You can hold and move this bar for faster scrolling. + You can hold and move this bar for faster scrolling