diff --git a/app/build.gradle.kts b/app/build.gradle.kts index f68a9f68..ecea9260 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,7 +11,7 @@ plugins { id("com.google.firebase.crashlytics") version "2.9.5" id("com.google.gms.google-services") version "4.3.15" id("org.jlleitschuh.gradle.ktlint") version "11.3.2" - id("androidx.navigation.safeargs") version "2.5.3" + id("androidx.navigation.safeargs.kotlin") version "2.5.3" id("com.google.firebase.appdistribution") version "4.0.0" } @@ -119,6 +119,8 @@ dependencies { // Android arch lifecycle implementation("androidx.lifecycle:lifecycle-extensions:2.2.0") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2") + implementation("androidx.lifecycle:lifecycle-livedata-ktx:2.7.0") + implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0") // Retrofit implementation("com.squareup.retrofit2:retrofit:2.9.0") @@ -189,11 +191,13 @@ dependencies { implementation("androidx.paging:paging-compose:${Deps.Version.PagingCompose}") implementation("androidx.lifecycle:lifecycle-viewmodel-compose:${Deps.Version.ComposeViewModel}") implementation("androidx.hilt:hilt-navigation-compose:${Deps.Version.ComposeHiltNavigation}") + implementation("com.airbnb.android:lottie-compose:6.3.0") + + // Coil + implementation("io.coil-kt:coil:2.5.0") + implementation("io.coil-kt:coil-compose:2.5.0") // Paging implementation("androidx.paging:paging-runtime-ktx:3.2.1") implementation("androidx.paging:paging-compose:3.2.1") - - // Coil - implementation("io.coil-kt:coil-compose:2.5.0") } diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/ErrorPlaceHolder.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/ErrorPlaceHolder.kt new file mode 100644 index 00000000..d67d2bb3 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/ErrorPlaceHolder.kt @@ -0,0 +1,42 @@ +package com.wafflestudio.siksha2.components.compose + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.ui.SikshaColors +import com.wafflestudio.siksha2.utils.dpToSp + +@Composable +fun ErrorPlaceHolder( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier, + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Image( + painter = painterResource(R.drawable.ic_warning), + contentDescription = null, + colorFilter = ColorFilter.tint(SikshaColors.Gray800), + modifier = Modifier.size(34.dp) + ) + Text( + text = "네트워크 연결이 불안정합니다.", + fontSize = dpToSp(20.dp), + fontWeight = FontWeight.Light, + modifier = Modifier.padding(top = 20.dp) + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/LoadingPlaceHolder.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/LoadingPlaceHolder.kt new file mode 100644 index 00000000..36744e93 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/LoadingPlaceHolder.kt @@ -0,0 +1,32 @@ +package com.wafflestudio.siksha2.components.compose + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition +import com.wafflestudio.siksha2.R + +@Composable +fun LoadingPlaceHolder( + modifier: Modifier = Modifier +) { + val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.ani_loading)) + val progress by animateLottieCompositionAsState(composition) + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = Modifier.size(70.dp) + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/TopBar.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/TopBar.kt index 1100c18d..e3e5b988 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/components/compose/TopBar.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/TopBar.kt @@ -10,15 +10,16 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.wafflestudio.siksha2.ui.SikshaColors -import com.wafflestudio.siksha2.ui.SikshaTypography @Composable fun TopBar( title: String, modifier: Modifier = Modifier, + textStyle: TextStyle = MaterialTheme.typography.subtitle1, navigationButton: @Composable () -> Unit = {} ) { Box( @@ -32,7 +33,7 @@ fun TopBar( modifier = Modifier.align(Alignment.Center), color = SikshaColors.White900, fontWeight = FontWeight.ExtraBold, - style = SikshaTypography.subtitle1 + style = textStyle ) Box( modifier = Modifier diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/LikeButton.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/LikeButton.kt new file mode 100644 index 00000000..e5375e29 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/LikeButton.kt @@ -0,0 +1,27 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import com.wafflestudio.siksha2.R + +@Composable +fun LikeButton( + isChecked: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit = {} +) { + Image( + painter = painterResource( + if (isChecked) { + R.drawable.ic_heart_filled + } else { + R.drawable.ic_heart_outline + } + ), + contentDescription = "좋아요", + modifier = modifier.clickable { onClick() } + ) +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuRatingBars.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuRatingBars.kt new file mode 100644 index 00000000..e1316af6 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuRatingBars.kt @@ -0,0 +1,92 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CornerSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.ui.SikshaColors +import com.wafflestudio.siksha2.utils.dpToSp +import kotlin.math.max + +@Composable +fun MenuRatingBar( + ratingIndex: Int, + ratio: Float, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier, + horizontalArrangement = Arrangement.Start, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = ratingIndex.toString(), + modifier = Modifier.padding(end = 3.dp), + fontSize = dpToSp(8.dp), + fontWeight = FontWeight.Bold, + color = SikshaColors.Gray500 + ) + Image( + painter = painterResource(R.drawable.ic_gray_star), + contentDescription = null, + modifier = Modifier + .padding(end = 9.dp) + .width(8.dp) + .height(8.dp) + ) + if (ratio > 0.0f) { + Box( + modifier = Modifier + .weight(ratio) + .height(5.dp) + .background( + color = SikshaColors.OrangeMain, + shape = RoundedCornerShape( + topStart = CornerSize(0.dp), + topEnd = CornerSize(2.dp), + bottomEnd = CornerSize(2.dp), + bottomStart = CornerSize(0.dp) + ) + ) + ) {} + } + if (ratio < 1.0f) { + Spacer(modifier = Modifier.weight(1.0f - ratio)) + } + } +} + +@Composable +fun MenuRatingBars( + distributions: List, + modifier: Modifier = Modifier +) { + val maxCount = max(distributions.max(), 1L) + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(5.dp) + ) { + distributions.reversed().forEachIndexed { index, cnt -> + MenuRatingBar( + ratingIndex = 5 - index, + ratio = cnt.toFloat() / maxCount.toFloat() + ) + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuRatingStars.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuRatingStars.kt new file mode 100644 index 00000000..fa840b4d --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuRatingStars.kt @@ -0,0 +1,55 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.wafflestudio.siksha2.R +import kotlin.math.roundToInt + +@Composable +private fun MenuReviewSingleStar( + modifier: Modifier = Modifier, + flag: Int +) { + Image( + painter = painterResource( + when { + flag <= 0 -> R.drawable.ic_full_star + flag == 1 -> R.drawable.ic_half_star + else -> R.drawable.ic_empty_star + } + ), + contentDescription = null, + modifier = modifier.heightIn(max = 48.dp).fillMaxHeight().aspectRatio(1f) + ) +} + +@Composable +fun MenuRatingStars( + rating: Float, + modifier: Modifier = Modifier, + dragEnabled: Boolean = false, + width: Int = 100, + height: Int = 18 +) { + val rounds = (rating * 2).roundToInt() + Row( + modifier = modifier.width(width.dp).height(height.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + for (i in 1..5) { + MenuReviewSingleStar( + flag = i * 2 - rounds + ) + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReview.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReview.kt new file mode 100644 index 00000000..82145d21 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReview.kt @@ -0,0 +1,123 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.defaultMinSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.models.Review +import com.wafflestudio.siksha2.ui.SikshaColors +import com.wafflestudio.siksha2.utils.dpToSp +import com.wafflestudio.siksha2.utils.toKoreanDate +import com.wafflestudio.siksha2.utils.toLocalDateTime + +@Composable +fun MenuReview( + review: Review, + modifier: Modifier = Modifier, + showImage: Boolean = true +) { + Column( + modifier = modifier + .fillMaxWidth() + .background(color = SikshaColors.White900) + .padding(bottom = 10.dp) + .defaultMinSize(minHeight = 90.dp) + .padding(top = 2.dp, bottom = 10.dp) + ) { + Box( + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier.align(Alignment.CenterStart) + ) { + Image( + modifier = Modifier.padding(bottom = 2.dp), + painter = painterResource(R.drawable.ic_review_profile), + contentDescription = "profilePic" + ) + Spacer(modifier = Modifier.width(7.dp)) + Column( + horizontalAlignment = Alignment.Start + ) { + Text( + text = "ID" + (review.userId), + color = SikshaColors.Black900, + fontSize = dpToSp(12.dp), + fontWeight = FontWeight.Bold + ) + MenuRatingStars( + rating = review.score.toFloat() + ) + } + } + Text( + text = review.createdAt.toLocalDateTime().toLocalDate()?.toKoreanDate() ?: "-", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(end = 10.dp), + fontSize = dpToSp(12.dp), + fontWeight = FontWeight.Bold, + color = SikshaColors.Gray500 + ) + } + Box( + modifier = Modifier + .fillMaxWidth() + .defaultMinSize(minHeight = 80.dp) + .padding(horizontal = 16.dp, vertical = 2.dp) + ) { + ReviewSpeechBubble( + modifier = Modifier + .fillMaxWidth() + .matchParentSize() + ) + Text( + text = review.comment ?: "", + modifier = Modifier + .align(Alignment.TopCenter) + .fillMaxWidth() + .padding(start = 30.dp, end = 10.dp, top = 13.dp, bottom = 15.dp), + color = SikshaColors.Gray800, + fontSize = dpToSp(dp = 12.dp), + fontWeight = FontWeight.Medium + ) + } + if (showImage) { + Row( + modifier = Modifier + .padding(start = 30.dp) + .horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + for (imageUri in review.etc?.images ?: listOf()) { + MenuReviewImage( + imageUri = Uri.parse(imageUri), + modifier = Modifier.size(100.dp) + .clip(shape = RoundedCornerShape(8.dp)) + ) + } + } + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImage.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImage.kt new file mode 100644 index 00000000..e45f0eb9 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImage.kt @@ -0,0 +1,56 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import coil.compose.rememberAsyncImagePainter +import com.wafflestudio.siksha2.R + +@Composable +fun MenuReviewImage( + imageUri: Uri, + modifier: Modifier = Modifier, + onDelete: () -> Unit = {}, + deletable: Boolean = false +) { + var imageDialogState by remember { mutableStateOf(false) } + + Box( + modifier = modifier + ) { + if (imageDialogState) { + MenuReviewImageDialog(url = imageUri, onDismiss = { imageDialogState = false }) + } + Image( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center) + .clickable { + imageDialogState = true + }, + painter = rememberAsyncImagePainter(imageUri), + contentDescription = null, + contentScale = ContentScale.Crop + ) + if (deletable) { + Image( + modifier = Modifier + .align(Alignment.TopEnd) + .clickable { onDelete() }, + painter = painterResource(id = R.drawable.ic_image_delete), + contentDescription = null + ) + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImageDialog.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImageDialog.kt new file mode 100644 index 00000000..c85ca0c2 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImageDialog.kt @@ -0,0 +1,59 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.widthIn +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import coil.compose.rememberAsyncImagePainter +import com.wafflestudio.siksha2.R + +@Composable +fun MenuReviewImageDialog( + url: Uri, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + Dialog( + onDismissRequest = onDismiss, + content = { + Column( + modifier = modifier + .padding(20.dp) + .size(width = 300.dp, height = 300.dp) + .fillMaxSize() + ) { + Image( + painter = painterResource(id = R.drawable.ic_close_white), + contentDescription = "닫기", + modifier = Modifier.padding(bottom = 10.dp) + .size(20.dp) + .clickable { + onDismiss() + } + .align(Alignment.End) + ) + Image( + painter = rememberAsyncImagePainter(url), + contentDescription = null, + contentScale = ContentScale.Crop, + modifier = Modifier + .heightIn(0.dp, 300.dp) + .widthIn(0.dp, 300.dp) + .align(Alignment.CenterHorizontally) + ) + } + } + ) +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImageShowMore.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImageShowMore.kt new file mode 100644 index 00000000..2a9abfd4 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/MenuReviewImageShowMore.kt @@ -0,0 +1,65 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import android.net.Uri +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material.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.layout.ContentScale +import androidx.compose.ui.unit.dp +import androidx.compose.ui.zIndex +import coil.compose.rememberAsyncImagePainter +import com.wafflestudio.siksha2.ui.SikshaColors +import com.wafflestudio.siksha2.utils.dpToSp + +@Composable +fun MenuReviewImageShowMore( + imageUri: Uri, + modifier: Modifier = Modifier, + onShowMore: () -> Unit = {}, + showMoreCount: Int = 0 +) { + Box( + modifier = modifier + ) { + Image( + modifier = Modifier + .fillMaxSize() + .align(Alignment.Center), + painter = rememberAsyncImagePainter(imageUri), + contentDescription = null, + contentScale = ContentScale.Crop + ) + Box( + modifier = Modifier + .fillMaxSize() + .background(color = Color(0x80000000)) + .zIndex(1f) + .align(Alignment.Center) + .clickable { onShowMore() } + ) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "+", + fontSize = dpToSp(12.dp), + color = SikshaColors.White900 + ) + Text( + text = showMoreCount.toString() + "건 더 보기", + fontSize = dpToSp(12.dp), + color = SikshaColors.White900 + ) + } + } + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/ReviewSpeechBubble.kt b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/ReviewSpeechBubble.kt new file mode 100644 index 00000000..e5224224 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/components/compose/menuDetail/ReviewSpeechBubble.kt @@ -0,0 +1,66 @@ +package com.wafflestudio.siksha2.components.compose.menuDetail + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.blur +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.wafflestudio.siksha2.R + +@Composable +fun ReviewSpeechBubble( + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + ) { + Image( + painter = painterResource(R.drawable.ic_speech_bubble), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = Modifier + .fillMaxSize() + .offset(x = (-1).dp, y = 1.dp) + .blur(4.dp), + colorFilter = ColorFilter.tint(Color(0x26000000)) + ) + Image( + painter = painterResource(R.drawable.ic_speech_bubble), + contentDescription = null, + contentScale = ContentScale.FillBounds, + modifier = Modifier.fillMaxSize(), + colorFilter = ColorFilter.tint(Color(0xffffffff)) + ) + } +} + +@Composable +@Preview +fun ReviewSpeechBubblePreview() { + Column { + ReviewSpeechBubble( + modifier = Modifier + .fillMaxWidth() + .height(80.dp) + .background(color = Color(0xff00ff00)) + ) + ReviewSpeechBubble( + modifier = Modifier + .fillMaxWidth() + .height(60.dp) + .background(color = Color(0xff0000ff)) + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/compose/ui/menuDetail/MenuDetailScreen.kt b/app/src/main/java/com/wafflestudio/siksha2/compose/ui/menuDetail/MenuDetailScreen.kt new file mode 100644 index 00000000..c29a7802 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/compose/ui/menuDetail/MenuDetailScreen.kt @@ -0,0 +1,548 @@ +package com.wafflestudio.siksha2.compose.ui.menuDetail + +import android.net.Uri +import android.widget.Toast +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +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.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Divider +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.components.compose.ErrorPlaceHolder +import com.wafflestudio.siksha2.components.compose.LoadingPlaceHolder +import com.wafflestudio.siksha2.components.compose.TopBar +import com.wafflestudio.siksha2.components.compose.menuDetail.LikeButton +import com.wafflestudio.siksha2.components.compose.menuDetail.MenuRatingBars +import com.wafflestudio.siksha2.components.compose.menuDetail.MenuRatingStars +import com.wafflestudio.siksha2.components.compose.menuDetail.MenuReview +import com.wafflestudio.siksha2.components.compose.menuDetail.MenuReviewImage +import com.wafflestudio.siksha2.components.compose.menuDetail.MenuReviewImageShowMore +import com.wafflestudio.siksha2.models.MealsOfDay +import com.wafflestudio.siksha2.models.Menu +import com.wafflestudio.siksha2.models.Review +import com.wafflestudio.siksha2.ui.SikshaColors +import com.wafflestudio.siksha2.ui.SikshaTheme +import com.wafflestudio.siksha2.ui.menuDetail.MenuDetailViewModel +import com.wafflestudio.siksha2.ui.menuDetail.MenuLoadingState +import com.wafflestudio.siksha2.utils.dpToSp +import kotlinx.coroutines.flow.flowOf +import java.time.LocalDate +import kotlin.math.min +import kotlin.math.round + +@Composable +fun MenuDetailRoute( + menuId: Long, + isTodayMenu: Boolean, + onNavigateUp: () -> Unit, + onNavigateToLeaveReview: () -> Unit, + onNavigateToReviewPhoto: (Long) -> Unit, + onNavigateToReview: (Long) -> Unit, + modifier: Modifier = Modifier, + menuDetailViewModel: MenuDetailViewModel = hiltViewModel() +) { + val menu by menuDetailViewModel.menu.observeAsState() // todo: LiveData대신 StateFlow 써서 non-null로 만들기 + val reviewDistribution by menuDetailViewModel.reviewDistribution.observeAsState() + val reviews = menuDetailViewModel.reviews.collectAsLazyPagingItems() + val imageReviews = menuDetailViewModel.reviewsWithImage.collectAsLazyPagingItems() + val loadingState by menuDetailViewModel.networkResultMenuLoadingState.observeAsState() + + LaunchedEffect(Unit) { + menuDetailViewModel.refreshMenu(menuId) + menuDetailViewModel.refreshReviewDistribution(menuId) + } + + MenuDetailScreen( + menu = menu, + reviewDistribution = reviewDistribution, + reviews = reviews, + imageReviews = imageReviews, + loadingState = loadingState, + isTodayMenu = isTodayMenu, + onClickLike = { + menu?.let { + menuDetailViewModel.toggleLike(it.id, it.isLiked ?: false) // todo: model, dto 구분하고 isLiked not-null로 만들기 + } + }, + onNavigateUp = onNavigateUp, + onNavigateToLeaveReview = onNavigateToLeaveReview, + onNavigateToReviewPhoto = onNavigateToReviewPhoto, + onNavigateToReview = onNavigateToReview, + modifier = modifier + ) +} + +@Composable +fun MenuDetailScreen( + menu: Menu?, + reviewDistribution: List?, + reviews: LazyPagingItems, + imageReviews: LazyPagingItems, + loadingState: MenuLoadingState?, + isTodayMenu: Boolean, + onClickLike: () -> Unit, + onNavigateUp: () -> Unit, + onNavigateToLeaveReview: () -> Unit, + onNavigateToReviewPhoto: (Long) -> Unit, + onNavigateToReview: (Long) -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize() + ) { + TopBar( + title = menu?.nameKr ?: stringResource(R.string.review_title), + textStyle = MaterialTheme.typography.subtitle1.copy( + fontSize = dpToSp(20.dp) + ), + navigationButton = { + Image( + painter = painterResource(R.drawable.ic_back_arrow), + contentDescription = "뒤로가기", + modifier = Modifier + .size(16.dp) + .clickable { + onNavigateUp() + } + ) + } + ) + + when (loadingState) { + MenuLoadingState.SUCCESS -> { + MenuDetailContent( + menu = menu, + reviewDistribution = reviewDistribution, + reviews = reviews, + imageReviews = imageReviews, + isTodayMenu = isTodayMenu, + onClickLike = onClickLike, + onNavigateToLeaveReview = onNavigateToLeaveReview, + onNavigateToReviewPhoto = onNavigateToReviewPhoto, + onNavigateToReview = onNavigateToReview + ) + } + + MenuLoadingState.LOADING -> { + LoadingPlaceHolder( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + + MenuLoadingState.FAILED -> { + ErrorPlaceHolder( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + + else -> {} + } + } +} + +@Composable +fun MenuDetailContent( + menu: Menu?, + reviewDistribution: List?, + reviews: LazyPagingItems, + imageReviews: LazyPagingItems, + isTodayMenu: Boolean, + onClickLike: () -> Unit, + onNavigateToLeaveReview: () -> Unit, + onNavigateToReviewPhoto: (Long) -> Unit, + onNavigateToReview: (Long) -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + LazyColumn( + modifier = modifier + .fillMaxWidth() + .background(SikshaColors.White900) + ) { + // 상단 별점 정보 + a + item { + MenuStatistics( + menu = menu, + reviewDistribution = reviewDistribution, + reviews = reviews, + onClickLike = onClickLike, + onLeaveReview = { + if (isTodayMenu) { + onNavigateToLeaveReview() + } else { + Toast + .makeText( + context, + "오늘 메뉴만 평가할 수 있습니다.", + Toast.LENGTH_SHORT + ) + .show() + } + } + ) + } + + // 사진 리뷰 + if (imageReviews.itemCount > 0) { + item { + BriefImageReviews( + menu = menu, + imageReviews = imageReviews, + onNavigateToReviewPhoto = onNavigateToReviewPhoto + ) + } + } + + // 일반 리뷰 + if (reviews.itemCount > 0) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(50.dp) + ) { + Text( + text = stringResource(R.string.menu_detail_review_gather), + fontSize = dpToSp(14.dp), + fontWeight = FontWeight.Medium, + modifier = Modifier.align(Alignment.CenterStart) + ) + Image( + painter = painterResource(R.drawable.ic_back_arrow), + contentDescription = "리뷰", + colorFilter = ColorFilter.tint(SikshaColors.Gray400), + modifier = Modifier + .align(Alignment.CenterEnd) + .rotate(180f) + .clickable { + menu?.let { + onNavigateToReview(it.id) + } + } + ) + } + } + items(reviews.itemCount) { + reviews[it]?.let { review -> + MenuReview( + review = review, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + showImage = true + ) + } + } + } else { + item { + Box( + modifier = Modifier + .padding(vertical = 36.dp) + .fillMaxWidth() + ) { + Text( + text = stringResource(R.string.review_nothing), + fontSize = dpToSp(18.dp), + fontWeight = FontWeight.Light, + modifier = Modifier.align(Alignment.Center), + color = SikshaColors.Gray600 + ) + } + } + } + } +} + +@Composable +fun MenuStatistics( + menu: Menu?, + reviewDistribution: List?, + reviews: LazyPagingItems, + onClickLike: () -> Unit, + onLeaveReview: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxWidth() + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 18.dp), + verticalArrangement = Arrangement.spacedBy(9.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + LikeButton( + isChecked = menu?.isLiked ?: false, + onClick = onClickLike, + modifier = Modifier.size(21.dp) + ) + Text( + text = stringResource(R.string.review_like_prefix) + + (menu?.likeCount ?: 0).toString() + + stringResource(R.string.review_like_suffix), + fontSize = dpToSp(14.dp), + fontWeight = FontWeight.Medium + ) + } + + Divider( + modifier = Modifier.padding(horizontal = 17.dp), + color = SikshaColors.OrangeMain, + thickness = 1.dp + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding( + top = 15.dp, + bottom = 15.dp, + start = 45.dp, + end = 25.dp + ) + ) { + Row( + modifier = Modifier.align(Alignment.CenterHorizontally), + horizontalArrangement = Arrangement.Center + ) { + Text( + text = stringResource(R.string.review_count_prefix), + color = SikshaColors.Gray800, + fontSize = dpToSp(10.dp), + fontWeight = FontWeight.Bold + ) + Text( + text = reviews.itemCount.toString(), + color = SikshaColors.OrangeMain, + fontSize = dpToSp(10.dp), + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.review_count_suffix_orange), + color = SikshaColors.OrangeMain, + fontSize = dpToSp(10.dp), + fontWeight = FontWeight.Bold + ) + Text( + text = stringResource(R.string.review_count_suffix_black), + color = SikshaColors.Gray800, + fontSize = dpToSp(10.dp), + fontWeight = FontWeight.Bold + ) + } + + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp, vertical = 10.dp), + horizontalArrangement = Arrangement.spacedBy(20.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "${ + menu?.score?.times(10)?.let { round(it) / 10 } ?: "0.0" + }", + fontSize = dpToSp(32.dp), + fontWeight = FontWeight.ExtraBold + ) + MenuRatingStars( + rating = menu?.score?.toFloat() ?: 0.0f + ) + } + reviewDistribution?.let { + MenuRatingBars( + distributions = it, + modifier = Modifier.weight(1f) + ) + } + } + + Box( + modifier = Modifier + .width(200.dp) + .height(32.dp) + .background( + color = SikshaColors.OrangeMain, + shape = RoundedCornerShape(50.dp) + ) + .clickable { + onLeaveReview() + } + .align(Alignment.CenterHorizontally) + ) { + Text( + text = stringResource(R.string.menu_detail_leave_review_button), + fontSize = dpToSp(14.dp), + fontWeight = FontWeight.ExtraBold, + color = SikshaColors.White900, + modifier = Modifier + .align(Alignment.Center) + ) + } + } + Spacer( + modifier = Modifier + .fillMaxWidth() + .height(10.dp) + .background(SikshaColors.Gray100) + ) + } +} + +@Composable +fun BriefImageReviews( + menu: Menu?, + imageReviews: LazyPagingItems, + onNavigateToReviewPhoto: (Long) -> Unit, + modifier: Modifier = Modifier +) { + val imagePreviewScrollState = rememberScrollState() + Box( + modifier = modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(50.dp) + ) { + Text( + text = stringResource(R.string.menu_detail_photo_review_gather), + fontSize = dpToSp(14.dp), + fontWeight = FontWeight.Medium, + modifier = Modifier.align(Alignment.CenterStart) + ) + Image( + painter = painterResource(R.drawable.ic_back_arrow), + contentDescription = stringResource(R.string.menu_detail_photo_review_gather), + colorFilter = ColorFilter.tint(SikshaColors.Gray400), + modifier = Modifier + .align(Alignment.CenterEnd) + .rotate(180f) + .clickable { + menu?.let { + onNavigateToReviewPhoto(it.id) + } + } + ) + } + Row( + modifier = Modifier + .padding(horizontal = 16.dp) + .horizontalScroll(imagePreviewScrollState), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + for (i: Int in 1..min(imageReviews.itemCount, 3)) { + when (val it = imageReviews.itemSnapshotList.items[i - 1].etc?.images?.get(0)) { + null -> Box( + modifier = Modifier + .size(120.dp) + .background(SikshaColors.Gray100) + .clip(RoundedCornerShape(10.dp)) + ) + + else -> { + if (i == 3) { + MenuReviewImageShowMore( + imageUri = Uri.parse(it), + modifier = Modifier + .size(120.dp) + .clip(RoundedCornerShape(10.dp)), + showMoreCount = imageReviews.itemCount - 2, + onShowMore = { + menu?.let { + onNavigateToReviewPhoto(it.id) + } + } + ) + } else { + MenuReviewImage( + imageUri = Uri.parse(it), + modifier = Modifier + .size(120.dp) + .clip(RoundedCornerShape(10.dp)) + ) + } + } + } + } + } +} + +private val testMenu = Menu( + id = 0L, + code = "", + date = LocalDate.now(), + type = MealsOfDay.BR, + restaurantId = 0L, + nameKr = "nameKr", + nameEn = "nameEn", + price = 10000L, + score = 3.39, + etc = listOf("https://picsum.photos/200", "https://picsum.photos/201"), + reviewCount = 10L, + isLiked = true, + likeCount = 100L +) + +@Preview +@Composable +fun MenuDetailScreenPreview() { + SikshaTheme { + MenuDetailScreen( + menu = testMenu, + reviewDistribution = listOf(1L, 2L, 3L, 4L, 5L), + reviews = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + imageReviews = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + loadingState = MenuLoadingState.SUCCESS, + isTodayMenu = true, + onClickLike = {}, + onNavigateUp = {}, + onNavigateToLeaveReview = {}, + onNavigateToReviewPhoto = {}, + onNavigateToReview = {}, + modifier = Modifier + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/compose/ui/menuDetail/review/ReviewScreen.kt b/app/src/main/java/com/wafflestudio/siksha2/compose/ui/menuDetail/review/ReviewScreen.kt new file mode 100644 index 00000000..97940ced --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/compose/ui/menuDetail/review/ReviewScreen.kt @@ -0,0 +1,138 @@ +package com.wafflestudio.siksha2.compose.ui.menuDetail.review + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +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.LazyColumn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.paging.PagingData +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.components.compose.TopBar +import com.wafflestudio.siksha2.components.compose.menuDetail.MenuReview +import com.wafflestudio.siksha2.models.Review +import com.wafflestudio.siksha2.ui.SikshaColors +import com.wafflestudio.siksha2.ui.SikshaTheme +import com.wafflestudio.siksha2.ui.menuDetail.MenuDetailViewModel +import com.wafflestudio.siksha2.utils.dpToSp +import kotlinx.coroutines.flow.flowOf + +@Composable +fun ReviewRoute( + showImages: Boolean, + onNavigateUp: () -> Unit, + modifier: Modifier = Modifier, + menuDetailViewModel: MenuDetailViewModel = hiltViewModel() +) { + val reviews = if (!showImages) { + menuDetailViewModel.reviews.collectAsLazyPagingItems() + } else { + menuDetailViewModel.reviewsWithImage.collectAsLazyPagingItems() + } + + ReviewScreen( + showImages = showImages, + reviews = reviews, + onNavigateUp = onNavigateUp, + modifier = modifier + ) +} + +@Composable +fun ReviewScreen( + showImages: Boolean, + reviews: LazyPagingItems, + onNavigateUp: () -> Unit, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize() + ) { + TopBar( + title = stringResource(R.string.review_title), + textStyle = MaterialTheme.typography.subtitle1.copy( + fontSize = dpToSp(20.dp) + ), + navigationButton = { + Image( + painter = painterResource(R.drawable.ic_back_arrow), + contentDescription = "뒤로가기", + modifier = Modifier + .clickable { + onNavigateUp() + } + ) + } + ) + + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) { + if (reviews.itemCount == 0) { + Text( + text = stringResource(R.string.review_nothing), + fontSize = dpToSp(18.dp), + fontWeight = FontWeight.Medium, + modifier = Modifier.align(Alignment.Center), + color = SikshaColors.Gray600 + ) + } else { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .background(color = SikshaColors.White900) + ) { + item { + Spacer(modifier = Modifier.height(16.dp)) + } + items(reviews.itemCount) { + reviews[it]?.let { review -> + MenuReview( + review = review, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + showImage = showImages + ) + } + } + item { + Spacer(modifier = Modifier.height(20.dp)) + } + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +fun ReviewScreenPreview() { + SikshaTheme { + ReviewScreen( + showImages = false, + reviews = flowOf(PagingData.empty()).collectAsLazyPagingItems(), + onNavigateUp = {} + ) + } +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt b/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt index 4de68a16..085fc989 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/repositories/MenuRepository.kt @@ -8,7 +8,6 @@ import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.models.MenuGroup import com.wafflestudio.siksha2.models.Review import com.wafflestudio.siksha2.network.SikshaApi -import com.wafflestudio.siksha2.network.dto.FetchReviewsResult import com.wafflestudio.siksha2.network.dto.LeaveReviewParam import com.wafflestudio.siksha2.network.dto.LeaveReviewResult import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewPagingSource @@ -27,7 +26,6 @@ class MenuRepository @Inject constructor( private val sikshaApi: SikshaApi, private val dailyMenusDao: DailyMenusDao ) { - // Client Heuristic: 앞 뒤 1일 정도를 캐싱해둔다. suspend fun syncWithServer(date: LocalDate) { withContext(Dispatchers.IO) { @@ -56,12 +54,12 @@ class MenuRepository @Inject constructor( return sikshaApi.fetchMenuById(menuId) } - fun getPagedReviewsByMenuIdFlow(menuId: Long): Flow> { - return Pager( - config = MenuReviewPagingSource.Config, - pagingSourceFactory = { MenuReviewPagingSource(sikshaApi, menuId) } - ).flow - } +// fun getPagedReviewsByMenuIdFlow(menuId: Long): Flow> { +// return Pager( +// config = MenuReviewPagingSource.Config, +// pagingSourceFactory = { MenuReviewPagingSource(sikshaApi, menuId) } +// ).flow +// } fun getPagedReviewsOnlyHaveImagesByMenuIdFlow(menuId: Long): Flow> { return Pager( @@ -70,6 +68,12 @@ class MenuRepository @Inject constructor( ).flow } + fun menuReviewPagingSource(menuId: Long): MenuReviewPagingSource = + MenuReviewPagingSource(sikshaApi, menuId) + + fun menuReviewWithImagePagingSource(menuId: Long): MenuReviewWithImagePagingSource = + MenuReviewWithImagePagingSource(sikshaApi, menuId) + suspend fun leaveMenuReview(menuId: Long, score: Double, comment: String): LeaveReviewResult { return sikshaApi.leaveMenuReview(LeaveReviewParam(menuId, score, comment)) } @@ -86,9 +90,9 @@ class MenuRepository @Inject constructor( return sikshaApi.fetchReviewDistribution(menuId).dist } - suspend fun getFirstReviewPhotoByMenuId(menuId: Long): FetchReviewsResult { - return sikshaApi.fetchReviewsWithImage(menuId, 1L, 5) - } +// suspend fun getFirstReviewPhotoByMenuId(menuId: Long): FetchReviewsResult { +// return sikshaApi.fetchReviewsWithImage(menuId, 1L, 5) +// } suspend fun likeMenuById(menuId: Long): Menu { return withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/SikshaColors.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/SikshaColors.kt index 80fb89fa..40073f47 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/SikshaColors.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/SikshaColors.kt @@ -3,7 +3,7 @@ package com.wafflestudio.siksha2.ui import androidx.compose.ui.graphics.Color object SikshaColors { - val OrangeMain = Color(0xFFFF952B) + val OrangeMain = Color(0xFFFF9522) val White900 = Color(0xFFFFFFFF) diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt index a2e859bd..8e49e416 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewFragment.kt @@ -130,7 +130,7 @@ class LeaveReviewFragment : Fragment() { } vm.leaveReviewState.observe(viewLifecycleOwner) { - binding.onLoadingContainer.root.setVisibleOrGone(it == MenuDetailViewModel.ReviewState.COMPRESSING) + binding.onLoadingContainer.root.setVisibleOrGone(it == LeaveReviewState.COMPRESSING) } binding.closeButton.setOnClickListener { diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewState.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewState.kt new file mode 100644 index 00000000..945106a4 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/LeaveReviewState.kt @@ -0,0 +1,6 @@ +package com.wafflestudio.siksha2.ui.menuDetail + +enum class LeaveReviewState { + WAITING, + COMPRESSING +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt index 67e58330..8562cdce 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailFragment.kt @@ -6,28 +6,18 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs -import androidx.paging.LoadState -import androidx.recyclerview.widget.LinearLayoutManager -import com.wafflestudio.siksha2.R +import com.wafflestudio.siksha2.compose.ui.menuDetail.MenuDetailRoute import com.wafflestudio.siksha2.databinding.FragmentMenuDetailBinding -import com.wafflestudio.siksha2.utils.dp -import com.wafflestudio.siksha2.utils.showToast -import com.wafflestudio.siksha2.utils.setVisibleOrGone +import com.wafflestudio.siksha2.ui.SikshaTheme import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch -import java.io.IOException -import kotlin.math.round @AndroidEntryPoint class MenuDetailFragment : Fragment() { private val vm: MenuDetailViewModel by activityViewModels() private lateinit var binding: FragmentMenuDetailBinding - private lateinit var reviewsAdapter: MenuReviewsAdapter private val args: MenuDetailFragmentArgs by navArgs() @@ -41,162 +31,32 @@ class MenuDetailFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - - reviewsAdapter = MenuReviewsAdapter(true, childFragmentManager) - - binding.reviewList.apply { - layoutManager = LinearLayoutManager(context) - adapter = reviewsAdapter - } - - viewLifecycleOwner.lifecycleScope.launch { - reviewsAdapter.loadStateFlow - .collectLatest { - if (it.refresh is LoadState.NotLoading) { - (reviewsAdapter.itemCount < 1).let { empty -> - binding.emptyList.setVisibleOrGone(empty) - binding.reviewList.setVisibleOrGone(empty.not()) - } - } - } - } - - vm.refreshMenu(args.menuId) - vm.refreshImages(args.menuId) - vm.refreshReviewDistribution(args.menuId) - - vm.networkResultState.observe(viewLifecycleOwner) { - binding.menuInfoContainer.setVisibleOrGone(it == MenuDetailViewModel.State.SUCCESS) - binding.onErrorContainer.root.setVisibleOrGone(it == MenuDetailViewModel.State.FAILED) - binding.onLoadingContainer.root.setVisibleOrGone(it == MenuDetailViewModel.State.LOADING) - } - - vm.menu.observe(viewLifecycleOwner) { menu -> - // for marquee - binding.menuTitle.isSelected = true - binding.menuTitle.text = menu?.nameKr - binding.menuRating.text = "${menu?.score?.times(10)?.let { round(it) / 10 } ?: "0.0"}" - binding.menuStars.rating = menu?.score?.toFloat() ?: 0.0f - binding.reviewCount.text = " ${menu?.reviewCount ?: 0}" - // Handle menu likes - menu.isLiked?.let { isLiked -> - binding.menuLikeButton.isSelected = isLiked - } - - // Handle like count - menu.likeCount?.let { count -> - binding.menuLikeCount.text = menu.likeCount?.let { "좋아요 $it 개" } ?: "-" - } - } - - vm.reviewDistribution.observe(viewLifecycleOwner) { distList -> - if (distList.isEmpty()) return@observe - val distBarList = listOf( - binding.distBar1, - binding.distBar2, - binding.distBar3, - binding.distBar4, - binding.distBar5 - ) - var maxCount = 1L - distList.forEach { if (maxCount < it) maxCount = it } - distBarList.forEachIndexed { index, bar -> - val params = bar.layoutParams - val ratio = distList[index].toDouble() / maxCount.toDouble() - if (ratio != 0.0) { - params.width = - (requireContext().dp(MAX_REVIEW_DIST_BAR_WIDTH_DP) * ratio).toInt() - } else { - params.width = requireContext().dp(NO_REVIEW_DIST_BAR_WIDTH_DP) - } - bar.layoutParams = params - bar.requestLayout() - } - } - - vm.imageCount.observe(viewLifecycleOwner) { imageCount -> - binding.layoutPhotoReview.setVisibleOrGone(imageCount > 0) - if (imageCount > 3) { - binding.reviewImageView3.showMorePhotos(imageCount - 3) - binding.reviewImageView3.setOnClickListener { - val action = - MenuDetailFragmentDirections.actionMenuDetailFragmentToReviewPhotoFragment( - args.menuId + binding.menuDetailComposeView.setContent { + SikshaTheme { + MenuDetailRoute( + menuId = args.menuId, + isTodayMenu = args.isTodayMenu, + onNavigateUp = { + findNavController().popBackStack() + }, + onNavigateToLeaveReview = { + findNavController().navigate( + MenuDetailFragmentDirections.actionMenuDetailFragmentToLeaveReviewFragment() // TODO: leaveReviewFragment로 이동 시 menuId arguement 전달하는 식으로 바꾸기 ) - findNavController().navigate(action) - } - } - } - - vm.imageUrlList.observe(viewLifecycleOwner) { imageUrlList -> - val imageReviewList = - listOf(binding.reviewImageView1, binding.reviewImageView2, binding.reviewImageView3) - for (i in 0 until 3) { - if (i < imageUrlList.size) { - imageReviewList[i].run { - setImage(imageUrlList[i]) - setVisibleOrGone(true) - } - } - } - for (i in 0 until 2) { - if (i < imageUrlList.size) { - imageReviewList[i].setOnClickListener { - val dialog = ReviewImageDialog.newInstance(imageUrlList[i]) - dialog.show(childFragmentManager, "review_image_${imageUrlList[i]}") - } - } - } - } - - viewLifecycleOwner.lifecycleScope.launch { - vm.getReviews(args.menuId).collectLatest { - reviewsAdapter.submitData(it) - } - } - - binding.backButton.setOnClickListener { - findNavController().popBackStack() - } - - binding.layoutCollectPhotoReviews.setOnClickListener { - val action = - MenuDetailFragmentDirections.actionMenuDetailFragmentToReviewPhotoFragment(args.menuId) - findNavController().navigate(action) - } - - binding.layoutCollectReviews.setOnClickListener { - val action = - MenuDetailFragmentDirections.actionMenuDetailFragmentToReviewFragment(args.menuId) - findNavController().navigate(action) - } - - binding.leaveReviewButton.setOnClickListener { - if (args.isTodayMenu) { - val action = - MenuDetailFragmentDirections.actionMenuDetailFragmentToLeaveReviewFragment() - findNavController().navigate(action) - } else { - showToast("오늘 메뉴만 평가할 수 있습니다.") - } - } - - binding.menuLikeButton.setOnClickListener { - vm.menu.value?.isLiked?.let { - viewLifecycleOwner.lifecycleScope.launch { - try { - vm.toggleLike(args.menuId, it) - } catch (e: IOException) { - showToast(getString(R.string.common_network_error)) - } - } + }, + onNavigateToReviewPhoto = { menuId -> + findNavController().navigate( + MenuDetailFragmentDirections.actionMenuDetailFragmentToReviewPhotoFragment(menuId) + ) + }, + onNavigateToReview = { menuId -> + findNavController().navigate( + MenuDetailFragmentDirections.actionMenuDetailFragmentToReviewFragment(menuId) + ) + }, + menuDetailViewModel = vm + ) } } } - - companion object { - private const val NO_REVIEW_DIST_BAR_WIDTH_DP = 8 - private const val MAX_REVIEW_DIST_BAR_WIDTH_DP = 180 - } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt index 721a2fad..b06f5f8c 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuDetailViewModel.kt @@ -6,8 +6,11 @@ import android.net.Uri import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.asFlow import androidx.lifecycle.viewModelScope +import androidx.paging.Pager import androidx.paging.PagingData +import androidx.paging.cachedIn import com.wafflestudio.siksha2.models.Menu import com.wafflestudio.siksha2.models.Review import com.wafflestudio.siksha2.repositories.MenuRepository @@ -18,7 +21,13 @@ import id.zelory.compressor.Compressor import id.zelory.compressor.constraint.format import id.zelory.compressor.constraint.resolution import id.zelory.compressor.constraint.size +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch import okhttp3.MediaType.Companion.toMediaTypeOrNull import okhttp3.MultipartBody @@ -39,9 +48,9 @@ class MenuDetailViewModel @Inject constructor( val commentHint: LiveData get() = _commentHint - private val _networkResultState = MutableLiveData() - val networkResultState: LiveData - get() = _networkResultState + private val _networkResultMenuLoadingState = MutableLiveData() + val networkResultMenuLoadingState: LiveData + get() = _networkResultMenuLoadingState private val _reviewDistribution = MutableLiveData>() val reviewDistribution: LiveData> @@ -51,55 +60,44 @@ class MenuDetailViewModel @Inject constructor( val imageUriList: LiveData> get() = _imageUriList - private val _imageUrlList = MutableLiveData>() - val imageUrlList: LiveData> - get() = _imageUrlList - - private val _imageCount = MutableLiveData(0) - val imageCount: LiveData - get() = _imageCount - - private val _leaveReviewState = MutableLiveData(ReviewState.WAITING) - val leaveReviewState: LiveData + private val _leaveReviewState = MutableLiveData(LeaveReviewState.WAITING) + val leaveReviewState: LiveData get() = _leaveReviewState + @OptIn(ExperimentalCoroutinesApi::class) + val reviews: StateFlow> = + _menu.asFlow().filterNotNull().flatMapLatest { menu -> + Pager( + config = MenuReviewPagingSource.Config, + pagingSourceFactory = { + menuRepository.menuReviewPagingSource(menu.id) + } + ).flow.cachedIn(viewModelScope) + }.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = PagingData.empty()) + + @OptIn(ExperimentalCoroutinesApi::class) + val reviewsWithImage: StateFlow> = + _menu.asFlow().filterNotNull().flatMapLatest { menu -> + Pager( + config = MenuReviewWithImagePagingSource.Config, + pagingSourceFactory = { + menuRepository.menuReviewWithImagePagingSource(menu.id) + } + ).flow.cachedIn(viewModelScope) + }.stateIn(viewModelScope, SharingStarted.Eagerly, initialValue = PagingData.empty()) + fun refreshMenu(menuId: Long) { - _networkResultState.value = State.LOADING + _networkResultMenuLoadingState.value = MenuLoadingState.LOADING viewModelScope.launch { try { _menu.value = menuRepository.getMenuById(menuId) - _networkResultState.value = State.SUCCESS + _networkResultMenuLoadingState.value = MenuLoadingState.SUCCESS } catch (e: IOException) { - _networkResultState.value = State.FAILED + _networkResultMenuLoadingState.value = MenuLoadingState.FAILED } } } - fun refreshImages(menuId: Long) { - viewModelScope.launch { - try { - val data = menuRepository.getFirstReviewPhotoByMenuId(menuId) - _imageCount.value = data.totalCount - val urlList = emptyList().toMutableList() - for (i in 0 until 3) { - if (i < data.result.size) { - data.result[i].etc?.images?.get(0)?.let { - urlList.add(it) - } - } - } - _imageUrlList.value = urlList - } catch (e: IOException) { - _imageUrlList.value = emptyList() - _networkResultState.value = State.FAILED - } - } - } - - fun getReviews(menuId: Long): Flow> { - return menuRepository.getPagedReviewsByMenuIdFlow(menuId) - } - fun getReviewsWithImages(menuId: Long): Flow> { return menuRepository.getPagedReviewsOnlyHaveImagesByMenuIdFlow(menuId) } @@ -150,22 +148,24 @@ class MenuDetailViewModel @Inject constructor( } fun notifySendReviewEnd() { - _leaveReviewState.value = ReviewState.WAITING + _leaveReviewState.value = LeaveReviewState.WAITING } - suspend fun toggleLike(id: Long, isCurrentlyLiked: Boolean) { - val updatedMenu = when (isCurrentlyLiked) { - true -> menuRepository.unlikeMenuById(id) - false -> menuRepository.likeMenuById(id) + fun toggleLike(id: Long, isCurrentlyLiked: Boolean) { + viewModelScope.launch { + val updatedMenu = when (isCurrentlyLiked) { + true -> menuRepository.unlikeMenuById(id) + false -> menuRepository.likeMenuById(id) + } + _menu.postValue(updatedMenu) } - _menu.postValue(updatedMenu) } suspend fun leaveReview(context: Context, score: Double, comment: String) { val menuId = _menu.value?.id ?: return if (_imageUriList.value?.isNotEmpty() == true) { context.showToast("이미지 압축 중입니다.") - _leaveReviewState.value = ReviewState.COMPRESSING + _leaveReviewState.value = LeaveReviewState.COMPRESSING val imageList = _imageUriList.value?.map { getCompressedImage(context, it) } @@ -189,15 +189,4 @@ class MenuDetailViewModel @Inject constructor( val requestBody = file.asRequestBody("image/jpeg".toMediaTypeOrNull()) return MultipartBody.Part.createFormData("images", file.name, requestBody) } - - enum class State { - LOADING, - SUCCESS, - FAILED - } - - enum class ReviewState { - WAITING, - COMPRESSING - } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuLoadingState.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuLoadingState.kt new file mode 100644 index 00000000..87f83179 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/MenuLoadingState.kt @@ -0,0 +1,7 @@ +package com.wafflestudio.siksha2.ui.menuDetail + +enum class MenuLoadingState { + LOADING, + SUCCESS, + FAILED +} diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewFragment.kt index 36cb6871..643eb8d0 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewFragment.kt @@ -6,24 +6,16 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.paging.LoadState -import androidx.recyclerview.widget.LinearLayoutManager +import com.wafflestudio.siksha2.compose.ui.menuDetail.review.ReviewRoute import com.wafflestudio.siksha2.databinding.FragmentReviewBinding +import com.wafflestudio.siksha2.ui.SikshaTheme import com.wafflestudio.siksha2.ui.menuDetail.MenuDetailViewModel -import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewsAdapter -import com.wafflestudio.siksha2.utils.setVisibleOrGone -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch class ReviewFragment : Fragment() { private lateinit var binding: FragmentReviewBinding private val vm: MenuDetailViewModel by activityViewModels() - private val args: ReviewFragmentArgs by navArgs() - private lateinit var reviewsAdapter: MenuReviewsAdapter override fun onCreateView( inflater: LayoutInflater, @@ -36,33 +28,14 @@ class ReviewFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - reviewsAdapter = MenuReviewsAdapter(false, childFragmentManager) - - binding.reviewList.apply { - layoutManager = LinearLayoutManager(context) - adapter = reviewsAdapter - } - - lifecycleScope.launch { - reviewsAdapter.loadStateFlow - .collectLatest { - if (it.refresh is LoadState.NotLoading) { - (reviewsAdapter.itemCount < 1).let { empty -> - binding.reviewList.setVisibleOrGone(empty.not()) - binding.textNoReviews.setVisibleOrGone(empty) - } - } - } - } - - lifecycleScope.launch { - vm.getReviews(args.menuId).collectLatest { - reviewsAdapter.submitData(it) + binding.reviewComposeView.setContent { + SikshaTheme { + ReviewRoute( + showImages = true, + onNavigateUp = { findNavController().popBackStack() }, + menuDetailViewModel = vm + ) } } - - binding.closeButton.setOnClickListener { - findNavController().popBackStack() - } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewPhotoFragment.kt b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewPhotoFragment.kt index 95e47c87..885416b5 100644 --- a/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewPhotoFragment.kt +++ b/app/src/main/java/com/wafflestudio/siksha2/ui/menuDetail/review/ReviewPhotoFragment.kt @@ -6,23 +6,15 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels -import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.findNavController -import androidx.navigation.fragment.navArgs -import androidx.paging.LoadState -import androidx.recyclerview.widget.LinearLayoutManager +import com.wafflestudio.siksha2.compose.ui.menuDetail.review.ReviewRoute import com.wafflestudio.siksha2.databinding.FragmentReviewPhotoBinding +import com.wafflestudio.siksha2.ui.SikshaTheme import com.wafflestudio.siksha2.ui.menuDetail.MenuDetailViewModel -import com.wafflestudio.siksha2.ui.menuDetail.MenuReviewsAdapter -import com.wafflestudio.siksha2.utils.setVisibleOrGone -import kotlinx.coroutines.flow.collectLatest -import kotlinx.coroutines.launch class ReviewPhotoFragment : Fragment() { private lateinit var binding: FragmentReviewPhotoBinding private val vm: MenuDetailViewModel by activityViewModels() - private val args: ReviewPhotoFragmentArgs by navArgs() - private lateinit var reviewsAdapter: MenuReviewsAdapter override fun onCreateView( inflater: LayoutInflater, @@ -35,33 +27,14 @@ class ReviewPhotoFragment : Fragment() { } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - reviewsAdapter = MenuReviewsAdapter(true, childFragmentManager) - - binding.reviewList.apply { - layoutManager = LinearLayoutManager(context) - adapter = reviewsAdapter - } - - lifecycleScope.launch { - reviewsAdapter.loadStateFlow - .collectLatest { - if (it.refresh is LoadState.NotLoading) { - (reviewsAdapter.itemCount < 1).let { empty -> - binding.reviewList.setVisibleOrGone(empty.not()) - binding.textNoReviews.setVisibleOrGone(empty) - } - } - } - } - - lifecycleScope.launch { - vm.getReviewsWithImages(args.menuId).collectLatest { - reviewsAdapter.submitData(it) + binding.reviewPhotoComposeView.setContent { + SikshaTheme { + ReviewRoute( + showImages = true, + onNavigateUp = { findNavController().popBackStack() }, + menuDetailViewModel = vm + ) } } - - binding.closeButton.setOnClickListener { - findNavController().popBackStack() - } } } diff --git a/app/src/main/java/com/wafflestudio/siksha2/utils/ComposeUtil.kt b/app/src/main/java/com/wafflestudio/siksha2/utils/ComposeUtil.kt new file mode 100644 index 00000000..c8af0044 --- /dev/null +++ b/app/src/main/java/com/wafflestudio/siksha2/utils/ComposeUtil.kt @@ -0,0 +1,8 @@ +package com.wafflestudio.siksha2.utils + +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.Dp + +@Composable +fun dpToSp(dp: Dp) = with(LocalDensity.current) { dp.toSp() } diff --git a/app/src/main/res/drawable/ic_speech_bubble.xml b/app/src/main/res/drawable/ic_speech_bubble.xml index 4fb8dcd4..6935e40a 100644 --- a/app/src/main/res/drawable/ic_speech_bubble.xml +++ b/app/src/main/res/drawable/ic_speech_bubble.xml @@ -5,6 +5,5 @@ android:viewportHeight="75"> + android:fillColor="#ffffff"/> diff --git a/app/src/main/res/layout/fragment_menu_detail.xml b/app/src/main/res/layout/fragment_menu_detail.xml index c8f4840f..0bd421a8 100644 --- a/app/src/main/res/layout/fragment_menu_detail.xml +++ b/app/src/main/res/layout/fragment_menu_detail.xml @@ -8,495 +8,9 @@ android:orientation="vertical" tools:context=".ui.menuDetail.MenuDetailFragment"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:id="@+id/menu_detail_compose_view"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_review.xml b/app/src/main/res/layout/fragment_review.xml index 7419ffe6..0eb28bfe 100644 --- a/app/src/main/res/layout/fragment_review.xml +++ b/app/src/main/res/layout/fragment_review.xml @@ -8,68 +8,9 @@ android:orientation="vertical" tools:context=".ui.menuDetail.review.ReviewFragment"> - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:id="@+id/review_compose_view"/> \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_review_photo.xml b/app/src/main/res/layout/fragment_review_photo.xml index b9803614..4de0ba14 100644 --- a/app/src/main/res/layout/fragment_review_photo.xml +++ b/app/src/main/res/layout/fragment_review_photo.xml @@ -8,68 +8,9 @@ android:orientation="vertical" tools:context=".ui.menuDetail.review.ReviewPhotoFragment"> - - - - - - - - - - - - - - + android:layout_height="match_parent" + android:id="@+id/review_photo_compose_view"/> \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 42cdfe59..7535e4a5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -42,6 +42,11 @@ 사진 리뷰 모아보기 리뷰 리뷰가 없습니다. + 좋아요  + + 총  + + 이 평가했어요! 순서를 지정할 식당이 없습니다. 즐겨찾기에 추가된 식당이 없습니다.\n식당 탭에서 별을 눌러 추가해보세요. 네트워크 연결이 불안정합니다.