diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/NekiTextField.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/NekiTextField.kt new file mode 100644 index 000000000..f897dd6dd --- /dev/null +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/NekiTextField.kt @@ -0,0 +1,279 @@ +package com.neki.android.core.designsystem + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.text.input.InputTransformation +import androidx.compose.foundation.text.input.OutputTransformation +import androidx.compose.foundation.text.input.TextFieldLineLimits +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +fun NekiTextField( + textFieldState: TextFieldState, + modifier: Modifier = Modifier, + titleLabel: String? = null, + placeholder: String = "", + maxLength: Int? = null, + isError: Boolean = false, + textStyle: TextStyle = NekiTheme.typography.body16Medium.copy( + color = NekiTheme.colorScheme.gray900, + ), + cursorBrush: Brush = SolidColor(NekiTheme.colorScheme.gray800), + lineLimits: TextFieldLineLimits = TextFieldLineLimits.SingleLine, + inputTransformation: InputTransformation? = null, + outputTransformation: OutputTransformation? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, +) { + val interactionSource = remember { MutableInteractionSource() } + val isFocused by interactionSource.collectIsFocusedAsState() + + val borderColor = when { + isError -> NekiTheme.colorScheme.primary600 + isFocused -> NekiTheme.colorScheme.gray700 + else -> NekiTheme.colorScheme.gray75 + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + titleLabel?.let { + Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 2.dp), + text = it, + style = NekiTheme.typography.body14Medium, + color = NekiTheme.colorScheme.gray700, + ) + } + + BasicTextField( + state = textFieldState, + modifier = Modifier + .fillMaxWidth() + .background( + color = NekiTheme.colorScheme.white, + shape = RoundedCornerShape(8.dp), + ) + .border( + width = 1.dp, + color = borderColor, + shape = RoundedCornerShape(8.dp), + ) + .padding(horizontal = 16.dp, vertical = 13.dp), + textStyle = textStyle, + inputTransformation = inputTransformation, + outputTransformation = outputTransformation, + interactionSource = interactionSource, + cursorBrush = cursorBrush, + lineLimits = lineLimits, + keyboardOptions = keyboardOptions, + decorator = { innerTextField -> + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Box(modifier = Modifier.weight(1f)) { + if (textFieldState.text.isEmpty()) { + Text( + text = placeholder, + style = NekiTheme.typography.body16Regular, + color = NekiTheme.colorScheme.gray300, + ) + } + innerTextField() + } + maxLength?.let { + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = "${textFieldState.text.length}/$maxLength", + style = NekiTheme.typography.caption12Regular, + color = NekiTheme.colorScheme.gray300, + ) + } + } + }, + ) + } +} + +@Composable +fun NekiTextFieldWithError( + textFieldState: TextFieldState, + modifier: Modifier = Modifier, + titleLabel: String? = null, + placeholder: String = "", + maxLength: Int? = null, + isError: Boolean = false, + errorMessage: String? = null, + textStyle: TextStyle = NekiTheme.typography.body16Medium.copy( + color = NekiTheme.colorScheme.gray900, + ), + cursorBrush: Brush = SolidColor(NekiTheme.colorScheme.gray800), + lineLimits: TextFieldLineLimits = TextFieldLineLimits.SingleLine, + inputTransformation: InputTransformation? = null, + outputTransformation: OutputTransformation? = null, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(6.dp), + ) { + NekiTextField( + textFieldState = textFieldState, + titleLabel = titleLabel, + placeholder = placeholder, + maxLength = maxLength, + isError = isError, + textStyle = textStyle, + cursorBrush = cursorBrush, + lineLimits = lineLimits, + inputTransformation = inputTransformation, + outputTransformation = outputTransformation, + keyboardOptions = keyboardOptions, + ) + + Text( + modifier = Modifier.heightIn(min = 16.dp), + text = if (isError) errorMessage.orEmpty() else "", + style = NekiTheme.typography.caption12Regular, + color = NekiTheme.colorScheme.primary600, + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun NekiTextFieldPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + NekiTextField( + textFieldState = remember { TextFieldState() }, + placeholder = "플레이스홀더", + ) + + NekiTextField( + textFieldState = remember { TextFieldState("입력된 텍스트") }, + maxLength = 20, + ) + + NekiTextField( + textFieldState = remember { TextFieldState("에러 상태") }, + isError = true, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun NekiTextFieldWithTitleLabelPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + NekiTextField( + textFieldState = remember { TextFieldState() }, + titleLabel = "닉네임", + placeholder = "닉네임을 입력해주세요", + ) + + NekiTextField( + textFieldState = remember { TextFieldState("입력된 텍스트") }, + titleLabel = "닉네임", + maxLength = 10, + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun NekiTextFieldWithErrorPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + NekiTextFieldWithError( + textFieldState = remember { TextFieldState() }, + placeholder = "플레이스홀더", + ) + + NekiTextFieldWithError( + textFieldState = remember { TextFieldState("입력된 텍스트") }, + placeholder = "플레이스홀더", + ) + + NekiTextFieldWithError( + textFieldState = remember { TextFieldState("에러 상태") }, + isError = true, + errorMessage = "올바른 값을 입력해주세요", + ) + } + } +} + +@Preview(showBackground = true) +@Composable +private fun NekiTextFieldWithErrorAndTitleLabelPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + NekiTextFieldWithError( + textFieldState = remember { TextFieldState() }, + titleLabel = "닉네임", + placeholder = "닉네임을 입력해주세요", + maxLength = 10, + ) + + NekiTextFieldWithError( + textFieldState = remember { TextFieldState("입력된 텍스트") }, + placeholder = "플레이스홀더", + maxLength = 10, + ) + + NekiTextFieldWithError( + textFieldState = remember { TextFieldState("에러 상태") }, + titleLabel = "닉네임", + isError = true, + errorMessage = "이미 사용 중인 닉네임입니다", + maxLength = 10, + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/actionbar/NekiActionBar.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/actionbar/NekiActionBar.kt new file mode 100644 index 000000000..1960f1a62 --- /dev/null +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/actionbar/NekiActionBar.kt @@ -0,0 +1,167 @@ +package com.neki.android.core.designsystem.actionbar + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.button.NekiIconButton +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +fun NekiStartActionBar( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier, + ) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = NekiTheme.colorScheme.gray75, + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterStart, + ) { + content() + } + } +} + +@Composable +fun NekiEndActionBar( + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + Column( + modifier = modifier, + ) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = NekiTheme.colorScheme.gray75, + ) + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.CenterEnd, + ) { + content() + } + } +} + +@Composable +fun NekiBothSidesActionBar( + modifier: Modifier = Modifier, + startContent: @Composable () -> Unit, + endContent: @Composable () -> Unit, +) { + Column( + modifier = modifier, + ) { + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + thickness = 1.dp, + color = NekiTheme.colorScheme.gray75, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + startContent() + endContent() + } + } +} + +@ComponentPreview +@Composable +private fun NekiStartActionBarPreview() { + NekiTheme { + NekiStartActionBar( + modifier = Modifier.fillMaxWidth(), + ) { + NekiIconButton( + modifier = Modifier.padding(8.dp), + onClick = {}, + ) { + Icon( + modifier = Modifier.size(28.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_arrow_left), + contentDescription = null, + tint = NekiTheme.colorScheme.gray900, + ) + } + } + } +} + +@ComponentPreview +@Composable +private fun NekiEndActionBarPreview() { + NekiTheme { + NekiEndActionBar( + modifier = Modifier.fillMaxWidth(), + ) { + NekiIconButton( + modifier = Modifier.padding(8.dp), + onClick = {}, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.icon_scrap_stroked), + contentDescription = null, + tint = NekiTheme.colorScheme.gray500, + ) + } + } + } +} + +@ComponentPreview +@Composable +private fun NekiBothSidesActionBarPreview() { + NekiTheme { + NekiBothSidesActionBar( + modifier = Modifier.fillMaxWidth(), + startContent = { + NekiIconButton( + modifier = Modifier.padding(8.dp), + onClick = {}, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.icon_arrow_left), + contentDescription = null, + tint = NekiTheme.colorScheme.gray900, + ) + } + }, + endContent = { + NekiIconButton( + modifier = Modifier.padding(8.dp), + onClick = {}, + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.icon_scrap_stroked), + contentDescription = null, + tint = NekiTheme.colorScheme.gray500, + ) + } + }, + ) + } +} diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/bottomsheet/NekiTextFieldBottomSheet.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/bottomsheet/NekiTextFieldBottomSheet.kt index a57a89977..9043fb1cf 100644 --- a/core/designsystem/src/main/java/com/neki/android/core/designsystem/bottomsheet/NekiTextFieldBottomSheet.kt +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/bottomsheet/NekiTextFieldBottomSheet.kt @@ -1,26 +1,15 @@ package com.neki.android.core.designsystem.bottomsheet -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.input.InputTransformation -import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.maxLength import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ModalBottomSheet @@ -28,14 +17,10 @@ import androidx.compose.material3.SheetState import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.min import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.NekiTextFieldWithError import com.neki.android.core.designsystem.button.CTAButtonGray import com.neki.android.core.designsystem.button.CTAButtonPrimary import com.neki.android.core.designsystem.ui.theme.NekiTheme @@ -124,20 +109,13 @@ private fun NekiTextFieldBottomSheetContent( Column( verticalArrangement = Arrangement.spacedBy(6.dp), ) { - NekiBottomSheetTextField( + NekiTextFieldWithError( textFieldState = textFieldState, placeholder = placeholder, maxLength = maxLength, isError = isError, keyboardOptions = keyboardOptions, - ) - - // Error message - Text( - modifier = Modifier.heightIn(min = 16.dp), - text = if (isError) errorMessage.orEmpty() else "", - style = NekiTheme.typography.caption12Regular, - color = NekiTheme.colorScheme.primary600, + errorMessage = errorMessage, ) } @@ -163,75 +141,6 @@ private fun NekiTextFieldBottomSheetContent( } } -@Composable -private fun NekiBottomSheetTextField( - textFieldState: TextFieldState, - modifier: Modifier = Modifier, - placeholder: String = "", - maxLength: Int? = null, - isError: Boolean = false, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, -) { - val interactionSource = remember { MutableInteractionSource() } - val isFocused by interactionSource.collectIsFocusedAsState() - - val borderColor = when { - isError -> NekiTheme.colorScheme.primary600 - isFocused -> NekiTheme.colorScheme.gray700 - else -> NekiTheme.colorScheme.gray75 - } - - BasicTextField( - state = textFieldState, - modifier = modifier - .fillMaxWidth() - .background( - color = NekiTheme.colorScheme.white, - shape = RoundedCornerShape(8.dp), - ) - .border( - width = 1.dp, - color = borderColor, - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 16.dp, vertical = 13.dp), - textStyle = NekiTheme.typography.body16Medium.copy( - color = NekiTheme.colorScheme.gray900, - ), - inputTransformation = maxLength?.let { InputTransformation.maxLength(it) }, - interactionSource = interactionSource, - cursorBrush = SolidColor(NekiTheme.colorScheme.gray800), - lineLimits = TextFieldLineLimits.SingleLine, - keyboardOptions = keyboardOptions, - decorator = { innerTextField -> - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Box(modifier = Modifier.weight(1f)) { - if (textFieldState.text.isEmpty()) { - Text( - text = placeholder, - style = NekiTheme.typography.body16Regular, - color = NekiTheme.colorScheme.gray300, - ) - } - innerTextField() - } - maxLength?.let { - Spacer(modifier = Modifier.width(4.dp)) - Text( - text = "${textFieldState.text.length}/$maxLength", - style = NekiTheme.typography.caption12Regular, - color = NekiTheme.colorScheme.gray300, - ) - } - } - }, - ) -} - @ComponentPreview @Composable private fun NekiTextFieldBottomSheetContentDefaultPreview() { diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/NekiTextButton.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/NekiTextButton.kt index cc9062c67..aaccab371 100644 --- a/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/NekiTextButton.kt +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/NekiTextButton.kt @@ -2,13 +2,12 @@ package com.neki.android.core.designsystem.button import androidx.compose.foundation.layout.PaddingValues import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.graphics.Color import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.modifier.MultipleEventsCutter import com.neki.android.core.designsystem.modifier.get @@ -20,6 +19,8 @@ fun NekiTextButton( modifier: Modifier = Modifier, enabled: Boolean = true, multipleEventsCutterEnabled: Boolean = true, + enabledTextColor: Color = NekiTheme.colorScheme.primary500, + disabledTextColor: Color = NekiTheme.colorScheme.gray200, contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, content: @Composable () -> Unit = {}, ) { @@ -36,8 +37,8 @@ fun NekiTextButton( contentPadding = contentPadding, enabled = enabled, colors = ButtonDefaults.textButtonColors( - contentColor = MaterialTheme.colorScheme.onSurfaceVariant, - disabledContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + contentColor = enabledTextColor, + disabledContentColor = disabledTextColor, ), ) { content() @@ -52,8 +53,7 @@ private fun NekiTextButtonPreview() { onClick = {}, ) { Text( - text = "Text Button", - textDecoration = TextDecoration.Underline, + text = "텍스트버튼", ) } } diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/TopBarTextButton.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/TopBarTextButton.kt deleted file mode 100644 index 252b204ca..000000000 --- a/core/designsystem/src/main/java/com/neki/android/core/designsystem/button/TopBarTextButton.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.neki.android.core.designsystem.button - -import androidx.compose.foundation.layout.offset -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.LayoutDirection -import com.neki.android.core.designsystem.ComponentPreview -import com.neki.android.core.designsystem.ui.theme.NekiTheme - -@Composable -fun TopBarTextButton( - buttonText: String, - modifier: Modifier = Modifier, - enabled: Boolean = true, - enabledTextColor: Color = NekiTheme.colorScheme.primary500, - disabledTextColor: Color = NekiTheme.colorScheme.gray200, - onClick: () -> Unit = {}, -) { - NekiTextButton( - modifier = modifier.offset( - x = ButtonDefaults.TextButtonContentPadding.calculateLeftPadding(LayoutDirection.Ltr), - ), - onClick = onClick, - enabled = enabled, - ) { - Text( - text = buttonText, - style = NekiTheme.typography.body16SemiBold, - color = if (enabled) enabledTextColor else disabledTextColor, - ) - } -} - -@ComponentPreview -@Composable -private fun EnabledTopBarTextButtonPreview() { - NekiTheme { - TopBarTextButton( - buttonText = "텍스트버튼", - onClick = {}, - ) - } -} - -@ComponentPreview -@Composable -private fun DisabledTopBarTextButtonPreview() { - NekiTheme { - TopBarTextButton( - buttonText = "텍스트버튼", - onClick = {}, - enabled = false, - ) - } -} 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 df3370121..626dbe6d8 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 @@ -1,54 +1,14 @@ package com.neki.android.core.designsystem.modifier import androidx.compose.foundation.background -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.Modifier -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.unit.Dp -import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeEffect -/** - * 사진 컴포넌트에 적용되는 그라데이션 배경 - * 좌하단에서 우상단으로 갈수록 어두워지는 효과 - */ -fun Modifier.photoBackground( - shape: Shape = RoundedCornerShape(12.dp), -): Modifier = this.background( - brush = Brush.linearGradient( - colorStops = arrayOf( - 0f to Color.Black.copy(alpha = 0f), - 0.7f to Color.Black.copy(alpha = 0.09f), - 1f to Color.Black.copy(alpha = 0.3f), - ), - start = Offset(0f, Float.POSITIVE_INFINITY), - end = Offset(Float.POSITIVE_INFINITY, 0f), - ), - shape = shape, -) - -/** - * 포즈 컴포넌트에 적용되는 그라데이션 배경 - * 상단에서 134/242 지점까지 어두워지는 효과 - */ -fun Modifier.poseBackground( - shape: Shape = RoundedCornerShape(12.dp), -): Modifier = this.background( - brush = Brush.verticalGradient( - colorStops = arrayOf( - 0f to Color.Black.copy(alpha = 0.2f), - 134f / 242f to Color.Black.copy(alpha = 0f), - ), - ), - shape = shape, -) - /** * 블러 효과가 적용된 배경을 설정하는 Modifier 확장 함수 * diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Stroke.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Stroke.kt new file mode 100644 index 000000000..d9940d3d3 --- /dev/null +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/modifier/Stroke.kt @@ -0,0 +1,30 @@ +package com.neki.android.core.designsystem.modifier + +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +fun Modifier.dashStroke( + color: Color, + strokeWidth: Dp = 1.dp, + dashLength: Dp = 5.dp, + gapLength: Dp = 5.dp, + cornerRadius: Dp = 0.dp, +): Modifier = this.drawBehind { + drawRoundRect( + color = color, + style = Stroke( + width = strokeWidth.toPx(), + pathEffect = PathEffect.dashPathEffect( + intervals = floatArrayOf(dashLength.toPx(), gapLength.toPx()), + phase = 0f, + ), + ), + cornerRadius = CornerRadius(cornerRadius.toPx()), + ) +} 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 index 1ea95a9bd..f3c402875 100644 --- 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 @@ -2,33 +2,57 @@ package com.neki.android.core.designsystem.popup import androidx.compose.foundation.Canvas 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.IntrinsicSize +import androidx.compose.foundation.layout.Row 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.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.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Dp 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.R +import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme +private const val DEFAULT_ARROW_POSITION = 16 + +enum class ArrowDirection { + Up, Down, +} + +enum class ToolTipColor { + Gray800, Gray25 +} + @Composable fun ToolTipPopup( tooltipText: String, - color: Color, offset: IntOffset, + arrowDirection: ArrowDirection, alignment: Alignment, - onDismissRequest: () -> Unit, + arrowAlignment: Alignment, + arrowPosition: Dp = DEFAULT_ARROW_POSITION.dp, + toolTipColor: ToolTipColor = ToolTipColor.Gray800, + hasCloseButton: Boolean = false, + onDismissRequest: () -> Unit = {}, + onClickCloseButton: () -> Unit = {}, ) { Popup( alignment = alignment, @@ -37,80 +61,321 @@ fun ToolTipPopup( ) { ToolTipContent( tooltipText = tooltipText, - color = color, + toolTipColor = toolTipColor, + arrowDirection = arrowDirection, + arrowPosition = arrowPosition, + arrowAlignment = arrowAlignment, + hasCloseButton = hasCloseButton, + onClickCloseButton = onClickCloseButton, ) } } +@Composable +private fun TriangleArrow( + direction: ArrowDirection, + color: Color, + modifier: Modifier = Modifier, + width: Dp = 10.dp, + height: Dp = 8.dp, +) { + Canvas( + modifier = modifier.size(width = width, height = height), + ) { + val cornerRadius = 1.dp.toPx() + val path = when (direction) { + ArrowDirection.Up -> 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() + } + + ArrowDirection.Down -> Path().apply { + moveTo(0f, 0f) + lineTo(size.width / 2 - cornerRadius, size.height - cornerRadius) + quadraticTo(size.width / 2, size.height, size.width / 2 + cornerRadius, size.height - cornerRadius) + lineTo(size.width, 0f) + close() + } + } + drawPath(path, color) + } +} + @Composable private fun ToolTipContent( tooltipText: String, - color: Color, + arrowDirection: ArrowDirection, + arrowAlignment: Alignment, modifier: Modifier = Modifier, + toolTipColor: ToolTipColor = ToolTipColor.Gray800, + arrowPosition: Dp = DEFAULT_ARROW_POSITION.dp, + hasCloseButton: Boolean = false, + onClickCloseButton: () -> Unit = {}, ) { + val backgroundColor = when (toolTipColor) { + ToolTipColor.Gray800 -> NekiTheme.colorScheme.gray800 + ToolTipColor.Gray25 -> NekiTheme.colorScheme.gray25 + } + val textColor = when (toolTipColor) { + ToolTipColor.Gray800 -> NekiTheme.colorScheme.white + ToolTipColor.Gray25 -> NekiTheme.colorScheme.gray900 + } + val iconTint = when (toolTipColor) { + ToolTipColor.Gray800 -> NekiTheme.colorScheme.gray25 + ToolTipColor.Gray25 -> NekiTheme.colorScheme.gray500 + } + 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) - } + if (arrowDirection == ArrowDirection.Up) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = arrowPosition), + contentAlignment = arrowAlignment, + ) { TriangleArrow(direction = arrowDirection, color = backgroundColor) } } - - // 몸통 - Box( + Row( modifier = Modifier - .background( - color = color, - shape = RoundedCornerShape(8.dp), - ) + .background(color = backgroundColor, shape = RoundedCornerShape(8.dp)) .padding(horizontal = 12.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.spacedBy(2.dp), + verticalAlignment = Alignment.CenterVertically, ) { Text( text = tooltipText, style = NekiTheme.typography.body14Medium, - color = NekiTheme.colorScheme.white, + color = textColor, ) + if (hasCloseButton) { + Icon( + modifier = Modifier + .size(16.dp) + .noRippleClickableSingle(onClick = onClickCloseButton), + imageVector = ImageVector.vectorResource(R.drawable.icon_close), + tint = iconTint, + contentDescription = null, + ) + } + } + if (arrowDirection == ArrowDirection.Down) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = arrowPosition), + contentAlignment = arrowAlignment, + ) { TriangleArrow(direction = arrowDirection, color = backgroundColor) } } } } @ComponentPreview @Composable -private fun ToolTipPopupPreview() { +private fun ToolTipArrowUpPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterStart, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.Center, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterEnd, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterStart, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.Center, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterEnd, + hasCloseButton = true, + ) + } + } +} + +@ComponentPreview +@Composable +private fun ToolTipArrowDownPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterStart, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.Center, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterEnd, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterStart, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.Center, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterEnd, + hasCloseButton = true, + ) + } + } +} + +@Preview +@Composable +private fun ToolTipArrowUpGrayPreview() { + NekiTheme { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterStart, + toolTipColor = ToolTipColor.Gray25, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.Center, + toolTipColor = ToolTipColor.Gray25, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterEnd, + toolTipColor = ToolTipColor.Gray25, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterStart, + toolTipColor = ToolTipColor.Gray25, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.Center, + toolTipColor = ToolTipColor.Gray25, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterEnd, + toolTipColor = ToolTipColor.Gray25, + hasCloseButton = true, + ) + } + } +} + +@Preview +@Composable +private fun ToolTipArrowDownGrayPreview() { NekiTheme { - Box(modifier = Modifier.padding(16.dp)) { + Column( + modifier = Modifier.padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterStart, + toolTipColor = ToolTipColor.Gray25, + ) ToolTipContent( - tooltipText = "툴팁 메시지입니다", - color = NekiTheme.colorScheme.gray800, + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.Center, + toolTipColor = ToolTipColor.Gray25, ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterEnd, + toolTipColor = ToolTipColor.Gray25, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterStart, + toolTipColor = ToolTipColor.Gray25, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.Center, + toolTipColor = ToolTipColor.Gray25, + hasCloseButton = true, + ) + ToolTipContent( + tooltipText = "텍스트", + arrowDirection = ArrowDirection.Down, + arrowAlignment = Alignment.CenterEnd, + toolTipColor = ToolTipColor.Gray25, + hasCloseButton = true, + ) + } + } +} + +@ComponentPreview +@Composable +private fun TriangleArrowPreview() { + NekiTheme { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + TriangleArrow(direction = ArrowDirection.Up, color = NekiTheme.colorScheme.gray800) + TriangleArrow(direction = ArrowDirection.Down, color = NekiTheme.colorScheme.gray800) } } } diff --git a/core/designsystem/src/main/java/com/neki/android/core/designsystem/topbar/NekiTopBar.kt b/core/designsystem/src/main/java/com/neki/android/core/designsystem/topbar/NekiTopBar.kt index d27b24b7a..e335eb3aa 100644 --- a/core/designsystem/src/main/java/com/neki/android/core/designsystem/topbar/NekiTopBar.kt +++ b/core/designsystem/src/main/java/com/neki/android/core/designsystem/topbar/NekiTopBar.kt @@ -35,37 +35,37 @@ fun NekiTitleTopBar( } @Composable -fun NekiLeftTitleTopBar( +private fun NekiTopBar( modifier: Modifier = Modifier, - title: @Composable (() -> Unit)? = null, - actions: @Composable (() -> Unit)? = null, + title: @Composable ((Modifier) -> Unit)? = null, + leadingIcon: @Composable ((Modifier) -> Unit)? = null, + actions: @Composable ((Modifier) -> Unit)? = null, ) { - Row( + Box( modifier = modifier .fillMaxWidth() .height(54.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, ) { - title?.invoke() - actions?.invoke() + leadingIcon?.invoke(Modifier.align(Alignment.CenterStart)) + title?.invoke(Modifier.align(Alignment.Center)) + actions?.invoke(Modifier.align(Alignment.CenterEnd)) } } @Composable -private fun NekiTopBar( +fun NekiLeftTitleTopBar( modifier: Modifier = Modifier, - title: @Composable ((Modifier) -> Unit)? = null, - leadingIcon: @Composable ((Modifier) -> Unit)? = null, - actions: @Composable ((Modifier) -> Unit)? = null, + title: @Composable (() -> Unit)? = null, + actions: @Composable (() -> Unit)? = null, ) { - Box( + Row( modifier = modifier .fillMaxWidth() .height(54.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, ) { - leadingIcon?.invoke(Modifier.align(Alignment.CenterStart)) - title?.invoke(Modifier.align(Alignment.Center)) - actions?.invoke(Modifier.align(Alignment.CenterEnd)) + title?.invoke() + actions?.invoke() } } diff --git a/core/designsystem/src/main/res/drawable/icon_empty_gallery_thumbnail.xml b/core/designsystem/src/main/res/drawable/icon_empty_gallery_thumbnail.xml new file mode 100644 index 000000000..7705b019e --- /dev/null +++ b/core/designsystem/src/main/res/drawable/icon_empty_gallery_thumbnail.xml @@ -0,0 +1,26 @@ + + + + + + diff --git a/core/designsystem/src/main/res/drawable/image_empty_folder.png b/core/designsystem/src/main/res/drawable/image_empty_folder.png new file mode 100644 index 000000000..c1e6dfed6 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_empty_folder.png differ diff --git a/core/designsystem/src/main/res/drawable/image_empty_photo.png b/core/designsystem/src/main/res/drawable/image_empty_photo.png new file mode 100644 index 000000000..0ac5e57d5 Binary files /dev/null and b/core/designsystem/src/main/res/drawable/image_empty_photo.png differ 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 ac9141969..f5c326e17 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 @@ -15,6 +15,7 @@ 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.layout.ContentScale import androidx.compose.ui.res.vectorResource @@ -41,6 +42,7 @@ fun FavoriteAlbumRowComponent( horizontalArrangement = Arrangement.spacedBy(16.dp), ) { FavoriteAlbumThumbnail( + isEmpty = album.photoCount == 0, thumbnailUrl = album.thumbnailUrl, ) @@ -68,6 +70,7 @@ fun AlbumRowComponent( horizontalArrangement = Arrangement.spacedBy(16.dp), ) { AlbumThumbnail( + isEmpty = album.photoCount == 0, thumbnailUrl = album.thumbnailUrl, ) @@ -88,6 +91,7 @@ fun AlbumRowComponent( @Composable private fun FavoriteAlbumThumbnail( thumbnailUrl: String?, + isEmpty: Boolean, modifier: Modifier = Modifier, ) { Box( @@ -96,45 +100,81 @@ private fun FavoriteAlbumThumbnail( .clip(RoundedCornerShape(8.dp)), contentAlignment = Alignment.Center, ) { - AsyncImage( - modifier = Modifier.matchParentSize(), - model = thumbnailUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - ) + if (isEmpty || thumbnailUrl == null) { + Box( + modifier = Modifier + .matchParentSize() + .background(NekiTheme.colorScheme.gray50), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(28.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_empty_gallery_thumbnail), + tint = NekiTheme.colorScheme.gray100, + contentDescription = null, + ) + } + } else { + AsyncImage( + modifier = Modifier + .background(color = Color.Black.copy(alpha = 0.04f)) + .matchParentSize(), + model = thumbnailUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + ) - Box( - modifier = Modifier - .matchParentSize() - .background(NekiTheme.colorScheme.favoriteAlbumCover.copy(alpha = 0.5f)), - ) + Box( + modifier = Modifier + .matchParentSize() + .background(NekiTheme.colorScheme.favoriteAlbumCover.copy(alpha = 0.5f)), + ) - Icon( - modifier = Modifier.size(20.dp), - imageVector = ImageVector.vectorResource(R.drawable.icon_heart_filled), - contentDescription = null, - tint = NekiTheme.colorScheme.white, - ) + Icon( + modifier = Modifier.size(20.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_heart_filled), + contentDescription = null, + tint = NekiTheme.colorScheme.white, + ) + } } } @Composable private fun AlbumThumbnail( thumbnailUrl: String?, + isEmpty: Boolean, modifier: Modifier = Modifier, ) { - AsyncImage( - modifier = modifier - .size(72.dp) - .clip(RoundedCornerShape(8.dp)) - .background( - color = NekiTheme.colorScheme.gray50, - shape = RoundedCornerShape(8.dp), - ), - model = thumbnailUrl, - contentDescription = null, - contentScale = ContentScale.Crop, - ) + if (isEmpty || thumbnailUrl == null) { + Box( + modifier = Modifier + .size(72.dp) + .clip(RoundedCornerShape(8.dp)) + .background(NekiTheme.colorScheme.gray50), + contentAlignment = Alignment.Center, + ) { + Icon( + modifier = Modifier.size(28.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_empty_gallery_thumbnail), + tint = NekiTheme.colorScheme.gray100, + contentDescription = null, + ) + } + } else { + AsyncImage( + modifier = modifier + .size(72.dp) + .clip(RoundedCornerShape(8.dp)) + .background( + color = NekiTheme.colorScheme.gray50, + shape = RoundedCornerShape(8.dp), + ), + model = thumbnailUrl, + contentDescription = null, + contentScale = ContentScale.Crop, + ) + } } @Composable @@ -165,12 +205,23 @@ private fun AlbumInfo( @Composable private fun FavoriteAlbumRowComponentPreview() { NekiTheme { - FavoriteAlbumRowComponent( - album = AlbumPreview( - id = 0, - title = "즐겨찾기", - ), - ) + Column { + FavoriteAlbumRowComponent( + album = AlbumPreview( + id = 0, + title = "즐겨찾기", + photoCount = 0, + ), + ) + FavoriteAlbumRowComponent( + album = AlbumPreview( + id = 0, + title = "즐겨찾기", + thumbnailUrl = "https://example.com/photo.jpg", + photoCount = 12, + ), + ) + } } } @@ -178,12 +229,23 @@ private fun FavoriteAlbumRowComponentPreview() { @Composable private fun AlbumRowComponentPreview() { NekiTheme { - AlbumRowComponent( - album = AlbumPreview( - id = 1, - title = "일반앨범제목", - ), - ) + Column { + AlbumRowComponent( + album = AlbumPreview( + id = 1, + title = "빈 앨범", + photoCount = 0, + ), + ) + AlbumRowComponent( + album = AlbumPreview( + id = 2, + title = "일반앨범제목", + thumbnailUrl = "https://example.com/photo.jpg", + photoCount = 5, + ), + ) + } } } @@ -191,20 +253,22 @@ private fun AlbumRowComponentPreview() { @Composable private fun AlbumRowComponentSelectablePreview() { NekiTheme { - Column(verticalArrangement = Arrangement.spacedBy(20.dp)) { + Column { AlbumRowComponent( album = AlbumPreview( id = 1, - title = "선택되지 않은 앨범", + title = "선택되지 않은 빈 앨범", + photoCount = 0, ), isSelectable = true, isSelected = false, ) - AlbumRowComponent( album = AlbumPreview( id = 2, title = "선택된 앨범", + thumbnailUrl = "https://example.com/photo.jpg", + photoCount = 8, ), isSelectable = true, isSelected = true, diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/DoubleButtonOptionBottomSheet.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/DoubleButtonOptionBottomSheet.kt index 5be1d7652..a34b200b1 100644 --- a/core/ui/src/main/java/com/neki/android/core/ui/component/DoubleButtonOptionBottomSheet.kt +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/DoubleButtonOptionBottomSheet.kt @@ -26,6 +26,7 @@ import com.neki.android.core.designsystem.button.CTAButtonGray import com.neki.android.core.designsystem.button.CTAButtonPrimary import com.neki.android.core.designsystem.modifier.noRippleClickable import com.neki.android.core.designsystem.ui.theme.NekiTheme +import com.neki.android.core.ui.compose.VerticalSpacer import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList @@ -83,18 +84,17 @@ internal fun DoubleButtonOptionBottomSheetContent( Column( modifier = modifier .fillMaxWidth() - .padding(horizontal = 20.dp) - .padding(bottom = 34.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), + .padding(top = 4.dp, bottom = 34.dp), ) { Text( + modifier = Modifier.padding(horizontal = 20.dp), text = title, style = NekiTheme.typography.title20SemiBold, color = NekiTheme.colorScheme.gray900, ) - + VerticalSpacer(12.dp) Column( - verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.fillMaxWidth(), ) { options.forEach { option -> OptionRow( @@ -104,9 +104,12 @@ internal fun DoubleButtonOptionBottomSheetContent( ) } } + VerticalSpacer(16.dp) Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 20.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), ) { CTAButtonGray( @@ -136,7 +139,7 @@ private fun OptionRow( modifier = modifier .fillMaxWidth() .noRippleClickable(onClick = onClick) - .padding(vertical = 12.dp), + .padding(horizontal = 20.dp, vertical = 14.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/DropdownPopup.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/DropdownPopup.kt new file mode 100644 index 000000000..934688057 --- /dev/null +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/DropdownPopup.kt @@ -0,0 +1,87 @@ +package com.neki.android.core.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +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.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupProperties +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.modifier.clickableSingle +import com.neki.android.core.designsystem.modifier.dropdownShadow +import com.neki.android.core.designsystem.ui.theme.NekiTheme +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +@Composable +fun DropdownPopup( + items: ImmutableList, + selectedItem: T?, + onSelect: (T) -> Unit, + onDismissRequest: () -> Unit, + itemLabel: (T) -> String, + modifier: Modifier = Modifier, + offset: IntOffset = IntOffset.Zero, + alignment: Alignment = Alignment.TopStart, +) { + Popup( + offset = offset, + alignment = alignment, + onDismissRequest = onDismissRequest, + properties = PopupProperties(focusable = true), + ) { + Column( + modifier = modifier + .dropdownShadow(shape = RoundedCornerShape(8.dp)) + .background( + color = NekiTheme.colorScheme.white, + shape = RoundedCornerShape(8.dp), + ) + .padding(vertical = 6.dp), + ) { + items.forEach { item -> + Text( + modifier = Modifier + .fillMaxWidth() + .background( + color = if (selectedItem == item) NekiTheme.colorScheme.gray50 + else NekiTheme.colorScheme.white, + ) + .clickableSingle { onSelect(item) } + .padding(horizontal = 16.dp, vertical = 5.dp), + text = itemLabel(item), + style = NekiTheme.typography.body14Medium, + ) + } + } + } +} + +private enum class PreviewDropdownOption(val label: String) { + NEWEST("최신순"), + OLDEST("오래된순"), + ; + + override fun toString(): String = label +} + +@ComponentPreview +@Composable +private fun DropdownPopupPreview() { + NekiTheme { + DropdownPopup( + items = PreviewDropdownOption.entries.toImmutableList(), + selectedItem = PreviewDropdownOption.NEWEST, + onSelect = {}, + onDismissRequest = {}, + itemLabel = { it.label }, + ) + } +} diff --git a/core/ui/src/main/java/com/neki/android/core/ui/component/FilterBar.kt b/core/ui/src/main/java/com/neki/android/core/ui/component/FilterBar.kt index a36067187..686333958 100644 --- a/core/ui/src/main/java/com/neki/android/core/ui/component/FilterBar.kt +++ b/core/ui/src/main/java/com/neki/android/core/ui/component/FilterBar.kt @@ -15,6 +15,7 @@ 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.Shape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp @@ -31,6 +32,7 @@ fun FilterBar( defaultChipDisplayText: String, modifier: Modifier = Modifier, visible: Boolean = true, + chipShape: Shape = CircleShape, onClickDownIconChip: () -> Unit = {}, onClickDefaultChip: () -> Unit = {}, ) { @@ -50,11 +52,13 @@ fun FilterBar( DownIconFilterChip( isSelected = isDownIconChipSelected, displayText = downIconChipDisplayText, + chipShape = chipShape, onClick = onClickDownIconChip, ) DefaultFilterChip( isSelected = isDefaultChipSelected, displayText = defaultChipDisplayText, + chipShape = chipShape, onClick = onClickDefaultChip, ) } @@ -66,12 +70,13 @@ private fun DownIconFilterChip( isSelected: Boolean, displayText: String, modifier: Modifier = Modifier, + chipShape: Shape = CircleShape, onClick: () -> Unit = {}, ) { Row( modifier = modifier .background( - shape = CircleShape, + shape = chipShape, color = if (isSelected) NekiTheme.colorScheme.gray800 else NekiTheme.colorScheme.gray50, ) @@ -100,12 +105,13 @@ private fun DefaultFilterChip( isSelected: Boolean, displayText: String, modifier: Modifier = Modifier, + chipShape: Shape = CircleShape, onClick: () -> Unit = {}, ) { Text( modifier = modifier .background( - shape = CircleShape, + shape = chipShape, color = if (isSelected) NekiTheme.colorScheme.gray800 else NekiTheme.colorScheme.gray50, ) 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 index 661f1104e..42192cde4 100644 --- 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 @@ -1,6 +1,7 @@ package com.neki.android.core.ui.component import androidx.compose.foundation.background +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -14,7 +15,7 @@ import com.neki.android.core.designsystem.ComponentPreview import com.neki.android.core.designsystem.ui.theme.NekiTheme @Composable -fun PhotoGridItemOverlay( +fun GridItemOverlay( modifier: Modifier = Modifier, shape: Shape = RoundedCornerShape(8.dp), ) { @@ -42,18 +43,35 @@ fun SelectedPhotoGridItemOverlay( shape: Shape = RoundedCornerShape(8.dp), ) { Box( - modifier = modifier.background( - color = Color.Black.copy(alpha = 0.2f), - shape = shape, - ), + modifier = modifier + .border( + width = 2.dp, + color = NekiTheme.colorScheme.primary400, + shape = shape, + ) + .background( + color = Color.Black.copy(alpha = 0.2f), + shape = shape, + ), ) } @ComponentPreview @Composable -private fun PhotoGridItemOverlayPreview() { +private fun GridItemOverlayPreview() { + NekiTheme { + GridItemOverlay( + modifier = Modifier + .size(80.dp), + ) + } +} + +@ComponentPreview +@Composable +private fun SelectedPhotoGridItemOverlayPreview() { NekiTheme { - PhotoGridItemOverlay( + SelectedPhotoGridItemOverlay( modifier = Modifier .size(80.dp), ) 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 7021384ba..c0a0b91c2 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 @@ -38,7 +38,7 @@ 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.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.EmptyAlbumContent 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,7 +109,7 @@ internal fun AlbumDetailScreen( } if (isEmpty) { - EmptyContent( + EmptyAlbumContent( title = if (uiState.isFavoriteAlbum) "즐겨찾기" else uiState.title, onClickBack = { onIntent(AlbumDetailIntent.ClickBackIcon) }, ) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyAlbumContent.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyAlbumContent.kt new file mode 100644 index 000000000..2bf38eab3 --- /dev/null +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyAlbumContent.kt @@ -0,0 +1,55 @@ +package com.neki.android.feature.archive.impl.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.topbar.BackTitleTopBar +import com.neki.android.core.designsystem.ui.theme.NekiTheme +import com.neki.android.feature.archive.impl.main.component.EmptyContent + +@Composable +internal fun EmptyAlbumContent( + title: String, + modifier: Modifier = Modifier, + onClickBack: () -> Unit = {}, +) { + Box( + modifier = modifier.fillMaxSize(), + ) { + EmptyTopBar( + title = title, + onClickBack = onClickBack, + ) + + EmptyContent( + modifier = Modifier.align(Alignment.Center), + emptyText = "아직 등록된 사진이 없어요\n찍은 네컷을 저장해보세요!", + ) + } +} + +@Composable +private fun EmptyTopBar( + title: String, + onClickBack: () -> Unit, + modifier: Modifier = Modifier, +) { + BackTitleTopBar( + modifier = modifier, + title = title, + onBack = onClickBack, + ) +} + +@ComponentPreview +@Composable +private fun EmptyAlbumContentPreview() { + NekiTheme { + EmptyAlbumContent( + title = "즐겨찾기", + ) + } +} diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt deleted file mode 100644 index 402073e14..000000000 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/EmptyContent.kt +++ /dev/null @@ -1,80 +0,0 @@ -package com.neki.android.feature.archive.impl.component - -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.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.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( - 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(16.dp), - ) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.icon_empty_content), - contentDescription = null, - tint = Color.Unspecified, - ) - Text( - text = emptyText, - style = NekiTheme.typography.body14Medium, - color = NekiTheme.colorScheme.gray300, - textAlign = TextAlign.Center, - ) - } - } -} - -@Composable -private fun EmptyTopBar( - title: String, - onClickBack: () -> Unit, - modifier: Modifier = Modifier, -) { - BackTitleTopBar( - modifier = modifier, - title = title, - onBack = onClickBack, - ) -} - -@ComponentPreview -@Composable -private fun EmptyContentPreview() { - NekiTheme { - EmptyContent( - title = "즐겨찾기", - ) - } -} diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt index 41e761506..cd6b4e0cb 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/component/SelectablePhotoItem.kt @@ -1,6 +1,5 @@ package com.neki.android.feature.archive.impl.component -import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -19,7 +18,7 @@ import com.neki.android.core.designsystem.modifier.noRippleClickable 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.GridItemOverlay import com.neki.android.core.ui.component.SelectedPhotoGridItemOverlay import com.neki.android.core.ui.component.SelectionCheckbox @@ -37,14 +36,7 @@ internal fun SelectablePhotoItem( ) { 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)), - ), + modifier = Modifier.clip(RoundedCornerShape(8.dp)), onClickItem = onClickItem, ) @@ -54,7 +46,7 @@ internal fun SelectablePhotoItem( shape = RoundedCornerShape(8.dp), ) } else { - PhotoGridItemOverlay( + GridItemOverlay( modifier = Modifier.matchParentSize(), shape = RoundedCornerShape(8.dp), ) 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 d3a8b095b..5b6785804 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 @@ -48,7 +48,7 @@ import com.neki.android.feature.archive.impl.main.component.ArchiveMainTopBar import com.neki.android.feature.archive.impl.main.component.AlbumUploadOption import com.neki.android.feature.archive.impl.main.component.SelectWithAlbumDialog import com.neki.android.feature.archive.impl.main.component.GotoTopButton -import com.neki.android.feature.archive.impl.main.component.NoPhotoContent +import com.neki.android.feature.archive.impl.main.component.EmptyContent import kotlinx.collections.immutable.persistentListOf import timber.log.Timber @@ -243,7 +243,12 @@ private fun ArchiveMainContent( if (uiState.recentPhotos.isEmpty()) { item(span = StaggeredGridItemSpan.FullLine) { - NoPhotoContent() + EmptyContent( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 70.dp), + emptyText = "아직 등록된 사진이 없어요\n찍은 네컷을 저장해보세요!", + ) } } 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 51cd6dc99..65faf8287 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 @@ -40,9 +40,12 @@ 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.dashStroke 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 com.neki.android.feature.archive.impl.const.ArchiveConst.ARCHIVE_ALBUM_ITEM_HEIGHT +import com.neki.android.feature.archive.impl.const.ArchiveConst.ARCHIVE_ALBUM_ITEM_WIDTH import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.rememberHazeState @@ -153,6 +156,43 @@ internal fun ArchiveMainAlbumList( } } +@Composable +private fun AddAlbumItem( + modifier: Modifier = Modifier, + onClick: () -> Unit = {}, +) { + Box( + modifier = modifier + .height(ARCHIVE_ALBUM_ITEM_HEIGHT.dp) + .width(ARCHIVE_ALBUM_ITEM_WIDTH.dp) + .clip(RoundedCornerShape(8.dp)) + .dashStroke( + color = NekiTheme.colorScheme.primary400, + strokeWidth = 2.dp, + cornerRadius = 8.dp, + ) + .noRippleClickable(onClick = onClick), + ) { + Column( + modifier = Modifier.align(Alignment.Center), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Icon( + modifier = Modifier.size(28.dp), + imageVector = ImageVector.vectorResource(R.drawable.icon_plus), + tint = NekiTheme.colorScheme.primary400, + contentDescription = null, + ) + Text( + text = "새 앨범 추가", + style = NekiTheme.typography.body14Medium, + color = NekiTheme.colorScheme.primary400, + ) + } + } +} + @Composable private fun ArchiveAlbumItem( title: String, @@ -166,23 +206,39 @@ private fun ArchiveAlbumItem( Box( modifier = modifier - .height(166.dp) + .cardShadow(shape = RoundedCornerShape(8.dp)) + .height(ARCHIVE_ALBUM_ITEM_HEIGHT.dp) .clip(RoundedCornerShape(8.dp)) .noRippleClickable(onClick = onClick), ) { - AsyncImage( - modifier = Modifier - .cardShadow(shape = RoundedCornerShape(8.dp)) - .matchParentSize() - .hazeSource(hazeState) - .then( - if (!thumbnailImage.isNullOrBlank()) Modifier - else Modifier.background(color = NekiTheme.colorScheme.gray50), - ), - model = thumbnailImage, - contentDescription = null, - contentScale = ContentScale.Crop, - ) + if (photoCount == 0 || thumbnailImage == null) { + Box( + modifier = Modifier + .matchParentSize() + .background(NekiTheme.colorScheme.gray25) + .hazeSource(hazeState), + ) { + Icon( + modifier = Modifier + .padding(top = 38.dp) + .size(40.dp) + .align(Alignment.TopCenter), + imageVector = ImageVector.vectorResource(R.drawable.icon_empty_gallery_thumbnail), + tint = NekiTheme.colorScheme.gray100, + contentDescription = null, + ) + } + } else { + AsyncImage( + modifier = Modifier + .background(color = Color.Black.copy(alpha = 0.04f)) + .matchParentSize() + .hazeSource(hazeState), + model = thumbnailImage, + contentDescription = null, + contentScale = ContentScale.Crop, + ) + } AlbumFolder( modifier = Modifier.align(Alignment.BottomCenter), hazeState = hazeState, @@ -202,7 +258,7 @@ private fun AlbumFolder( isFavorite: Boolean = false, ) { AlbumFolderLayout( - modifier = modifier.width(124.dp), + modifier = modifier.width(ARCHIVE_ALBUM_ITEM_WIDTH.dp), hazeState = hazeState, color = if (isFavorite) NekiTheme.colorScheme.favoriteAlbumCover else NekiTheme.colorScheme.defaultAlbumCover, @@ -282,12 +338,31 @@ private fun AlbumFolderLayout( @ComponentPreview @Composable -private fun FavoriteAlbumItemPreview() { +private fun AddAlbumItemPreview() { NekiTheme { Box(modifier = Modifier.padding(8.dp)) { + AddAlbumItem() + } + } +} + +@ComponentPreview +@Composable +private fun FavoriteAlbumItemPreview() { + NekiTheme { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ArchiveAlbumItem( + isFavorite = true, + title = "즐겨찾기", + photoCount = 0, + ) ArchiveAlbumItem( isFavorite = true, title = "네키 화이팅화이팅", + thumbnailImage = "https://example.com/photo.jpg", photoCount = 10, ) } @@ -298,9 +373,17 @@ private fun FavoriteAlbumItemPreview() { @Composable private fun ArchiveAlbumItemPreview() { NekiTheme { - Box(modifier = Modifier.padding(8.dp)) { + Row( + modifier = Modifier.padding(8.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ArchiveAlbumItem( + title = "빈 앨범", + photoCount = 0, + ) ArchiveAlbumItem( title = "네키 화이팅화이팅", + thumbnailImage = "https://example.com/photo.jpg", photoCount = 10, ) } 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 14b36cc0d..a341d713e 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 @@ -16,7 +16,7 @@ import com.neki.android.core.designsystem.R 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.GridItemOverlay @Composable internal fun ArchiveMainPhotoItem( @@ -32,7 +32,7 @@ internal fun ArchiveMainPhotoItem( onClickItem = onClickItem, ) - PhotoGridItemOverlay( + GridItemOverlay( modifier = Modifier.matchParentSize(), shape = RoundedCornerShape(8.dp), ) 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 443d40050..f87644bd2 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 @@ -29,6 +29,7 @@ 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.ArrowDirection 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 @@ -107,9 +108,10 @@ private fun ArchiveToolTip( ToolTipPopup( tooltipText = "버튼을 눌러 네컷을 추가할 수 있어요", - color = NekiTheme.colorScheme.gray800, offset = offset, alignment = Alignment.TopEnd, + arrowDirection = ArrowDirection.Up, + arrowAlignment = Alignment.CenterEnd, onDismissRequest = onDismissRequest, ) } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/EmptyContent.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/EmptyContent.kt new file mode 100644 index 000000000..100d47921 --- /dev/null +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/EmptyContent.kt @@ -0,0 +1,54 @@ +package com.neki.android.feature.archive.impl.main.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.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.ui.theme.NekiTheme + +@Composable +internal fun EmptyContent( + emptyText: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Image( + modifier = Modifier + .width(148.dp) + .height(112.dp), + painter = painterResource(R.drawable.image_empty_photo), + contentDescription = null, + ) + Text( + text = emptyText, + style = NekiTheme.typography.body14Medium, + color = NekiTheme.colorScheme.gray200, + textAlign = TextAlign.Center, + ) + } +} + +@ComponentPreview +@Composable +private fun EmptyContentPreview() { + NekiTheme { + EmptyContent( + emptyText = "아직 등록된 사진이 없어요\n찍은 네컷을 네키에 저장해보세요!", + ) + } +} diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/NoPhotoContent.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/NoPhotoContent.kt deleted file mode 100644 index cfc049feb..000000000 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/main/component/NoPhotoContent.kt +++ /dev/null @@ -1,47 +0,0 @@ -package com.neki.android.feature.archive.impl.main.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.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.neki.android.core.designsystem.ui.theme.NekiTheme - -@Composable -internal fun NoPhotoContent( - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier - .fillMaxWidth() - .padding(vertical = 70.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(22.dp), - ) { - Box( - modifier = Modifier - .clip(CircleShape) - .size(104.dp) - .background( - color = NekiTheme.colorScheme.gray50, - shape = CircleShape, - ), - ) - Text( - text = "아직 등록된 사진이 없어요\n찍은 네컷을 네키에 저장해보세요!", - style = NekiTheme.typography.body14Medium, - color = NekiTheme.colorScheme.gray300, - textAlign = TextAlign.Center, - ) - } -} 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 4282a8bd0..aa8c35d72 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,7 +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.EmptyAlbumContent 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 @@ -123,7 +123,7 @@ internal fun AllPhotoScreen( } if (isEmpty) { - EmptyContent( + EmptyAlbumContent( title = "모든 사진", onClickBack = { onIntent(AllPhotoIntent.ClickTopBarBackIcon) }, ) diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/AllPhotoFilterBar.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/AllPhotoFilterBar.kt index b7bded7f7..59ca2dd33 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/AllPhotoFilterBar.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/AllPhotoFilterBar.kt @@ -1,31 +1,22 @@ package com.neki.android.feature.archive.impl.photo.component -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding 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.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Popup -import androidx.compose.ui.window.PopupProperties import com.neki.android.core.designsystem.ComponentPreview -import com.neki.android.core.designsystem.modifier.clickableSingle -import com.neki.android.core.designsystem.modifier.dropdownShadow import com.neki.android.core.designsystem.ui.theme.NekiTheme +import com.neki.android.core.ui.component.DropdownPopup import com.neki.android.core.ui.component.FilterBar import com.neki.android.feature.archive.impl.photo.PhotoFilter +import kotlinx.collections.immutable.toImmutableList @Composable internal fun AllPhotoFilterBar( @@ -51,59 +42,23 @@ internal fun AllPhotoFilterBar( onClickDefaultChip = onClickFavoriteChip, ) if (showFilterPopup) { - PhotoFilterPopup( - selectedFilter = selectedFilter, + val density = LocalDensity.current + val popupOffsetX = with(density) { 20.dp.toPx().toInt() } + val popupOffsetY = with(density) { 46.dp.toPx().toInt() } + + DropdownPopup( + items = PhotoFilter.entries.toImmutableList(), + selectedItem = selectedFilter, + onSelect = onClickFilterRow, onDismissRequest = onDismissPopup, - onClickFilterRow = onClickFilterRow, + itemLabel = { it.label }, + modifier = Modifier.width(96.dp), + offset = IntOffset(x = popupOffsetX, y = popupOffsetY), ) } } } -@Composable -private fun PhotoFilterPopup( - selectedFilter: PhotoFilter, - onDismissRequest: () -> Unit, - onClickFilterRow: (PhotoFilter) -> Unit, -) { - val density = LocalDensity.current - val popupOffsetX = with(density) { 20.dp.toPx().toInt() } - val popupOffsetY = with(density) { 46.dp.toPx().toInt() } - - Popup( - offset = IntOffset(x = popupOffsetX, y = popupOffsetY), - alignment = Alignment.TopStart, - onDismissRequest = onDismissRequest, - properties = PopupProperties(focusable = true), - ) { - Column( - modifier = Modifier - .dropdownShadow(shape = RoundedCornerShape(8.dp)) - .background( - color = NekiTheme.colorScheme.white, - shape = RoundedCornerShape(8.dp), - ) - .width(96.dp) - .padding(vertical = 6.dp), - ) { - PhotoFilter.entries.forEach { filter -> - Text( - modifier = Modifier - .fillMaxWidth() - .background( - color = if (selectedFilter == filter) NekiTheme.colorScheme.gray50 - else NekiTheme.colorScheme.white, - ) - .clickableSingle { onClickFilterRow(filter) } - .padding(horizontal = 16.dp, vertical = 5.dp), - text = filter.label, - style = NekiTheme.typography.body14Medium, - ) - } - } - } -} - @ComponentPreview @Composable private fun AllPhotoFilterBarDefaultPreview() { diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/PhotoActionBar.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/PhotoActionBar.kt index f63120a05..d085a7875 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/PhotoActionBar.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo/component/PhotoActionBar.kt @@ -3,12 +3,8 @@ package com.neki.android.feature.archive.impl.photo.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -18,6 +14,7 @@ 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.actionbar.NekiBothSidesActionBar import com.neki.android.core.designsystem.modifier.noRippleClickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @@ -35,41 +32,37 @@ internal fun PhotoActionBar( enter = expandVertically(expandFrom = Alignment.Top), exit = shrinkVertically(shrinkTowards = Alignment.Top), ) { - Column( - modifier = modifier.fillMaxWidth(), - ) { - HorizontalDivider( - modifier = Modifier.fillMaxWidth(), - thickness = 1.dp, - color = NekiTheme.colorScheme.gray75, - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(20.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { + NekiBothSidesActionBar( + modifier = Modifier.fillMaxWidth(), + startContent = { Icon( - modifier = Modifier.noRippleClickableSingle( - enabled = isEnabled, - onClick = onClickDownload, - ), + modifier = Modifier + .padding(20.dp) + .noRippleClickableSingle( + enabled = isEnabled, + onClick = onClickDownload, + ), imageVector = ImageVector.vectorResource(R.drawable.icon_download), contentDescription = null, - tint = if (isEnabled) NekiTheme.colorScheme.gray600 else NekiTheme.colorScheme.gray200, + tint = if (isEnabled) NekiTheme.colorScheme.gray700 + else NekiTheme.colorScheme.gray200, ) + }, + endContent = { Icon( - modifier = Modifier.noRippleClickableSingle( - enabled = isEnabled, - onClick = onClickDelete, - ), + modifier = Modifier + .padding(20.dp) + .noRippleClickableSingle( + enabled = isEnabled, + onClick = onClickDelete, + ), imageVector = ImageVector.vectorResource(R.drawable.icon_trash), contentDescription = null, - tint = if (isEnabled) NekiTheme.colorScheme.gray600 else NekiTheme.colorScheme.gray200, + tint = if (isEnabled) NekiTheme.colorScheme.gray700 + else NekiTheme.colorScheme.gray200, ) - } - } + }, + ) } } @@ -90,7 +83,7 @@ private fun PhotoActionBarDisabledPreview() { NekiTheme { PhotoActionBar( visible = true, - isEnabled = true, + isEnabled = false, ) } } diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt index f3256d625..04bbf638e 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/PhotoDetailScreen.kt @@ -4,22 +4,20 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.HorizontalDivider import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.neki.android.core.designsystem.DevicePreview 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.LoadingDialog import com.neki.android.core.navigation.result.LocalResultEventBus +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 @@ -83,12 +81,6 @@ internal fun PhotoDetailScreen( contentScale = ContentScale.Fit, ) - HorizontalDivider( - modifier = Modifier.fillMaxWidth(), - thickness = 1.dp, - color = NekiTheme.colorScheme.gray75, - ) - PhotoDetailActionBar( isFavorite = uiState.photo.isFavorite, onClickDownload = { onIntent(PhotoDetailIntent.ClickDownloadIcon) }, diff --git a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt index 4ed8f8da4..22d7759bf 100644 --- a/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt +++ b/feature/archive/impl/src/main/kotlin/com/neki/android/feature/archive/impl/photo_detail/component/PhotoDetailActionBar.kt @@ -7,13 +7,13 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.actionbar.NekiBothSidesActionBar import com.neki.android.core.designsystem.modifier.noRippleClickable import com.neki.android.core.designsystem.ui.theme.NekiTheme @@ -25,51 +25,48 @@ internal fun PhotoDetailActionBar( onClickFavorite: () -> Unit = {}, onClickDelete: () -> Unit = {}, ) { - Row( - modifier = modifier - .fillMaxWidth() - .padding(20.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - Icon( - modifier = Modifier - .size(28.dp) - .noRippleClickable { onClickDownload() }, - imageVector = ImageVector.vectorResource(R.drawable.icon_download), - contentDescription = null, - tint = NekiTheme.colorScheme.gray500, - ) + NekiBothSidesActionBar( + modifier = modifier.fillMaxWidth(), + startContent = { + Row( + modifier = Modifier.padding(20.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Icon( + modifier = Modifier + .size(28.dp) + .noRippleClickable { onClickDownload() }, + imageVector = ImageVector.vectorResource(R.drawable.icon_download), + contentDescription = null, + tint = NekiTheme.colorScheme.gray700, + ) + Icon( + modifier = Modifier + .size(28.dp) + .noRippleClickable { onClickFavorite() }, + imageVector = ImageVector.vectorResource( + if (isFavorite) R.drawable.icon_heart_filled + else R.drawable.icon_heart_stroked, + ), + contentDescription = null, + tint = if (isFavorite) NekiTheme.colorScheme.primary400 + else NekiTheme.colorScheme.gray700, + ) + } + }, + endContent = { Icon( modifier = Modifier + .padding(20.dp) .size(28.dp) - .noRippleClickable { onClickFavorite() }, - imageVector = ImageVector.vectorResource( - if (isFavorite) R.drawable.icon_heart_filled - else R.drawable.icon_heart_stroked, - ), + .noRippleClickable { onClickDelete() }, + imageVector = ImageVector.vectorResource(R.drawable.icon_trash), contentDescription = null, - tint = if (isFavorite) { - NekiTheme.colorScheme.primary400 - } else { - NekiTheme.colorScheme.gray700 - }, + tint = NekiTheme.colorScheme.gray700, ) - } - - Icon( - modifier = Modifier - .size(28.dp) - .noRippleClickable { onClickDelete() }, - imageVector = ImageVector.vectorResource(R.drawable.icon_trash), - contentDescription = null, - tint = NekiTheme.colorScheme.gray600, - ) - } + }, + ) } @ComponentPreview diff --git a/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/const/MyPageConst.kt b/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/const/MyPageConst.kt new file mode 100644 index 000000000..2306c71d6 --- /dev/null +++ b/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/const/MyPageConst.kt @@ -0,0 +1,5 @@ +package com.neki.android.feature.mypage.impl.const + +internal object MyPageConst { + internal const val NICKNAME_MAX_LENGTH = 12 +} diff --git a/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/profile/EditProfileScreen.kt b/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/profile/EditProfileScreen.kt index 326f6fa16..2288688eb 100644 --- a/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/profile/EditProfileScreen.kt +++ b/feature/mypage/impl/src/main/java/com/neki/android/feature/mypage/impl/profile/EditProfileScreen.kt @@ -1,21 +1,12 @@ package com.neki.android.feature.mypage.impl.profile -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.input.InputTransformation -import androidx.compose.foundation.text.input.TextFieldLineLimits import androidx.compose.foundation.text.input.maxLength import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.Text import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.PickVisualMediaRequest import androidx.activity.result.contract.ActivityResultContracts @@ -28,11 +19,11 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.neki.android.core.designsystem.ComponentPreview +import com.neki.android.core.designsystem.NekiTextField import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.ui.component.LoadingDialog import com.neki.android.core.ui.compose.collectWithLifecycle @@ -48,6 +39,7 @@ import com.neki.android.feature.mypage.impl.profile.component.EditProfileImage import com.neki.android.feature.mypage.impl.profile.component.ProfileEditTopBar import com.neki.android.feature.mypage.impl.profile.component.SelectProfileImageDialog import com.neki.android.feature.mypage.impl.profile.component.ProfileImageOption +import com.neki.android.feature.mypage.impl.const.MyPageConst import timber.log.Timber @Composable @@ -120,60 +112,16 @@ fun EditProfileScreen( profileImage = displayProfileImage, onClickCameraIcon = { onIntent(MyPageIntent.ClickCameraIcon) }, ) - Column( + NekiTextField( + textFieldState = textFieldState, modifier = Modifier .fillMaxWidth() .padding(horizontal = 20.dp), - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - Text( - text = "닉네임", - style = NekiTheme.typography.body14Medium, - color = NekiTheme.colorScheme.gray700, - ) - BasicTextField( - state = textFieldState, - modifier = Modifier - .fillMaxWidth() - .background( - color = NekiTheme.colorScheme.white, - shape = RoundedCornerShape(8.dp), - ) - .border( - width = 1.dp, - color = if (textFieldState.text.isEmpty()) NekiTheme.colorScheme.gray75 else NekiTheme.colorScheme.gray700, - shape = RoundedCornerShape(8.dp), - ) - .padding(horizontal = 16.dp, vertical = 13.dp), - textStyle = NekiTheme.typography.body16Medium.copy( - color = NekiTheme.colorScheme.gray900, - ), - inputTransformation = InputTransformation.maxLength(12), - cursorBrush = SolidColor(NekiTheme.colorScheme.gray800), - lineLimits = TextFieldLineLimits.SingleLine, - decorator = { innerTextField -> - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Box(modifier = Modifier.weight(1f)) { - if (textFieldState.text.isEmpty()) { - Text( - text = "닉네임을 입력해주세요.", - style = NekiTheme.typography.body16Regular, - color = NekiTheme.colorScheme.gray300, - ) - } - innerTextField() - } - Text( - text = "${textFieldState.text.length}/12", - style = NekiTheme.typography.caption12Regular, - color = NekiTheme.colorScheme.gray300, - ) - } - }, - ) - } + titleLabel = "닉네임", + placeholder = "닉네임을 입력해주세요.", + maxLength = MyPageConst.NICKNAME_MAX_LENGTH, + inputTransformation = InputTransformation.maxLength(MyPageConst.NICKNAME_MAX_LENGTH), + ) } if (uiState.isShowImageSelectDialog) { diff --git a/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/component/QRScannerContent.kt b/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/component/QRScannerContent.kt index 40c3b1513..ba83e8d8d 100644 --- a/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/component/QRScannerContent.kt +++ b/feature/photo-upload/impl/src/main/java/com/neki/android/feature/photo_upload/impl/qrscan/component/QRScannerContent.kt @@ -161,7 +161,7 @@ internal fun QRScannerContent( NekiIconButton( modifier = Modifier .padding(top = 37.dp) - .size(48.dp) + .size(56.dp) .clip(CircleShape) .background( if (isTorchEnabled) Color.White @@ -170,6 +170,7 @@ internal fun QRScannerContent( onClick = { onIntent(QRScanIntent.ToggleTorch) }, ) { Icon( + modifier = Modifier.size(28.dp), imageVector = ImageVector.vectorResource( if (isTorchEnabled) R.drawable.icon_torch_on else R.drawable.icon_torch_off, diff --git a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailScreen.kt b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailScreen.kt index 113c37264..f7a6f8d84 100644 --- a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailScreen.kt +++ b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/PoseDetailScreen.kt @@ -3,23 +3,18 @@ package com.neki.android.feature.pose.impl.detail import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.neki.android.core.designsystem.DevicePreview -import com.neki.android.core.designsystem.button.NekiIconButton import com.neki.android.core.designsystem.topbar.BackTitleTopBar import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Pose @@ -27,7 +22,7 @@ import com.neki.android.core.navigation.result.LocalResultEventBus import com.neki.android.core.ui.compose.collectWithLifecycle import com.neki.android.core.ui.toast.NekiToast import com.neki.android.feature.pose.api.PoseResult -import com.neki.android.core.designsystem.R +import com.neki.android.feature.pose.impl.detail.component.PoseActionBar @Composable internal fun PoseDetailRoute( @@ -87,22 +82,10 @@ internal fun PoseDetailScreen( thickness = 1.dp, color = NekiTheme.colorScheme.gray75, ) - NekiIconButton( - modifier = Modifier - .align(Alignment.End) - .padding(8.dp), - onClick = { onIntent(PoseDetailIntent.ClickScrapIcon) }, - ) { - Icon( - imageVector = ImageVector.vectorResource( - if (uiState.pose.isScrapped) R.drawable.icon_scrap_filled - else R.drawable.icon_scrap_stroked, - ), - contentDescription = null, - tint = if (uiState.pose.isScrapped) NekiTheme.colorScheme.gray900 - else NekiTheme.colorScheme.gray500, - ) - } + PoseActionBar( + isScrapped = uiState.pose.isScrapped, + onClickScrap = { onIntent(PoseDetailIntent.ClickScrapIcon) }, + ) } } diff --git a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/component/PoseActionBar.kt b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/component/PoseActionBar.kt new file mode 100644 index 000000000..9c0b73011 --- /dev/null +++ b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/detail/component/PoseActionBar.kt @@ -0,0 +1,59 @@ +package com.neki.android.feature.pose.impl.detail.component + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.actionbar.NekiEndActionBar +import com.neki.android.core.designsystem.button.NekiIconButton +import com.neki.android.core.designsystem.ui.theme.NekiTheme + +@Composable +internal fun PoseActionBar( + isScrapped: Boolean, + modifier: Modifier = Modifier, + onClickScrap: () -> Unit = {}, +) { + NekiEndActionBar( + modifier = modifier.fillMaxWidth(), + ) { + NekiIconButton( + modifier = Modifier.padding(8.dp), + onClick = onClickScrap, + ) { + Icon( + modifier = Modifier.size(28.dp), + imageVector = ImageVector.vectorResource( + if (isScrapped) R.drawable.icon_scrap_filled + else R.drawable.icon_scrap_stroked, + ), + contentDescription = null, + tint = if (isScrapped) NekiTheme.colorScheme.gray900 + else NekiTheme.colorScheme.gray500, + ) + } + } +} + +@ComponentPreview +@Composable +private fun ScrappedPoseActionBarPreview() { + NekiTheme { + PoseActionBar(isScrapped = true) + } +} + +@ComponentPreview +@Composable +private fun UnScrappedPoseActionBarPreview() { + NekiTheme { + PoseActionBar(isScrapped = false) + } +} diff --git a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/PoseListContent.kt b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/PoseListContent.kt index d12ddbe76..a7a605fad 100644 --- a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/PoseListContent.kt +++ b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/PoseListContent.kt @@ -21,7 +21,6 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.vectorResource @@ -33,9 +32,9 @@ 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.noRippleClickable -import com.neki.android.core.designsystem.modifier.poseBackground import com.neki.android.core.designsystem.ui.theme.NekiTheme import com.neki.android.core.model.Pose +import com.neki.android.core.ui.component.GridItemOverlay import com.neki.android.feature.pose.impl.const.PoseConst.POSE_LAYOUT_BOTTOM_PADDING import com.neki.android.feature.pose.impl.const.PoseConst.POSE_LAYOUT_VERTICAL_SPACING @@ -101,10 +100,9 @@ private fun PoseItem( } }, ) - Box( - modifier = Modifier - .matchParentSize() - .poseBackground(shape = RectangleShape), + GridItemOverlay( + modifier = Modifier.matchParentSize(), + shape = RoundedCornerShape(12.dp), ) if (pose.isScrapped) { Icon( diff --git a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/RecommendationChip.kt b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/RecommendationChip.kt index 35c2c8cbe..728975cdf 100644 --- a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/RecommendationChip.kt +++ b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/main/component/RecommendationChip.kt @@ -17,6 +17,7 @@ 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.buttonShadow import com.neki.android.core.designsystem.modifier.clickableSingle import com.neki.android.core.designsystem.ui.theme.NekiTheme @@ -27,6 +28,7 @@ internal fun RecommendationChip( ) { Row( modifier = modifier + .buttonShadow() .clip(CircleShape) .clickableSingle(onClick = onClick) .background(shape = CircleShape, color = NekiTheme.colorScheme.gray800) @@ -35,7 +37,7 @@ internal fun RecommendationChip( verticalAlignment = Alignment.CenterVertically, ) { Icon( - modifier = Modifier.size(20.dp), + modifier = Modifier.size(24.dp), imageVector = ImageVector.vectorResource(R.drawable.icon_repeat), contentDescription = null, tint = NekiTheme.colorScheme.primary400, diff --git a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseFloatingBar.kt b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseFloatingBar.kt index 824f44374..0b560a094 100644 --- a/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseFloatingBar.kt +++ b/feature/pose/impl/src/main/java/com/neki/android/feature/pose/impl/random/component/RandomPoseFloatingBar.kt @@ -22,9 +22,9 @@ import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview 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.button.NekiIconButton import com.neki.android.core.designsystem.ui.theme.NekiTheme -import com.neki.android.core.designsystem.R @Composable internal fun RandomPoseFloatingBarContent( @@ -82,10 +82,6 @@ private fun RandomPoseFloatingBar( modifier = modifier .fillMaxWidth() .clip(CircleShape) - .background( - color = NekiTheme.colorScheme.white.copy(alpha = 0.6f), - shape = CircleShape, - ) .border( width = 1.dp, brush = Brush.verticalGradient( @@ -96,6 +92,10 @@ private fun RandomPoseFloatingBar( ), shape = CircleShape, ) + .background( + color = NekiTheme.colorScheme.white.copy(alpha = 0.3f), + shape = CircleShape, + ) .padding(8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically,