diff --git a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt index b791c0b..7427a70 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/CalendarViewModel.kt @@ -25,6 +25,12 @@ class CalendarViewModel { var selectedDate by mutableStateOf(today) private set + var isCollapsed by mutableStateOf(false) + private set + + var collapseProgress by mutableStateOf(0f) + private set + val currentYear: Int get() = selectedDate.year @Suppress("DEPRECATION") val currentMonth: Int get() = selectedDate.monthNumber @@ -33,6 +39,16 @@ class CalendarViewModel { selectedDate = date } + fun collapse() { + isCollapsed = true + collapseProgress = 1f + } + + fun expand() { + isCollapsed = false + collapseProgress = 0f + } + fun getIsoWeekNumber(date: LocalDate): Int { val jan4 = LocalDate(date.year, 1, 4) val jan4DayOfWeek = jan4.dayOfWeek.ordinal diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt new file mode 100644 index 0000000..572b0d7 --- /dev/null +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/BottomCard.kt @@ -0,0 +1,58 @@ +package plus.rua.project.ui + +import androidx.compose.animation.core.animate +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import plus.rua.project.CalendarViewModel + +@Composable +fun BottomCard( + viewModel: CalendarViewModel, + modifier: Modifier = Modifier +) { + val coroutineScope = rememberCoroutineScope() + + Surface( + modifier = modifier + .fillMaxWidth() + .pointerInput(Unit) { + detectVerticalDragGestures { _, dragAmount -> + if (dragAmount < 0 && !viewModel.isCollapsed) { + viewModel.collapse() + } + } + }, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + color = MaterialTheme.colorScheme.surfaceVariant, + shadowElevation = 4.dp + ) { + Box(modifier = Modifier.fillMaxSize()) { + Box( + modifier = Modifier + .align(Alignment.TopCenter) + .padding(top = 8.dp, bottom = 8.dp) + .clip(RoundedCornerShape(2.dp)) + .background(Color.Gray.copy(alpha = 0.4f)) + .fillMaxWidth(0.15f) + .height(4.dp) + ) + } + } +} diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt index e7a02ec..1942cb5 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthPage.kt @@ -1,12 +1,15 @@ package plus.rua.project.ui +import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha import androidx.compose.ui.unit.dp import kotlinx.datetime.DatePeriod import kotlinx.datetime.LocalDate @@ -20,28 +23,57 @@ fun CalendarMonthPage( selectedDate: LocalDate, today: LocalDate, onDateClick: (LocalDate) -> Unit, + collapseProgress: Float, modifier: Modifier = Modifier ) { val days = remember(year, month) { generateMonthDays(year, month) } + val weeks = days.chunked(7) + val selectedWeekIndex = remember(weeks, selectedDate) { + weeks.indexOfFirst { week -> week.any { it.date == selectedDate } } + } + Column(modifier = modifier) { - days.chunked(7).forEach { week -> - Row( - modifier = Modifier - .fillMaxWidth() - .padding(vertical = 2.dp) - ) { - week.forEach { dayData -> - DayCell( - date = dayData.date, - isCurrentMonth = dayData.isCurrentMonth, - isSelected = dayData.date == selectedDate, - isToday = dayData.date == today, - onClick = { onDateClick(dayData.date) }, - modifier = Modifier.weight(1f) - ) + weeks.forEachIndexed { weekIndex, week -> + val animatedProgress = animateFloatAsState( + targetValue = collapseProgress, + label = "collapse-$weekIndex" + ).value + + val isAboveSelected = weekIndex < selectedWeekIndex + val isBelowSelected = weekIndex > selectedWeekIndex + + val offsetY = when { + isAboveSelected -> -animatedProgress * 200f + isBelowSelected -> animatedProgress * 200f + else -> 0f + } + + val alpha = when { + isAboveSelected || isBelowSelected -> 1f - animatedProgress + else -> 1f + } + + if (alpha > 0.01f) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp) + .offset(y = offsetY.dp) + .alpha(alpha) + ) { + week.forEach { dayData -> + DayCell( + date = dayData.date, + isCurrentMonth = dayData.isCurrentMonth, + isSelected = dayData.date == selectedDate, + isToday = dayData.date == today, + onClick = { onDateClick(dayData.date) }, + modifier = Modifier.weight(1f) + ) + } } } } @@ -66,4 +98,4 @@ private fun generateMonthDays(year: Int, month: Int): List { isCurrentMonth = date.monthNumber == month && date.year == year ) } -} +} \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt index 84d9f4a..062e995 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarMonthView.kt @@ -27,21 +27,40 @@ fun CalendarMonthView( var currentYear by remember { mutableIntStateOf(viewModel.currentYear) } var currentMonth by remember { mutableIntStateOf(viewModel.currentMonth) } - Column(modifier = modifier.fillMaxSize().statusBarsPadding().padding(horizontal = 16.dp)) { - MonthHeader( - year = currentYear, - month = currentMonth, - weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate) - ) - WeekdayHeader(modifier = Modifier.fillMaxWidth()) - CalendarPager( - selectedDate = viewModel.selectedDate, - today = today, - onDateClick = { date -> viewModel.selectDate(date) }, - onMonthChanged = { year, month -> - currentYear = year - currentMonth = month - }, + Column(modifier = modifier.fillMaxSize().statusBarsPadding()) { + Column(modifier = Modifier.padding(horizontal = 16.dp)) { + MonthHeader( + year = currentYear, + month = currentMonth, + weekNumber = viewModel.getIsoWeekNumber(viewModel.selectedDate) + ) + WeekdayHeader(modifier = Modifier.fillMaxWidth()) + if (viewModel.isCollapsed) { + WeekPager( + selectedDate = viewModel.selectedDate, + today = today, + onDateClick = { date -> viewModel.selectDate(date) }, + onWeekChanged = { weekMonday -> + currentYear = weekMonday.year + @Suppress("DEPRECATION") + currentMonth = weekMonday.monthNumber + } + ) + } else { + CalendarPager( + selectedDate = viewModel.selectedDate, + today = today, + onDateClick = { date -> viewModel.selectDate(date) }, + onMonthChanged = { year, month -> + currentYear = year + currentMonth = month + }, + collapseProgress = viewModel.collapseProgress + ) + } + } + BottomCard( + viewModel = viewModel, modifier = Modifier.weight(1f) ) } diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt index affd37b..584e3cf 100644 --- a/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/CalendarPager.kt @@ -20,6 +20,7 @@ fun CalendarPager( today: LocalDate, onDateClick: (LocalDate) -> Unit, onMonthChanged: (year: Int, month: Int) -> Unit, + collapseProgress: Float, modifier: Modifier = Modifier ) { val initialYearMonth = remember { today.toYearMonth() } @@ -61,7 +62,8 @@ fun CalendarPager( } } } - } + }, + collapseProgress = collapseProgress ) } } diff --git a/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt new file mode 100644 index 0000000..2f00bc7 --- /dev/null +++ b/shared/src/commonMain/kotlin/plus/rua/project/ui/WeekPager.kt @@ -0,0 +1,80 @@ +package plus.rua.project.ui + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerDefaults +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.launch +import kotlinx.datetime.DatePeriod +import kotlinx.datetime.LocalDate +import kotlinx.datetime.minus +import kotlinx.datetime.plus + +private const val START_PAGE = Int.MAX_VALUE / 2 + +@Composable +fun WeekPager( + selectedDate: LocalDate, + today: LocalDate, + onDateClick: (LocalDate) -> Unit, + onWeekChanged: (LocalDate) -> Unit, + modifier: Modifier = Modifier +) { + val initialWeekMonday = remember { selectedDate.toWeekMonday() } + val pagerState = rememberPagerState( + initialPage = START_PAGE, + pageCount = { Int.MAX_VALUE } + ) + + LaunchedEffect(pagerState) { + snapshotFlow { pagerState.settledPage }.collect { page -> + val weekMonday = pageToWeekMonday(page, initialWeekMonday) + onWeekChanged(weekMonday) + } + } + + HorizontalPager( + state = pagerState, + beyondViewportPageCount = 1, + flingBehavior = PagerDefaults.flingBehavior(state = pagerState), + modifier = modifier + ) { page -> + val weekMonday = pageToWeekMonday(page, initialWeekMonday) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp) + ) { + (0 until 7).forEach { dayOffset -> + val date = weekMonday.plus(DatePeriod(days = dayOffset)) + DayCell( + date = date, + isCurrentMonth = true, + isSelected = date == selectedDate, + isToday = date == today, + onClick = { onDateClick(date) }, + modifier = Modifier.weight(1f) + ) + } + } + } +} + +private fun LocalDate.toWeekMonday(): LocalDate { + val dayOfWeekOrdinal = dayOfWeek.ordinal // Monday=0 ... Sunday=6 + return minus(DatePeriod(days = dayOfWeekOrdinal)) +} + +private fun pageToWeekMonday(page: Int, initial: LocalDate): LocalDate { + val offset = page - START_PAGE + return initial.plus(DatePeriod(days = offset * 7)) +} \ No newline at end of file