diff --git a/core/src/main/java/com/terning/core/extension/LocalDateExt.kt b/core/src/main/java/com/terning/core/extension/LocalDateExt.kt new file mode 100644 index 000000000..edcf61c96 --- /dev/null +++ b/core/src/main/java/com/terning/core/extension/LocalDateExt.kt @@ -0,0 +1,6 @@ +package com.terning.core.extension + +import java.time.LocalDate + +fun LocalDate.getStringAsTitle(): String = + "${year}년 ${monthValue.toString().padStart(2, '0')}월" \ No newline at end of file diff --git a/feature/build.gradle.kts b/feature/build.gradle.kts index d76df3c05..4f9a062e2 100644 --- a/feature/build.gradle.kts +++ b/feature/build.gradle.kts @@ -91,6 +91,10 @@ dependencies { implementation(libs.ossLicense) implementation(libs.lottie) + //Compose Preview + implementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.ui.tooling.preview) + // KakaoDependencies implementation(libs.kakao.user) diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarDay.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarDay.kt new file mode 100644 index 000000000..e059e36e9 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/CalendarDay.kt @@ -0,0 +1,103 @@ +package com.terning.feature.calendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.Black +import com.terning.core.designsystem.theme.Grey150 +import com.terning.core.designsystem.theme.Grey200 +import com.terning.core.designsystem.theme.TerningMain +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.core.designsystem.theme.White +import com.terning.feature.calendar.models.DayModel +import java.time.LocalDate + +@Composable +fun CalendarDay( + modifier: Modifier = Modifier, + dayData: DayModel, + isSelected: Boolean, + isToday: Boolean, + onDateSelected: (LocalDate) -> Unit +) { + val backgroundColor = + if (isSelected) TerningMain else if (isToday) Grey200 else Color.Transparent + val textColor = + if (dayData.isOutDate) { + Grey150 + } else { + if (isSelected) + White + else + Black + } + + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Box( + modifier = Modifier + .size(22.dp) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + if (!dayData.isOutDate) { + onDateSelected(dayData.date) + } + } + ) + .background( + color = backgroundColor, + shape = CircleShape + ), + contentAlignment = Alignment.Center + ) { + Text( + text = dayData.date.dayOfMonth.toString(), + color = textColor, + style = TerningTheme.typography.calendar + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun CalendarDayPreview() { + TerningTheme { + Row { + CalendarDay( + dayData = DayModel(LocalDate.now(), false), + isSelected = true, + isToday = true, + onDateSelected = {} + ) + CalendarDay( + dayData = DayModel(LocalDate.now(), false), + isSelected = false, + isToday = true, + onDateSelected = {} + ) + CalendarDay( + dayData = DayModel(LocalDate.now(), false), + isSelected = false, + isToday = false, + onDateSelected = {} + ) + } + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarMonth.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarMonth.kt new file mode 100644 index 000000000..e9d41dcf0 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/CalendarMonth.kt @@ -0,0 +1,86 @@ +package com.terning.feature.calendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +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.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.Grey150 +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.feature.calendar.models.MonthData +import com.terning.feature.calendar.models.Scrap +import java.time.LocalDate +import java.time.YearMonth + +@Composable +fun CalendarMonth( + modifier: Modifier = Modifier, + monthData: MonthData, + onDateSelected: (LocalDate) -> Unit, + selectedDate: LocalDate, + scrapLists: List> = listOf() +) { + Column( + modifier = modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + ) { + val month = monthData.calendarMonth.weekDays + for (week in month) { + Row( + modifier = Modifier.weight(1f), + ) { + for (day in week) { + Column( + modifier = Modifier + .weight(1f) + .padding(top = 15.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + CalendarDay( + dayData = day, + isSelected = selectedDate == day.date, + isToday = day.date == LocalDate.now(), + onDateSelected = onDateSelected + ) + if(!day.isOutDate) { + val index = day.date.dayOfWeek.value - 1 + CalendarScrap( + scrapList = scrapLists[index] + ) + } + } + } + } + if(month.indexOf(week) != month.lastIndex) { + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(color = Grey150) + ) + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun CalendarMonthPreview() { + TerningTheme { + CalendarMonth( + monthData = MonthData(YearMonth.now()), + selectedDate = LocalDate.now(), + onDateSelected = {}, + scrapLists = listOf() + ) + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarMonths.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarMonths.kt new file mode 100644 index 000000000..4f179af8d --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/CalendarMonths.kt @@ -0,0 +1,48 @@ +package com.terning.feature.calendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.terning.core.designsystem.theme.White +import com.terning.feature.calendar.models.CalendarDefaults.flingBehavior +import com.terning.feature.calendar.models.CalendarState.Companion.getDateByPage +import com.terning.feature.calendar.models.MonthData +import com.terning.feature.calendar.models.Scrap +import java.time.LocalDate +import java.time.YearMonth + +@Composable +fun CalendarMonths( + modifier: Modifier = Modifier, + listState: LazyListState, + onDateSelected: (LocalDate) -> Unit, + pages: Int, + selectedDate: LocalDate, + scrapLists: List> +) { + LazyRow( + modifier = modifier + .background(White), + state = listState, + userScrollEnabled = true, + flingBehavior = flingBehavior( + state = listState + ) + ) { + items(pages) { page -> + val date = getDateByPage(page) + val monthData = MonthData(YearMonth.of(date.year, date.month)) + + CalendarMonth( + modifier = Modifier.fillParentMaxSize(), + selectedDate = selectedDate, + onDateSelected = onDateSelected, + monthData = monthData, + scrapLists = scrapLists + ) + } + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarRoute.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarRoute.kt new file mode 100644 index 000000000..c706c5715 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/CalendarRoute.kt @@ -0,0 +1,116 @@ +package com.terning.feature.calendar + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +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.lazy.rememberLazyListState +import androidx.compose.material3.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import com.terning.core.designsystem.theme.Grey200 +import com.terning.feature.calendar.component.CalendarTopBar +import com.terning.feature.calendar.component.WeekDaysHeader +import com.terning.feature.calendar.models.CalendarState +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch + +@Composable +fun CalendarRoute() { + CalendarScreen() +} + +@Composable +fun CalendarScreen( + modifier: Modifier = Modifier, + viewModel: CalendarViewModel = hiltViewModel() +) { + val selectedDate by viewModel.selectedDate.collectAsState() + val state by remember{ mutableStateOf(CalendarState()) } + + val listState = rememberLazyListState( + initialFirstVisibleItemIndex = state.getInitialPage() + ) + + var currentDate by remember { mutableStateOf(selectedDate) } + var currentPage by remember { mutableIntStateOf(listState.firstVisibleItemIndex)} + + var isListExpanded by remember { mutableStateOf(false) } + var isWeekEnabled by remember { mutableStateOf(false) } + + LaunchedEffect(key1 = listState) { + snapshotFlow { listState.firstVisibleItemIndex } + .distinctUntilChanged() + .collect{ + val swipeDirection = (listState.firstVisibleItemIndex - currentPage).coerceIn(-1, 1).toLong() + currentDate = currentDate.plusMonths(swipeDirection) + currentPage = listState.firstVisibleItemIndex + } + } + + LaunchedEffect(key1 = selectedDate) { + isWeekEnabled = true + } + + BackHandler { + isWeekEnabled = false + } + + Scaffold( + modifier = modifier, + topBar = { + val coroutineScope = rememberCoroutineScope() + CalendarTopBar( + date = currentDate, + isListExpanded = isListExpanded, + onListButtonClicked = { isListExpanded = !isListExpanded }, + onMonthNavigationButtonClicked = { direction -> + coroutineScope.launch { + listState.animateScrollToItem( + index = listState.firstVisibleItemIndex + direction, + ) + } + } + ) + } + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxWidth() + .padding(top = paddingValues.calculateTopPadding()) + ) { + WeekDaysHeader() + Spacer(modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .background(color = Grey200) + ) + CalendarMonths( + modifier = Modifier.fillMaxSize(), + selectedDate = selectedDate, + onDateSelected = { + viewModel.updateSelectedDate(it) + }, + listState = listState, + pages = state.getPageCount(), + scrapLists = viewModel.mockScrapList + ) + } + + } +} diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarRouth.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarRouth.kt deleted file mode 100644 index 76ef0be7f..000000000 --- a/feature/src/main/java/com/terning/feature/calendar/CalendarRouth.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.terning.feature.calendar - -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier - -@Composable -fun CalendarRoute() { - CalendarScreen() -} - -@Composable -fun CalendarScreen() { - Column(modifier = Modifier.fillMaxSize()) { - Text(text = "캘린더 스크린") - } -} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarScrap.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarScrap.kt new file mode 100644 index 000000000..a28d47c19 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/CalendarScrap.kt @@ -0,0 +1,55 @@ +package com.terning.feature.calendar + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.core.designsystem.theme.White +import com.terning.feature.calendar.models.Scrap + +@Composable +fun CalendarScrap( + modifier: Modifier = Modifier, + scrapList: List +) { + LazyColumn( + modifier = modifier.fillMaxWidth().padding(top = 3.dp) + ) { + items(scrapList.subList(0, MAX_SCRAP_COUNT.coerceAtMost(scrapList.size))) { scrap -> + Text( + text = scrap.text, + style = TerningTheme.typography.body5, + textAlign = TextAlign.Center, + color = White, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .fillMaxWidth() + .background(color = scrap.backgroundColor) + ) + } + + item{ + if(scrapList.size > MAX_SCRAP_COUNT) { + Text( + text = "+${(scrapList.size - MAX_SCRAP_COUNT)}", + style = TerningTheme.typography.detail4, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + } + } + + } +} + +private const val MAX_SCRAP_COUNT = 2 + diff --git a/feature/src/main/java/com/terning/feature/calendar/CalendarViewModel.kt b/feature/src/main/java/com/terning/feature/calendar/CalendarViewModel.kt new file mode 100644 index 000000000..d57863782 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/CalendarViewModel.kt @@ -0,0 +1,93 @@ +package com.terning.feature.calendar + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.terning.core.designsystem.theme.CalBlue1 +import com.terning.core.designsystem.theme.CalGreen1 +import com.terning.core.designsystem.theme.CalGreen2 +import com.terning.core.designsystem.theme.CalPink +import com.terning.core.designsystem.theme.CalPurple +import com.terning.core.designsystem.theme.CalRed +import com.terning.core.designsystem.theme.CalYellow +import com.terning.feature.calendar.models.Scrap +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import java.time.LocalDate +import javax.inject.Inject + +@HiltViewModel +class CalendarViewModel @Inject constructor( +) : ViewModel() { + private val _selectedDate = MutableStateFlow(LocalDate.now()) + val selectedDate get() = _selectedDate.asStateFlow() + + fun updateSelectedDate(date: LocalDate) = viewModelScope.launch { + if (_selectedDate.value != date) { + _selectedDate.value = date + } + } + + //To be erased in future + val mockScrapList: List> + get() { + val list: MutableList> = mutableListOf() + for (i in 0..30) { + when (i % 6) { + 0 -> { + list.add( + i, + listOf() + ) + } + + 1 -> { + list.add( + i, + listOf( + Scrap("Task1_1", CalBlue1), + ) + ) + } + + 2 -> { + list.add( + i, + listOf( + Scrap("Task2_1", CalPink), + Scrap("Task2_2", CalGreen1) + ) + ) + } + 3 -> { + list.add( + i, + listOf() + ) + } + 4 -> { + list.add( + i, + listOf() + ) + } + 5 -> { + list.add( + i, + listOf( + Scrap("Task3_1", CalPink), + Scrap("Task3_2", CalPurple), + Scrap("Task3_3", CalRed), + Scrap("Task3_4", CalBlue1), + Scrap("Task3_5", CalGreen2), + Scrap("Task3_6", CalYellow) + ) + ) + } + } + } + return list.toList() + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/component/CalendarTopBar.kt b/feature/src/main/java/com/terning/feature/calendar/component/CalendarTopBar.kt new file mode 100644 index 000000000..c9730b832 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/component/CalendarTopBar.kt @@ -0,0 +1,98 @@ +package com.terning.feature.calendar.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.Black +import com.terning.core.designsystem.theme.Grey300 +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.core.extension.getStringAsTitle +import com.terning.core.extension.noRippleClickable +import com.terning.feature.R +import java.time.LocalDate + +@Composable +fun CalendarTopBar( + modifier: Modifier = Modifier, + date: LocalDate, + isListExpanded: Boolean, + onListButtonClicked: () -> Unit, + onMonthNavigationButtonClicked: (Int) -> Unit, +) { + Box( + modifier = modifier + .fillMaxWidth() + .background(color = Color.White) + .padding( + top = 23.dp, + bottom = 22.dp, + start = 22.dp, + end = 22.dp) + ) { + Row( + modifier = Modifier.align(Alignment.Center), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painter = painterResource(id = R.drawable.ic_calendar_previous), + contentDescription = stringResource(id = R.string.calendar_button_description_previous), + tint = Grey300, + modifier = Modifier.noRippleClickable { onMonthNavigationButtonClicked(-1) } + ) + Text( + text = date.getStringAsTitle(), + style = TerningTheme.typography.title2, + color = Black, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Icon( + painter = painterResource(id = R.drawable.ic_calendar_next), + contentDescription = stringResource(id = R.string.calendar_button_description_next), + tint = Grey300, + modifier = Modifier.noRippleClickable { onMonthNavigationButtonClicked(1) } + ) + } + Box( + modifier = Modifier + .align(Alignment.CenterEnd) + ) { + Icon( + painter = painterResource( + id = if (isListExpanded) { + R.drawable.ic_calendar_list_selected + } else { + R.drawable.ic_calendar_list_unselected + } + ), + contentDescription = stringResource(id = R.string.calendar_button_description_list), + modifier = Modifier.noRippleClickable { onListButtonClicked() }, + tint = if (isListExpanded) Color.Green else Color.LightGray, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun CalendarTopBarPreview() { + TerningTheme { + CalendarTopBar( + date = LocalDate.now(), + isListExpanded = false, + onListButtonClicked = {}, + onMonthNavigationButtonClicked = {} + ) + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/component/WeekDaysHeader.kt b/feature/src/main/java/com/terning/feature/calendar/component/WeekDaysHeader.kt new file mode 100644 index 000000000..d4983af70 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/component/WeekDaysHeader.kt @@ -0,0 +1,58 @@ +package com.terning.feature.calendar.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.terning.core.designsystem.theme.Black +import com.terning.core.designsystem.theme.SundayRed +import com.terning.core.designsystem.theme.TerningTheme +import com.terning.feature.R + +@Composable +fun WeekDaysHeader( + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .fillMaxWidth() + .background(Color.White) + .padding( + horizontal = 20.dp, + vertical = 18.dp), + ){ + val dayOfWeek = listOf( + R.string.calendar_text_sunday, + R.string.calendar_text_monday, + R.string.calendar_text_tuesday, + R.string.calendar_text_wednesday, + R.string.calendar_text_thursday, + R.string.calendar_text_friday, + R.string.calendar_text_saturday,) + dayOfWeek.forEach { day -> + Text( + modifier = Modifier.weight(1f), + text = stringResource(id = day), + style = TerningTheme.typography.body7, + color = if(day == R.string.calendar_text_sunday) SundayRed else Black, + textAlign = TextAlign.Center + ) + } + } +} + +@Preview(showBackground = true) +@Composable +fun WeekDaysHeaderPreview() { + TerningTheme{ + WeekDaysHeader() + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/models/CalendarDefault.kt b/feature/src/main/java/com/terning/feature/calendar/models/CalendarDefault.kt new file mode 100644 index 000000000..59a554d7d --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/models/CalendarDefault.kt @@ -0,0 +1,34 @@ +package com.terning.feature.calendar.models + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.gestures.FlingBehavior +import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider +import androidx.compose.foundation.gestures.snapping.SnapPosition +import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember + +internal object CalendarDefaults { + /** + * Returns the fling behavior for the given [LazyListState]. + * From Github [https://github.com/kizitonwose/Calendar] + */ + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun flingBehavior(state: LazyListState): FlingBehavior { + val snappingLayout = remember(state) { + val provider = SnapLayoutInfoProvider(state, SnapPosition.Start) + CalendarSnapLayoutInfoProvider(provider) + } + return rememberSnapFlingBehavior(snappingLayout) + } +} + +@ExperimentalFoundationApi +@Suppress("FunctionName") +private fun CalendarSnapLayoutInfoProvider( + snapLayoutInfoProvider: SnapLayoutInfoProvider, +): SnapLayoutInfoProvider = object : SnapLayoutInfoProvider by snapLayoutInfoProvider { + override fun calculateApproachOffset(velocity: Float, decayOffset: Float): Float = 0f +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/models/CalendarState.kt b/feature/src/main/java/com/terning/feature/calendar/models/CalendarState.kt new file mode 100644 index 000000000..98e3cfd94 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/models/CalendarState.kt @@ -0,0 +1,58 @@ +package com.terning.feature.calendar.models + +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import java.time.LocalDate +import java.time.YearMonth + +@Stable +class CalendarState internal constructor( + startMonth: YearMonth = YearMonth.of(START_YEAR, 1), + endMonth: YearMonth = YearMonth.of(END_YEAR, 12), + firstVisibleMonth: YearMonth = YearMonth.now(), +) { + private var _startMonth by mutableStateOf(startMonth) + var startMonth: YearMonth + get() = _startMonth + set(value) { + if (value != _startMonth) { + _startMonth = value + } + } + + private var _endMonth by mutableStateOf(endMonth) + var endMonth: YearMonth + get() = _endMonth + set(value) { + if (value != _endMonth) { + _endMonth = value + } + } + + private var _firstVisibleMonth by mutableStateOf(firstVisibleMonth) + var firstVisibleMonth: YearMonth + get() = _firstVisibleMonth + set(value) { + if (value != _firstVisibleMonth) { + _firstVisibleMonth = value + } + } + + fun getPageCount(): Int = (END_YEAR - START_YEAR) * 12 + + fun getInitialPage(): Int = + (firstVisibleMonth.year - START_YEAR) * 12 + firstVisibleMonth.monthValue - 1 + + companion object { + const val START_YEAR = 2020 + const val END_YEAR = 2030 + + fun getDateByPage(page: Int): LocalDate = LocalDate.of( + START_YEAR + page / 12, + page % 12 + 1, + 1 + ) + } +} \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/models/DayModel.kt b/feature/src/main/java/com/terning/feature/calendar/models/DayModel.kt new file mode 100644 index 000000000..891a9f470 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/models/DayModel.kt @@ -0,0 +1,10 @@ +package com.terning.feature.calendar.models + +import androidx.compose.runtime.Immutable +import java.time.LocalDate + +@Immutable +data class DayModel( + val date: LocalDate, + val isOutDate: Boolean = false +) \ No newline at end of file diff --git a/feature/src/main/java/com/terning/feature/calendar/models/MonthData.kt b/feature/src/main/java/com/terning/feature/calendar/models/MonthData.kt new file mode 100644 index 000000000..425f7beb6 --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/models/MonthData.kt @@ -0,0 +1,41 @@ +package com.terning.feature.calendar.models + +import androidx.compose.runtime.Immutable +import java.time.YearMonth + +/** + * [MonthData] is responsible for managing a month's overall characteristics + * it consists of following properties:- + * + * [inDays] represents the number of days in the previous month that should be shown before the first day of the month. + * [outDays] represents the number of days in the next month that should be shown after the last day of the month.] + * [totalDays] represents the total number of days shown on calendar + * [calendarMonth] represents the list of days of the month, a list of [DayModel] + */ + + +@Immutable +data class MonthData( + val month: YearMonth +) { + private val firstDayOfWeek = month.atDay(1).dayOfWeek.value + private val previousMonth = month.minusMonths(1) + private val nextMonth = month.plusMonths(1) + + val inDays = firstDayOfWeek % 7 + val monthDays = month.lengthOfMonth() + val outDays = (7 - ((inDays + monthDays) % 7)) % 7 + val totalDays = monthDays + inDays + outDays + + private val rows = (0 until totalDays).chunked(7) + + val calendarMonth = MonthModel(month, rows.map { week -> week.map {dayOffset -> getDay(dayOffset)}}) + + private fun getDay(dayOffset: Int): DayModel { + val firstDayOnCalendar = month.atDay(1).minusDays(inDays.toLong()) + val date = firstDayOnCalendar.plusDays(dayOffset.toLong()) + val isOutDate = YearMonth.of(date.year, date.monthValue) != month + + return DayModel(date, isOutDate) + } +} diff --git a/feature/src/main/java/com/terning/feature/calendar/models/MonthModel.kt b/feature/src/main/java/com/terning/feature/calendar/models/MonthModel.kt new file mode 100644 index 000000000..01a10041d --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/models/MonthModel.kt @@ -0,0 +1,37 @@ +package com.terning.feature.calendar.models + +import androidx.compose.runtime.Immutable +import java.time.YearMonth + +@Immutable +data class MonthModel( + val yearMonth: YearMonth, + val weekDays: List> +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as MonthModel + + if (yearMonth != other.yearMonth) return false + if (weekDays.first().first() != other.weekDays.first().first()) return false + if (weekDays.last().last() != other.weekDays.last().last()) return false + + return true + } + + override fun hashCode(): Int { + var result = yearMonth.hashCode() + result = 31 * result + weekDays.first().first().hashCode() + result = 31 * result + weekDays.last().last().hashCode() + return result + } + + override fun toString(): String { + return "CalendarMonth { " + + "first = ${weekDays.first().first()}, " + + "last = ${weekDays.last().last()} " + + "} " + } +} diff --git a/feature/src/main/java/com/terning/feature/calendar/models/Scrap.kt b/feature/src/main/java/com/terning/feature/calendar/models/Scrap.kt new file mode 100644 index 000000000..73698591a --- /dev/null +++ b/feature/src/main/java/com/terning/feature/calendar/models/Scrap.kt @@ -0,0 +1,10 @@ +package com.terning.feature.calendar.models + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class Scrap( + val text: String, + val backgroundColor: Color +) diff --git a/feature/src/main/res/drawable/ic_calendar_list_selected.xml b/feature/src/main/res/drawable/ic_calendar_list_selected.xml new file mode 100644 index 000000000..686920353 --- /dev/null +++ b/feature/src/main/res/drawable/ic_calendar_list_selected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/feature/src/main/res/drawable/ic_calendar_list_unselected.xml b/feature/src/main/res/drawable/ic_calendar_list_unselected.xml new file mode 100644 index 000000000..fa8821985 --- /dev/null +++ b/feature/src/main/res/drawable/ic_calendar_list_unselected.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/feature/src/main/res/drawable/ic_calendar_next.xml b/feature/src/main/res/drawable/ic_calendar_next.xml new file mode 100644 index 000000000..36a9b4af7 --- /dev/null +++ b/feature/src/main/res/drawable/ic_calendar_next.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/src/main/res/drawable/ic_calendar_previous.xml b/feature/src/main/res/drawable/ic_calendar_previous.xml new file mode 100644 index 000000000..f9001fd4b --- /dev/null +++ b/feature/src/main/res/drawable/ic_calendar_previous.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/feature/src/main/res/drawable/ic_nav_home.xml b/feature/src/main/res/drawable/ic_nav_home.xml index 6fcd9a120..9b5f41b94 100644 --- a/feature/src/main/res/drawable/ic_nav_home.xml +++ b/feature/src/main/res/drawable/ic_nav_home.xml @@ -3,7 +3,7 @@ android:height="18dp" android:viewportWidth="18" android:viewportHeight="18"> - - + + \ No newline at end of file diff --git a/feature/src/main/res/values/strings.xml b/feature/src/main/res/values/strings.xml index 126401fa0..2247cdd3a 100644 --- a/feature/src/main/res/values/strings.xml +++ b/feature/src/main/res/values/strings.xml @@ -23,4 +23,17 @@ 관심있는 인턴 공고 키워드를 검색해보세요 + + + + + + + + + + previous + next + list + \ No newline at end of file