Улучшение отображения аватаров: добавлена поддержка текста с эмодзи и улучшена логика отображения в AvatarImage. Обновлен SharedMediaFastScrollOverlay для корректного отображения при изменении размера. Исправлено сообщение подсказки в строках.
This commit is contained in:
@@ -199,6 +199,7 @@ fun AvatarPlaceholder(
|
|||||||
} else {
|
} else {
|
||||||
getAvatarText(publicKey)
|
getAvatarText(publicKey)
|
||||||
}
|
}
|
||||||
|
val resolvedFontSize = fontSize ?: (size.value / 2.5).sp
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -207,15 +208,34 @@ fun AvatarPlaceholder(
|
|||||||
.background(avatarColors.backgroundColor),
|
.background(avatarColors.backgroundColor),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
AppleEmojiText(
|
if (containsEmojiAvatarText(avatarText)) {
|
||||||
text = avatarText,
|
AppleEmojiText(
|
||||||
color = avatarColors.textColor,
|
text = avatarText,
|
||||||
fontSize = fontSize ?: (size.value / 2.5).sp,
|
color = avatarColors.textColor,
|
||||||
fontWeight = FontWeight.Medium,
|
fontSize = resolvedFontSize,
|
||||||
maxLines = 1,
|
fontWeight = FontWeight.Medium,
|
||||||
overflow = android.text.TextUtils.TruncateAt.END,
|
maxLines = 1,
|
||||||
enableLinks = false
|
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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
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.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Color
|
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.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.layout.onSizeChanged
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.res.stringResource
|
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.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -57,12 +65,14 @@ fun SharedMediaFastScrollOverlay(
|
|||||||
) {
|
) {
|
||||||
if (!visible) return
|
if (!visible) return
|
||||||
|
|
||||||
val thumbWidth = 24.dp
|
val density = androidx.compose.ui.platform.LocalDensity.current
|
||||||
val thumbHeight = 44.dp
|
val handleSize = 48.dp
|
||||||
val thumbHeightPx = with(androidx.compose.ui.platform.LocalDensity.current) { thumbHeight.toPx() }
|
val handleSizePx = with(density) { handleSize.toPx() }
|
||||||
val monthBubbleOffsetXPx = with(androidx.compose.ui.platform.LocalDensity.current) { (-90).dp.roundToPx() }
|
val bubbleOffsetX = with(density) { (-96).dp.roundToPx() }
|
||||||
|
|
||||||
|
var rootHeightPx by remember { mutableIntStateOf(0) }
|
||||||
var trackHeightPx by remember { mutableIntStateOf(0) }
|
var trackHeightPx by remember { mutableIntStateOf(0) }
|
||||||
|
var monthBubbleHeightPx by remember { mutableIntStateOf(0) }
|
||||||
var isDragging by remember { mutableStateOf(false) }
|
var isDragging by remember { mutableStateOf(false) }
|
||||||
var dragProgress by remember { mutableFloatStateOf(progress.coerceIn(0f, 1f)) }
|
var dragProgress by remember { mutableFloatStateOf(progress.coerceIn(0f, 1f)) }
|
||||||
var hintVisible by remember(showHint) { mutableStateOf(showHint) }
|
var hintVisible by remember(showHint) { mutableStateOf(showHint) }
|
||||||
@@ -87,11 +97,18 @@ fun SharedMediaFastScrollOverlay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val shownProgress = if (isDragging) dragProgress else normalizedProgress
|
val shownProgress = if (isDragging) dragProgress else normalizedProgress
|
||||||
val trackTravelPx = (trackHeightPx - thumbHeightPx).coerceAtLeast(1f)
|
val trackTravelPx = (trackHeightPx - handleSizePx).coerceAtLeast(1f)
|
||||||
val thumbOffsetY = (trackTravelPx * shownProgress).coerceIn(0f, trackTravelPx)
|
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(
|
Box(
|
||||||
modifier = modifier.fillMaxSize()
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.onSizeChanged { rootHeightPx = it.height }
|
||||||
) {
|
) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = hintVisible && !isDragging,
|
visible = hintVisible && !isDragging,
|
||||||
@@ -99,7 +116,7 @@ fun SharedMediaFastScrollOverlay(
|
|||||||
exit = fadeOut(),
|
exit = fadeOut(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.padding(end = 44.dp)
|
.padding(end = 60.dp)
|
||||||
) {
|
) {
|
||||||
SharedMediaFastScrollHint(isDarkTheme = isDarkTheme)
|
SharedMediaFastScrollHint(isDarkTheme = isDarkTheme)
|
||||||
}
|
}
|
||||||
@@ -107,105 +124,172 @@ fun SharedMediaFastScrollOverlay(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.padding(end = 8.dp)
|
.padding(end = 2.dp)
|
||||||
.fillMaxHeight(0.86f)
|
.fillMaxHeight(0.86f)
|
||||||
.width(40.dp)
|
.width(56.dp)
|
||||||
.onSizeChanged { trackHeightPx = it.height }
|
.onSizeChanged { trackHeightPx = it.height }
|
||||||
.pointerInput(trackHeightPx, thumbHeightPx) {
|
.pointerInput(trackTravelPx, handleSizePx) {
|
||||||
if (trackHeightPx <= 0) return@pointerInput
|
if (trackTravelPx <= 0f) return@pointerInput
|
||||||
fun updateProgress(y: Float) {
|
var dragFromHandle = false
|
||||||
val fraction = ((y - thumbHeightPx / 2f) / trackTravelPx).coerceIn(0f, 1f)
|
|
||||||
dragProgress = fraction
|
|
||||||
onDragProgressChanged(fraction)
|
|
||||||
}
|
|
||||||
detectDragGestures(
|
detectDragGestures(
|
||||||
onDragStart = { offset ->
|
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
|
isDragging = true
|
||||||
|
dragProgress = latestShownProgress
|
||||||
if (hintVisible) {
|
if (hintVisible) {
|
||||||
hintVisible = false
|
hintVisible = false
|
||||||
onHintDismissed()
|
onHintDismissed()
|
||||||
}
|
}
|
||||||
updateProgress(offset.y)
|
|
||||||
},
|
},
|
||||||
onDragEnd = { isDragging = false },
|
onDragEnd = {
|
||||||
onDragCancel = { isDragging = false },
|
if (dragFromHandle) {
|
||||||
onDrag = { change, _ ->
|
isDragging = false
|
||||||
updateProgress(change.position.y)
|
}
|
||||||
|
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)
|
TelegramDateHandle(
|
||||||
val thumbColor = if (isDarkTheme) Color(0xFF29364A) else Color(0xFF2B4E73)
|
isDarkTheme = isDarkTheme,
|
||||||
val thumbBorderColor = if (isDarkTheme) Color(0x6688A7CC) else Color(0x554B7DB0)
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.Center)
|
.align(Alignment.TopEnd)
|
||||||
.width(3.dp)
|
.offset { IntOffset(0, handleOffsetYPx.roundToInt()) }
|
||||||
.fillMaxHeight()
|
.size(handleSize)
|
||||||
.clip(RoundedCornerShape(2.dp))
|
|
||||||
.background(trackColor)
|
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
AnimatedVisibility(
|
||||||
modifier = Modifier
|
visible = isDragging && monthLabel.isNotBlank(),
|
||||||
.align(Alignment.TopCenter)
|
enter = fadeIn(),
|
||||||
.offset { IntOffset(0, thumbOffsetY.roundToInt()) }
|
exit = fadeOut(),
|
||||||
.size(width = thumbWidth, height = thumbHeight)
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(12.dp))
|
.align(Alignment.TopEnd)
|
||||||
.background(thumbColor)
|
.offset { IntOffset(bubbleOffsetX, bubbleY) }
|
||||||
.border(1.dp, thumbBorderColor, RoundedCornerShape(12.dp)),
|
) {
|
||||||
contentAlignment = Alignment.Center
|
SharedMediaMonthBubble(
|
||||||
) {
|
monthLabel = monthLabel,
|
||||||
Icon(
|
isDarkTheme = isDarkTheme,
|
||||||
imageVector = Icons.Default.UnfoldMore,
|
onMeasured = { monthBubbleHeightPx = it }
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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
|
@Composable
|
||||||
private fun SharedMediaFastScrollHint(isDarkTheme: Boolean) {
|
private fun SharedMediaFastScrollHint(isDarkTheme: Boolean) {
|
||||||
val background = if (isDarkTheme) Color(0xE6212934) else Color(0xE8263F63)
|
val background = if (isDarkTheme) Color(0xEA2A323D) else Color(0xEA26374E)
|
||||||
val iconBackground = if (isDarkTheme) Color(0x553A4A60) else Color(0x553A5A84)
|
val iconBackground = if (isDarkTheme) Color(0x553C4656) else Color(0x55324A67)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(RoundedCornerShape(10.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(background)
|
.background(background)
|
||||||
.padding(horizontal = 10.dp, vertical = 8.dp)
|
.padding(horizontal = 10.dp, vertical = 8.dp)
|
||||||
.widthIn(max = 250.dp),
|
.widthIn(max = 300.dp)
|
||||||
|
.heightIn(min = 34.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(22.dp)
|
.size(20.dp)
|
||||||
.clip(RoundedCornerShape(6.dp))
|
.clip(RoundedCornerShape(5.dp))
|
||||||
.background(iconBackground),
|
.background(iconBackground),
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
@@ -213,7 +297,7 @@ private fun SharedMediaFastScrollHint(isDarkTheme: Boolean) {
|
|||||||
imageVector = Icons.Default.UnfoldMore,
|
imageVector = Icons.Default.UnfoldMore,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = Color.White.copy(alpha = 0.92f),
|
tint = Color.White.copy(alpha = 0.92f),
|
||||||
modifier = Modifier.size(14.dp)
|
modifier = Modifier.size(12.dp)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -52,6 +52,9 @@ import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
|||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.LocalConfiguration
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
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.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.Velocity
|
import androidx.compose.ui.unit.Velocity
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
@@ -561,6 +565,8 @@ fun OtherProfileScreen(
|
|||||||
val mediaScreenWidth = LocalConfiguration.current.screenWidthDp.dp
|
val mediaScreenWidth = LocalConfiguration.current.screenWidthDp.dp
|
||||||
val mediaCellSize = (mediaScreenWidth - mediaSpacing * (mediaColumns - 1)) / mediaColumns
|
val mediaCellSize = (mediaScreenWidth - mediaSpacing * (mediaColumns - 1)) / mediaColumns
|
||||||
val mediaDecodeSemaphore = remember { Semaphore(4) }
|
val mediaDecodeSemaphore = remember { Semaphore(4) }
|
||||||
|
var rootHeightPx by remember { mutableIntStateOf(0) }
|
||||||
|
var sharedTabsBottomPx by remember { mutableIntStateOf(0) }
|
||||||
val profileListState = rememberLazyListState()
|
val profileListState = rememberLazyListState()
|
||||||
var mediaFastScrollHintDismissed by rememberSaveable(user.publicKey) { mutableStateOf(false) }
|
var mediaFastScrollHintDismissed by rememberSaveable(user.publicKey) { mutableStateOf(false) }
|
||||||
// Use stable key for bitmap cache - don't recreate on size change
|
// Use stable key for bitmap cache - don't recreate on size change
|
||||||
@@ -622,9 +628,20 @@ fun OtherProfileScreen(
|
|||||||
formatMediaMonthLabel(sharedContent.mediaPhotos[itemIndex].timestamp)
|
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 {
|
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 =
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
|
.onSizeChanged { rootHeightPx = it.height }
|
||||||
.nestedScroll(nestedScrollConnection)
|
.nestedScroll(nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
// Scrollable content
|
// Scrollable content
|
||||||
@@ -796,6 +814,10 @@ fun OtherProfileScreen(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
|
.onGloballyPositioned { coords ->
|
||||||
|
sharedTabsBottomPx =
|
||||||
|
(coords.positionInRoot().y + coords.size.height).roundToInt()
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
OtherProfileSharedTabs(
|
OtherProfileSharedTabs(
|
||||||
selectedTab = selectedTab,
|
selectedTab = selectedTab,
|
||||||
@@ -1030,21 +1052,32 @@ fun OtherProfileScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SharedMediaFastScrollOverlay(
|
if (overlayHeightPx > 0) {
|
||||||
visible = mediaFastScrollVisible,
|
Box(
|
||||||
progress = mediaFastScrollProgress,
|
modifier = Modifier
|
||||||
monthLabel = mediaFastScrollMonthLabel,
|
.align(Alignment.TopEnd)
|
||||||
isDarkTheme = isDarkTheme,
|
.offset { IntOffset(0, overlayTopPx) }
|
||||||
showHint = mediaFastScrollVisible && !mediaFastScrollHintDismissed,
|
.fillMaxWidth()
|
||||||
onHintDismissed = { mediaFastScrollHintDismissed = true },
|
.height(with(density) { overlayHeightPx.toDp() })
|
||||||
onDragProgressChanged = { fraction ->
|
.clipToBounds()
|
||||||
if (!mediaFastScrollVisible) return@SharedMediaFastScrollOverlay
|
) {
|
||||||
val targetOffset = (mediaMaxScrollPx * fraction).roundToInt()
|
SharedMediaFastScrollOverlay(
|
||||||
coroutineScope.launch {
|
visible = mediaFastScrollVisible,
|
||||||
profileListState.scrollToItem(index = 2, scrollOffset = targetOffset)
|
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
|
// 🎨 COLLAPSING HEADER with METABALL EFFECT
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Rosetta</string>
|
<string name="app_name">Rosetta</string>
|
||||||
<string name="shared_media_fast_scroll_hint">You can hold and move this bar for faster scrolling.</string>
|
<string name="shared_media_fast_scroll_hint">You can hold and move this bar for faster scrolling</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user