Улучшение отображения аватаров: добавлена поддержка текста с эмодзи и улучшена логика отображения в AvatarImage. Обновлен SharedMediaFastScrollOverlay для корректного отображения при изменении размера. Исправлено сообщение подсказки в строках.

This commit is contained in:
2026-03-06 19:19:01 +05:00
parent 8bce15cc19
commit 6a269f93db
4 changed files with 240 additions and 118 deletions

View File

@@ -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()
} }
} }

View File

@@ -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)
)
}

View File

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

View File

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