Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[TNT-180] 트레이니 주간 캘린더 연동 #67

Merged
merged 9 commits into from
Feb 8, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package co.kr.tnt.designsystem.component.calendar.utils

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import com.kizitonwose.calendar.compose.CalendarLayoutInfo
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.core.CalendarMonth
import kotlinx.coroutines.flow.filterNotNull
import java.time.LocalDate
import java.time.YearMonth

/**
* 주어진 [viewportPercent] 를 기준으로, 현재 화면상으로 가장 많이 보이는 '월' 을 반환합니다.
*/
@Composable
fun rememberMostVisibleMonth(
state: CalendarState,
viewportPercent: Float = 90f,
): YearMonth {
val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) }
LaunchedEffect(state) {
snapshotFlow { state.layoutInfo.firstMostVisibleMonth(viewportPercent) }
.filterNotNull()
.collect { month -> visibleMonth.value = month }
}
return visibleMonth.value.yearMonth
}

private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50f): CalendarMonth? {
return if (visibleMonthsInfo.isEmpty()) {
null
} else {
val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f
visibleMonthsInfo.firstOrNull { itemInfo ->
if (itemInfo.offset < 0) {
itemInfo.offset + itemInfo.size >= viewportSize
} else {
itemInfo.size - itemInfo.offset >= viewportSize
}
}?.month
}
}

/**
* 화면에 보여지는 7일 중 과반 이상 보여지는 '월' 을 반환합니다.
*/
@Composable
fun rememberMostVisibleYearMonth(
state: WeekCalendarState,
): YearMonth {
val visibleYearMonth = remember { mutableStateOf(YearMonth.now()) }

LaunchedEffect(state) {
snapshotFlow { state.firstVisibleWeek.days.map { it.date } }
.collect { visibleDays ->
val mostVisibleYearMonth = getMostFrequentYearMonth(visibleDays)
visibleYearMonth.value = mostVisibleYearMonth
}
}

return visibleYearMonth.value
}

private fun getMostFrequentYearMonth(visibleDays: List<LocalDate>): YearMonth {
return visibleDays
.groupingBy { YearMonth.from(it) }
.eachCount()
.maxBy { it.value }
.key
}
18 changes: 18 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_alarm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5.202,17.5H18.798C19.844,17.5 20.551,16.434 20.146,15.471L19.701,14.415C19.238,13.316 19,12.136 19,10.944V9C19,5.134 15.866,2 12,2C8.134,2 5,5.134 5,9V10.944C5,12.136 4.762,13.316 4.299,14.415L3.854,15.471C3.449,16.434 4.156,17.5 5.202,17.5Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#262626"/>
<path
android:pathData="M9,18C9,18 9,21 12,21C15,21 15,18 15,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#262626"
android:strokeLineCap="round"/>
</vector>
74 changes: 74 additions & 0 deletions core/ui/src/main/java/co/kr/tnt/ui/component/TnTHomeTopBar.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package co.kr.tnt.ui.component

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import co.kr.tnt.core.designsystem.R
import co.kr.tnt.designsystem.component.calendar.TnTCalendarSelector
import co.kr.tnt.designsystem.theme.TnTTheme
import java.time.YearMonth

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TnTHomeTopBar(
modifier: Modifier = Modifier,
yearMonth: YearMonth = YearMonth.now(),
onClickSelectorPrevious: () -> Unit = { },
onClickSelectorNext: () -> Unit = { },
onClickNotification: () -> Unit = { },
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
) {
Column {
CenterAlignedTopAppBar(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp,
),
title = {
TnTCalendarSelector(
yearMonth = yearMonth,
onClickPrevious = onClickSelectorPrevious,
onClickNext = onClickSelectorNext,
)
},
actions = {
IconButton(
onClick = onClickNotification,
modifier = Modifier.size(32.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_alarm),
contentDescription = "Go to notification screen",
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = TnTTheme.colors.commonColors.Common0,
),
windowInsets = windowInsets,
expandedHeight = 48.dp,
)
}
}

@Preview(showBackground = true)
@Composable
private fun TnTHomeTopBarPreview() {
TnTTheme {
TnTHomeTopBar()
}
}
22 changes: 22 additions & 0 deletions core/ui/src/main/java/co/kr/tnt/ui/utils/FileUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ package co.kr.tnt.ui.utils

import android.content.Context
import android.database.Cursor
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import android.provider.MediaStore
import android.util.Log
import java.io.File
import java.io.FileOutputStream

fun Uri.toFile(context: Context): File? {
return getRealPathFromUri(this, context)?.let { filePath ->
Expand All @@ -27,3 +30,22 @@ fun getRealPathFromUri(uri: Uri, context: Context): String? {
}
return null
}

fun Uri.convertToAllowedImageFormat(context: Context): File {
val inputStream = context.contentResolver.openInputStream(this)
val bitmap = BitmapFactory.decodeStream(inputStream)

val convertedFile = File(context.cacheDir, "image.png")
val outputStream = FileOutputStream(convertedFile)

bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
outputStream.flush()
outputStream.close()

return convertedFile
}

fun isAllowedImageFormat(file: File): Boolean {
val allowedExtensions = listOf("jpg", "jpeg", "png", "svg")
return file.extension.lowercase() in allowedExtensions
}
12 changes: 12 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/model/RecordList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.kr.tnt.domain.model

import java.time.LocalDate

data class RecordList(
val recordDate: LocalDate,
val recordType: RecordType,
val recordTime: String,
val recordImage: String?,
val recordContents: String,
val hasFeedback: Boolean,
)
1 change: 1 addition & 0 deletions feature/trainee/home/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ android {

dependencies {
implementation(libs.kotlinx.immutable)
implementation(libs.calendar.compose)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package co.kr.tnt.trainee.home

import co.kr.tnt.domain.model.RecordList
import co.kr.tnt.ui.base.UiEvent
import co.kr.tnt.ui.base.UiSideEffect
import co.kr.tnt.ui.base.UiState
import java.time.LocalDate

class TraineeHomeContract {
data class TraineeHomeUiState(
val selectedDate: LocalDate = LocalDate.now(),
val markedDates: List<LocalDate> = emptyList(),
val recordList: List<RecordList> = emptyList(),
) : UiState

sealed interface TraineeHomeUiEvent : UiEvent {
data object OnClickNextWeek : TraineeHomeUiEvent
data object OnClickPreviousWeek : TraineeHomeUiEvent
data class OnClickDay(val date: LocalDate) : TraineeHomeUiEvent
}

sealed interface TraineeHomeEffect : UiSideEffect {
data class ShowToast(val message: String) : TraineeHomeEffect
}
}
Loading