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