Add new drawable resources for icons and themes

- Created `archive_filled.xml` for filled archive icon.
- Added `bookmark_outlined.xml` for outlined bookmark icon.
- Introduced `day_theme_filled.xml` for day theme icon.
- Added `folder_outlined.xml` for outlined folder icon.
- Created `gear_outlined.xml` for outlined gear icon.
- Introduced `night_mode.xml` for night mode icon.
This commit is contained in:
2026-02-13 17:37:03 +05:00
parent e17b03c1c5
commit 93ce53d3d5
30 changed files with 1269 additions and 147 deletions

View File

@@ -12,6 +12,7 @@ import androidx.compose.animation.*
import androidx.compose.animation.core.*
import androidx.compose.foundation.*
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.gestures.animateScrollBy
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.WindowInsets
@@ -59,6 +60,7 @@ import com.rosetta.messenger.biometric.BiometricAvailability
import com.rosetta.messenger.biometric.BiometricPreferences
import com.rosetta.messenger.repository.AvatarRepository
import com.rosetta.messenger.ui.components.BlurredAvatarBackground
import com.rosetta.messenger.ui.onboarding.PrimaryBlue
import com.rosetta.messenger.ui.onboarding.PrimaryBlueDark
@@ -427,19 +429,21 @@ fun ProfileScreen(
// ═══════════════════════════════════════════════════════════════
// TELEGRAM ARCHITECTURE: LazyColumn = RecyclerView
// Item 0 = spacer высотой с expanded header (как Telegram item 0)
// scrollOffset вычисляется из позиции скролла (как Telegram extraHeight)
// Контент и хедер двигаются SYNC — один скролл двигает всё
// Item 0 = spacer высотой maxScrollOffset (ровно сколько нужно проскроллить)
// scrollOffset = firstVisibleItemScrollOffset (напрямую, без coerce)
// LazyColumn имеет padding-top = collapsedHeight чтобы контент
// не залезал под collapsed хедер.
// Мёртвой зоны нет — каждый пиксель скролла двигает хедер.
// ═══════════════════════════════════════════════════════════════
val listState = rememberLazyListState()
val expandedHeaderDp = with(density) { expandedHeightPx.toDp() }
val spacerHeightDp = with(density) { maxScrollOffset.toDp() }
val collapsedHeightDp = with(density) { collapsedHeightPx.toDp() }
// Derive scrollOffset from LazyColumn scroll — как Telegram checkListViewScroll()
// item 0 top position → extraHeight
// scrollOffset напрямую из LazyColumn — как Telegram checkListViewScroll()
val scrollOffset by remember {
derivedStateOf {
if (listState.firstVisibleItemIndex == 0) {
listState.firstVisibleItemScrollOffset.toFloat().coerceAtMost(maxScrollOffset)
listState.firstVisibleItemScrollOffset.toFloat()
} else {
maxScrollOffset
}
@@ -484,17 +488,13 @@ fun ProfileScreen(
// isPulledDown имеет ВЫСШИЙ ПРИОРИТЕТ - игнорирует isDragging
// ═══════════════════════════════════════════════════════════════
val targetOverscroll = when {
isPulledDown -> maxOverscroll // 🔥 ВЫСШИЙ ПРИОРИТЕТ: snap сработал - держим раскрытым!
isDragging -> overscrollOffset // Во время drag (до порога) - следуем за пальцем
overscrollOffset > snapThreshold -> maxOverscroll // Перешли порог - snap к max
else -> 0f // Не дотянули - snap обратно
isPulledDown -> maxOverscroll
isDragging -> overscrollOffset
overscrollOffset > snapThreshold -> maxOverscroll
else -> 0f
}
// 🔥 FIX: Когда isPulledDown=true - анимация должна быть МГНОВЕННОЙ
// чтобы аватарка сразу заполнилась после порога
val currentProgress = (overscrollOffset / maxOverscroll).coerceIn(0f, 1f)
// Плавная spring анимация для snap (без bounce для гладкости)
// Spring анимация для snap (без bounce)
val animatedOverscroll by animateFloatAsState(
targetValue = targetOverscroll,
animationSpec = if (isDragging && !isPulledDown) {
@@ -590,7 +590,7 @@ fun ProfileScreen(
source: NestedScrollSource
): Offset {
// Overscroll при свайпе вниз от верха (когда LazyColumn в начале)
if (available.y > 0 && scrollOffset == 0f) {
if (available.y > 0 && !listState.canScrollBackward) {
isDragging = true
val resistance = if (isPulledDown) 1f else 0.5f
val delta = available.y * resistance
@@ -625,7 +625,8 @@ fun ProfileScreen(
}
}
// HEADER SNAP — как Telegram smoothScrollBy в ACTION_UP
// HEADER SNAP — Telegram smoothScrollBy(dy, EASE_OUT_QUINT)
// animateScrollBy = Compose эквивалент smoothScrollBy
val currentOffset = scrollOffset
if (currentOffset > 0f && currentOffset < maxScrollOffset) {
val progress = currentOffset / maxScrollOffset
@@ -635,14 +636,18 @@ fun ProfileScreen(
progress >= 0.6f -> true
else -> false
}
if (snapToCollapsed) {
// Snap to collapsed — доскроллить spacer вверх
listState.animateScrollToItem(0, maxScrollOffset.toInt())
val snapDelta = if (snapToCollapsed) {
maxScrollOffset - currentOffset // скролл вперёд = collapse
} else {
// Snap to expanded — вернуть spacer в начало
listState.animateScrollToItem(0, 0)
-currentOffset // скролл назад = expand
}
// Поглощаем velocity — LazyColumn не fling'ит
listState.animateScrollBy(
value = snapDelta,
animationSpec = tween(
durationMillis = 250,
easing = CubicBezierEasing(0.25f, 1f, 0.5f, 1f)
)
)
return available
}
@@ -725,16 +730,16 @@ fun ProfileScreen(
.nestedScroll(nestedScrollConnection)
) {
// Scrollable content — Telegram architecture:
// Item 0 = spacer (как Telegram RecyclerView item 0)
// Скролл LazyColumn двигает КОНТЕНТ + хедер вместе
// Item 0 = spacer (ровно maxScrollOffset px) → каждый пиксель скролла двигает хедер
// padding-top = collapsedHeightDp → контент не залезает под collapsed хедер
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize()
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(top = collapsedHeightDp)
) {
// Item 0: spacer высотой с раскрытый хедер
// Когда скроллим вверх — spacer уходит, scrollOffset растёт
// Item 0: spacer = ровно сколько нужно проскроллить для collapse
item {
Spacer(modifier = Modifier.fillMaxWidth().height(expandedHeaderDp))
Spacer(modifier = Modifier.fillMaxWidth().height(spacerHeightDp))
}
item {
Spacer(modifier = Modifier.height(16.dp))
@@ -961,6 +966,43 @@ fun ProfileScreen(
}
}
)
// ═══════════════════════════════════════════════════════════
// 📷 CAMERA BUTTON — at boundary between header and content
// Positioned at bottom-right of header, half overlapping content area
// Fades out when collapsed or when avatar is expanded
// ═══════════════════════════════════════════════════════════
val cameraButtonAlpha = (1f - collapseProgress * 2.5f).coerceIn(0f, 1f) *
(1f - expansionProgress * 4f).coerceIn(0f, 1f)
if (cameraButtonAlpha > 0.01f) {
val cameraButtonSize = 52.dp
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(
x = (-16).dp,
y = headerHeight - cameraButtonSize / 2
)
.size(cameraButtonSize)
.graphicsLayer { alpha = cameraButtonAlpha }
.shadow(
elevation = 4.dp,
shape = CircleShape,
clip = false
)
.clip(CircleShape)
.background(Color.White)
.clickable { showPhotoPicker = true },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = TablerIcons.CameraPlus,
contentDescription = "Change avatar",
tint = Color(0xFF8E8E93),
modifier = Modifier.size(24.dp)
)
}
}
}
// 🖼️ Кастомный быстрый Photo Picker
@@ -1169,46 +1211,24 @@ private fun CollapsingProfileHeader(
}
// ═══════════════════════════════════════════════════════════
// <EFBFBD> ADD/CHANGE AVATAR BUTTON — bottom-right of avatar circle
// Fades out on collapse and expansion
// ═══════════════════════════════════════════════════════════
val cameraButtonAlpha = avatarAlpha * (1f - expandFraction * 4f).coerceIn(0f, 1f)
if (cameraButtonAlpha > 0.01f) {
val cameraButtonSize = 44.dp
// Position: bottom-right of the avatar circle
val avatarCenterXPos = screenWidth / 2
val avatarCenterYPos = avatarY + avatarSize / 2
// Offset to bottom-right edge of circle (45° from center)
val offsetFromCenter = avatarSize / 2 * 0.7f // cos(45°) ≈ 0.707
val cameraX = avatarCenterXPos + offsetFromCenter - cameraButtonSize / 2
val cameraY = avatarCenterYPos + offsetFromCenter - cameraButtonSize / 2
Box(
modifier = Modifier
.offset(x = cameraX, y = cameraY)
.size(cameraButtonSize)
.graphicsLayer { alpha = cameraButtonAlpha }
.clip(CircleShape)
.background(Color(0xFF3A3A3C))
.clickable { onSetPhotoClick() },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = TablerIcons.CameraPlus,
contentDescription = "Change avatar",
tint = Color.White,
modifier = Modifier.size(22.dp)
)
}
}
// ═══════════════════════════════════════════════════════════
// <20>🔙 BACK BUTTON
// 🔙 BACK BUTTON - Aligned with text vertical center
// ═══════════════════════════════════════════════════════════
Box(
modifier =
Modifier.padding(top = statusBarHeight)
.padding(start = 4.dp, top = 4.dp)
Modifier
.align(Alignment.TopStart)
.offset(x = 4.dp, y = textY)
.graphicsLayer {
val centerOffsetY =
with(density) {
androidx.compose
.ui
.unit
.lerp(24.dp, 18.dp, collapseProgress)
.toPx()
}
translationY = -centerOffsetY
}
.size(48.dp),
contentAlignment = Alignment.Center
) {
@@ -1223,13 +1243,23 @@ private fun CollapsingProfileHeader(
}
// ═══════════════════════════════════════════════════════════
// ⋮ MENU BUTTON / 💾 SAVE BUTTON
// ⋮ MENU BUTTON / 💾 SAVE BUTTON - Aligned with text vertical center
// ═══════════════════════════════════════════════════════════
Box(
modifier =
Modifier.align(Alignment.TopEnd)
.padding(top = statusBarHeight)
.padding(end = 4.dp, top = 4.dp),
.offset(x = -4.dp, y = textY)
.graphicsLayer {
val centerOffsetY =
with(density) {
androidx.compose
.ui
.unit
.lerp(24.dp, 18.dp, collapseProgress)
.toPx()
}
translationY = -centerOffsetY
},
contentAlignment = Alignment.Center
) {
AnimatedVisibility(visible = hasChanges, enter = fadeIn(), exit = fadeOut()) {
@@ -1750,30 +1780,35 @@ private fun TelegramToggleItem(
}
}
// Material 2 / old Telegram style switch
// Telegram-style switch with outlined thumb
val thumbOffset by animateFloatAsState(
targetValue = if (isEnabled) 1f else 0f,
animationSpec = tween(durationMillis = 150),
animationSpec = tween(durationMillis = 200),
label = "thumb"
)
val trackColor by animateColorAsState(
targetValue = if (isEnabled) accentColor.copy(alpha = 0.5f)
else if (isDarkTheme) Color(0xFF39393D) else Color(0xFFBDBDBD),
animationSpec = tween(durationMillis = 150),
targetValue = if (isEnabled) accentColor
else if (isDarkTheme) Color(0xFF5A5A5E) else Color(0xFF999999),
animationSpec = tween(durationMillis = 200),
label = "track"
)
val thumbColor by animateColorAsState(
val thumbBorderColor by animateColorAsState(
targetValue = if (isEnabled) accentColor
else if (isDarkTheme) Color(0xFF8E8E93) else Color(0xFFF1F1F1),
animationSpec = tween(durationMillis = 150),
label = "thumbColor"
else if (isDarkTheme) Color(0xFF5A5A5E) else Color(0xFF999999),
animationSpec = tween(durationMillis = 200),
label = "thumbBorder"
)
val trackWidth = 38.dp
val trackHeight = 22.dp
val thumbSize = 26.dp
val borderWidth = 2.dp
val thumbTravel = trackWidth - thumbSize + borderWidth
Box(
modifier = Modifier
.width(37.dp)
.height(20.dp)
.clip(RoundedCornerShape(10.dp))
.background(trackColor)
.width(trackWidth)
.height(thumbSize)
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
@@ -1781,13 +1816,25 @@ private fun TelegramToggleItem(
),
contentAlignment = Alignment.CenterStart
) {
// Track
Box(
modifier = Modifier
.offset(x = (17.dp * thumbOffset))
.size(20.dp)
.shadow(2.dp, CircleShape)
.width(trackWidth)
.height(trackHeight)
.clip(RoundedCornerShape(trackHeight / 2))
.background(trackColor)
.align(Alignment.Center)
)
// Thumb with border
Box(
modifier = Modifier
.offset(x = thumbTravel * thumbOffset)
.size(thumbSize)
.clip(CircleShape)
.background(thumbColor)
.background(thumbBorderColor)
.padding(borderWidth)
.clip(CircleShape)
.background(Color.White)
)
}
}