diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index e9144caad..a75284653 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -13,4 +13,6 @@ dependencies { api(libs.kakao.user) implementation(libs.androidx.security.crypto) implementation(libs.androidx.core.ktx) + + implementation(libs.androidx.exifinterface) } diff --git a/core/common/src/main/java/com/neki/android/core/common/util/ByteArray.kt b/core/common/src/main/java/com/neki/android/core/common/util/ByteArray.kt index 3b3610dee..d88bf2feb 100644 --- a/core/common/src/main/java/com/neki/android/core/common/util/ByteArray.kt +++ b/core/common/src/main/java/com/neki/android/core/common/util/ByteArray.kt @@ -3,7 +3,9 @@ package com.neki.android.core.common.util import android.content.Context import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.Matrix import android.net.Uri +import androidx.exifinterface.media.ExifInterface import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.io.ByteArrayOutputStream @@ -16,8 +18,22 @@ fun Uri.toByteArray( quality: Int = DEFAULT_QUALITY, format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, ): ByteArray? { + val orientation = context.contentResolver.openInputStream(this)?.use { input -> + ExifInterface(input).getAttributeInt( + ExifInterface.TAG_ORIENTATION, + ExifInterface.ORIENTATION_UNDEFINED, + ) + } ?: ExifInterface.ORIENTATION_UNDEFINED val bytes = context.contentResolver.openInputStream(this)?.use { it.readBytes() } ?: return null - return bytes.compress(quality, format) + val bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return null + val rotatedBitmap = bitmap.applyOrientation(orientation) + + return ByteArrayOutputStream().use { outputStream -> + rotatedBitmap.compress(format, quality, outputStream) + if (rotatedBitmap !== bitmap) bitmap.recycle() + rotatedBitmap.recycle() + outputStream.toByteArray() + } } suspend fun String.urlToByteArray( @@ -28,6 +44,29 @@ suspend fun String.urlToByteArray( bytes.compress(quality, format) } +private fun Bitmap.applyOrientation(orientation: Int): Bitmap { + val matrix = Matrix() + when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f) + ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f) + ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f) + ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.postScale(-1f, 1f) + ExifInterface.ORIENTATION_FLIP_VERTICAL -> matrix.postScale(1f, -1f) + ExifInterface.ORIENTATION_TRANSPOSE -> { + matrix.postRotate(90f) + matrix.postScale(-1f, 1f) + } + + ExifInterface.ORIENTATION_TRANSVERSE -> { + matrix.postRotate(270f) + matrix.postScale(-1f, 1f) + } + + else -> return this + } + return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) +} + private fun ByteArray.compress( quality: Int, format: Bitmap.CompressFormat, diff --git a/core/data/src/main/java/com/neki/android/core/data/remote/api/PhotoService.kt b/core/data/src/main/java/com/neki/android/core/data/remote/api/PhotoService.kt index 051d14236..2ef868aec 100644 --- a/core/data/src/main/java/com/neki/android/core/data/remote/api/PhotoService.kt +++ b/core/data/src/main/java/com/neki/android/core/data/remote/api/PhotoService.kt @@ -54,7 +54,7 @@ class PhotoService @Inject constructor( }.body() } - // 즐겨찾기 사진 조회 + // 즐겨찾는 앨범 조회 suspend fun getFavoritePhotos( page: Int = 0, size: Int = 20, diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/logo/NekiTypoLogo.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/logo/NekiTypoLogo.kt new file mode 100644 index 000000000..a3fe50d5c --- /dev/null +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/logo/NekiTypoLogo.kt @@ -0,0 +1,79 @@ +package com.neki.android.core.designsystem.logo + +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.R +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +private fun NekiTypoLogo( + color: Color, + modifier: Modifier = Modifier, +) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.icon_neki_logo_typo), + contentDescription = null, + tint = color, + modifier = modifier, + ) +} + +@Composable +fun PrimaryNekiTypoLogo( + modifier: Modifier = Modifier, +) { + NekiTypoLogo( + color = NekiTheme.colorScheme.primary400, + modifier = modifier, + ) +} + +@Composable +fun GrayNekiTypoLogo( + modifier: Modifier = Modifier, +) { + NekiTypoLogo( + color = NekiTheme.colorScheme.gray900, + modifier = modifier, + ) +} + +@Composable +fun WhiteNekiTypoLogo( + modifier: Modifier = Modifier, +) { + NekiTypoLogo( + color = NekiTheme.colorScheme.white, + modifier = modifier, + ) +} + +@ComponentPreview +@Composable +private fun PrimaryNekiTypoLogoPreview() { + NekiTheme { + PrimaryNekiTypoLogo() + } +} + +@ComponentPreview +@Composable +private fun GrayNekiTypoLogoPreview() { + NekiTheme { + GrayNekiTypoLogo() + } +} + +@Preview +@Composable +private fun WhiteNekiTypoLogoPreview() { + NekiTheme { + WhiteNekiTypoLogo() + } +} diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt index b93915064..df3370121 100644 --- a/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Background.kt @@ -2,14 +2,11 @@ package com.neki.android.core.designsystem.modifier import androidx.compose.foundation.background import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.alpha import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState @@ -56,28 +53,29 @@ fun Modifier.poseBackground( * 블러 효과가 적용된 배경을 설정하는 Modifier 확장 함수 * * @param hazeState Haze 블러 효과를 관리하는 상태 객체 - * @param enabled 블러 효과 활성화 여부 (false일 경우 단색 배경 적용) + * @param alpha 블러 틴트에 적용될 색상의 알파 값 * @param color 블러 효과에 적용될 배경 색상 - * @param defaultBackgroundColor 블러 비활성화 시 적용될 기본 배경 색상 * @param blurRadius 블러 효과의 반경 + * @param enabled 블러 효과 활성화 여부 (false일 경우 단색 배경 적용) + * @param defaultBackgroundColor 블러 비활성화 시 적용될 기본 배경 색상 */ -@Composable fun Modifier.backgroundHazeBlur( hazeState: HazeState, + alpha: Float, + color: Color, + blurRadius: Dp, enabled: Boolean = true, - color: Color = Color(0xFF202227).copy(alpha = 0.9f), defaultBackgroundColor: Color = color, - blurRadius: Dp = 12.dp, ): Modifier = if (enabled) { this.hazeEffect( state = hazeState, style = HazeStyle( backgroundColor = color, - tint = HazeTint( - color.copy(alpha = if (color.luminance() >= 0.5) 0.6f else 0.65f), - ), + tint = HazeTint(color.copy(alpha = alpha)), blurRadius = blurRadius, ), ) - } else this.background(color = defaultBackgroundColor) + } else this.background( + color = defaultBackgroundColor, + ) diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/popup/ToolTipPopup.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/popup/ToolTipPopup.kt new file mode 100644 index 000000000..1ea95a9bd --- /dev/null +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/popup/ToolTipPopup.kt @@ -0,0 +1,116 @@ +package com.neki.android.core.designsystem.popup + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +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.shape.RoundedCornerShape +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.graphics.Path +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +fun ToolTipPopup( + tooltipText: String, + color: Color, + offset: IntOffset, + alignment: Alignment, + onDismissRequest: () -> Unit, +) { + Popup( + alignment = alignment, + offset = offset, + onDismissRequest = onDismissRequest, + ) { + ToolTipContent( + tooltipText = tooltipText, + color = color, + ) + } +} + +@Composable +private fun ToolTipContent( + tooltipText: String, + color: Color, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.width(IntrinsicSize.Max), + ) { + // 꼬리 (오른쪽 정렬, 오른쪽에서 16dp) + Box( + modifier = Modifier + .fillMaxWidth() + .padding(end = 16.dp), + contentAlignment = Alignment.CenterEnd, + ) { + Canvas( + modifier = Modifier.size(width = 10.dp, height = 8.dp), + ) { + val cornerRadius = 1.dp.toPx() + val path = Path().apply { + // 왼쪽 하단에서 시작 + moveTo(0f, size.height) + // 왼쪽 하단 -> 꼭대기 (둥근 모서리) + lineTo( + size.width / 2 - cornerRadius, + cornerRadius, + ) + quadraticTo( + size.width / 2, + 0f, + size.width / 2 + cornerRadius, + cornerRadius, + ) + // 꼭대기 -> 오른쪽 하단 + lineTo(size.width, size.height) + close() + } + drawPath(path, color) + } + } + + // 몸통 + Box( + modifier = Modifier + .background( + color = color, + shape = RoundedCornerShape(8.dp), + ) + .padding(horizontal = 12.dp, vertical = 8.dp), + ) { + Text( + text = tooltipText, + style = NekiTheme.typography.body14Medium, + color = NekiTheme.colorScheme.white, + ) + } + } +} + +@ComponentPreview +@Composable +private fun ToolTipPopupPreview() { + NekiTheme { + Box(modifier = Modifier.padding(16.dp)) { + ToolTipContent( + tooltipText = "툴팁 메시지입니다", + color = NekiTheme.colorScheme.gray800, + ) + } + } +} diff --git a/core/designsystem/src/main/res/drawable/icon_empty_content.xml b/core/designsystem/src/main/res/drawable/icon_empty_content.xml new file mode 100644 index 000000000..4206622d3 --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_empty_content.xml @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/core/designsystem/src/main/res/drawable/icon_neki_logo_typo.xml b/core/designsystem/src/main/res/drawable/icon_neki_logo_typo.xml new file mode 100644 index 000000000..2a701695d --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_neki_logo_typo.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/core/ui/build.gradle.kts b/core/ui/build.gradle.kts index d4a92c5a3..5be11ac34 100644 --- a/core/ui/build.gradle.kts +++ b/core/ui/build.gradle.kts @@ -18,6 +18,8 @@ dependencies { implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.kotlinx.collections.immutable) + implementation(libs.lottie.compose) + api(libs.coil.compose) api(libs.coil.network.okhttp) } diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt index 606c7b9f7..c48cc51b1 100644 --- a/core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/AlbumRowComponent.kt @@ -45,7 +45,7 @@ fun FavoriteAlbumRowComponent( ) AlbumInfo( - title = "즐겨찾는 사진", + title = "즐겨찾기", photoCount = album.photoCount, ) } @@ -168,7 +168,7 @@ private fun FavoriteAlbumRowComponentPreview() { FavoriteAlbumRowComponent( album = AlbumPreview( id = 0, - title = "즐겨찾는 사진", + title = "즐겨찾기", ), ) } diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/ItemOverlay.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/ItemOverlay.kt new file mode 100644 index 000000000..661f1104e --- /dev/null +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/ItemOverlay.kt @@ -0,0 +1,61 @@ +package com.neki.android.core.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +fun PhotoGridItemOverlay( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(8.dp), +) { + Box( + modifier = modifier + .background( + color = Color.Black.copy(alpha = 0.04f), + shape = shape, + ) + .background( + brush = Brush.verticalGradient( + colorStops = arrayOf( + 0f to Color.Black.copy(alpha = 0.2f), + 134f / 242f to Color.Black.copy(alpha = 0f), + ), + ), + shape = shape, + ), + ) +} + +@Composable +fun SelectedPhotoGridItemOverlay( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(8.dp), +) { + Box( + modifier = modifier.background( + color = Color.Black.copy(alpha = 0.2f), + shape = shape, + ), + ) +} + +@ComponentPreview +@Composable +private fun PhotoGridItemOverlayPreview() { + NekiTheme { + PhotoGridItemOverlay( + modifier = Modifier + .size(80.dp), + ) + } +} diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/LoadingIndicator.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/LoadingIndicator.kt index c6f16c7db..89e89829b 100644 --- a/core/ui/src/main/java/com/neki/android/core/ui/component/LoadingIndicator.kt +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/LoadingIndicator.kt @@ -1,20 +1,26 @@ package com.neki.android.core.ui.component -import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.LottieConstants +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.ui.theme.NekiTheme +import com.neki.android.core.ui.R @Composable fun LoadingDialog( modifier: Modifier = Modifier, - circleColor: Color = NekiTheme.colorScheme.primary300, - backgroundColor: Color = NekiTheme.colorScheme.primary100, + size: Dp = 200.dp, properties: DialogProperties = DialogProperties(), onDismissRequest: () -> Unit = {}, ) { @@ -22,11 +28,8 @@ fun LoadingDialog( onDismissRequest = onDismissRequest, properties = properties, ) { - CircularProgressIndicator( - modifier = modifier, - strokeWidth = 6.dp, - color = circleColor, - trackColor = backgroundColor, + LoadingIndicator( + modifier = modifier.size(size), ) } } @@ -34,14 +37,20 @@ fun LoadingDialog( @Composable fun LoadingIndicator( modifier: Modifier = Modifier, - circleColor: Color = NekiTheme.colorScheme.primary300, - backgroundColor: Color = NekiTheme.colorScheme.primary100, + size: Dp = 80.dp, ) { - CircularProgressIndicator( - modifier = modifier, - strokeWidth = 6.dp, - color = circleColor, - trackColor = backgroundColor, + val composition by rememberLottieComposition( + spec = LottieCompositionSpec.RawRes(R.raw.loading_animation), + ) + val progress by animateLottieCompositionAsState( + composition = composition, + iterations = LottieConstants.IterateForever, + ) + + LottieAnimation( + composition = composition, + progress = { progress }, + modifier = modifier.size(size), ) } @@ -54,3 +63,11 @@ private fun LoadingDialogPreview() { ) } } + +@ComponentPreview +@Composable +private fun LoadingIndicatorPreview() { + NekiTheme { + LoadingIndicator() + } +} diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/PhotoComponent.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/PhotoComponent.kt index 2cb84b113..da60ed37f 100644 --- a/core/ui/src/main/java/com/neki/android/core/ui/component/PhotoComponent.kt +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/PhotoComponent.kt @@ -1,7 +1,6 @@ package com.neki.android.core.ui.component import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight @@ -22,11 +21,10 @@ fun PhotoComponent( photo: Photo, modifier: Modifier = Modifier, onClickItem: (Photo) -> Unit = {}, - additionalContent: @Composable BoxScope.() -> Unit = {}, ) { Box( modifier = modifier - .clip(RoundedCornerShape(12.dp)) + .clip(RoundedCornerShape(8.dp)) .noRippleClickable { onClickItem(photo) }, ) { AsyncImage( @@ -37,8 +35,6 @@ fun PhotoComponent( contentDescription = null, contentScale = ContentScale.FillWidth, ) - - additionalContent() } } diff --git a/core/ui/src/main/res/raw/loading_animation.json b/core/ui/src/main/res/raw/loading_animation.json new file mode 100644 index 000000000..cc841a1d7 --- /dev/null +++ b/core/ui/src/main/res/raw/loading_animation.json @@ -0,0 +1 @@ +{"nm":"Main Scene","ddd":0,"h":1080,"w":1920,"meta":{"g":"@lottiefiles/creator 1.74.0"},"layers":[{"ty":4,"nm":"Shape Layer 4","sr":1,"st":0,"op":360,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-284,92,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[50,50,100],"t":25},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[75,75,100],"t":39},{"s":[50,50,100],"t":55}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[1142,540],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[1142,540,0],"t":25,"ti":[0,0,0],"to":[0,-6.667,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[1142,500,0],"t":39,"ti":[0,-6.667,0],"to":[0,0,0]},{"s":[1142,540,0],"t":55}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[25],"t":25},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":39},{"s":[25],"t":55}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[120,120],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.3373,0.2784],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-284,92],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":1},{"ty":4,"nm":"Shape Layer 3","sr":1,"st":0,"op":360,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-284,92,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[50,50,100],"t":17},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[75,75,100],"t":31},{"s":[50,50,100],"t":47}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[1022,540],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[1022,540,0],"t":17,"ti":[0,0,0],"to":[0,-6.667,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[1022,500,0],"t":31,"ti":[0,-6.667,0],"to":[0,0,0]},{"s":[1022,540,0],"t":47}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[25],"t":17},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":31},{"s":[25],"t":47}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[120,120],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.3373,0.2784],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-284,92],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":2},{"ty":4,"nm":"Shape Layer 2","sr":1,"st":0,"op":360,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-284,92,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[50,50,100],"t":9},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[75,75,100],"t":23},{"s":[50,50,100],"t":39}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.167,"y":0.167},"i":{"x":0.833,"y":0.833},"s":[902,540],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[902,540,0],"t":9,"ti":[0,0,0],"to":[0,-6.667,0]},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[902,500,0],"t":23,"ti":[0,0,0],"to":[0,0,0]},{"s":[902,540,0],"t":39}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[25],"t":9},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":23},{"s":[25],"t":39}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[120,120],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.3373,0.2784],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-284,92],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":3},{"ty":4,"nm":"Shape Layer 1","sr":1,"st":0,"op":360,"ip":0,"hd":false,"ddd":0,"bm":0,"hasMask":false,"ao":0,"ks":{"a":{"a":0,"k":[-284,92,0],"ix":1},"s":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[50,50,100],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[75,75,100],"t":14},{"s":[50,50,100],"t":30}],"ix":6},"sk":{"a":0,"k":0},"p":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[782,540],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[782,500,0],"t":14,"ti":[0,-6.667,0],"to":[0,0,0]},{"s":[782,540,0],"t":30}],"ix":2},"r":{"a":0,"k":0,"ix":10},"sa":{"a":0,"k":0},"o":{"a":1,"k":[{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[25],"t":0},{"o":{"x":0.333,"y":0},"i":{"x":0.667,"y":1},"s":[100],"t":14},{"s":[25],"t":30}],"ix":11}},"shapes":[{"ty":"gr","bm":0,"hd":false,"mn":"ADBE Vector Group","nm":"Ellipse 1","ix":1,"cix":2,"np":3,"it":[{"ty":"el","bm":0,"hd":false,"mn":"ADBE Vector Shape - Ellipse","nm":"Ellipse Path 1","d":1,"p":{"a":0,"k":[0,0],"ix":3},"s":{"a":0,"k":[120,120],"ix":2}},{"ty":"fl","bm":0,"hd":false,"mn":"ADBE Vector Graphic - Fill","nm":"Fill 1","c":{"a":0,"k":[1,0.3373,0.2784],"ix":4},"r":1,"o":{"a":0,"k":100,"ix":5}},{"ty":"tr","a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"sk":{"a":0,"k":0,"ix":4},"p":{"a":0,"k":[-284,92],"ix":2},"r":{"a":0,"k":0,"ix":6},"sa":{"a":0,"k":0,"ix":5},"o":{"a":0,"k":100,"ix":7}}]}],"ind":4}],"v":"5.7.0","fr":60,"op":81,"ip":0,"assets":[]} \ No newline at end of file diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album/AllAlbumScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album/AllAlbumScreen.kt index 2648fc37a..3fa5f9545 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album/AllAlbumScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album/AllAlbumScreen.kt @@ -2,7 +2,6 @@ package com.neki.android.feature.archive.impl.album import androidx.activity.compose.BackHandler import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -90,7 +89,6 @@ internal fun AllAlbumScreen( LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), ) { item { FavoriteAlbumRowComponent( @@ -165,7 +163,7 @@ private fun AllAlbumScreenPreview() { NekiTheme { AllAlbumScreen( uiState = AllAlbumState( - favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾는 사진", photoCount = 3), + favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾기", photoCount = 3), albums = persistentListOf( AlbumPreview(id = 1, title = "제주도 여행 2024", photoCount = 4), AlbumPreview(id = 2, title = "가족 생일파티", photoCount = 2), @@ -183,7 +181,7 @@ private fun AllAlbumScreenSelectingPreview() { NekiTheme { AllAlbumScreen( uiState = AllAlbumState( - favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾는 사진", photoCount = 3), + favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾기", photoCount = 3), albums = persistentListOf( AlbumPreview(id = 1, title = "제주도 여행 2024", photoCount = 4), AlbumPreview(id = 2, title = "가족 생일파티", photoCount = 2), diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt index aad0acc6e..7021384ba 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/AlbumDetailScreen.kt @@ -9,33 +9,36 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.runtime.Composable +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.paging.LoadState +import androidx.paging.PagingData import androidx.paging.compose.LazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.itemKey -import com.neki.android.core.designsystem.topbar.BackTitleTextButtonTopBar -import com.neki.android.core.designsystem.topbar.BackTitleTopBar import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Photo import com.neki.android.core.ui.component.DoubleButtonOptionBottomSheet import com.neki.android.core.ui.component.LoadingDialog import com.neki.android.core.ui.compose.collectWithLifecycle import com.neki.android.core.ui.toast.NekiToast -import com.neki.android.feature.archive.impl.album_detail.component.EmptyContent +import com.neki.android.feature.archive.impl.album_detail.component.AlbumDetailTopBar import com.neki.android.feature.archive.impl.component.DeletePhotoDialog +import com.neki.android.feature.archive.impl.component.EmptyContent import com.neki.android.feature.archive.impl.component.SelectablePhotoItem import com.neki.android.feature.archive.impl.const.ArchiveConst.ARCHIVE_GRID_ITEM_SPACING import com.neki.android.feature.archive.impl.const.ArchiveConst.PHOTO_GRAY_LAYOUT_BOTTOM_PADDING @@ -45,6 +48,7 @@ import com.neki.android.feature.archive.impl.model.SelectMode import com.neki.android.feature.archive.impl.photo.component.PhotoActionBar import com.neki.android.feature.archive.impl.util.ImageDownloader import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.flow.flowOf @Composable internal fun AlbumDetailRoute( @@ -93,21 +97,73 @@ internal fun AlbumDetailScreen( ) { val lazyState = rememberLazyStaggeredGridState() - val isRefreshing = pagingItems.loadState.refresh is LoadState.Loading - val isEmpty = pagingItems.itemCount == 0 && pagingItems.loadState.refresh is LoadState.NotLoading + val isRefreshing by remember { + derivedStateOf { pagingItems.loadState.refresh is LoadState.Loading } + } + val isEmpty by remember { + derivedStateOf { pagingItems.itemCount == 0 && pagingItems.loadState.refresh is LoadState.NotLoading } + } BackHandler(enabled = true) { onIntent(AlbumDetailIntent.OnBackPressed) } + if (isEmpty) { + EmptyContent( + title = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title, + onClickBack = { onIntent(AlbumDetailIntent.ClickBackIcon) }, + ) + } else { + AlbumDetailContent( + uiState = uiState, + pagingItems = pagingItems, + lazyState = lazyState, + onIntent = onIntent, + ) + } + + if (isRefreshing || uiState.isLoading) { + LoadingDialog() + } + + if (uiState.isShowDeleteDialog) { + DeletePhotoDialog( + onDismissRequest = { onIntent(AlbumDetailIntent.DismissDeleteDialog) }, + onClickDelete = { onIntent(AlbumDetailIntent.ClickDeleteDialogConfirmButton) }, + onClickCancel = { onIntent(AlbumDetailIntent.ClickDeleteDialogCancelButton) }, + ) + } + + if (uiState.isShowDeleteBottomSheet) { + DoubleButtonOptionBottomSheet( + title = "사진을 삭제하시겠어요?", + options = PhotoDeleteOption.entries.toImmutableList(), + selectedOption = uiState.selectedDeleteOption, + primaryButtonText = "삭제하기", + secondaryButtonText = "취소", + onDismissRequest = { onIntent(AlbumDetailIntent.DismissDeleteBottomSheet) }, + onClickSecondaryButton = { onIntent(AlbumDetailIntent.ClickDeleteBottomSheetCancelButton) }, + onClickPrimaryButton = { onIntent(AlbumDetailIntent.ClickDeleteBottomSheetConfirmButton) }, + onOptionSelect = { onIntent(AlbumDetailIntent.SelectDeleteOption(it)) }, + ) + } +} + +@Composable +internal fun AlbumDetailContent( + uiState: AlbumDetailState, + pagingItems: LazyPagingItems, + lazyState: LazyStaggeredGridState, + modifier: Modifier = Modifier, + onIntent: (AlbumDetailIntent) -> Unit = {}, +) { Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(NekiTheme.colorScheme.white), ) { AlbumDetailTopBar( - hasNoPhoto = isEmpty, - title = if (uiState.isFavoriteAlbum) "즐겨찾는 사진" else uiState.title, + title = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title, selectMode = uiState.selectMode, onClickBack = { onIntent(AlbumDetailIntent.ClickBackIcon) }, onClickSelect = { onIntent(AlbumDetailIntent.ClickSelectButton) }, @@ -173,73 +229,29 @@ internal fun AlbumDetailScreen( onClickDelete = { onIntent(AlbumDetailIntent.ClickDeleteIcon) }, ) } +} - if (isRefreshing || uiState.isLoading) { - LoadingDialog() - } - - if (isEmpty) { - EmptyContent( - isFavorite = uiState.isFavoriteAlbum, +@Preview +@Composable +private fun AlbumDetailScreenPreview() { + val photos = (0..10).map { + Photo( + id = it.toLong(), + imageUrl = "", + isFavorite = false, + date = "2024-04-2$it", ) } - if (uiState.isShowDeleteDialog) { - DeletePhotoDialog( - onDismissRequest = { onIntent(AlbumDetailIntent.DismissDeleteDialog) }, - onClickDelete = { onIntent(AlbumDetailIntent.ClickDeleteDialogConfirmButton) }, - onClickCancel = { onIntent(AlbumDetailIntent.ClickDeleteDialogCancelButton) }, - ) - } + val pagingData = PagingData.from(photos) + val pagingItems = flowOf(pagingData).collectAsLazyPagingItems() - if (uiState.isShowDeleteBottomSheet) { - DoubleButtonOptionBottomSheet( - title = "사진을 삭제하시겠어요?", - options = PhotoDeleteOption.entries.toImmutableList(), - selectedOption = uiState.selectedDeleteOption, - primaryButtonText = "삭제하기", - secondaryButtonText = "취소", - onDismissRequest = { onIntent(AlbumDetailIntent.DismissDeleteBottomSheet) }, - onClickSecondaryButton = { onIntent(AlbumDetailIntent.ClickDeleteBottomSheetCancelButton) }, - onClickPrimaryButton = { onIntent(AlbumDetailIntent.ClickDeleteBottomSheetConfirmButton) }, - onOptionSelect = { onIntent(AlbumDetailIntent.SelectDeleteOption(it)) }, - ) - } -} - -@Composable -private fun AlbumDetailTopBar( - title: String, - selectMode: SelectMode, - onClickBack: () -> Unit, - onClickSelect: () -> Unit, - onClickCancel: () -> Unit, - modifier: Modifier = Modifier, - hasNoPhoto: Boolean = false, -) { - if (hasNoPhoto) { - BackTitleTopBar( - modifier = modifier, - title = title, - onBack = onClickBack, - ) - } else { - BackTitleTextButtonTopBar( - modifier = modifier, - title = title, - buttonLabel = when (selectMode) { - SelectMode.DEFAULT -> "선택" - SelectMode.SELECTING -> "취소" - }, - enabledTextColor = when (selectMode) { - SelectMode.DEFAULT -> NekiTheme.colorScheme.primary500 - SelectMode.SELECTING -> NekiTheme.colorScheme.gray800 - }, - onBack = onClickBack, - onClickTextButton = when (selectMode) { - SelectMode.DEFAULT -> onClickSelect - SelectMode.SELECTING -> onClickCancel - }, + NekiTheme { + AlbumDetailScreen( + uiState = AlbumDetailState( + title = "앨범 상세", + ), + pagingItems = pagingItems, ) } } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt new file mode 100644 index 000000000..780b10473 --- /dev/null +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/AlbumDetailTopBar.kt @@ -0,0 +1,58 @@ +package com.neki.android.feature.archive.impl.album_detail.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.topbar.BackTitleTextButtonTopBar +import com.neki.android.core.designsystem.ui.theme.NekiTheme +import com.neki.android.feature.archive.impl.model.SelectMode + +@Composable +internal fun AlbumDetailTopBar( + title: String, + selectMode: SelectMode, + modifier: Modifier = Modifier, + onClickBack: () -> Unit = {}, + onClickSelect: () -> Unit = {}, + onClickCancel: () -> Unit = {}, +) { + BackTitleTextButtonTopBar( + modifier = modifier, + title = title, + buttonLabel = when (selectMode) { + SelectMode.DEFAULT -> "선택" + SelectMode.SELECTING -> "취소" + }, + enabledTextColor = when (selectMode) { + SelectMode.DEFAULT -> NekiTheme.colorScheme.primary500 + SelectMode.SELECTING -> NekiTheme.colorScheme.gray800 + }, + onBack = onClickBack, + onClickTextButton = when (selectMode) { + SelectMode.DEFAULT -> onClickSelect + SelectMode.SELECTING -> onClickCancel + }, + ) +} + +@ComponentPreview +@Composable +private fun AlbumDetailTopBarPreview() { + NekiTheme { + AlbumDetailTopBar( + title = "Album Title", + selectMode = SelectMode.DEFAULT, + ) + } +} + +@ComponentPreview +@Composable +private fun AlbumDetailTopBarSelectingPreview() { + NekiTheme { + AlbumDetailTopBar( + title = "Album Title", + selectMode = SelectMode.SELECTING, + ) + } +} diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/EmptyContent.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt similarity index 50% rename from feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/EmptyContent.kt rename to feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt index b7709dec4..402073e14 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/album_detail/component/EmptyContent.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt @@ -1,47 +1,53 @@ -package com.neki.android.feature.archive.impl.album_detail.component +package com.neki.android.feature.archive.impl.component -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.fillMaxSize -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.R +import com.neki.android.core.designsystem.topbar.BackTitleTopBar import com.neki.android.core.designsystem.ui.theme.NekiTheme +private const val EMPTY_TEXT = "아직 등록된 사진이 없어요\n새로운 사진을 등록하고 앨범에 추가해보세요!" + @Composable internal fun EmptyContent( - isFavorite: Boolean, + title: String, modifier: Modifier = Modifier, + emptyText: String = EMPTY_TEXT, + onClickBack: () -> Unit = {}, ) { Box( modifier = modifier.fillMaxSize(), ) { + EmptyTopBar( + title = title, + onClickBack = onClickBack, + ) + Column( modifier = Modifier.align(Alignment.Center), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(22.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), ) { - Box( - modifier = Modifier - .size(104.dp) - .clip(CircleShape) - .background( - color = NekiTheme.colorScheme.gray50, - shape = CircleShape, - ), + Icon( + imageVector = ImageVector.vectorResource(R.drawable.icon_empty_content), + contentDescription = null, + tint = Color.Unspecified, ) Text( - text = if (isFavorite) "아직 등록된 사진이 없어요\n아카이빙 페이지에서 추가해보세요!" - else "아직 등록된 사진이 없어요\n아카이빙 페이지에서 추가해보세요!", + text = emptyText, style = NekiTheme.typography.body14Medium, color = NekiTheme.colorScheme.gray300, textAlign = TextAlign.Center, @@ -50,14 +56,17 @@ internal fun EmptyContent( } } -@ComponentPreview @Composable -private fun FavoriteEmptyContentPreview() { - NekiTheme { - EmptyContent( - isFavorite = true, - ) - } +private fun EmptyTopBar( + title: String, + onClickBack: () -> Unit, + modifier: Modifier = Modifier, +) { + BackTitleTopBar( + modifier = modifier, + title = title, + onBack = onClickBack, + ) } @ComponentPreview @@ -65,7 +74,7 @@ private fun FavoriteEmptyContentPreview() { private fun EmptyContentPreview() { NekiTheme { EmptyContent( - isFavorite = false, + title = "즐겨찾기", ) } } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelcetablePhotoItem.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt similarity index 76% rename from feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelcetablePhotoItem.kt rename to feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt index efc06c283..f5ce6067b 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelcetablePhotoItem.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt @@ -1,7 +1,7 @@ package com.neki.android.feature.archive.impl.component -import androidx.compose.foundation.background import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -10,55 +10,53 @@ 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.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.modifier.noRippleClickable -import com.neki.android.core.designsystem.modifier.photoBackground import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Photo import com.neki.android.core.ui.component.PhotoComponent +import com.neki.android.core.ui.component.PhotoGridItemOverlay +import com.neki.android.core.ui.component.SelectedPhotoGridItemOverlay import com.neki.android.core.ui.component.SelectionCheckbox @Composable internal fun SelectablePhotoItem( photo: Photo, isSelected: Boolean, - isSelectMode: Boolean = false, modifier: Modifier = Modifier, + isSelectMode: Boolean = false, onClickItem: (Photo) -> Unit = {}, onClickSelect: (Photo) -> Unit = {}, - onClickFavorite: (Photo) -> Unit = {}, ) { - PhotoComponent( - photo = photo, - modifier = modifier.then( - if (isSelected) Modifier - .border( - width = 2.dp, - color = NekiTheme.colorScheme.primary400, - shape = RoundedCornerShape(12.dp), - ) - .background( - color = Color.Black.copy(alpha = 0.2f), - shape = RoundedCornerShape(12.dp), - ) else Modifier - .clip(RoundedCornerShape(12.dp)) - .photoBackground(), - ), - onClickItem = onClickItem, + Box( + modifier = modifier, ) { - if (isSelectMode) { - SelectionCheckbox( - isSelected = isSelected, - modifier = Modifier - .align(Alignment.TopStart) - .padding(top = 12.dp, start = 12.dp) - .noRippleClickable { onClickSelect(photo) }, - unselectedColor = NekiTheme.colorScheme.white.copy(alpha = 0.2f), + PhotoComponent( + photo = photo, + modifier = Modifier.then( + if (isSelected) + Modifier.border( + width = 2.dp, + color = NekiTheme.colorScheme.primary400, + shape = RoundedCornerShape(8.dp), + ) else Modifier.clip(RoundedCornerShape(8.dp)), + ), + onClickItem = onClickItem, + ) + + if (isSelected) { + SelectedPhotoGridItemOverlay( + modifier = Modifier.matchParentSize(), + shape = RoundedCornerShape(8.dp), + ) + } else { + PhotoGridItemOverlay( + modifier = Modifier.matchParentSize(), + shape = RoundedCornerShape(8.dp), ) } @@ -67,13 +65,23 @@ internal fun SelectablePhotoItem( modifier = Modifier .align(Alignment.TopEnd) .padding(top = 10.dp, end = 10.dp) - .size(20.dp) - .noRippleClickable { onClickFavorite(photo) }, + .size(20.dp), imageVector = ImageVector.vectorResource(R.drawable.icon_heart), contentDescription = null, tint = NekiTheme.colorScheme.white, ) } + + if (isSelectMode) { + SelectionCheckbox( + isSelected = isSelected, + modifier = Modifier + .align(Alignment.TopStart) + .padding(top = 12.dp, start = 12.dp) + .noRippleClickable { onClickSelect(photo) }, + unselectedColor = NekiTheme.colorScheme.white.copy(alpha = 0.2f), + ) + } } } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainContract.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainContract.kt index beac2bf90..dd9debe4d 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainContract.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainContract.kt @@ -32,7 +32,8 @@ sealed interface ArchiveMainIntent { // TopBar Intent data object ClickAddIcon : ArchiveMainIntent - data object DismissAddDialog : ArchiveMainIntent + data object DismissAddPopup : ArchiveMainIntent + data object DismissToolTipPopup : ArchiveMainIntent data object ClickQRScanRow : ArchiveMainIntent data object ClickGalleryUploadRow : ArchiveMainIntent diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainScreen.kt index 8f79a6d99..392c89161 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainScreen.kt @@ -119,7 +119,8 @@ internal fun ArchiveMainScreen( onClickQRScan = { onIntent(ArchiveMainIntent.ClickQRScanRow) }, onClickGallery = { onIntent(ArchiveMainIntent.ClickGalleryUploadRow) }, onClickNewAlbum = { onIntent(ArchiveMainIntent.ClickAddNewAlbumRow) }, - onDismissPopup = { onIntent(ArchiveMainIntent.DismissAddDialog) }, + onDismissAddPopup = { onIntent(ArchiveMainIntent.DismissAddPopup) }, + onDismissToolTipPopup = { onIntent(ArchiveMainIntent.DismissToolTipPopup) }, ) ArchiveMainContent( uiState = uiState, @@ -275,7 +276,7 @@ private fun ArchiveMainScreenPreview() { val favoriteAlbum = AlbumPreview( id = 0, - title = "즐겨찾는 사진", + title = "즐겨찾기", thumbnailUrl = "https://picsum.photos/seed/fav1/200/300", photoCount = 5, ) @@ -296,7 +297,7 @@ private fun ArchiveMainScreenPreview() { private fun ArchiveMainScreenEmptyPreview() { val favoriteAlbum = AlbumPreview( id = 0, - title = "즐겨찾는 사진", + title = "즐겨찾기", ) NekiTheme { diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt index e148ac045..48d58e1d2 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/ArchiveMainViewModel.kt @@ -60,7 +60,8 @@ class ArchiveMainViewModel @Inject constructor( // TopBar Intent ArchiveMainIntent.ClickAddIcon -> reduce { copy(isShowAddDialog = true) } - ArchiveMainIntent.DismissAddDialog -> reduce { copy(isShowAddDialog = false) } + ArchiveMainIntent.DismissAddPopup -> reduce { copy(isShowAddDialog = false) } + ArchiveMainIntent.DismissToolTipPopup -> reduce { copy(isFirstEntered = false) } ArchiveMainIntent.ClickQRScanRow -> { reduce { copy(isShowAddDialog = false) } postSideEffect(ArchiveMainSideEffect.NavigateToQRScan) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainAlbumList.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainAlbumList.kt index c897679e9..f812bff67 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainAlbumList.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainAlbumList.kt @@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -22,31 +23,100 @@ 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.draw.drawBehind -import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Outline import androidx.compose.ui.graphics.Path -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.graphics.drawscope.scale +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R +import com.neki.android.core.designsystem.modifier.backgroundHazeBlur import com.neki.android.core.designsystem.modifier.cardShadow import com.neki.android.core.designsystem.modifier.noRippleClickable import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.AlbumPreview +import dev.chrisbanes.haze.HazeState +import dev.chrisbanes.haze.hazeSource +import dev.chrisbanes.haze.rememberHazeState import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.toImmutableList private const val VIEWPORT_W = 124f private const val VIEWPORT_H = 65f +private val AlbumFolderShape = object : Shape { + override fun createOutline( + size: Size, + layoutDirection: LayoutDirection, + density: Density, + ): Outline { + val scaleX = size.width / VIEWPORT_W + val scaleY = size.height / VIEWPORT_H + + val path = Path().apply { + moveTo(124f * scaleX, 57f * scaleY) + cubicTo(124f * scaleX, 61.42f * scaleY, 120.42f * scaleX, 65f * scaleY, 116f * scaleX, 65f * scaleY) + lineTo(8f * scaleX, 65f * scaleY) + cubicTo( + 3.58f * scaleX, + 65f * scaleY, + 0f, + 61.42f * scaleY, + 0f, + 57f * scaleY, + ) + lineTo(0f, 8f * scaleY) + cubicTo( + 0f, + 3.58f * scaleY, + 3.58f * scaleX, + 0f, + 8f * scaleX, + 0f, + ) + lineTo(58.54f * scaleX, 0f) + cubicTo( + 61.07f * scaleX, + 0f, + 63.45f * scaleX, + 1.2f * scaleY, + 64.96f * scaleX, + 3.23f * scaleY, + ) + lineTo(69.2f * scaleX, 8.93f * scaleY) + cubicTo( + 70.7f * scaleX, + 10.95f * scaleY, + 73.06f * scaleX, + 12.15f * scaleY, + 75.58f * scaleX, + 12.16f * scaleY, + ) + lineTo(116.04f * scaleX, 12.35f * scaleY) + cubicTo( + 120.44f * scaleX, + 12.36f * scaleY, + 124f * scaleX, + 15.94f * scaleY, + 124f * scaleX, + 20.34f * scaleY, + ) + lineTo(124f * scaleX, 57f * scaleY) + close() + } + return Outline.Generic(path) + } +} + @Composable internal fun ArchiveMainAlbumList( favoriteAlbum: AlbumPreview, @@ -87,11 +157,13 @@ internal fun ArchiveMainAlbumList( private fun ArchiveAlbumItem( title: String, photoCount: Int, - thumbnailImage: String? = null, modifier: Modifier = Modifier, + thumbnailImage: String? = null, isFavorite: Boolean = false, onClick: () -> Unit = {}, ) { + val hazeState = rememberHazeState() + Box( modifier = modifier .height(166.dp) @@ -102,6 +174,7 @@ private fun ArchiveAlbumItem( modifier = Modifier .cardShadow(shape = RoundedCornerShape(8.dp)) .matchParentSize() + .hazeSource(hazeState) .then( if (!thumbnailImage.isNullOrBlank()) Modifier else Modifier.background(color = NekiTheme.colorScheme.gray50), @@ -112,6 +185,7 @@ private fun ArchiveAlbumItem( ) AlbumFolder( modifier = Modifier.align(Alignment.BottomCenter), + hazeState = hazeState, title = title, photoCount = photoCount, isFavorite = isFavorite, @@ -124,38 +198,40 @@ private fun AlbumFolder( title: String, photoCount: Int, modifier: Modifier = Modifier, + hazeState: HazeState = rememberHazeState(), isFavorite: Boolean = false, ) { AlbumFolderLayout( modifier = modifier.width(124.dp), - color = if (isFavorite) NekiTheme.colorScheme.favoriteAlbumCover.copy(alpha = 0.9f) - else NekiTheme.colorScheme.defaultAlbumCover.copy(alpha = 0.92f), + hazeState = hazeState, + color = if (isFavorite) NekiTheme.colorScheme.favoriteAlbumCover + else NekiTheme.colorScheme.defaultAlbumCover, ) { Row( modifier = Modifier + .fillMaxWidth() .padding( - top = 15.dp, - bottom = 10.dp, + top = 10.dp, start = 10.dp, + bottom = 8.dp, end = 8.dp, ), - horizontalArrangement = Arrangement.spacedBy(10.dp), + horizontalArrangement = Arrangement.SpaceBetween, ) { Column( - modifier = Modifier.width(80.dp), + modifier = Modifier.widthIn(max = 86.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( text = "${photoCount}장", - style = NekiTheme.typography.caption12Medium, + style = NekiTheme.typography.body14Medium, color = NekiTheme.colorScheme.white.copy(alpha = 0.7f), ) Text( - modifier = Modifier.widthIn(max = 80.dp), text = title, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = NekiTheme.typography.body14SemiBold, + style = NekiTheme.typography.body16SemiBold, color = NekiTheme.colorScheme.white, ) } @@ -163,7 +239,7 @@ private fun AlbumFolder( if (isFavorite) { Box( modifier = Modifier - .padding(bottom = 3.dp) + .padding(bottom = 2.dp) .align(Alignment.Bottom) .clip(CircleShape) .background( @@ -173,7 +249,7 @@ private fun AlbumFolder( .padding(4.dp), ) { Icon( - modifier = Modifier.size(8.dp), + modifier = Modifier.size(12.dp), imageVector = ImageVector.vectorResource(R.drawable.icon_heart), contentDescription = null, tint = NekiTheme.colorScheme.white, @@ -187,53 +263,30 @@ private fun AlbumFolder( @Composable private fun AlbumFolderLayout( modifier: Modifier = Modifier, + hazeState: HazeState = rememberHazeState(), color: Color = Color(0xFFFF5647), content: @Composable BoxScope.() -> Unit = {}, ) { Box( modifier = modifier - .drawBehind { - val scaleX = size.width / VIEWPORT_W - val scaleY = size.height / VIEWPORT_H - - scale(scaleX, scaleY, pivot = Offset.Zero) { - val path = Path().apply { - // 오른쪽 하단에서 시작 (코너 8만큼 위) - moveTo(124f, 57f) - // 오른쪽 하단 코너 - cubicTo(124f, 61.42f, 120.42f, 65f, 116f, 65f) - // 하단 직선 - lineTo(8f, 65f) - // 왼쪽 하단 코너 - cubicTo(3.58f, 65f, 0f, 61.42f, 0f, 57f) - // 왼쪽 직선 - lineTo(0f, 8f) - // 왼쪽 상단 코너 - cubicTo(0f, 3.58f, 3.58f, 0f, 8f, 0f) - // 상단 탭 모양 - lineTo(58.54f, 0f) - cubicTo(61.07f, 0f, 63.45f, 1.2f, 64.96f, 3.23f) - lineTo(69.2f, 8.93f) - cubicTo(70.7f, 10.95f, 73.06f, 12.15f, 75.58f, 12.16f) - lineTo(116.04f, 12.35f) - cubicTo(120.44f, 12.36f, 124f, 15.94f, 124f, 20.34f) - // 오른쪽 직선 - lineTo(124f, 57f) - close() - } - drawPath(path, color, style = Fill) - } - }, + .clip(AlbumFolderShape) + .backgroundHazeBlur( + hazeState = hazeState, + color = color, + blurRadius = 4.dp, + alpha = 0.92f, + ), content = content, ) } @ComponentPreview @Composable -private fun ArchiveAlbumItemPreview() { +private fun FavoriteAlbumItemPreview() { NekiTheme { Box(modifier = Modifier.padding(8.dp)) { ArchiveAlbumItem( + isFavorite = true, title = "네키 화이팅화이팅", photoCount = 10, ) @@ -243,34 +296,24 @@ private fun ArchiveAlbumItemPreview() { @ComponentPreview @Composable -private fun FavoriteAlbumFolderPreview() { +private fun ArchiveAlbumItemPreview() { NekiTheme { - AlbumFolder( - isFavorite = true, - title = "즐겨찾는 사진", - photoCount = 12, - ) + Box(modifier = Modifier.padding(8.dp)) { + ArchiveAlbumItem( + title = "네키 화이팅화이팅", + photoCount = 10, + ) + } } } @ComponentPreview @Composable -private fun DefaultAlbumFolderPreview() { - NekiTheme { - AlbumFolder( - isFavorite = false, - title = "네키 화이팅화이팅", - photoCount = 10, - ) - } -} - -@Preview -@Composable private fun ArchiveMainAlbumListPreview() { NekiTheme { ArchiveMainAlbumList( favoriteAlbum = AlbumPreview(), + albumList = List(3) { AlbumPreview(id = it.toLong()) }.toImmutableList(), ) } } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainPhotoItem.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainPhotoItem.kt index e2fe110d8..375b9d3ee 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainPhotoItem.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainPhotoItem.kt @@ -1,7 +1,9 @@ package com.neki.android.feature.archive.impl.main.component +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -11,10 +13,10 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R -import com.neki.android.core.designsystem.modifier.photoBackground import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Photo import com.neki.android.core.ui.component.PhotoComponent +import com.neki.android.core.ui.component.PhotoGridItemOverlay @Composable internal fun ArchiveMainPhotoItem( @@ -22,11 +24,19 @@ internal fun ArchiveMainPhotoItem( modifier: Modifier = Modifier, onClickItem: (Photo) -> Unit = {}, ) { - PhotoComponent( - photo = photo, - modifier = modifier.photoBackground(), - onClickItem = onClickItem, + Box( + modifier = modifier, ) { + PhotoComponent( + photo = photo, + onClickItem = onClickItem, + ) + + PhotoGridItemOverlay( + modifier = Modifier.matchParentSize(), + shape = RoundedCornerShape(8.dp), + ) + if (photo.isFavorite) { Icon( modifier = Modifier diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/PhotoTitleRow.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTitleRow.kt similarity index 88% rename from feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/PhotoTitleRow.kt rename to feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTitleRow.kt index 38fb4d530..e3a790323 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/PhotoTitleRow.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTitleRow.kt @@ -1,9 +1,9 @@ package com.neki.android.feature.archive.impl.main.component import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -17,7 +17,6 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.button.NekiTextButton import com.neki.android.core.designsystem.ui.theme.NekiTheme -import com.neki.android.feature.archive.impl.const.ArchiveConst.ARCHIVE_ROW_TEXT_BUTTON_PADDING @Composable internal fun ArchiveMainTitleRow( @@ -33,11 +32,11 @@ internal fun ArchiveMainTitleRow( ) { Text( text = title, - style = NekiTheme.typography.title20SemiBold, + style = NekiTheme.typography.title20Bold, color = NekiTheme.colorScheme.gray900, ) NekiTextButton( - modifier = Modifier.offset(x = ARCHIVE_ROW_TEXT_BUTTON_PADDING.dp), + contentPadding = PaddingValues(vertical = 10.dp), onClick = onClickShowAllAlbum, ) { Row( diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTopBar.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTopBar.kt index 0533bfee6..7b1629d81 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTopBar.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/ArchiveMainTopBar.kt @@ -1,6 +1,5 @@ package com.neki.android.feature.archive.impl.main.component -import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -9,17 +8,14 @@ import androidx.compose.foundation.layout.Row 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.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider 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.graphics.Path import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.vectorResource @@ -30,7 +26,9 @@ import androidx.compose.ui.window.PopupProperties import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.R import com.neki.android.core.designsystem.button.NekiIconButton +import com.neki.android.core.designsystem.logo.PrimaryNekiTypoLogo import com.neki.android.core.designsystem.modifier.dropdownShadow +import com.neki.android.core.designsystem.popup.ToolTipPopup import com.neki.android.core.designsystem.topbar.NekiLeftTitleTopBar import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.feature.archive.impl.R as ArchiveR @@ -44,18 +42,14 @@ internal fun ArchiveMainTopBar( onClickQRScan: () -> Unit = {}, onClickGallery: () -> Unit = {}, onClickNewAlbum: () -> Unit = {}, - onDismissPopup: () -> Unit = {}, + onDismissAddPopup: () -> Unit = {}, + onDismissToolTipPopup: () -> Unit = {}, showTooltip: Boolean = true, ) { NekiLeftTitleTopBar( modifier = modifier, title = { - Box( - modifier = Modifier - .height(28.dp) - .width(56.dp) - .background(color = Color(0xFFB7B9C3)), - ) + PrimaryNekiTypoLogo() }, actions = { Row( @@ -74,14 +68,16 @@ internal fun ArchiveMainTopBar( if (showAddPopup) { AddPhotoPopup( - onDismissRequest = onDismissPopup, + onDismissRequest = onDismissAddPopup, onClickQRScan = onClickQRScan, onClickGallery = onClickGallery, onClickNewAlbum = onClickNewAlbum, ) } if (showTooltip) { - ToolTip() + ArchiveToolTip( + onDismissRequest = onDismissToolTipPopup, + ) } } NekiIconButton( @@ -99,68 +95,21 @@ internal fun ArchiveMainTopBar( } @Composable -private fun ToolTip() { - val caretColor = NekiTheme.colorScheme.gray800 +private fun ArchiveToolTip( + onDismissRequest: () -> Unit = {}, +) { val density = LocalDensity.current - val popupOffsetX = with(density) { 1.dp.toPx().toInt() } val popupOffsetY = with(density) { 47.dp.toPx().toInt() } + val offset = IntOffset(popupOffsetX, popupOffsetY) - Popup( + ToolTipPopup( + tooltipText = "버튼을 눌러 네컷을 추가할 수 있어요", + color = NekiTheme.colorScheme.gray800, + offset = offset, alignment = Alignment.TopEnd, - offset = IntOffset(popupOffsetX, popupOffsetY), - ) { - Column( - modifier = Modifier.width(IntrinsicSize.Max), - ) { - // 꼬리 (오른쪽 정렬, 오른쪽에서 16dp) - Box( - modifier = Modifier - .fillMaxWidth() - .padding(end = 16.dp), - contentAlignment = Alignment.CenterEnd, - ) { - Canvas(modifier = Modifier.size(width = 10.dp, height = 8.dp)) { - val cornerRadius = 2.dp.toPx() - val path = Path().apply { - // 왼쪽 하단에서 시작 - moveTo(0f, size.height) - // 왼쪽 하단 -> 꼭대기 (둥근 모서리) - lineTo( - size.width / 2 - cornerRadius, - cornerRadius, - ) - quadraticTo( - size.width / 2, - 0f, - size.width / 2 + cornerRadius, - cornerRadius, - ) - // 꼭대기 -> 오른쪽 하단 - lineTo(size.width, size.height) - close() - } - drawPath(path, caretColor) - } - } - - // 몸통 - Box( - modifier = Modifier - .background( - color = NekiTheme.colorScheme.gray800, - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 12.dp, vertical = 8.dp), - ) { - Text( - text = "버튼을 눌러 네컷을 추가할 수 있어요", - style = NekiTheme.typography.body14Medium, - color = NekiTheme.colorScheme.white, - ) - } - } - } + onDismissRequest = onDismissRequest, + ) } @Composable diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt index 8cf2fce06..4282a8bd0 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/AllPhotoScreen.kt @@ -41,6 +41,7 @@ import com.neki.android.core.ui.component.LoadingDialog import com.neki.android.core.ui.compose.collectWithLifecycle import com.neki.android.core.ui.toast.NekiToast import com.neki.android.feature.archive.impl.component.DeletePhotoDialog +import com.neki.android.feature.archive.impl.component.EmptyContent import com.neki.android.feature.archive.impl.component.SelectablePhotoItem import com.neki.android.feature.archive.impl.const.ArchiveConst.ARCHIVE_GRID_ITEM_SPACING import com.neki.android.feature.archive.impl.const.ArchiveConst.PHOTO_GRAY_LAYOUT_BOTTOM_PADDING @@ -109,6 +110,52 @@ internal fun AllPhotoScreen( pagingItems: LazyPagingItems, lazyState: LazyStaggeredGridState = rememberLazyStaggeredGridState(), onIntent: (AllPhotoIntent) -> Unit = {}, +) { + val isRefreshing by remember(pagingItems) { + derivedStateOf { pagingItems.loadState.refresh is LoadState.Loading } + } + val isEmpty by remember(pagingItems) { + derivedStateOf { pagingItems.itemCount == 0 && pagingItems.loadState.refresh is LoadState.NotLoading } + } + + BackHandler(enabled = true) { + onIntent(AllPhotoIntent.OnBackPressed) + } + + if (isEmpty) { + EmptyContent( + title = "모든 사진", + onClickBack = { onIntent(AllPhotoIntent.ClickTopBarBackIcon) }, + ) + } else { + AllPhotoContent( + uiState = uiState, + pagingItems = pagingItems, + lazyState = lazyState, + onIntent = onIntent, + ) + } + + if (isRefreshing || uiState.isLoading) { + LoadingDialog() + } + + if (uiState.isShowDeleteDialog) { + DeletePhotoDialog( + onDismissRequest = { onIntent(AllPhotoIntent.DismissDeleteDialog) }, + onClickDelete = { onIntent(AllPhotoIntent.ClickDeleteDialogConfirmButton) }, + onClickCancel = { onIntent(AllPhotoIntent.DismissDeleteDialog) }, + ) + } +} + +@Composable +private fun AllPhotoContent( + uiState: AllPhotoState, + pagingItems: LazyPagingItems, + lazyState: LazyStaggeredGridState, + modifier: Modifier = Modifier, + onIntent: (AllPhotoIntent) -> Unit = {}, ) { val density = LocalDensity.current var filterBarHeightPx by remember { mutableIntStateOf(0) } @@ -121,18 +168,8 @@ internal fun AllPhotoScreen( } } - val isRefreshing by remember { - derivedStateOf { - pagingItems.loadState.refresh is LoadState.Loading && pagingItems.itemCount == 0 - } - } - - BackHandler(enabled = true) { - onIntent(AllPhotoIntent.OnBackPressed) - } - Column( - modifier = Modifier + modifier = modifier .fillMaxSize() .background(NekiTheme.colorScheme.white), ) { @@ -218,18 +255,6 @@ internal fun AllPhotoScreen( onClickDelete = { onIntent(AllPhotoIntent.ClickDeleteIcon) }, ) } - - if (isRefreshing || uiState.isLoading) { - LoadingDialog() - } - - if (uiState.isShowDeleteDialog) { - DeletePhotoDialog( - onDismissRequest = { onIntent(AllPhotoIntent.DismissDeleteDialog) }, - onClickDelete = { onIntent(AllPhotoIntent.ClickDeleteDialogConfirmButton) }, - onClickCancel = { onIntent(AllPhotoIntent.DismissDeleteDialog) }, - ) - } } @DevicePreview diff --git a/feature/photo-upload/impl/build.gradle.kts b/feature/photo-upload/impl/build.gradle.kts index 9f0dac88c..58c6d266d 100644 --- a/feature/photo-upload/impl/build.gradle.kts +++ b/feature/photo-upload/impl/build.gradle.kts @@ -21,12 +21,14 @@ android { defaultConfig { buildConfigField("String", "BRAND_PROPOSAL_URL", properties["BRAND_PROPOSAL_URL"].toString()) + buildConfigField("String", "PHOTOISM_URL", properties["PHOTOISM_URL"].toString()) buildConfigField("String", "PHOTOISM_IMAGE_URL", properties["PHOTOISM_IMAGE_URL"].toString()) - buildConfigField("String", "PHOTOISM_IMG_URL_MIME_TYPE", properties["PHOTOISM_IMG_URL_MIME_TYPE"].toString()) + buildConfigField("String", "PHOTOISM_IMAGE_URL_MIME_TYPE", properties["PHOTOISM_IMAGE_URL_MIME_TYPE"].toString()) + buildConfigField("String", "LIFE_FOUR_CUT_URL", properties["LIFE_FOUR_CUT_URL"].toString()) buildConfigField("String", "LIFE_FOUR_CUT_IMAGE_URL", properties["LIFE_FOUR_CUT_IMAGE_URL"].toString()) - buildConfigField("String", "LIFE_FOUR_CUT_URL_MIME_TYPE", properties["LIFE_FOUR_CUT_URL_MIME_TYPE"].toString()) + buildConfigField("String", "LIFE_FOUR_CUT_IMAGE_URL_MIME_TYPE", properties["LIFE_FOUR_CUT_IMAGE_URL_MIME_TYPE"].toString()) } } diff --git a/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/album/UploadAlbumScreen.kt b/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/album/UploadAlbumScreen.kt index 0c46218ce..8153999c0 100644 --- a/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/album/UploadAlbumScreen.kt +++ b/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/album/UploadAlbumScreen.kt @@ -1,7 +1,6 @@ package com.neki.android.feature.photo_upload.impl.album import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -71,7 +70,6 @@ internal fun UploadAlbumScreen( LazyColumn( modifier = Modifier.fillMaxSize(), contentPadding = PaddingValues(vertical = 8.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), ) { item { FavoriteAlbumRowComponent(album = uiState.favoriteAlbum) @@ -103,7 +101,7 @@ private fun UploadAlbumScreenPreview() { NekiTheme { UploadAlbumScreen( uiState = UploadAlbumState( - favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾는 사진", photoCount = 3), + favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾기", photoCount = 3), albums = persistentListOf( AlbumPreview(id = 1, title = "제주도 여행 2024", photoCount = 4), AlbumPreview(id = 2, title = "가족 생일파티", photoCount = 2), @@ -121,7 +119,7 @@ private fun UploadAlbumScreenSelectingPreview() { NekiTheme { UploadAlbumScreen( uiState = UploadAlbumState( - favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾는 사진", photoCount = 3), + favoriteAlbum = AlbumPreview(id = 0, title = "즐겨찾기", photoCount = 3), albums = persistentListOf( AlbumPreview(id = 1, title = "제주도 여행 2024", photoCount = 4), AlbumPreview(id = 2, title = "가족 생일파티", photoCount = 2), diff --git a/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/util/PhotoWebViewClient.kt b/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/util/PhotoWebViewClient.kt index dbbc554d6..60554d2a4 100644 --- a/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/util/PhotoWebViewClient.kt +++ b/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/util/PhotoWebViewClient.kt @@ -20,13 +20,13 @@ class PhotoWebViewClient( when { // 포토이즘 - url.startsWith(BuildConfig.PHOTOISM_IMAGE_URL) && url.endsWith(BuildConfig.PHOTOISM_IMG_URL_MIME_TYPE) -> { + url.startsWith(BuildConfig.PHOTOISM_IMAGE_URL) && url.endsWith(BuildConfig.PHOTOISM_IMAGE_URL_MIME_TYPE) -> { Timber.d("포토이즘 이미지") onImageUrlDetected(url) } // 인생네컷 - url.startsWith(BuildConfig.LIFE_FOUR_CUT_IMAGE_URL) && url.endsWith(BuildConfig.LIFE_FOUR_CUT_URL_MIME_TYPE) -> { + url.startsWith(BuildConfig.LIFE_FOUR_CUT_IMAGE_URL) && url.endsWith(BuildConfig.LIFE_FOUR_CUT_IMAGE_URL_MIME_TYPE) -> { Timber.d("인생네컷 이미지") onImageUrlDetected(url) } diff --git a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseTutorialOverlay.kt b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseTutorialOverlay.kt index c8952e9db..4146216e7 100644 --- a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseTutorialOverlay.kt +++ b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseTutorialOverlay.kt @@ -23,7 +23,6 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.modifier.backgroundHazeBlur -import com.neki.android.core.designsystem.modifier.noRippleClickable import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.ui.compose.HorizontalSpacer import com.neki.android.core.ui.compose.VerticalSpacer @@ -35,15 +34,20 @@ import dev.chrisbanes.haze.rememberHazeState @Composable internal fun RandomPoseTutorialOverlay( onClickStart: () -> Unit, - hazeState: HazeState = rememberHazeState(), modifier: Modifier = Modifier, + hazeState: HazeState = rememberHazeState(), ) { Box( - modifier = modifier.noRippleClickable {}, // 터치 이벤트 소비용 + modifier = modifier, ) { Column( modifier = Modifier - .backgroundHazeBlur(hazeState) + .backgroundHazeBlur( + hazeState = hazeState, + color = Color(0xFF202227), + blurRadius = 12.dp, + alpha = 0.9f, + ) .fillMaxSize() .padding(top = 60.dp, bottom = 34.dp), ) { diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cc46f00d4..d2dc051e9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -37,6 +37,8 @@ barcodeScanning = "17.3.0" kakao = "2.23.1" coil = "3.3.0" haze = "1.7.1" +lottie = "6.7.1" +exifinterface = "1.4.2" ossLicensesLib = "17.2.1" ossLicensesPlugin = "0.10.7" @@ -107,6 +109,8 @@ coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp" haze = { group = "dev.chrisbanes.haze", name = "haze", version.ref = "haze" } haze-materials = { group = "dev.chrisbanes.haze", name = "haze-materials", version.ref = "haze" } +lottie-compose = { group = "com.airbnb.android", name = "lottie-compose", version.ref = "lottie" } + androidx-paging-runtime = { group = "androidx.paging", name = "paging-runtime", version.ref = "paging" } oss-licenses = { group = "com.google.android.gms", name = "play-services-oss-licenses", version.ref = "ossLicensesLib" } @@ -116,6 +120,7 @@ kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-p android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } compose-compiler-gradle-plugin = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } material = { group = "com.google.android.material", name = "material", version.ref = "material" } +androidx-exifinterface = { group = "androidx.exifinterface", name = "exifinterface", version.ref = "exifinterface" } [plugins] # Plugins defined by this project