UI refactor

This commit is contained in:
2025-10-15 21:57:19 +09:00
parent c00924be85
commit f57cd956bd
7 changed files with 391 additions and 124 deletions

View File

@@ -1,5 +1,6 @@
package kr.smartsoltech.wellshe.ui.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
@@ -95,7 +96,8 @@ fun PhasePill(
Surface(
modifier = modifier,
shape = RoundedCornerShape(50),
color = color.copy(alpha = 0.2f)
color = Color.Transparent,
border = BorderStroke(1.dp, color)
) {
Text(
text = label,

View File

@@ -139,6 +139,13 @@ fun CycleScreenPreview() {
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
QuickActionsCard(
onMarkStart = { },
onMarkEnd = { },
onAddSymptom = { },
onAddNote = { }
)
StatCard(
title = "След. менструация",
value = previewState.forecast?.nextPeriodStart?.format(
@@ -158,22 +165,6 @@ fun CycleScreenPreview() {
)
}
QuickActionsCard(
onMarkStart = { },
onMarkEnd = { },
onAddSymptom = { },
onAddNote = { }
)
InfoCard(
title = "Симптомы сегодня",
content = previewState.todaySymptoms
)
InfoCard(
title = "Прогноз недели",
content = previewState.weekInsight
)
}
}
}

View File

@@ -0,0 +1,244 @@
package kr.smartsoltech.wellshe.ui.cycle.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import kr.smartsoltech.wellshe.model.CycleForecast
import kr.smartsoltech.wellshe.ui.theme.*
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import java.time.temporal.ChronoUnit
import java.util.*
/**
* Вычисление аналитики цикла для отображения текущего статуса
*/
@Composable
fun CycleStatusCard(
forecast: CycleForecast?,
modifier: Modifier = Modifier
) {
if (forecast == null) return
val today = LocalDate.now()
val cycleAnalytics = computeCycleAnalytics(today, forecast)
val dateFormatter = DateTimeFormatter.ofPattern("dd MMM", Locale("ru"))
Card(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = CycleTabColor.copy(alpha = 0.15f)
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = "Статус на сегодня",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
// Текущий день цикла и фаза
Text(
text = "День цикла ${cycleAnalytics.cycleDay} (${cycleAnalytics.phaseName} фаза)",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
Divider(modifier = Modifier.padding(vertical = 4.dp))
// Ближайшие события
Text(
text = "Ближайшие события:",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Medium
)
// Показываем информацию о фертильном окне
if (!cycleAnalytics.flags.isFertileToday) {
val fertileStartFormatted = cycleAnalytics.ranges.fertile.first.format(dateFormatter)
val fertileEndFormatted = cycleAnalytics.ranges.fertile.second.format(dateFormatter)
Text(
text = "• Фертильное окно: $fertileStartFormatted$fertileEndFormatted",
style = MaterialTheme.typography.bodyMedium
)
} else {
Text(
text = "• Фертильное окно: сейчас",
style = MaterialTheme.typography.bodyMedium
)
}
// Информация о дне овуляции
val ovulationFormatted = cycleAnalytics.next.ovulation.format(dateFormatter)
val daysToOvulation = cycleAnalytics.deltas.daysToOvulation
val ovulationText = when {
daysToOvulation < 0 -> "• Овуляция: была $ovulationFormatted (${-daysToOvulation} дн. назад)"
daysToOvulation == 0 -> "• Овуляция: сегодня"
else -> "• Овуляция: $ovulationFormatted (через $daysToOvulation дн.)"
}
Text(
text = ovulationText,
style = MaterialTheme.typography.bodyMedium
)
// Информация о ПМС
if (cycleAnalytics.flags.isPMSToday) {
Text(
text = "• ПМС: сейчас",
style = MaterialTheme.typography.bodyMedium
)
} else if (today.isBefore(cycleAnalytics.ranges.pms.first)) {
val pmsStartFormatted = cycleAnalytics.ranges.pms.first.format(dateFormatter)
Text(
text = "• ПМС: с $pmsStartFormatted",
style = MaterialTheme.typography.bodyMedium
)
}
// Информация о следующей менструации
val nextPeriodFormatted = cycleAnalytics.next.nextPeriodStart.format(dateFormatter)
val daysToNextPeriod = cycleAnalytics.deltas.daysToNextPeriod
val nextPeriodText = when {
cycleAnalytics.flags.isMenstruationToday -> "• Менструация: сейчас"
else -> "• След. менструация: $nextPeriodFormatted (через $daysToNextPeriod дн.)"
}
Text(
text = nextPeriodText,
style = MaterialTheme.typography.bodyMedium
)
// Дополнительная подсказка о текущем статусе
if (cycleAnalytics.flags.isMenstruationToday ||
cycleAnalytics.flags.isFertileToday ||
cycleAnalytics.flags.isOvulationToday ||
cycleAnalytics.flags.isPMSToday) {
Divider(modifier = Modifier.padding(vertical = 4.dp))
val tipText = when {
cycleAnalytics.flags.isOvulationToday -> "Пик вероятности зачатия сегодня. Важно следить за самочувствием, возможны изменения настроения и энергии."
cycleAnalytics.flags.isMenstruationToday -> "В эти дни важно обеспечить организму покой и комфорт. Следите за самочувствием и избегайте чрезмерных нагрузок."
cycleAnalytics.flags.isFertileToday -> "Повышенная вероятность зачатия. Хорошее время для физической активности и новых начинаний."
cycleAnalytics.flags.isPMSToday -> "Возможны колебания настроения и некоторый дискомфорт. Важно следить за гидратацией и сбалансированным питанием."
else -> ""
}
Text(
text = tipText,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
/**
* Вычисление аналитических данных цикла
*/
private fun computeCycleAnalytics(today: LocalDate, forecast: CycleForecast): CycleAnalytics {
// Используем поля из модели CycleForecast
val nextPeriodStart = forecast.nextPeriodStart
val ovulation = forecast.nextOvulation
val fertileStart = forecast.fertileStart
val fertileEnd = forecast.fertileEnd
val pmsStart = forecast.pmsStart
val periodEnd = forecast.periodEnd
// Вычисляем примерную дату начала текущего цикла
val estimatedCycleStart = if (today < nextPeriodStart) {
nextPeriodStart.minusDays(28) // Примерная длина цикла, если точных данных нет
} else {
nextPeriodStart
}
// Считаем текущий день цикла
val cycleDay = ChronoUnit.DAYS.between(estimatedCycleStart, today) + 1
// Определяем фазы
val isMenstruationToday = today in nextPeriodStart..periodEnd
val isFertileToday = today in fertileStart..fertileEnd
val isOvulationToday = today == ovulation
val isPMSToday = today in pmsStart..nextPeriodStart.minusDays(1)
// Определяем текущую фазу
val phaseName = when {
isMenstruationToday -> "Менструация"
today.isBefore(ovulation) -> "Фолликулярная"
today == ovulation -> "Овуляция"
today.isAfter(ovulation) -> "Лютеиновая"
else -> "Неопределена"
}
// Вычисляем дни до следующих событий
val daysToOvulation = ChronoUnit.DAYS.between(today, ovulation)
val daysToNextPeriod = ChronoUnit.DAYS.between(today, nextPeriodStart)
return CycleAnalytics(
cycleDay = cycleDay.toInt(),
phaseName = phaseName,
ranges = CycleRanges(
period = Pair(nextPeriodStart, periodEnd),
fertile = Pair(fertileStart, fertileEnd),
pms = Pair(pmsStart, nextPeriodStart.minusDays(1))
),
next = NextEvents(
ovulation = ovulation,
nextPeriodStart = nextPeriodStart
),
flags = CycleFlags(
isMenstruationToday = isMenstruationToday,
isFertileToday = isFertileToday,
isOvulationToday = isOvulationToday,
isPMSToday = isPMSToday
),
deltas = TimingDeltas(
daysToOvulation = daysToOvulation.toInt(),
daysToNextPeriod = daysToNextPeriod.toInt()
)
)
}
/**
* Класс для аналитики цикла
*/
data class CycleAnalytics(
val cycleDay: Int,
val phaseName: String,
val ranges: CycleRanges,
val next: NextEvents,
val flags: CycleFlags,
val deltas: TimingDeltas
)
data class CycleRanges(
val period: Pair<LocalDate, LocalDate>,
val fertile: Pair<LocalDate, LocalDate>,
val pms: Pair<LocalDate, LocalDate>
)
data class NextEvents(
val ovulation: LocalDate,
val nextPeriodStart: LocalDate
)
data class CycleFlags(
val isMenstruationToday: Boolean,
val isFertileToday: Boolean,
val isOvulationToday: Boolean,
val isPMSToday: Boolean
)
data class TimingDeltas(
val daysToOvulation: Int,
val daysToNextPeriod: Int
)

View File

@@ -9,9 +9,11 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.KeyboardArrowLeft
import androidx.compose.material.icons.filled.KeyboardArrowRight
import androidx.compose.material.icons.filled.Tune
import androidx.compose.material.icons.filled.WbSunny
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -38,7 +40,8 @@ fun CycleCalendarCard(
onPrev: () -> Unit,
onNext: () -> Unit,
forecast: CycleForecast?,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
onOpenSettings: () -> Unit = {} // Добавлен новый параметр для обработки нажатия на кнопку настроек
) {
val daysOfWeek = listOf("Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс")
val monthFormat = DateTimeFormatter.ofPattern("LLLL yyyy", Locale("ru"))
@@ -47,14 +50,14 @@ fun CycleCalendarCard(
modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(20.dp),
colors = CardDefaults.cardColors(
containerColor = CycleTabColor.copy(alpha = 0.3f)
containerColor = Color.Transparent
)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
// Заголовок карточки с иконкой
// Заголовок карточки с иконкой и кнопкой настроек
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -72,6 +75,15 @@ fun CycleCalendarCard(
fontWeight = FontWeight.SemiBold
)
}
// Кнопка настроек цикла
IconButton(onClick = onOpenSettings) {
Icon(
imageVector = Icons.Filled.Tune,
contentDescription = "Настройки цикла",
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// Заголовок месяца с кнопками навигации
@@ -129,10 +141,16 @@ fun CycleCalendarCard(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
PhasePill(label = "Менструация", color = Color(0xFFE57373))
PhasePill(label = "Фертильное окно", color = Color(0xFF4CAF50))
PhasePill(label = "Овуляция", color = Color(0xFF3F51B5))
PhasePill(label = "ПМС", color = Color(0xFFFFA726))
PhasePill(label = "Менструация", color = PeriodColor)
PhasePill(label = "Фертильное окно", color = FertileColor)
PhasePill(label = "Овуляция", color = OvulationColor)
PhasePill(label = "ПМС", color = PmsColor)
}
// Блок расширенной аналитики "Статус на сегодня"
if (forecast != null) {
Spacer(modifier = Modifier.height(16.dp))
CycleStatusCard(forecast = forecast)
}
}
}
@@ -155,7 +173,7 @@ fun CalendarGrid(
// Вычисляем смещение первого дня месяца
val dayOfWeekValue = firstDay.dayOfWeek.value // 1 для понедельника, 7 для воскресенья
val firstDayOffset = if (dayOfWeekValue == 7) 0 else dayOfWeekValue
val firstDayOffset = dayOfWeekValue - 1 // Смещение (0 для понедельника)
// Создаем полную сетку календаря (6 недель по 7 дней)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
@@ -170,7 +188,7 @@ fun CalendarGrid(
if (isValidDay) {
val date = month.atDay(dayNumber)
CalendarDay(date = date, today = today, forecast = forecast)
CalendarDay(date = date, today = today, forecast = forecast, modifier = Modifier.weight(1f))
} else {
// Пустая ячейка для выравнивания
Box(
@@ -192,7 +210,8 @@ fun CalendarGrid(
fun CalendarDay(
date: LocalDate,
today: LocalDate,
forecast: CycleForecast?
forecast: CycleForecast?,
modifier: Modifier = Modifier
) {
// Определяем, в какой фазе цикла находится день
val isPeriod = forecast?.let { date in it.nextPeriodStart..it.periodEnd } ?: false
@@ -201,25 +220,31 @@ fun CalendarDay(
val isOvulation = forecast?.let { date == it.nextOvulation } ?: false
val isToday = date == today
// Определяем цвет фона
val backgroundColor = when {
isPeriod -> PeriodColor
isFertile -> FertileColor
isPms -> PmsColor
else -> Color.Transparent
// Создаем базовый модификатор для всех дней
var finalModifier = modifier
.size(36.dp)
.clip(RoundedCornerShape(8.dp))
// Добавляем контур в зависимости от фазы (приоритет: Ovulation > Period > Fertile > PMS)
finalModifier = when {
isOvulation -> finalModifier
.border(BorderStroke(2.dp, OvulationColor), RoundedCornerShape(8.dp))
isPeriod -> finalModifier
.border(BorderStroke(1.dp, PeriodColor), RoundedCornerShape(8.dp))
isFertile -> finalModifier
.border(BorderStroke(1.dp, FertileColor), RoundedCornerShape(8.dp))
isPms -> finalModifier
.border(BorderStroke(1.dp, PmsColor), RoundedCornerShape(8.dp))
else -> finalModifier
.border(BorderStroke(1.dp, Color.Transparent), RoundedCornerShape(8.dp))
}
// Добавляем модификаторы в зависимости от фазы
val baseModifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(backgroundColor)
// Особое выделение для овуляции
val finalModifier = if (isOvulation) {
baseModifier.border(BorderStroke(2.dp, OvulationBorder), CircleShape)
} else {
baseModifier
// Добавляем дополнительный тонкий контур для выделения сегодняшнего дня
if (isToday && !isOvulation && !isPeriod && !isFertile && !isPms) {
finalModifier = finalModifier.border(
BorderStroke(1.dp, MaterialTheme.colorScheme.outline.copy(alpha = 0.6f)),
RoundedCornerShape(8.dp)
)
}
Box(
@@ -230,7 +255,7 @@ fun CalendarDay(
text = date.dayOfMonth.toString(),
style = MaterialTheme.typography.labelMedium,
fontWeight = if (isToday) FontWeight.Bold else FontWeight.Normal,
color = if (isToday) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurface
color = MaterialTheme.colorScheme.onSurface
)
}
}

View File

@@ -1,24 +1,13 @@
package kr.smartsoltech.wellshe.ui.navigation
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Analytics
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.WbSunny
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.NavigationBarItemDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -36,21 +25,22 @@ fun BottomNavigation(
modifier: Modifier = Modifier
) {
NavigationBar(
modifier = modifier.fillMaxWidth(),
modifier = modifier
.fillMaxWidth()
.height(80.dp), // Минимальная высота Material3
containerColor = MaterialTheme.colorScheme.background,
tonalElevation = 8.dp
tonalElevation = 8.dp,
windowInsets = WindowInsets(0.dp) // Убираем стандартные отступы
) {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
val items = listOf(
BottomNavItem.Cycle,
BottomNavItem.Body,
BottomNavItem.Mood,
BottomNavItem.Analytics,
BottomNavItem.Profile
)
val items = BottomNavItem.items
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
items.forEach { item ->
val selected = currentDestination?.hierarchy?.any { it.route == item.route } == true
@@ -63,55 +53,65 @@ fun BottomNavigation(
BottomNavItem.Profile -> ProfileTabColor
}
NavigationBarItem(
icon = {
if (selected) {
Icon(
imageVector = item.icon,
contentDescription = item.title,
// Создаем кастомный элемент навигации с привязкой к верхнему краю
Column(
modifier = Modifier
.size(24.dp)
.clip(RoundedCornerShape(4.dp))
.background(backgroundColor)
.padding(2.dp)
)
} else {
Icon(
imageVector = item.icon,
contentDescription = item.title,
modifier = Modifier.size(24.dp)
)
}
},
label = {
Text(
text = item.title,
style = MaterialTheme.typography.labelSmall,
textAlign = TextAlign.Center
)
},
selected = selected,
onClick = {
.weight(1f)
.clickable {
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
// Avoid multiple copies of the same destination
launchSingleTop = true
// Restore state when reselecting a previously selected item
// Restore state when reselecting
restoreState = true
}
},
colors = NavigationBarItemDefaults.colors(
selectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer,
selectedTextColor = MaterialTheme.colorScheme.onSurface,
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
}
.padding(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Top // Привязываем контент к верхнему краю
) {
// Иконка - размещаем вверху
if (selected) {
Icon(
imageVector = item.icon,
contentDescription = item.title,
modifier = Modifier
.padding(top = 4.dp)
.size(36.dp)
.clip(RoundedCornerShape(6.dp))
.background(backgroundColor)
.padding(4.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
)
} else {
Icon(
imageVector = item.icon,
contentDescription = item.title,
modifier = Modifier
.padding(top = 4.dp)
.size(32.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Текстовая метка
Spacer(modifier = Modifier.height(2.dp))
Text(
text = item.title,
style = MaterialTheme.typography.labelSmall,
color = if (selected)
MaterialTheme.colorScheme.onSurface
else
MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
maxLines = 1
)
}
}
}
}
}

View File

@@ -8,10 +8,14 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.compose.rememberNavController
import kr.smartsoltech.wellshe.ui.theme.WellSheTheme
// Добавляем явный импорт для BottomNavigation из того же пакета
// Важно импортировать именно эту функцию, а не Material3 компонент с тем же именем
import kr.smartsoltech.wellshe.ui.navigation.BottomNavigation
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun WellSheNavigation() {

View File

@@ -39,8 +39,6 @@ val ErrorRedLight = Color(0xFFFFCDD2)
// Фоновые цвета
val BackgroundPrimary = Color(0xFFFFFFFF)
val BackgroundSecondary = Color(0xFFFAFAFA)
val BackgroundTertiary = Color(0xFFF5F5F5)
// Цвета для графиков и статистики
val ChartPink = Color(0xFFE91E63)
@@ -51,17 +49,20 @@ val ChartOrange = Color(0xFFFF9800)
val ChartRed = Color(0xFFF44336)
// Цвета для фаз цикла
val PeriodColor = Color(0xFFFFD6E0) // Розовый для менструации
val FertileColor = Color(0xFFD6F5E3) // Зелёный для фертильного окна
val PmsColor = Color(0xFFFFF2CC) // Янтарный для ПМС
val OvulationBorder = Color(0xFF6366F1) // Индиго для обводки дня овуляции
val PeriodColor = Color(0xFFE57373) // Менструация (розовый)
val FertileColor = Color(0xFF4CAF50) // Фертильное окно (зеленый)
val OvulationColor = Color(0xFF3F51B5) // Овуляция (синий)
val OvulationBorder = Color(0xFF3F51B5) // Граница для дня овуляции
val PmsColor = Color(0xFFFFA726) // ПМС (оранжевый/янтарный)
// Цвета для вкладок
val CycleTabColor = Color(0xFFFFF8E1) // Янтарный для вкладки Цикл
val BodyTabColor = Color(0xFFE3F2FD) // Синий для вкладки Тело
val MoodTabColor = Color(0xFFFCE4EC) // Розовый для вкладки Настроение
val AnalyticsTabColor = Color(0xFFE0F2F1) // Изумрудный для вкладки Аналитика
val ProfileTabColor = Color(0xFFF5F5F5) // Серый для вкладки Профиль
// Цвет вкладки цикла
val CycleTabColor = Color(0xFFFFD54F) // Янтарный цвет для вкладки цикла
// Цвета для вкладок приложения
val AnalyticsTabColor = Color(0xFFB2DFDB) // Бирюзовый для вкладки Аналитика
val BodyTabColor = Color(0xFFBBDEFB) // Голубой для вкладки Тело
val MoodTabColor = Color(0xFFF8BBD0) // Розовый для вкладки Настроение
val ProfileTabColor = Color(0xFFD1C4E9) // Лавандовый для вкладки Профиль
// Акцентные цвета для разделов
val WaterColor = Color(0xFF2196F3) // Синий для воды