diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index 361fe5cb8..35eb1ddfb 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -2,6 +2,5 @@
-
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index aac53f2f3..4d94f3921 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -95,7 +95,7 @@ dependencies {
projects.feature.examResult,
projects.feature.solveProblem,
projects.feature.notification,
- projects.feature.createProblem,
+ projects.feature.createExam,
projects.feature.startExam,
projects.feature.detail,
projects.feature.profile,
diff --git a/build-logic/src/main/kotlin/team/duckie/app/android/convention/ApplicationConstants.kt b/build-logic/src/main/kotlin/team/duckie/app/android/convention/ApplicationConstants.kt
index ed3e1a391..3179eaea9 100644
--- a/build-logic/src/main/kotlin/team/duckie/app/android/convention/ApplicationConstants.kt
+++ b/build-logic/src/main/kotlin/team/duckie/app/android/convention/ApplicationConstants.kt
@@ -14,9 +14,9 @@ import org.gradle.api.JavaVersion
*/
internal object ApplicationConstants {
const val minSdk = 23
- const val targetSdk = 33
- const val compileSdk = 33
- const val versionCode = 24
+ const val targetSdk = 34
+ const val compileSdk = 34
+ const val versionCode = 30
const val versionName = "1.3.0"
val javaVersion = JavaVersion.VERSION_17
}
diff --git a/common/android/src/main/kotlin/team/duckie/app/android/common/android/image/MediaUtil.kt b/common/android/src/main/kotlin/team/duckie/app/android/common/android/image/MediaUtil.kt
index 89734fe71..45a35c3ee 100644
--- a/common/android/src/main/kotlin/team/duckie/app/android/common/android/image/MediaUtil.kt
+++ b/common/android/src/main/kotlin/team/duckie/app/android/common/android/image/MediaUtil.kt
@@ -14,6 +14,8 @@ import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import android.os.Build
+import com.google.firebase.crashlytics.FirebaseCrashlytics
+import team.duckie.app.android.common.kotlin.AllowMagicNumber
import java.io.BufferedInputStream
import java.io.File
import java.io.FileOutputStream
@@ -24,13 +26,13 @@ import java.util.UUID
* Media 처리용 유틸 클래스
* // TODO(riflockle7): 추후 ImageUtil 로 바꿀지 고민
*/
+@AllowMagicNumber("for MediaUtil")
object MediaUtil {
private const val maxWidth = 1600
private const val maxHeight = 1600
private const val bitmapQuality = 100
- private const val rotate90 = 90
- private const val rotate180 = 180
- private const val rotate270 = 270
+
+ private var bitmap: Bitmap? = null
/**
* 업로드할 이미지를 가져온다.
@@ -58,9 +60,9 @@ object MediaUtil {
decodeBitmapFromUri(uri, applicationContext, maxSizeLimitEnable)?.apply {
compress(Bitmap.CompressFormat.JPEG, bitmapQuality, fos)
- recycle()
} ?: throw NullPointerException("bitmap 생성 오류")
+ bitmap = null
fos.flush()
fos.close()
@@ -76,7 +78,6 @@ object MediaUtil {
// 인자 값으로 넘어온 입력 스트림을 나중에 사용하기 위해 저장하는 BufferedInputStream 사용
val input = BufferedInputStream(context.contentResolver.openInputStream(uri))
input.mark(input.available()) // 입력 스트림의 특정 위치를 기억
- var bitmap: Bitmap?
BitmapFactory.Options().run {
if (maxSizeLimitEnable) {
@@ -130,23 +131,44 @@ object MediaUtil {
ExifInterface(uri.path!!)
}
- return when (
- exif.getAttributeInt(
- ExifInterface.TAG_ORIENTATION,
- ExifInterface.ORIENTATION_NORMAL,
- )
- ) {
- ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(bitmap, rotate90)
- ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(bitmap, rotate180)
- ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(bitmap, rotate270)
- else -> bitmap
- }
+ val orientation = exif.getAttributeInt(
+ ExifInterface.TAG_ORIENTATION,
+ ExifInterface.ORIENTATION_NORMAL,
+ )
+ return rotateBitmap(bitmap, orientation)
}
/** 이미지를 회전한다. */
- private fun rotateImage(bitmap: Bitmap, degree: Int): Bitmap? {
+ private fun rotateBitmap(bitmap: Bitmap, orientation: Int): Bitmap? {
val matrix = Matrix()
- matrix.postRotate(degree.toFloat())
- return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ when (orientation) {
+ ExifInterface.ORIENTATION_NORMAL -> return bitmap
+ ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
+ ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
+ ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
+ matrix.setRotate(180f)
+ matrix.postScale(-1f, 1f)
+ }
+
+ ExifInterface.ORIENTATION_TRANSPOSE -> {
+ matrix.setRotate(90f)
+ matrix.postScale(-1f, 1f)
+ }
+
+ ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
+ ExifInterface.ORIENTATION_TRANSVERSE -> {
+ matrix.setRotate(-90f)
+ matrix.postScale(-1f, 1f)
+ }
+
+ ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
+ else -> return bitmap
+ }
+ return try {
+ Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+ } catch (e: OutOfMemoryError) {
+ FirebaseCrashlytics.getInstance().recordException(e)
+ null
+ }
}
}
diff --git a/common/android/src/main/kotlin/team/duckie/app/android/common/android/timer/ProblemTimer.kt b/common/android/src/main/kotlin/team/duckie/app/android/common/android/timer/ProblemTimer.kt
index 85825c6f3..d138b0eed 100644
--- a/common/android/src/main/kotlin/team/duckie/app/android/common/android/timer/ProblemTimer.kt
+++ b/common/android/src/main/kotlin/team/duckie/app/android/common/android/timer/ProblemTimer.kt
@@ -44,4 +44,8 @@ class ProblemTimer(
fun stop() {
timerJob?.cancel()
}
+
+ fun setTotalTime(time: Float) {
+ _remainingTime.value = time
+ }
}
diff --git a/common/android/src/main/kotlin/team/duckie/app/android/common/android/ui/const/Extras.kt b/common/android/src/main/kotlin/team/duckie/app/android/common/android/ui/const/Extras.kt
index da91eb0d5..cc9bc515b 100644
--- a/common/android/src/main/kotlin/team/duckie/app/android/common/android/ui/const/Extras.kt
+++ b/common/android/src/main/kotlin/team/duckie/app/android/common/android/ui/const/Extras.kt
@@ -21,6 +21,9 @@ object Extras {
const val SearchTag = "ExtraSearchTag"
const val StartGuide = "StartGuide"
+ /** using for SearchActivity */
+ const val AutoFocusing = "AutoFocusing"
+
/** using for FriendsActivity */
const val FriendType = "ExtraFriendType"
const val ProfileNickName = "ProfileNickName"
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/Keyboard.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/Keyboard.kt
index cfa8b05cd..4d70ace24 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/Keyboard.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/Keyboard.kt
@@ -5,17 +5,25 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalMaterialApi::class)
+
package team.duckie.app.android.common.compose
import android.graphics.Rect
import android.view.ViewTreeObserver
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetState
+import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.LocalView
import team.duckie.app.android.common.kotlin.AllowMagicNumber
@@ -63,3 +71,17 @@ fun Modifier.composedWithKeyboardVisibility(
.composed { if (keyboardVisible.value) whenKeyboardVisible() else this }
.composed { if (!keyboardVisible.value) whenKeyboardHidden() else this }
}
+
+@Composable
+fun HideKeyboardWhenBottomSheetHidden(sheetState: ModalBottomSheetState) {
+ val keyboard = LocalSoftwareKeyboardController.current
+
+ LaunchedEffect(Unit) {
+ val sheetStateFlow = snapshotFlow { sheetState.currentValue }
+ sheetStateFlow.collect { state ->
+ if (state == ModalBottomSheetValue.Hidden) {
+ keyboard?.hide()
+ }
+ }
+ }
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagerState.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagerState.kt
index e28999974..506df36f9 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagerState.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagerState.kt
@@ -31,3 +31,7 @@ suspend inline fun PagerState.moveNextPage(maxPage: Int) {
fun PagerState.isCurrentPage(targetIndex: Int): Boolean {
return currentPageOffsetFraction == 0f && targetIndex == currentPage
}
+
+fun PagerState.isTargetPage(targetIndex: Int): Boolean {
+ return targetIndex == targetPage
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagingItemsKey.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagingItemsKey.kt
index c77aa1e9f..f7116edbc 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagingItemsKey.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/PagingItemsKey.kt
@@ -30,3 +30,11 @@ fun itemsPagingKey(
key(index) ?: index
}
}
+
+/**
+ * 서버측 에러로 PK가 중복으로 내려오는 경우 방치
+ *
+ * @return PK + [secondId] 형식의 String
+ */
+fun Int?.getUniqueKey(secondId: Int): String =
+ this.toString() + secondId
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/constants.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/constants.kt
index 8388c1a38..53d2dde1f 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/constants.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/constants.kt
@@ -11,3 +11,12 @@ package team.duckie.app.android.common.compose
// 328 x 240 비율에서 이미지의 높이값을 가져올 수 있는 비율값
const val GetHeightRatioW328H240 = 328.toFloat() / 240
+
+// 4 x 1 비율에서 이미지의 높이값을 가져올 수 있는 비율값
+const val GetHeightRatioW360H90 = 4.toFloat() / 1
+
+// 85 x 63 비율에서 이미지의 높이값을 가져올 수 있는 비율값
+const val GetHeightRatioW85H63 = 85.toFloat() / 63
+
+// 129 x 84 비율에서 이미지의 높이값을 가져올 수 있는 비율값
+const val GetHeightRatioW129H84 = 129.toFloat() / 84
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/BackPressedHeadLineTopAppBar.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/BackPressedHeadLineTopAppBar.kt
index 11b69f507..e4c6bc0b6 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/BackPressedHeadLineTopAppBar.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/BackPressedHeadLineTopAppBar.kt
@@ -72,6 +72,7 @@ fun BackPressedHeadLineTopAppBar(
fun BackPressedHeadLine2TopAppBar(
title: String,
isLoading: Boolean = false,
+ trailingContent: (@Composable () -> Unit)? = null,
onBackPressed: () -> Unit,
) {
BackPressedTopAppBar(onBackPressed = onBackPressed) {
@@ -79,5 +80,9 @@ fun BackPressedHeadLine2TopAppBar(
modifier = Modifier.skeleton(isLoading),
text = title,
)
+ Spacer(weight = 1f)
+ if (trailingContent != null) {
+ trailingContent()
+ }
}
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/Divider.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/Divider.kt
index f4cd2cf43..294e60293 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/Divider.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/Divider.kt
@@ -54,3 +54,28 @@ fun DuckieDivider(
.background(color = color.value),
)
}
+
+/**
+ * QuackDivider 를 그리기 위한 리소스들을 정의합니다.
+ */
+private object QuackDividerDefaults {
+ val Color = QuackColor.Gray3
+ val Height = 1.dp
+}
+
+/**
+ * 덕키에서 사용되는 구분선(divider)을 그립니다.
+ *
+ * @param modifier 이 컴포넌트에 사용할 [Modifier]
+ */
+@Composable
+public fun QuackDivider(modifier: Modifier = Modifier) {
+ with(QuackDividerDefaults) {
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .height(Height)
+ .background(color = Color.value),
+ )
+ }
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckExamCoverItem.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckExamCoverItem.kt
index 76e2ad1a8..abec7ead4 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckExamCoverItem.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckExamCoverItem.kt
@@ -28,6 +28,7 @@ import coil.compose.AsyncImage
import team.duckie.app.android.common.compose.R
import team.duckie.app.android.common.compose.ui.icon.v1.MoreId
import team.duckie.app.android.common.kotlin.runIf
+import team.duckie.app.android.common.kotlin.toDecimalFormat
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.icon.QuackIcon
@@ -140,7 +141,7 @@ internal fun DuckSmallCoverInternal(
horizontalArrangement = Arrangement.spacedBy(2.dp),
) {
QuackText(
- text = "${stringResource(id = R.string.examinee)} ${duckTestCoverItem.solvedCount}",
+ text = "${stringResource(id = R.string.examinee)} ${duckTestCoverItem.solvedCount.toDecimalFormat()}",
typography = QuackTypography.Body2.change(color = QuackColor.Gray2),
)
QuackText(
@@ -148,7 +149,7 @@ internal fun DuckSmallCoverInternal(
typography = QuackTypography.Body2.change(color = QuackColor.Gray2),
)
QuackText(
- text = "${stringResource(id = R.string.heart)} ${duckTestCoverItem.heartCount}",
+ text = "${stringResource(id = R.string.heart)} ${duckTestCoverItem.heartCount.toDecimalFormat()}",
typography = QuackTypography.Body2.change(color = QuackColor.Gray2),
)
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckieHorizontalPagerIndicator.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckieHorizontalPagerIndicator.kt
index d9be1005a..569c0b04e 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckieHorizontalPagerIndicator.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/DuckieHorizontalPagerIndicator.kt
@@ -75,10 +75,20 @@ fun DuckieHorizontalPagerIndicator(
Box(
Modifier
.offset {
- val scrollPosition = (pagerState.currentPage + pagerState.currentPageOffsetFraction)
- .coerceIn(0f, pagerState.currentPage.coerceAtLeast(0).toFloat())
+ val scrollPosition =
+ (pagerState.currentPage + pagerState.currentPageOffsetFraction)
+ .coerceIn(
+ minimumValue = 0f,
+ maximumValue = pagerState.currentPage
+ .coerceAtLeast(0)
+ .toFloat(),
+ )
IntOffset(
- x = ((spacingPx + indicatorWidthPx) * scrollPosition).toInt(),
+ x = if (pagerState.targetPage == 0) {
+ 0
+ } else {
+ ((spacingPx + indicatorWidthPx) * scrollPosition).toInt()
+ },
y = 0,
)
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/EmptyText.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/EmptyText.kt
index 98c419036..7fc50805c 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/EmptyText.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/EmptyText.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import team.duckie.quackquack.material.QuackColor
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/FavoriteTagSection.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/FavoriteTagSection.kt
index f3b93e2f3..4eb3b04ee 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/FavoriteTagSection.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/FavoriteTagSection.kt
@@ -14,16 +14,15 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import team.duckie.app.android.common.compose.ui.quack.todo.QuackLazyVerticalGridTag
-import team.duckie.app.android.common.compose.ui.quack.todo.QuackSingeLazyRowTag
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackText
import team.duckie.quackquack.ui.sugar.QuackTitle2
-import team.duckie.quackquack.ui.icon.QuackIcon as QuackV1Icon
/**
* 관심태그 섹션
@@ -48,10 +47,8 @@ fun FavoriteTagSection(
title: String,
horizontalPadding: PaddingValues = PaddingValues(0.dp),
verticalArrangement: Arrangement.HorizontalOrVertical = Arrangement.spacedBy(0.dp),
- // TODO(riflockle7): 다음 작업에서 QuackV2 로 대응하기
- trailingIcon: QuackV1Icon? = null,
+ trailingIcon: ImageVector? = null,
onTrailingClick: ((Int) -> Unit)? = null,
- singleLine: Boolean = true,
emptySection: @Composable () -> Unit,
tags: ImmutableList,
onTagClick: (Int) -> Unit,
@@ -74,25 +71,15 @@ fun FavoriteTagSection(
// 태그가 없을 경우 표시되는 Section
emptySection()
} else {
- if (singleLine) {
- // 한 줄로 표현되는 태그 목록
- QuackSingeLazyRowTag(
- items = tagList,
- contentPadding = horizontalPadding,
- tagTypeResId = trailingIcon?.drawableId,
- onClick = { index -> onTagClick(index) },
- )
- } else {
- // 여러 줄로 표현되는 태그 목록
- QuackLazyVerticalGridTag(
- contentPadding = horizontalPadding,
- horizontalSpace = 4.dp,
- items = tagList,
- tagTypeResId = trailingIcon?.drawableId,
- onClick = { index -> onTagClick(index) },
- itemChunkedSize = 4,
- )
- }
+ QuackLazyVerticalGridTag(
+ contentPadding = horizontalPadding,
+ horizontalSpace = 4.dp,
+ items = tagList,
+ trailingIcon = trailingIcon,
+ onTrailingClick = onTrailingClick,
+ onClick = { index -> onTagClick(index) },
+ itemChunkedSize = 4,
+ )
}
// 추가 버튼
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/PhotoPicker.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/PhotoPicker.kt
index 04bcc156e..45f8e31cd 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/PhotoPicker.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/PhotoPicker.kt
@@ -33,7 +33,6 @@ import com.ujizin.camposer.state.rememberCameraState
import kotlinx.collections.immutable.ImmutableList
import team.duckie.app.android.common.compose.R
import team.duckie.app.android.common.compose.ui.icon.v1.CameraId
-import team.duckie.app.android.common.compose.ui.icon.v1.CloseId
import team.duckie.app.android.common.compose.ui.quack.todo.QuackGridLayout
import team.duckie.app.android.common.compose.ui.quack.todo.QuackSelectableImage
import team.duckie.app.android.common.compose.ui.quack.todo.QuackTopAppBar
@@ -42,6 +41,8 @@ import team.duckie.app.android.common.kotlin.runIf
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackImage
import team.duckie.quackquack.ui.QuackText
@@ -116,7 +117,11 @@ fun PhotoPicker(
Column(modifier = modifier.zIndex(zIndex)) {
QuackTopAppBar(
- leadingIconResId = QuackIcon.CloseId,
+ modifier = Modifier.padding(
+ start = 12.dp,
+ end = 16.dp,
+ ),
+ leadingIcon = QuackIcon.Outlined.Close,
onLeadingIconClick = onCloseClick,
centerText = stringResource(R.string.topappbar_filter_full),
trailingContent = {
@@ -128,11 +133,7 @@ fun PhotoPicker(
rippleEnabled = false,
onClick = onAddClick,
)
- }
- .padding(
- horizontal = 16.dp,
- vertical = 15.dp,
- ),
+ },
text = stringResource(R.string.topappbar_add),
typography = QuackTypography.Subtitle.change(
color = if (isAddable) {
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/TextTabLayout.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/TextTabLayout.kt
index d4a7148fe..03cfa56e9 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/TextTabLayout.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/TextTabLayout.kt
@@ -23,8 +23,8 @@ import team.duckie.quackquack.ui.QuackText
@Composable
fun TextTabLayout(
titles: ImmutableList,
- selectedTabStyle: QuackTypography = QuackTypography.HeadLine2,
- tabStyle: QuackTypography = QuackTypography.Title2,
+ selectedTabStyle: QuackTypography = QuackTypography.HeadLine1,
+ tabStyle: QuackTypography = QuackTypography.HeadLine2,
selectedTabIndex: Int,
onTabSelected: (Int) -> Unit,
space: Dp = 12.dp,
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/BasicContentLayout.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/BasicContentLayout.kt
new file mode 100644
index 000000000..34a333291
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/BasicContentLayout.kt
@@ -0,0 +1,166 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.content
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.Layout
+import androidx.compose.ui.layout.MeasurePolicy
+import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.asLoose
+import team.duckie.app.android.common.compose.centerVerticalWithMaxHeight
+import team.duckie.app.android.common.compose.ui.skeleton
+import team.duckie.app.android.common.kotlin.fastFirstOrNull
+import team.duckie.app.android.common.kotlin.npe
+import team.duckie.app.android.common.kotlin.runIf
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.sugar.QuackBody3
+import team.duckie.quackquack.ui.sugar.QuackSubtitle2
+
+@Stable
+private object UserContentWithButtonDefaults {
+ const val LeadingImageLayoutId = "LeadingImageLayoutId"
+ const val TitleLayoutId = "TitleLayoutId"
+ const val DescriptionLayoutId = "DescriptionLayoutId"
+ const val TrailingButtonLayoutId = "TrailingButtonLayoutId"
+}
+
+/**
+ * 해당 버튼을 사용하기 위해선 [trailingButton] 에 modifier 를 넣어야 합니다.
+ */
+@Composable
+internal fun BasicContentWithButtonLayout(
+ modifier: Modifier = Modifier,
+ contentId: Int,
+ nickname: String,
+ rippleEnabled: Boolean = true,
+ description: String,
+ onClickLayout: ((Int) -> Unit)? = null,
+ visibleTrailingButton: Boolean = false,
+ isTitleCenter: Boolean = false,
+ visibleHorizontalPadding: Boolean = true,
+ leadingImageContent: @Composable (modifier: Modifier) -> Unit,
+ trailingButton: @Composable (modifier: Modifier) -> Unit,
+ isLoading: Boolean = false,
+) = with(UserContentWithButtonDefaults) {
+ Layout(
+ modifier = modifier
+ .quackClickable(
+ rippleEnabled = rippleEnabled,
+ onClick = {
+ if (onClickLayout != null) {
+ onClickLayout(contentId)
+ }
+ },
+ )
+ .fillMaxWidth()
+ .height(56.dp)
+ .padding(vertical = 12.dp)
+ .runIf(visibleHorizontalPadding) {
+ padding(horizontal = 16.dp)
+ },
+ content = {
+ leadingImageContent(
+ modifier = modifier
+ .layoutId(LeadingImageLayoutId)
+ .skeleton(isLoading),
+ )
+ QuackSubtitle2(
+ modifier = Modifier
+ .layoutId(TitleLayoutId)
+ .padding(start = 8.dp)
+ .skeleton(isLoading),
+ text = nickname,
+ )
+ QuackBody3(
+ modifier = Modifier
+ .layoutId(DescriptionLayoutId)
+ .padding(start = 8.dp)
+ .skeleton(isLoading),
+ text = description,
+ )
+ trailingButton.invoke(
+ modifier = modifier
+ .layoutId(TrailingButtonLayoutId)
+ .skeleton(isLoading),
+ )
+ },
+ measurePolicy = getUserContentLayoutMeasurePolicy(
+ isTitleCenter = isTitleCenter,
+ visibleTrailingButton = visibleTrailingButton,
+ ),
+ )
+}
+
+private fun getUserContentLayoutMeasurePolicy(
+ isTitleCenter: Boolean,
+ visibleTrailingButton: Boolean,
+) = MeasurePolicy { measurables, constraints ->
+ with(UserContentWithButtonDefaults) {
+ val extraLooseConstraints = constraints.asLoose(width = true)
+
+ val leadingImagePlaceable = measurables.fastFirstOrNull { measurable ->
+ measurable.layoutId == LeadingImageLayoutId
+ }?.measure(extraLooseConstraints) ?: npe()
+
+ val titlePlaceable = measurables.fastFirstOrNull { measurable ->
+ measurable.layoutId == TitleLayoutId
+ }?.measure(extraLooseConstraints) ?: npe()
+
+ val descriptionPlaceable = measurables.fastFirstOrNull { measurable ->
+ measurable.layoutId == DescriptionLayoutId
+ }?.measure(extraLooseConstraints) ?: npe()
+
+ val trailingButtonPlaceable = measurables.fastFirstOrNull { measurable ->
+ measurable.layoutId == TrailingButtonLayoutId
+ }?.measure(extraLooseConstraints) ?: npe()
+
+ layout(
+ width = constraints.maxWidth,
+ height = constraints.maxHeight,
+ ) {
+ leadingImagePlaceable.place(
+ x = 0,
+ y = 0,
+ )
+
+ titlePlaceable.place(
+ x = leadingImagePlaceable.width,
+ y = if (isTitleCenter) {
+ constraints.centerVerticalWithMaxHeight(titlePlaceable.height)
+ } else {
+ 0
+ },
+ )
+
+ descriptionPlaceable.place(
+ x = leadingImagePlaceable.width,
+ y = titlePlaceable.height,
+ )
+
+ if (visibleTrailingButton) {
+ trailingButtonPlaceable.place(
+ x = constraints.maxWidth - trailingButtonPlaceable.width,
+ y = constraints.centerVerticalWithMaxHeight(trailingButtonPlaceable.height),
+ )
+ }
+ }
+ }
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/IgnoreContentLayout.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/IgnoreContentLayout.kt
new file mode 100644
index 000000000..ede4f834e
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/IgnoreContentLayout.kt
@@ -0,0 +1,126 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
+package team.duckie.app.android.common.compose.ui.content
+
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.width
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.CoverImageRatio
+import team.duckie.app.android.common.compose.R
+import team.duckie.app.android.common.compose.ui.quack.QuackProfileImage
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackButton
+import team.duckie.quackquack.ui.QuackButtonStyle
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+
+private val ProfileImageSize: DpSize = DpSize(32.dp, 32.dp)
+
+@Composable
+fun ExamIgnoreLayout(
+ modifier: Modifier = Modifier,
+ examId: Int,
+ examThumbnailUrl: String,
+ name: String,
+ likeNum: Int? = null,
+ solverNum: Int? = null,
+ onClickUserProfile: ((Int) -> Unit)? = null,
+ visibleTrailingButton: Boolean = true,
+ onClickTrailingButton: (Int) -> Unit,
+ isLoading: Boolean = false,
+ rippleEnabled: Boolean = false,
+) {
+ BasicContentWithButtonLayout(
+ isLoading = isLoading,
+ modifier = modifier,
+ contentId = examId,
+ nickname = name,
+ onClickLayout = onClickUserProfile,
+ description = buildString {
+ append(solverNum ?: "")
+ append(if (likeNum != null) "· 좋아요 $likeNum" else "")
+ },
+ trailingButton = {
+ QuackButton( // FIXME(limsaehyun) enabled 버튼 모양이 disabled로 바뀌어버림
+ modifier = it.quackClickable {
+ onClickTrailingButton(examId)
+ },
+ text = stringResource(id = R.string.cancel_igonre),
+ style = QuackButtonStyle.PrimaryOutlinedSmall,
+ onClick = { },
+ enabled = false,
+ )
+ },
+ visibleTrailingButton = visibleTrailingButton,
+ leadingImageContent = {
+ QuackImage(
+ modifier = it
+ .width(68.dp)
+ .aspectRatio(CoverImageRatio),
+ src = examThumbnailUrl,
+ contentScale = ContentScale.Crop,
+ )
+ },
+ isTitleCenter = likeNum == null && solverNum == null,
+ visibleHorizontalPadding = false,
+ rippleEnabled = rippleEnabled,
+ )
+}
+
+@Composable
+fun UserIgnoreLayout(
+ modifier: Modifier = Modifier,
+ userId: Int,
+ profileImageIrl: String,
+ nickname: String,
+ favoriteTag: String,
+ rippleEnabled: Boolean = true,
+ tier: String,
+ onClickUserProfile: ((Int) -> Unit)? = null,
+ visibleTrailingButton: Boolean = true,
+ onClickTrailingButton: (Int) -> Unit,
+ isLoading: Boolean = false,
+) {
+ BasicContentWithButtonLayout(
+ isLoading = isLoading,
+ modifier = modifier,
+ contentId = userId,
+ nickname = nickname,
+ description = tier + if (favoriteTag.isNotEmpty()) "· $favoriteTag" else "",
+ onClickLayout = onClickUserProfile,
+ trailingButton = {
+ QuackButton( // FIXME(limsaehyun) enabled 버튼 모양이 disabled로 바뀌어버림
+ modifier = it.quackClickable {
+ onClickTrailingButton(userId)
+ },
+ text = stringResource(id = R.string.cancel_igonre),
+ style = QuackButtonStyle.PrimaryOutlinedSmall,
+ onClick = { },
+ enabled = false,
+ )
+ },
+ visibleTrailingButton = visibleTrailingButton,
+ leadingImageContent = {
+ QuackProfileImage(
+ modifier = it,
+ profileUrl = profileImageIrl,
+ size = ProfileImageSize,
+ )
+ },
+ isTitleCenter = tier.isEmpty() || favoriteTag.isEmpty(),
+ visibleHorizontalPadding = false,
+ rippleEnabled = rippleEnabled,
+ )
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/UserFollowingLayout.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/UserFollowingLayout.kt
new file mode 100644
index 000000000..1fbd390e9
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/content/UserFollowingLayout.kt
@@ -0,0 +1,70 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.content
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.R
+import team.duckie.app.android.common.compose.ui.quack.QuackProfileImage
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.modifier.quackClickable
+
+private val ProfileImageSize: DpSize = DpSize(32.dp, 32.dp)
+
+@Composable
+fun UserFollowingLayout(
+ modifier: Modifier = Modifier,
+ userId: Int,
+ profileImgUrl: String,
+ nickname: String,
+ favoriteTag: String,
+ tier: String,
+ isFollowing: Boolean,
+ onClickUserProfile: ((Int) -> Unit)? = null,
+ visibleTrailingButton: Boolean = false,
+ onClickTrailingButton: (Boolean) -> Unit,
+ isLoading: Boolean = false,
+) {
+ BasicContentWithButtonLayout(
+ isLoading = isLoading,
+ modifier = modifier,
+ contentId = userId,
+ nickname = nickname,
+ description = tier + if (favoriteTag.isNotEmpty()) "· $favoriteTag" else "",
+ onClickLayout = onClickUserProfile,
+ trailingButton = {
+ QuackText(
+ modifier = it
+ .quackClickable(
+ onClick = {
+ onClickTrailingButton(!isFollowing)
+ },
+ rippleEnabled = false,
+ ),
+ text = stringResource(id = if (isFollowing) R.string.following else R.string.follow),
+ typography = QuackTypography.Body2.change(
+ color = if (isFollowing) QuackColor.Gray1 else QuackColor.DuckieOrange,
+ ),
+ )
+ },
+ visibleTrailingButton = visibleTrailingButton,
+ leadingImageContent = {
+ QuackProfileImage(
+ modifier = it,
+ profileUrl = profileImgUrl,
+ size = ProfileImageSize,
+ )
+ },
+ isTitleCenter = tier.isEmpty() || favoriteTag.isEmpty(),
+ )
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/domain/DuckieTagAddBottomSheet.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/domain/DuckieTagAddBottomSheet.kt
index b7e2a8aa1..476348a18 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/domain/DuckieTagAddBottomSheet.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/domain/DuckieTagAddBottomSheet.kt
@@ -48,11 +48,13 @@ import team.duckie.app.android.common.compose.invisible
import team.duckie.app.android.common.compose.rememberToast
import team.duckie.app.android.common.compose.systemBarPaddings
import team.duckie.app.android.common.compose.ui.ImeSpacer
+import team.duckie.app.android.common.compose.ui.QuackDivider
import team.duckie.app.android.common.compose.ui.icon.v1.ArrowSendId
-import team.duckie.app.android.common.compose.ui.icon.v1.CloseId
import team.duckie.app.android.common.compose.ui.quack.QuackNoUnderlineTextField
+import team.duckie.app.android.common.compose.ui.quack.blackAndPrimaryColor
import team.duckie.app.android.common.compose.ui.quack.todo.QuackCircleTag
import team.duckie.app.android.common.kotlin.fastForEachIndexed
+import team.duckie.app.android.common.kotlin.takeBy
import team.duckie.app.android.domain.tag.model.Tag
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
@@ -64,6 +66,8 @@ import team.duckie.quackquack.ui.QuackText
import team.duckie.quackquack.ui.sugar.QuackTitle2
import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+const val TagTitleMaxLength = 10
+
/**
* 태그를 추가할 수 있는 [ModalBottomSheetLayout]
* primitive type 인 [String] 대신 [Tag] 를 직접 사용한다
@@ -198,7 +202,6 @@ private fun DuckieTagAddBottomSheetContent(
QuackCircleTag(
text = tag.name,
isSelected = false,
- trailingIconResId = QuackIcon.CloseId,
) {
inputtedTags.remove(inputtedTags[index])
}
@@ -215,17 +218,18 @@ private fun DuckieTagAddBottomSheetContent(
}
}
}
-
- // TODO(riflockle7): 사용해도 괜찮을지 검토 필요
+ QuackDivider()
QuackNoUnderlineTextField(
text = tagInput,
- onTextChanged = { tagInput = it },
+ onTextChanged = {
+ tagInput = it.takeBy(TagTitleMaxLength, tagInput)
+ },
placeholderText = stringResource(R.string.tag_add_manual_placeholder),
- startPadding = 16.dp,
- trailingEndPadding = 10.dp,
trailingIcon = QuackIcon.ArrowSendId,
trailingIconOnClick = ::updateTagInput,
keyboardActions = KeyboardActions { updateTagInput() },
+ trailingIconTint = blackAndPrimaryColor(tagInput).value,
+ paddingValues = PaddingValues(all = 16.dp),
)
ImeSpacer()
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v1/DuckieIcon.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v1/DuckieIcon.kt
index 4a512684c..96f9db0dc 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v1/DuckieIcon.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v1/DuckieIcon.kt
@@ -8,8 +8,8 @@
package team.duckie.app.android.common.compose.ui.icon.v1
import team.duckie.app.android.common.compose.R
-import team.duckie.quackquack.ui.icon.QuackIcon as QuackV1Icon
import team.duckie.quackquack.material.icon.QuackIcon as QuackV2Icon
+import team.duckie.quackquack.ui.icon.QuackIcon as QuackV1Icon
val QuackV1Icon.Companion.DefaultProfile get() = R.drawable.ic_default_profile
val QuackV1Icon.Companion.Notice get() = R.drawable.ic_notice_24
@@ -18,12 +18,12 @@ val QuackV1Icon.Companion.Create get() = R.drawable.ic_create_24
val QuackV1Icon.Companion.Crown get() = R.drawable.ic_crown_12
-val QuackV1Icon.Companion.Clock get() = R.drawable.ic_clock_12
-
// Quack V2 의 QuackIcon 을 통해 Quack V1 의 drawable Resource 를 가져오는 확장 변수
val QuackV2Icon.ArrowBackId: Int get() = QuackV1Icon.ArrowBack.drawableId
+val QuackV2Icon.ArrowRightId: Int get() = QuackV1Icon.ArrowRight.drawableId
+
val QuackV2Icon.ArrowSendId: Int get() = QuackV1Icon.ArrowSend.drawableId
val QuackV2Icon.CloseId: Int get() = QuackV1Icon.Close.drawableId
@@ -36,16 +36,27 @@ val QuackV2Icon.CameraId: Int get() = QuackV1Icon.Camera.drawableId
val QuackV2Icon.TextLogoId: Int get() = QuackV1Icon.TextLogo.drawableId
+val QuackV2Icon.ProfileId: Int get() = QuackV1Icon.Profile.drawableId
+
val QuackV2Icon.DefaultProfileId: Int get() = QuackV1Icon.DefaultProfile
+val QuackV2Icon.CheckId: Int get() = QuackV1Icon.Check.drawableId
+
+val QuackV2Icon.CreateId: Int get() = QuackV1Icon.Create
+
+val QuackV2Icon.AreaId: Int get() = QuackV1Icon.Area.drawableId
+
val Int.toQuackV1Icon: QuackV1Icon?
get() = when (this) {
QuackV1Icon.ArrowBack.drawableId -> QuackV1Icon.ArrowBack
+ QuackV1Icon.ArrowRight.drawableId -> QuackV1Icon.ArrowRight
QuackV1Icon.ArrowSend.drawableId -> QuackV1Icon.ArrowSend
QuackV1Icon.Close.drawableId -> QuackV1Icon.Close
QuackV1Icon.Search.drawableId -> QuackV1Icon.Search
QuackV1Icon.More.drawableId -> QuackV1Icon.More
QuackV1Icon.Camera.drawableId -> QuackV1Icon.Camera
QuackV1Icon.TextLogo.drawableId -> QuackV1Icon.TextLogo
+ QuackV1Icon.Check.drawableId -> QuackV1Icon.Check
+ QuackV1Icon.Area.drawableId -> QuackV1Icon.Area
else -> null
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v2/Clock.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v2/Clock.kt
new file mode 100644
index 000000000..9e1f31294
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/icon/v2/Clock.kt
@@ -0,0 +1,72 @@
+package team.duckie.app.android.common.compose.ui.icon.v2
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
+import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.graphics.vector.ImageVector.Builder
+import androidx.compose.ui.graphics.vector.path
+import androidx.compose.ui.unit.dp
+import team.duckie.quackquack.material.icon.QuackIcon
+
+public val QuackIcon.Clock: ImageVector
+ get() {
+ if (_vector != null) {
+ return _vector!!
+ }
+ _vector = Builder(
+ name = "Vector",
+ defaultWidth = 12.0.dp,
+ defaultHeight = 12.0.dp,
+ viewportWidth = 12.0f,
+ viewportHeight = 12.0f,
+ ).apply {
+ path(
+ fill = SolidColor(Color(0xFF222222)),
+ stroke = null,
+ strokeLineWidth = 0.0f,
+ strokeLineCap = Butt,
+ strokeLineJoin = Miter,
+ strokeLineMiter = 4.0f,
+ pathFillType = NonZero,
+ ) {
+ moveTo(6.0f, 1.2f)
+ curveTo(4.727f, 1.2f, 3.5061f, 1.7057f, 2.6059f, 2.6059f)
+ curveTo(1.7057f, 3.5061f, 1.2f, 4.727f, 1.2f, 6.0f)
+ curveTo(1.2f, 7.273f, 1.7057f, 8.4939f, 2.6059f, 9.3941f)
+ curveTo(3.5061f, 10.2943f, 4.727f, 10.8f, 6.0f, 10.8f)
+ curveTo(7.273f, 10.8f, 8.4939f, 10.2943f, 9.3941f, 9.3941f)
+ curveTo(10.2943f, 8.4939f, 10.8f, 7.273f, 10.8f, 6.0f)
+ curveTo(10.8f, 4.727f, 10.2943f, 3.5061f, 9.3941f, 2.6059f)
+ curveTo(8.4939f, 1.7057f, 7.273f, 1.2f, 6.0f, 1.2f)
+ close()
+ moveTo(0.0f, 6.0f)
+ curveTo(0.0f, 2.6862f, 2.6862f, 0.0f, 6.0f, 0.0f)
+ curveTo(9.3138f, 0.0f, 12.0f, 2.6862f, 12.0f, 6.0f)
+ curveTo(12.0f, 9.3138f, 9.3138f, 12.0f, 6.0f, 12.0f)
+ curveTo(2.6862f, 12.0f, 0.0f, 9.3138f, 0.0f, 6.0f)
+ close()
+ moveTo(6.0f, 2.4f)
+ curveTo(6.1591f, 2.4f, 6.3117f, 2.4632f, 6.4243f, 2.5757f)
+ curveTo(6.5368f, 2.6883f, 6.6f, 2.8409f, 6.6f, 3.0f)
+ verticalLineTo(5.7516f)
+ lineTo(8.2242f, 7.3758f)
+ curveTo(8.3335f, 7.489f, 8.394f, 7.6405f, 8.3926f, 7.7978f)
+ curveTo(8.3912f, 7.9552f, 8.3281f, 8.1056f, 8.2169f, 8.2169f)
+ curveTo(8.1056f, 8.3281f, 7.9552f, 8.3912f, 7.7978f, 8.3926f)
+ curveTo(7.6405f, 8.394f, 7.489f, 8.3335f, 7.3758f, 8.2242f)
+ lineTo(5.5758f, 6.4242f)
+ curveTo(5.4633f, 6.3117f, 5.4f, 6.1591f, 5.4f, 6.0f)
+ verticalLineTo(3.0f)
+ curveTo(5.4f, 2.8409f, 5.4632f, 2.6883f, 5.5757f, 2.5757f)
+ curveTo(5.6883f, 2.4632f, 5.8409f, 2.4f, 6.0f, 2.4f)
+ close()
+ }
+ }
+ .build()
+ return _vector!!
+ }
+
+private var _vector: ImageVector? = null
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackNoUnderlineTextField.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackNoUnderlineTextField.kt
index f9a8a1d01..37e206af5 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackNoUnderlineTextField.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackNoUnderlineTextField.kt
@@ -8,6 +8,7 @@
package team.duckie.app.android.common.compose.ui.quack
import androidx.annotation.DrawableRes
+import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -20,8 +21,8 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.Text
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
@@ -54,6 +55,14 @@ import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.icon.QuackIcon
import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
+
+@NonRestartableComposable
+@Composable
+fun blackAndPrimaryColor(text: String) = animateColorAsState(
+ targetValue = if (text.isEmpty()) QuackColor.Unspecified.value else QuackColor.DuckieOrange.value,
+ label = "writeCommentButtonColor",
+)
@Composable
fun QuackNoUnderlineTextField(
@@ -62,10 +71,13 @@ fun QuackNoUnderlineTextField(
onTextChanged: (text: String) -> Unit,
placeholderText: String? = null,
paddingValues: PaddingValues = PaddingValues(
- vertical = 8.dp,
- horizontal = 12.dp,
+ top = 8.dp,
+ bottom = 8.dp,
+ start = 12.dp,
+ end = 12.dp,
),
startPadding: Dp = 0.dp,
+ leadingIconOnClick: (() -> Unit)? = null,
@DrawableRes leadingIcon: Int? = null,
trailingEndPadding: Dp = 0.dp,
@DrawableRes trailingIcon: Int? = null,
@@ -118,6 +130,7 @@ fun QuackNoUnderlineTextField(
trailingIconOnClick = trailingIconOnClick,
trailingIconSize = trailingIconSize,
trailingIconTint = trailingIconTint,
+ leadingIconOnClick = leadingIconOnClick,
)
},
)
@@ -135,13 +148,13 @@ private fun TextFieldDecoration(
textField: @Composable () -> Unit,
isPlaceholder: Boolean,
placeholderText: String?,
- @DrawableRes
- leadingIcon: Int?,
+ @DrawableRes leadingIcon: Int?,
trailingIcon: Int?,
startPadding: Dp = 0.dp,
leadingEndPadding: Dp = 0.dp,
trailingStartPadding: Dp = 16.dp,
trailingIconSize: Dp = 24.dp,
+ leadingIconOnClick: (() -> Unit)? = null,
trailingIconOnClick: (() -> Unit)?,
trailingIconTint: Color = Color.Unspecified,
) = with(TextFieldDecorationLayoutId) {
@@ -155,7 +168,7 @@ private fun TextFieldDecoration(
.size(DpSize(16.dp, 16.dp))
.padding(end = leadingEndPadding)
.quackClickable(
- onClick = trailingIconOnClick,
+ onClick = leadingIconOnClick,
rippleEnabled = false,
)
.padding(end = 16.dp),
@@ -163,15 +176,14 @@ private fun TextFieldDecoration(
)
}
if (isPlaceholder && placeholderText != null) {
- Text(
+ QuackText(
modifier = Modifier
.layoutId(PlaceholderId)
.padding(start = startPadding),
text = placeholderText,
- style = QuackTypography.Body1.asComposeStyle().copy(
- color = QuackColor.Gray2.value,
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Gray2,
),
- maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackProfileImage.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackProfileImage.kt
index d6039b704..11c1156ff 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackProfileImage.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/QuackProfileImage.kt
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.rememberAsyncImagePainter
import team.duckie.app.android.common.compose.R
+import team.duckie.app.android.common.kotlin.runIf
import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.material.shape.SquircleShape
@@ -60,12 +61,11 @@ fun QuackProfileImage(
modifier = modifier
.size(size)
.clip(shape)
- .quackClickable(
- rippleEnabled = false,
- ) {
- if (onClick != null) {
- onClick()
- }
+ .runIf(onClick != null) {
+ quackClickable(
+ rippleEnabled = true,
+ onClick = onClick,
+ )
},
painter = asyncImagePainter,
contentScale = contentScale,
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackCircleTag.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackCircleTag.kt
index 0871986fd..a0b76a0fb 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackCircleTag.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackCircleTag.kt
@@ -5,25 +5,30 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.common.compose.ui.quack.todo
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import team.duckie.app.android.common.compose.ui.icon.v1.toQuackV1Icon
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.ui.QuackTag
+import team.duckie.quackquack.ui.QuackTagStyle
+import team.duckie.quackquack.ui.trailingIcon
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
@Composable
fun QuackCircleTag(
modifier: Modifier = Modifier,
text: String,
- trailingIconResId: Int? = null,
isSelected: Boolean,
onClick: (() -> Unit)? = null,
) {
- team.duckie.quackquack.ui.component.QuackCircleTag(
- modifier = modifier,
+ QuackTag(
text = text,
- trailingIcon = trailingIconResId?.toQuackV1Icon,
- isSelected = isSelected,
- onClick = onClick,
- )
+ style = QuackTagStyle.Outlined,
+ modifier = modifier.trailingIcon(OutlinedGroup.Close, onClick = onClick ?: {}),
+ selected = isSelected,
+ ) {}
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackDropDownCard.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackDropDownCard.kt
new file mode 100644
index 000000000..7fe615f85
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackDropDownCard.kt
@@ -0,0 +1,92 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.quack.todo
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.kotlin.runIf
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowDown
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackText
+
+/**
+ * 덕키에서 Drop Down 을 표시하는 컴포넌트를 구현합니다.
+ * [QuackDropDownCard] 는 다음과 같은 특징을 갖습니다.
+ *
+ * - 항상 trailing content 로 [QuackIcon.ArrowDown] 을 갖습니다.
+ * - 단순 rounding 형태의 Card 를 갖기 때문에 이름에 Card 가 추가됐습니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param text 표시할 텍스트
+ * @param showBorder 테두리를 표시할지 여부
+ * @param onClick 클릭했을 때 호출될 람다
+ */
+@Composable
+fun QuackDropDownCard(
+ modifier: Modifier = Modifier,
+ text: String,
+ showBorder: Boolean = true,
+ onClick: (() -> Unit)? = null,
+): Unit = Row(
+ modifier = modifier
+ .clip(shape = RoundedCornerShape(size = 8.dp))
+ .quackClickable(
+ onClick = onClick,
+ )
+ .background(
+ color = QuackColor.White.value,
+ shape = RoundedCornerShape(size = 8.dp),
+ )
+ .runIf(showBorder) {
+ quackBorder(
+ border = QuackBorder(
+ color = QuackColor.Gray3,
+ ),
+ shape = RoundedCornerShape(size = 8.dp),
+ )
+ },
+ verticalAlignment = Alignment.CenterVertically,
+) {
+ QuackText(
+ modifier = Modifier.padding(
+ PaddingValues(
+ top = 8.dp,
+ bottom = 8.dp,
+ start = 12.dp,
+ end = 4.dp,
+ ),
+ ),
+ text = text,
+ typography = QuackTypography.Body1,
+ singleLine = true,
+ )
+ QuackIcon(
+ modifier = Modifier
+ .size(DpSize(16.dp, 16.dp))
+ .padding(PaddingValues(end = 8.dp)),
+ icon = OutlinedGroup.ArrowDown,
+ tint = QuackColor.Gray1,
+ )
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackErrorableTextField.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackErrorableTextField.kt
new file mode 100644
index 000000000..85d31f76d
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackErrorableTextField.kt
@@ -0,0 +1,202 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.quack.todo
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.animateColorAsState
+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.padding
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+
+sealed class QuackErrorableTextFieldState {
+ object Normal : QuackErrorableTextFieldState()
+ class Success(val successText: String) : QuackErrorableTextFieldState()
+ class Error(val errorText: String) : QuackErrorableTextFieldState()
+}
+
+@Composable
+fun QuackErrorableTextField(
+ modifier: Modifier = Modifier,
+ text: String,
+ onTextChanged: (text: String) -> Unit,
+ placeholderText: String,
+ maxLength: Int,
+ textFieldState: QuackErrorableTextFieldState,
+ imeAction: ImeAction = ImeAction.Done,
+ keyboardActions: KeyboardActions = KeyboardActions(),
+) {
+ Column(modifier = modifier) {
+ BasicTextField(
+ modifier = modifier,
+ value = text,
+ onValueChange = onTextChanged,
+ keyboardOptions = KeyboardOptions(
+ imeAction = imeAction,
+ ),
+ keyboardActions = keyboardActions,
+ textStyle = QuackTypography.HeadLine2.asComposeStyle(),
+ singleLine = true,
+ ) { innerTextField ->
+ QuackTextFieldDecorationBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .bottomBorder(
+ strokeWidth = 1.dp,
+ textFieldState = textFieldState,
+ ),
+ leadingContent = {
+ Box(
+ modifier = Modifier
+ .weight(1f)
+ .padding(bottom = 8.dp),
+ ) {
+ if (text.isEmpty()) {
+ QuackText(
+ text = placeholderText,
+ typography = QuackTypography.HeadLine2.change(
+ color = QuackColor.Gray2,
+ ),
+ )
+ }
+ innerTextField()
+ }
+ },
+ trailingContent = {
+ Row(
+ modifier = Modifier.padding(bottom = 8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(1.dp),
+ ) {
+ QuackText(
+ text = "${text.length}",
+ typography = QuackTypography.Subtitle.change(
+ color = if (text.isEmpty()) {
+ QuackColor.Gray2
+ } else {
+ QuackColor.Black
+ },
+ ),
+ )
+ QuackText(
+ text = "/",
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.Gray2,
+ ),
+ )
+ QuackText(
+ text = "$maxLength",
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.Gray2,
+ ),
+ )
+ }
+ },
+ )
+ }
+ Crossfade(
+ modifier = Modifier.padding(top = 4.dp),
+ targetState = textFieldState,
+ label = "",
+ ) { state ->
+ when (state) {
+ is QuackErrorableTextFieldState.Error -> {
+ QuackText(
+ text = state.errorText,
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Alert,
+ ),
+ )
+ }
+
+ is QuackErrorableTextFieldState.Success -> {
+ QuackText(
+ text = state.successText,
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Success,
+ ),
+ )
+ }
+
+ QuackErrorableTextFieldState.Normal -> {}
+ }
+ }
+ }
+}
+
+@Composable
+private fun QuackTextFieldDecorationBox(
+ modifier: Modifier = Modifier,
+ leadingContent: (@Composable () -> Unit)? = null,
+ trailingContent: (@Composable () -> Unit)? = null,
+) {
+ Row(
+ modifier = modifier,
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ leadingContent?.invoke() ?: Spacer(modifier = Modifier.weight(1f))
+ trailingContent?.invoke() ?: Spacer(modifier = Modifier.weight(1f))
+ }
+}
+
+private fun Modifier.bottomBorder(
+ strokeWidth: Dp,
+ textFieldState: QuackErrorableTextFieldState,
+) = composed(
+ factory = {
+ val color by animateColorAsState(
+ targetValue = when (textFieldState) {
+ is QuackErrorableTextFieldState.Error -> {
+ QuackColor.Alert.value
+ }
+
+ QuackErrorableTextFieldState.Normal -> {
+ QuackColor.Gray2.value
+ }
+
+ else -> QuackColor.Success.value
+ },
+ label = "",
+ )
+ val density = LocalDensity.current
+ val strokeWidthPx = density.run { strokeWidth.toPx() }
+
+ Modifier.drawBehind {
+ val width = size.width
+ val height = size.height - strokeWidthPx / 2
+
+ drawLine(
+ color = color,
+ start = Offset(x = 0f, y = height),
+ end = Offset(x = width, y = height),
+ strokeWidth = strokeWidthPx,
+ )
+ }
+ },
+)
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackLazyVerticalGridTag.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackLazyVerticalGridTag.kt
index 9b2abd390..726689fce 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackLazyVerticalGridTag.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackLazyVerticalGridTag.kt
@@ -5,16 +5,61 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class, ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.common.compose.ui.quack.todo
+import androidx.compose.foundation.horizontalScroll
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.layout.LazyLayout
+import androidx.compose.foundation.rememberScrollState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import team.duckie.app.android.common.compose.ui.icon.v1.toQuackV1Icon
-import team.duckie.quackquack.ui.component.QuackTagType
+import kotlinx.collections.immutable.ImmutableList
+import team.duckie.app.android.common.kotlin.fastForEachIndexed
+import team.duckie.app.android.common.kotlin.runtimeCheck
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackTag
+import team.duckie.quackquack.ui.QuackTagStyle
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.trailingIcon
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+/**
+ * [LazyVerticalGrid] 형식으로 주어진 태그들을 배치합니다.
+ * 이 컴포넌트는 항상 상위 컴포저블의 가로 길이만큼 width 가 지정되고,
+ * 한 줄에 최대 [itemChunkedSize]개가 들어갈 수 있습니다. 또한 가로와 세로 스크롤을 모두 지원합니다.
+ *
+ * 퍼포먼스 측면에서 [LazyLayout] 를 사용하는 것이 좋지만, 덕키의 경우
+ * 표시해야 하는 태그의 개수가 많지 않기 때문에 컴포저블을 직접 그려도
+ * 성능에 중대한 영향을 미치지 않을 것으로 판단하여 [LazyColumn] 과
+ * [Row] + [Modifier.horizontalScroll] 를 사용하여 구현하였습니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param contentPadding 이 컴포넌트의 광역에 적용될 [PaddingValues]
+ * @param title 상단에 표시될 제목. 만약 null 을 제공할 시 표시되지 않습니다.
+ * @param items 표시할 태그들의 제목. **중복되는 태그 제목은 허용하지 않습니다.**
+ * 이 항목은 바뀔 수 있으므로 [ImmutableList] 가 아닌 일반 [List] 로 받습니다.
+ * @param itemSelections 태그들의 선택 여부.
+ * 이 항목은 바뀔 수 있으므로 [ImmutableList] 가 아닌 [List] 로 받습니다.
+ * @param itemChunkedSize 한 칸에 들어갈 최대 아이템의 개수
+ * @param horizontalSpace 아이템들의 가로 간격
+ * @param verticalSpace 아이템들의 세로 간격
+ * @param trailingIconResId trailingIcon 에 들어갈 ResourceId. 없을 시 아이콘이 없습니다.
+ * @param onClick 사용자가 태그를 클릭했을 때 호출되는 람다.
+ * 람다식의 인자로는 선택된 태그의 index 가 들어옵니다.
+ */
@Composable
fun QuackLazyVerticalGridTag(
modifier: Modifier = Modifier,
@@ -25,19 +70,64 @@ fun QuackLazyVerticalGridTag(
itemChunkedSize: Int,
horizontalSpace: Dp = 8.dp,
verticalSpace: Dp = 8.dp,
- tagTypeResId: Int?,
+ trailingIcon: ImageVector? = null,
+ onTrailingClick: ((Int) -> Unit)? = null,
onClick: (index: Int) -> Unit,
) {
- team.duckie.quackquack.ui.component.QuackLazyVerticalGridTag(
- modifier = modifier,
- contentPadding = contentPadding,
- title = title,
- items = items,
- itemSelections = itemSelections,
- itemChunkedSize = itemChunkedSize,
- horizontalSpace = horizontalSpace,
- verticalSpace = verticalSpace,
- tagType = QuackTagType.Circle(tagTypeResId?.toQuackV1Icon),
- onClick = onClick,
- )
+ if (itemSelections != null) {
+ runtimeCheck(items.size == itemSelections.size) {
+ "The size of items and the size of itemsSelection must always be the same. " +
+ "[items.size (${items.size}) != itemsSelection.size (${itemSelections.size})]"
+ }
+ }
+
+ val chunkedItems = remember(items) {
+ items.chunked(itemChunkedSize)
+ }
+
+ Column(
+ modifier = modifier
+ .fillMaxWidth()
+ .padding(contentPadding),
+ ) {
+ if (title != null) {
+ QuackText(
+ modifier = Modifier.padding(bottom = 12.dp),
+ text = title,
+ typography = QuackTypography.Title2,
+ singleLine = true,
+ )
+ }
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ verticalArrangement = Arrangement.spacedBy(horizontalSpace),
+ ) {
+ chunkedItems.fastForEachIndexed { rowIndex, items ->
+ Row(
+ modifier = Modifier
+ .fillMaxWidth()
+ .horizontalScroll(state = rememberScrollState()),
+ horizontalArrangement = Arrangement.spacedBy(verticalSpace),
+ ) {
+ items.fastForEachIndexed { index, item ->
+ val currentIndex = rowIndex * itemChunkedSize + index
+ QuackTag(
+ text = item,
+ style = QuackTagStyle.Filled,
+ modifier = if (trailingIcon != null) {
+ Modifier.trailingIcon(
+ trailingIcon,
+ onClick = { onTrailingClick?.invoke(index) },
+ )
+ } else {
+ Modifier
+ },
+ selected = itemSelections?.get(currentIndex) ?: false,
+ onClick = { onClick(index) },
+ )
+ }
+ }
+ }
+ }
+ }
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackReactionTextArea.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackReactionTextArea.kt
index 6cc0d5a09..335c56054 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackReactionTextArea.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackReactionTextArea.kt
@@ -40,7 +40,7 @@ private fun getQuackReactionTextAreaMeasurePolicy(
with(QuackReactionTextAreaLayoutId) {
val quizReviewPlaceHolderPlaceable =
- measurables.getPlaceable(QuizReviewPlaceHolder, looseConstraints)
+ measurables.getPlaceable(QuizReviewPlaceHolder, extraLooseConstraints)
val quizReviewTextAreaPlaceable =
measurables.getPlaceable(QuizReviewTextArea, extraLooseConstraints)
val reactionLimitTextPlaceable =
@@ -50,12 +50,12 @@ private fun getQuackReactionTextAreaMeasurePolicy(
width = constraints.maxWidth,
height = constraints.maxHeight,
) {
- quizReviewTextAreaPlaceable.place(x = 0, y = 0)
+ quizReviewTextAreaPlaceable.placeRelative(x = 0, y = 0)
if (placeholderVisible) {
- quizReviewPlaceHolderPlaceable.place(x = 0, y = 0)
+ quizReviewPlaceHolderPlaceable.placeRelative(x = 0, y = 0)
}
- reactionLimitTextPlaceable.place(
- x = constraints.maxWidth - reactionLimitTextPlaceable.width,
+ reactionLimitTextPlaceable?.placeRelative(
+ x = quizReviewTextAreaPlaceable.width - reactionLimitTextPlaceable.width,
y = quizReviewTextAreaPlaceable.height - reactionLimitTextPlaceable.height,
)
}
@@ -64,12 +64,15 @@ private fun getQuackReactionTextAreaMeasurePolicy(
@Composable
fun QuackReactionTextArea(
+ modifier: Modifier = Modifier,
reaction: String,
onReactionChanged: (String) -> Unit,
maxLength: Int = RANKER_REACTION_MAX_LENGTH,
+ placeHolderText: String = RANKER_REACTION_PLACE_HOLDER,
+ visibleCurrentLength: Boolean = true,
) = with(QuackReactionTextAreaLayoutId) {
Layout(
- modifier = Modifier.height(100.dp),
+ modifier = modifier.height(100.dp),
measurePolicy = getQuackReactionTextAreaMeasurePolicy(
placeholderVisible = reaction.isEmpty(),
),
@@ -91,23 +94,25 @@ fun QuackReactionTextArea(
modifier = Modifier
.layoutId(QuizReviewPlaceHolder)
.padding(all = 16.dp),
- text = RANKER_REACTION_PLACE_HOLDER,
+ text = placeHolderText,
typography = QuackTypography.Body1.change(
color = QuackColor.Gray2,
),
)
- QuackText(
- modifier = Modifier
- .layoutId(ReactionLimitText)
- .padding(
- bottom = 12.dp,
- end = 12.dp,
+ if (visibleCurrentLength) {
+ QuackText(
+ modifier = Modifier
+ .layoutId(ReactionLimitText)
+ .padding(
+ bottom = 12.dp,
+ end = 12.dp,
+ ),
+ text = "${reaction.length} / $RANKER_REACTION_MAX_LENGTH",
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Gray2,
),
- text = "${reaction.length} / $RANKER_REACTION_MAX_LENGTH",
- typography = QuackTypography.Body1.change(
- color = QuackColor.Gray2,
- ),
- )
+ )
+ }
},
)
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSelectableImage.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSelectableImage.kt
index 89a03bc45..40e169f5a 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSelectableImage.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSelectableImage.kt
@@ -7,39 +7,133 @@
package team.duckie.app.android.common.compose.ui.quack.todo
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.DpSize
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackSelectableImageType
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.zIndex
+import team.duckie.app.android.common.compose.ui.icon.v1.CheckId
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSelectableImageType.CheckOverlay
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSelectableImageType.TopEndCheckBox
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackRoundCheckBox
+import team.duckie.app.android.common.kotlin.runIf
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.ui.QuackImage
+/**
+ * 오른쪽 상단에 체크박스와 함께 이미지 혹은 [QuackIcon] 을 표시합니다.
+ *
+ * @param modifier 이 컴포저블에서 사용할 [Modifier]
+ * @param isSelected 현재 이미지가 선택됐는지 여부
+ * @param src 표시할 리소스. 만약 null 이 들어온다면 리소스를 그리지 않습니다.
+ * @param size 리소스의 크기를 지정합니다. null 이 들어오면 기본 크기로 표시합니다.
+ * @param tint 적용할 틴트 값
+ * @param shape 컴포넌트의 모양
+ * @param selectableType selection 이 표시될 방식
+ * @param rippleEnabled 클릭됐을 때 ripple 발생 여부
+ * @param onClick 클릭됐을 때 실행할 람다식
+ * @param contentScale 적용할 content scale 정책
+ * @param contentDescription 이미지의 설명
+ */
@Composable
fun QuackSelectableImage(
modifier: Modifier = Modifier,
isSelected: Boolean,
src: Any?,
size: DpSize? = null,
- tint: QuackColor? = null,
+ tint: QuackColor = QuackColor.Unspecified,
shape: Shape = RectangleShape,
+ selectableType: QuackSelectableImageType = TopEndCheckBox,
rippleEnabled: Boolean = true,
onClick: (() -> Unit)? = null,
contentScale: ContentScale = ContentScale.FillBounds,
contentDescription: String? = null,
) {
- team.duckie.quackquack.ui.component.QuackSelectableImage(
+ QuackSurface(
modifier = modifier,
- isSelected = isSelected,
- src = src,
- size = size,
- tint = tint,
shape = shape,
- selectableType = QuackSelectableImageType.TopEndCheckBox,
+ border = BorderStroke(1.dp, QuackColor.Gray3.value)
+ .takeIf { isSelected }
+ .takeIf { selectableType == TopEndCheckBox },
rippleEnabled = rippleEnabled,
onClick = onClick,
- contentScale = contentScale,
- contentDescription = contentDescription,
+ contentAlignment = Alignment.TopEnd,
+ ) {
+ QuackImage(
+ src = src,
+ modifier = Modifier
+ .zIndex(1f)
+ .runIf(size != null) { size(size!!) },
+ tint = tint,
+ contentScale = contentScale,
+ contentDescription = contentDescription,
+ )
+
+ when (selectableType) {
+ TopEndCheckBox -> {
+ QuackRoundCheckBox(
+ modifier = Modifier
+ .padding(paddingValues = PaddingValues(all = 7.dp))
+ .zIndex(2f),
+ checked = isSelected,
+ )
+ }
+
+ CheckOverlay -> {
+ AnimatedVisibility(
+ modifier = Modifier
+ .matchParentSize()
+ .zIndex(2f),
+ visible = isSelected,
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(color = QuackColor.Dimmed.value),
+ contentAlignment = Alignment.Center,
+ ) {
+ QuackImage(
+ src = QuackIcon.CheckId,
+ modifier = Modifier.size(selectableType.size!!),
+ tint = selectableType.tint!!,
+ )
+ }
+ }
+ }
+ }
+ }
+}
+
+/**
+ * [QuackSelectableImage] 에서 selection 이 표시될 방식을 나타냅니다.
+ *
+ * @property TopEndCheckBox 오른쪽 상단에 [QuackRoundCheckBox] 로 표시
+ * @property CheckOverlay 이미지 전체에 [QuackIcon.Check] 로 오버레이 표시 및
+ * [QuackColor.Dimmed] 로 dimmed 처리
+ *
+ * @param size 만약 [CheckOverlay] 방식일 때 [QuackIcon.Check] 의 사이즈
+ * @param tint 만약 [CheckOverlay] 방식일 때 [QuackIcon.Check] 의 틴트
+ */
+sealed class QuackSelectableImageType(
+ internal val size: DpSize? = null,
+ internal val tint: QuackColor? = null,
+) {
+ object TopEndCheckBox : QuackSelectableImageType()
+ object CheckOverlay : QuackSelectableImageType(
+ size = DpSize(width = 28.dp, height = 28.dp),
+ tint = QuackColor.White,
)
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSingeLazyRowTag.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSingeLazyRowTag.kt
index b70f5194f..66d8fc550 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSingeLazyRowTag.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSingeLazyRowTag.kt
@@ -5,39 +5,125 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.common.compose.ui.quack.todo
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import team.duckie.app.android.common.compose.ui.icon.v1.toQuackV1Icon
-import team.duckie.quackquack.ui.component.QuackTagType
+import kotlinx.collections.immutable.ImmutableList
+import team.duckie.app.android.common.kotlin.runtimeCheck
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackTag
+import team.duckie.quackquack.ui.QuackTagStyle
+import team.duckie.quackquack.ui.trailingIcon
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+/**
+ * [LazyRow] 형식으로 주어진 태그들을 **한 줄로** 배치합니다.
+ * 이 컴포넌트는 항상 상위 컴포저블의 가로 길이만큼 width 가 지정됩니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param contentPadding 이 컴포넌트의 광역에 적용될 [PaddingValues]
+ * @param title 상단에 표시될 제목. 만약 null 을 제공할 시 표시되지 않습니다.
+ * @param items 표시할 태그들의 제목. **중복되는 태그 제목은 허용하지 않습니다.**
+ * 이 항목은 바뀔 수 있으므로 [ImmutableList] 가 아닌 일반 [List] 로 받습니다.
+ * @param itemSelections 태그들의 선택 여부.
+ * 이 항목은 바뀔 수 있으므로 [ImmutableList] 가 아닌 [List] 로 받습니다.
+ * @param horizontalSpace 아이템들의 가로 간격
+ * @param tagType [QuackLazyVerticalGridTag] 에서 표시할 태그의 타입을 지정합니다.
+ * 여러 종류의 태그가 [QuackLazyVerticalGridTag] 으로 표시될 수 있게 태그의 타입을 따로 받습니다.
+ * @param key a factory of stable and unique keys representing the item. Using the same key
+ * for multiple items in the list is not allowed. Type of the key should be saveable
+ * via Bundle on Android. If null is passed the position in the list will represent the key.
+ * When you specify the key the scroll position will be maintained based on the key, which
+ * means if you add/remove items before the current visible item the item with the given key
+ * will be kept as the first visible one.
+ * @param contentType a factory of the content types for the item. The item compositions of
+ * the same type could be reused more efficiently. Note that null is a valid type and items of such
+ * type will be considered compatible.
+ * @param onClick 사용자가 태그를 클릭했을 때 호출되는 람다.
+ * 람다식의 인자로는 선택된 태그의 index 가 들어옵니다.
+ */
@Composable
-fun QuackSingeLazyRowTag(
+fun QuackOutLinedSingeLazyRowTag(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(all = 0.dp),
title: String? = null,
items: List,
itemSelections: List? = null,
horizontalSpace: Dp = 8.dp,
- tagTypeResId: Int?,
+ trailingIcon: ImageVector? = null,
key: ((index: Int, item: String) -> Any)? = null,
contentType: (index: Int, item: String) -> Any? = { _, _ -> null },
+ onTrailingIconClick: ((index: Int) -> Unit)? = null,
onClick: (index: Int) -> Unit,
) {
- team.duckie.quackquack.ui.component.QuackSingeLazyRowTag(
- modifier = modifier,
- contentPadding = contentPadding,
- title = title,
- items = items,
- itemSelections = itemSelections,
- horizontalSpace = horizontalSpace,
- tagType = QuackTagType.Circle(tagTypeResId?.toQuackV1Icon),
- key = key,
- contentType = contentType,
- onClick = onClick,
- )
+ if (itemSelections != null) {
+ runtimeCheck(
+ value = items.size == itemSelections.size,
+ ) {
+ "The size of items and the size of itemsSelection must always be the same. " +
+ "[items.size (${items.size}) != itemsSelection.size (${itemSelections.size})]"
+ }
+ }
+
+ val layoutDirection = LocalLayoutDirection.current
+
+ Column(modifier = modifier.fillMaxWidth()) {
+ if (title != null) {
+ team.duckie.quackquack.ui.QuackText(
+ modifier = Modifier.padding(
+ start = contentPadding.calculateStartPadding(layoutDirection),
+ end = contentPadding.calculateEndPadding(layoutDirection),
+ bottom = 12.dp,
+ ),
+ text = title,
+ typography = QuackTypography.Title2,
+ singleLine = true,
+ )
+ }
+
+ LazyRow(
+ modifier = Modifier.fillMaxWidth(),
+ contentPadding = contentPadding,
+ horizontalArrangement = Arrangement.spacedBy(horizontalSpace),
+ ) {
+ itemsIndexed(
+ items = items,
+ key = key,
+ contentType = contentType,
+ ) { index, item ->
+ QuackTag(
+ text = item,
+ style = QuackTagStyle.Outlined,
+ modifier = if (trailingIcon != null) {
+ Modifier.trailingIcon(
+ trailingIcon,
+ onClick = { onTrailingIconClick?.invoke(index) },
+ )
+ } else {
+ Modifier
+ },
+ selected = itemSelections?.get(index) ?: false,
+ onClick = {
+ onClick(index)
+ },
+ )
+ }
+ }
+ }
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSurface.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSurface.kt
new file mode 100644
index 000000000..c3416a8f9
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackSurface.kt
@@ -0,0 +1,112 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.quack.todo
+
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.graphics.Shape
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackAnimationSpec
+import team.duckie.app.android.common.kotlin.runIf
+import team.duckie.quackquack.animation.animateQuackColorAsState
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.quackClickable
+
+/**
+ * 모든 Quack 컴포넌트에서 최하위로 사용되는 컴포넌트입니다.
+ * 컴포넌트의 기본 모양을 정의합니다.
+ *
+ * **애니메이션 가능한 모든 요소들에는 자동으로 애니메이션이 적용됩니다.**
+ * animationSpec 으로는 항상 [QuackAnimationSpec] 을 사용합니다.
+ *
+ * @param modifier 컴포저블에 적용할 [Modifier].
+ * 기본값은 수정 없는 본질의 Modifier 입니다.
+ * @param shape 컴포저블의 [Shape]. 기본값은 [RectangleShape] 입니다.
+ * @param backgroundColor 컴포저블의 배경 색상.
+ * 기본값은 정의되지 않은 색상인 [QuackColor.Unspecified] 입니다.
+ * @param border 컴포저블의 테두리.
+ * null 이 입력된다면 테두리를 설정하지 않습니다. 기본값은 null 입니다.
+ * @param elevation 컴포저블의 그림자 고도.
+ * 기본값은 0 입니다. 즉, 그림자를 사용하지 않습니다.
+ * @param rippleEnabled 컴포저블이 클릭됐을 때 리플 효과를 적용할지 여부.
+ * 기본값은 true 입니다.
+ * @param rippleColor 컴포저블이 클릭됐을 때 리플 효과의 색상.
+ * 기본값은 정해지지 않은 색상인 [QuackColor.Unspecified] 입니다.
+ * [rippleEnabled] 이 켜져 있을 때만 사용됩니다.
+ * @param onClick 컴포저블이 클릭됐을 때 실행할 람다식.
+ * null 이 입력된다면 클릭 이벤트를 추가하지 않습니다.
+ * 기본값은 null 입니다. 즉, 클릭 이벤트를 추가하지 않습니다.
+ * @param contentAlignment 컴포저블의 정렬 상태. 기본값은 Center 입니다.
+ * @param propagateMinConstraints 최소 제약 조건을 전파할지 여부. 기본값은 false 입니다.
+ * @param content 표시할 컴포저블. [BoxScope] 를 receive 로 받습니다.
+ */
+// TODO: Modifier.quackSurface 로 변경
+// @NonRestartableComposable; 여기서 사용하는 Box 는 inline 됨
+@Composable
+fun QuackSurface(
+ modifier: Modifier = Modifier,
+ shape: Shape = RectangleShape,
+ backgroundColor: QuackColor = QuackColor.Unspecified,
+ border: BorderStroke? = null,
+ elevation: Dp = 0.dp,
+ rippleEnabled: Boolean = true,
+ rippleColor: QuackColor = QuackColor.Unspecified,
+ onClick: (() -> Unit)? = null,
+ contentAlignment: Alignment = Alignment.Center,
+ propagateMinConstraints: Boolean = false,
+ content: @Composable BoxScope.() -> Unit,
+) {
+ val backgroundColorAnimation by animateQuackColorAsState(
+ targetValue = backgroundColor,
+ )
+
+ Box(
+ modifier = modifier
+ .shadow(
+ elevation = elevation,
+ shape = shape,
+ clip = false,
+ )
+ .clip(
+ shape = shape,
+ )
+ .background(
+ color = backgroundColorAnimation.value,
+ shape = shape,
+ )
+ .quackClickable(
+ onClick = onClick,
+ rippleEnabled = rippleEnabled,
+ rippleColor = rippleColor,
+ )
+ .runIf(border != null) {
+ border(
+ border = border!!,
+ shape = shape,
+ )
+ }
+ .animateContentSize(
+ animationSpec = QuackAnimationSpec(),
+ ),
+ contentAlignment = contentAlignment,
+ propagateMinConstraints = propagateMinConstraints,
+ content = content,
+ )
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackTopAppBar.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackTopAppBar.kt
index 7128f7dc6..accd645d5 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackTopAppBar.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/QuackTopAppBar.kt
@@ -7,44 +7,353 @@
package team.duckie.app.android.common.compose.ui.quack.todo
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
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.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.icon.v1.TextLogoId
import team.duckie.app.android.common.compose.ui.icon.v1.toQuackV1Icon
+import team.duckie.app.android.common.compose.util.expendedQuackClickable
+import team.duckie.app.android.common.kotlin.runtimeCheck
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.icon.QuackIcon as QuackV1Icon
-/** 덕키에서 사용하는 TopAppBar */
+/**
+ * 덕키의 Top Navigation Bar 를 그립니다.
+ * [QuackTopAppBar] 는 몇몇 중요한 특징이 있습니다.
+ *
+ * - 최소 하나의 값이 제공돼야 합니다.
+ * - 항상 상위 컴포저블의 가로 길이에 꽉차게 그립니다.
+ * - [showLogoAtCenter] 이 true 라면 [centerText] 값은 무시됩니다.
+ * 로고 리소스로는 [QuackIcon.TextLogoId] 를 사용합니다.
+ * - [centerText] 값이 있다면 [showLogoAtCenter] 값은 무시됩니다.
+ * 또한 [centerText] 는 trailing content 로 아이콘을 배치할 수 있습니다.
+ * - [trailingExtraIcon] 과 [trailingIcon] 이 하나라도 들어왔다면 [trailingText] 는 무시되며,
+ * `[trailingExtraIcon] [trailingIcon]` 순서로 배치됩니다.
+ * - [trailingText] 값이 입력되면 [trailingExtraIcon] 과 [trailingIcon] 값은 무시됩니다.
+ * - [trailingContent] 이 있다면 [trailingIcon], [trailingExtraIcon], [trailingText],
+ * [onTrailingIconClick], [onTrailingExtraIconClick], [onTrailingTextClick] 값은 무시됩니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param leadingIcon leading content 로 배치할 아이콘 resId
+ * @param leadingText leading content 로 배치할 텍스트. 선택적으로 값을 받습니다.
+ * @param onLeadingIconClick [leadingIcon] 이 클릭됐을 때 실행될 람다
+ * @param showLogoAtCenter center content 로 덕키의 로고를 배치할지 여부
+ * @param centerText center content 에 로고 대신에 표시할 텍스트
+ * @param centerTextTrailingIcon [centerText] 의 trailing content 로 배치할 아이콘 resId
+ * @param onCenterClick center content 가 클릭됐을 때 실행될 람다
+ * @param trailingContent trailing content 로 배치할 컴포넌트
+ * @param trailingIcon trailing content 로 배치할 아이콘
+ * @param trailingExtraIcon trailing content 에 추가로 배치할 아이콘
+ * @param trailingText trailing content 에 배치할 텍스트
+ * @param onTrailingIconClick [trailingIcon] 이 클릭됐을 때 실행될 람다
+ * @param onTrailingExtraIconClick [trailingExtraIcon] 이 클릭됐을 때 실행될 람다
+ * @param onTrailingTextClick [trailingText] 가 클릭됐을 때 실행될 람다
+ */
@Composable
fun QuackTopAppBar(
modifier: Modifier = Modifier,
- leadingIconResId: Int? = null,
+ leadingIcon: ImageVector? = null,
leadingText: String? = null,
onLeadingIconClick: (() -> Unit)? = null,
showLogoAtCenter: Boolean? = null,
centerText: String? = null,
- centerTextTrailingIconResId: Int? = null,
+ centerTextTrailingIcon: ImageVector? = null,
onCenterClick: (() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null,
- trailingIconResId: Int? = null,
- trailingExtraIconResId: Int? = null,
+ trailingIcon: ImageVector? = null,
+ trailingExtraIcon: ImageVector? = null,
trailingText: String? = null,
onTrailingIconClick: (() -> Unit)? = null,
onTrailingExtraIconClick: (() -> Unit)? = null,
onTrailingTextClick: (() -> Unit)? = null,
) {
- team.duckie.quackquack.ui.component.QuackTopAppBar(
- modifier = modifier,
- leadingIcon = leadingIconResId?.toQuackV1Icon,
- leadingText = leadingText,
- onLeadingIconClick = onLeadingIconClick,
- showLogoAtCenter = showLogoAtCenter,
- centerText = centerText,
- centerTextTrailingIcon = centerTextTrailingIconResId?.toQuackV1Icon,
- onCenterClick = onCenterClick,
- trailingContent = trailingContent,
- trailingIcon = trailingIconResId?.toQuackV1Icon,
- trailingExtraIcon = trailingExtraIconResId?.toQuackV1Icon,
- trailingText = trailingText,
- onTrailingIconClick = onTrailingIconClick,
- onTrailingExtraIconClick = onTrailingExtraIconClick,
- onTrailingTextClick = onTrailingTextClick,
+ runtimeCheck(
+ leadingIcon != null ||
+ leadingText != null ||
+ onLeadingIconClick != null ||
+ showLogoAtCenter != null ||
+ centerText != null ||
+ centerTextTrailingIcon != null ||
+ onCenterClick != null ||
+ trailingContent != null ||
+ trailingIcon != null ||
+ trailingExtraIcon != null ||
+ trailingText != null ||
+ onTrailingIconClick != null ||
+ onTrailingExtraIconClick != null ||
+ onTrailingTextClick != null,
+ ) {
+ "At least one param setting is required."
+ }
+
+ if (trailingContent != null) {
+ runtimeCheck(
+ trailingIcon == null && trailingExtraIcon == null && trailingText == null &&
+ onTrailingIconClick == null && onTrailingExtraIconClick == null &&
+ onTrailingTextClick == null,
+ ) {
+ "trailingContent 가 입력되었을 때는 다른 trailing content 인자들을 이용하실 수 없습니다."
+ }
+ }
+
+ Row(
+ modifier = modifier
+ .fillMaxWidth()
+ .background(color = QuackTopAppBarDefaults.BackgroundColor.value),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween,
+ ) {
+ QuackTopAppBarDefaults.LeadingContent(
+ icon = leadingIcon,
+ text = leadingText,
+ onIconClick = onLeadingIconClick,
+ )
+ QuackTopAppBarDefaults.CenterContent(
+ showLogo = showLogoAtCenter,
+ text = centerText,
+ textTrailingIcon = centerTextTrailingIcon,
+ onClick = onCenterClick,
+ )
+ // https://github.com/duckie-team/quack-quack-android/issues/412
+ // TrailingContent content 가 없어도 width 를 차지함
+ if (trailingContent != null) {
+ trailingContent()
+ } else {
+ QuackTopAppBarDefaults.TrailingContent(
+ icon = trailingIcon,
+ extraIcon = trailingExtraIcon,
+ text = trailingText,
+ onIconClick = onTrailingIconClick,
+ onExtraIconClick = onTrailingExtraIconClick,
+ onTextClick = onTrailingTextClick,
+ )
+ }
+ }
+}
+
+/**
+ * QuackTopAppBar 를 그리기 위한 리소스들을 정의합니다.
+ */
+private object QuackTopAppBarDefaults {
+ val BackgroundColor = QuackColor.White
+
+ private val CenterTypography = QuackTypography.Body1
+ private val TrailingTypography = QuackTypography.Subtitle.change(
+ color = QuackColor.Gray2,
)
+
+ private val LogoIcon: QuackV1Icon = QuackIcon.TextLogoId.toQuackV1Icon!!
+ private val LogoIconSize = DpSize(
+ width = 72.dp,
+ height = 24.dp,
+ )
+ private val LogoPadding = PaddingValues(
+ vertical = 12.dp,
+ )
+
+ private val CenterTextPadding = PaddingValues(
+ vertical = 15.dp,
+ )
+ private val CenterIconTint = QuackColor.Gray1
+
+ /**
+ * 모든 영역에서 사용되는 공통 아이콘 사이즈
+ */
+ private val IconSize: Dp = 24.dp
+
+ /**
+ * leading content 를 배치합니다.
+ *
+ * @param icon 배치할 아이콘
+ * @param text 배치할 텍스트. 선택적으로 값을 받습니다.
+ * @param onIconClick [icon] 이 클릭됐을 때 실행될 람다
+ */
+ @Composable
+ fun LeadingContent(
+ icon: ImageVector?,
+ text: String? = null,
+ onIconClick: (() -> Unit)? = null,
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ icon?.let {
+ QuackIcon(
+ modifier = Modifier
+ .expendedQuackClickable(
+ rippleEnabled = true,
+ onClick = onIconClick,
+ )
+ .size(DpSize(44.dp, 44.dp))
+ .padding(10.dp),
+ icon = icon,
+ )
+ }
+ text?.let {
+ // TODO: 최대 width 처리
+ // 아마 커스텀 레이아웃 필요할 듯
+ QuackText(
+ text = text,
+ typography = QuackTypography.HeadLine2,
+ singleLine = true,
+ )
+ }
+ }
+ }
+
+ /**
+ * center content 를 배치합니다.
+ *
+ * - [showLogo] 이 true 라면 [text] 값은 무시됩니다.
+ * 로고 리소스로는 [QuackIcon.TextLogoId] 를 사용합니다.
+ * - [text] 값이 있다면 [showLogo] 값은 무시됩니다.
+ * 또한 [text] 는 trailing icon 을 가질 수 있습니다.
+ *
+ * @param showLogo 덕키의 로고를 배치할지 여부
+ * @param text 로고 대신에 표시할 텍스트
+ * @param textTrailingIcon [text] 의 trailing content 로 표시될 아이콘
+ * @param onClick center content 가 클릭됐을 때 실행될 람다
+ */
+ @Composable
+ fun CenterContent(
+ showLogo: Boolean? = null,
+ text: String? = null,
+ textTrailingIcon: ImageVector? = null,
+ onClick: (() -> Unit)? = null,
+ ) {
+ if (showLogo == true) {
+ runtimeCheck(
+ value = text == null,
+ ) {
+ "로고와 텍스트를 동시에 표시할 수 없습니다"
+ }
+ }
+ Row(
+ modifier = Modifier.quackClickable(
+ rippleEnabled = false,
+ onClick = onClick,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ if (showLogo == true) {
+ QuackImage(
+ modifier = Modifier
+ .size(LogoIconSize)
+ .padding(LogoPadding),
+ src = LogoIcon,
+ )
+ } else {
+ text?.let {
+ QuackText(
+ modifier = Modifier.padding(
+ paddingValues = CenterTextPadding,
+ ),
+ text = text,
+ typography = CenterTypography,
+ singleLine = true,
+ )
+ }
+ textTrailingIcon?.let {
+ QuackIcon(
+ icon = textTrailingIcon,
+ tint = CenterIconTint,
+ size = 24.dp,
+ )
+ }
+ }
+ }
+ }
+
+ /**
+ * trailing content 를 그립니다.
+ *
+ * - [extraIcon] 과 [icon] 이 하나라도 들어왔다면 [text] 는 무시되며,
+ * `[extraIcon] [icon]` 순서로 배치됩니다.
+ * - [text] 값이 입력되면 [extraIcon] 과 [icon] 값은 무시됩니다.
+ *
+ * @param icon 배치할 아이콘
+ * @param extraIcon 추가로 배치할 아이콘
+ * @param text 배치할 텍스트
+ * @param onIconClick [icon] 이 클릭됐을 때 실행될 람다
+ * @param onExtraIconClick [extraIcon] 이 클릭됐을 때 실행될 람다
+ * @param onTextClick [text] 가 클릭됐을 때 실행될 람다
+ */
+ @Composable
+ fun TrailingContent(
+ icon: ImageVector? = null,
+ extraIcon: ImageVector? = null,
+ text: String? = null,
+ onIconClick: (() -> Unit)? = null,
+ onExtraIconClick: (() -> Unit)? = null,
+ onTextClick: (() -> Unit)? = null,
+ ) {
+ if (icon != null || extraIcon != null) {
+ runtimeCheck(
+ value = text == null,
+ ) {
+ "아이콘과 텍스트를 동시에 표시할 수 없습니다"
+ }
+ }
+ if (text != null) {
+ runtimeCheck(
+ value = icon == null && extraIcon == null,
+ ) {
+ "텍스트와 아이콘을 동시에 표시할 수 없습니다"
+ }
+ }
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ text?.let {
+ QuackText(
+ modifier = Modifier
+ .expendedQuackClickable(
+ rippleEnabled = true,
+ onClick = onTextClick,
+ ),
+ text = text,
+ typography = TrailingTypography,
+ singleLine = true,
+ )
+ }
+ extraIcon?.let {
+ QuackIcon(
+ icon = extraIcon,
+ modifier = Modifier
+ .expendedQuackClickable(
+ rippleEnabled = true,
+ onClick = onExtraIconClick,
+ ),
+ )
+ }
+ icon?.let {
+ QuackIcon(
+ icon = icon,
+ modifier = Modifier
+ .expendedQuackClickable(
+ rippleEnabled = true,
+ onClick = onIconClick,
+ ),
+ size = IconSize,
+ )
+ }
+ }
+ }
}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/animation/AnimationSpec.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/animation/AnimationSpec.kt
new file mode 100644
index 000000000..01a3fbf1d
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/animation/AnimationSpec.kt
@@ -0,0 +1,64 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.quack.todo.animation
+
+import androidx.compose.animation.core.AnimationSpec
+import androidx.compose.animation.core.DurationBasedAnimationSpec
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.SnapSpec
+import androidx.compose.animation.core.TweenSpec
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+import androidx.compose.runtime.Stable
+
+/**
+ * 꽥꽥에서 사용할 [AnimationSpec] 에 대한 정보
+ */
+object QuackAnimationSpec {
+ /**
+ * 일부 환경에서는 애니메이션이 없이 진행돼야 할 때도 있습니다.
+ * 이 값을 true 로 설정하면 모든 애니메이션을 무시합니다.
+ *
+ * **이 값의 변경은 모든 애니메이션에 영향을 미치므로 신중하게 사용해야 합니다.**
+ */
+ var snapMode: Boolean = false
+
+ /**
+ * 꽥꽥에서 사용할 [애니메이션의 기본 스팩][AnimationSpec]
+ *
+ * @return 덕키에서 사용할 [AnimationSpec]. [snapMode] 에 따라 반환값이 달라집니다.
+ * false 라면 덕키에서 사용하는 애니메이션 스팩인 [TweenSpec] 이 반환되고,
+ * true 라면 [SnapSpec] 이 반환됩니다.
+ *
+ * @see snapMode
+ */
+ operator fun invoke(): DurationBasedAnimationSpec = when (snapMode) {
+ true -> snap()
+ else -> tween(
+ durationMillis = 250,
+ easing = LinearEasing,
+ )
+ }
+}
+
+/**
+ * [QuackAnimationSpec.snapMode] 대신에 한 번만 선택적으로 애니메이션 여부를
+ * 결정하기 위해 사용할 수 있습니다.
+ *
+ * @param useAnimation 애니메이션을 사용할지 여부
+ *
+ * @return [useAnimation] 여부에 따른 [DurationBasedAnimationSpec]
+ */
+@Suppress("FunctionName")
+@Stable
+public fun QuackOptionalAnimationSpec(
+ useAnimation: Boolean,
+): DurationBasedAnimationSpec = when (useAnimation) {
+ true -> QuackAnimationSpec()
+ else -> snap()
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/animation/toggle.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/animation/toggle.kt
new file mode 100644
index 000000000..0c37863e5
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/quack/todo/animation/toggle.kt
@@ -0,0 +1,585 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/quack-quack-android/blob/master/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.quack.todo.animation
+
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.BorderStroke
+import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.Stable
+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.Color
+import androidx.compose.ui.graphics.Path
+import androidx.compose.ui.graphics.PathMeasure
+import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.Stroke
+import androidx.compose.ui.state.ToggleableState
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSurface
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackToggleIconSize.Compact
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackToggleIconSize.Normal
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackToggleIconSize.Small
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
+import kotlin.math.floor
+
+/**
+ * [QuackToggleButton] 에서 표시할 아이콘의 사이즈를 정의합니다.
+ *
+ * @property Normal 보통 사이즈로 표시합니다. (24 dp)
+ * @property Small 약간 축소된 사이즈로 표시합니다. (18 dp)
+ * @property Compact 많이 축소된 사이즈로 표시합니다. (14 dp)
+ */
+enum class QuackToggleIconSize(
+ val size: DpSize,
+) {
+ Normal(
+ size = DpSize(
+ width = 24.dp,
+ height = 24.dp,
+ ),
+ ),
+ Small(
+ size = DpSize(
+ width = 18.dp,
+ height = 18.dp,
+ ),
+ ),
+ Compact(
+ size = DpSize(
+ width = 14.dp,
+ height = 14.dp,
+ ),
+ ),
+}
+
+/**
+ * QuackToggle 을 그리는데 필요한 리소스를 구성합니다.
+ */
+private object QuackToggleDefaults {
+ // Copied from AOSP
+ object DrawConstaints {
+ const val TransitionLabel = "CheckTransition"
+ const val BoxOutDuration = 100
+ const val StopLocation = 0.5f
+
+ val StrokeWidth = 1.5.dp
+ const val CheckCrossX = 0.4f
+ const val CheckCrossY = 0.7f
+ const val LeftX = 0.25f
+ const val LeftY = 0.55f
+ const val RightX = 0.75f
+ const val RightY = 0.35f
+ }
+
+ object RoundCheck {
+ /**
+ * 주어진 상황에 맞는 테두리를 계산합니다.
+ *
+ * @param isChecked 현재 선택된 상태인지 여부
+ *
+ * @return [isChecked] 여부에 따른 [QuackBorder]
+ */
+ @Stable
+ fun borderFor(
+ isChecked: Boolean,
+ ) = BorderStroke(
+ width = 1.dp,
+ color = when (isChecked) {
+ true -> QuackColor.DuckieOrange.value
+ else -> QuackColor.White.value
+ },
+ )
+
+ /**
+ * 주어진 상황에 맞는 배경 색상을 계산합니다.
+ *
+ * @param isChecked 현재 선택된 상태인지 여부
+ *
+ * @return [isChecked] 여부에 따른 [QuackColor]
+ */
+ @Stable
+ fun backgroundColorFor(
+ isChecked: Boolean,
+ ) = when (isChecked) {
+ true -> QuackColor.DuckieOrange
+ else -> QuackColor.Black.change(
+ alpha = 0.2f,
+ )
+ }
+
+ val ContainerSize = DpSize(
+ width = 24.dp,
+ height = 24.dp,
+ )
+ val ContainerShape = CircleShape
+
+ val CheckColor = QuackColor.White
+ val CheckSize = DpSize(
+ width = 18.dp,
+ height = 18.dp,
+ )
+ }
+
+ object SquareCheck {
+ /**
+ * 주어진 상황에 맞는 배경 색상을 계산합니다.
+ *
+ * @param isChecked 현재 선택된 상태인지 여부
+ *
+ * @return [isChecked] 여부에 따른 [QuackColor]
+ */
+ @Stable
+ fun backgroundColorFor(
+ isChecked: Boolean,
+ ) = when (isChecked) {
+ true -> QuackColor.DuckieOrange
+ else -> QuackColor.Gray3
+ }
+
+ val ContainerSize = DpSize(
+ width = 24.dp,
+ height = 24.dp,
+ )
+ val ContainerShape = RoundedCornerShape(
+ size = 4.dp,
+ )
+
+ val CheckColor = QuackColor.White
+ val CheckSize = DpSize(
+ width = 18.dp,
+ height = 18.dp,
+ )
+ }
+
+ object ToggleButton {
+ val IconSize = Small
+ val Typography = QuackTypography.Body2.change(
+ color = QuackColor.Gray1,
+ )
+
+ /**
+ * ```
+ * [Icon][Typography]
+ * ```
+ *
+ * 에서 `Icon` 과 `Typography` 간의 사이 간격을 나타냅니다.
+ */
+ val ItemSpacedBy = 4.dp
+ }
+}
+
+/**
+ * 덕키의 원형 CheckBox 를 구현합니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param checked 체크되었는지 여부
+ * @param onClick 체크시 호출되는 콜백
+ */
+@Composable
+fun QuackRoundCheckBox(
+ modifier: Modifier = Modifier,
+ checked: Boolean,
+ onClick: (() -> Unit)? = null,
+): Unit = with(
+ receiver = QuackToggleDefaults.RoundCheck,
+) {
+ QuackSurface(
+ modifier = modifier.size(
+ size = ContainerSize,
+ ),
+ shape = ContainerShape,
+ backgroundColor = backgroundColorFor(
+ isChecked = checked,
+ ),
+ border = borderFor(
+ isChecked = checked,
+ ),
+ onClick = onClick,
+ ) {
+ Check(
+ value = ToggleableState(
+ value = checked,
+ ),
+ checkColor = CheckColor,
+ size = CheckSize,
+ )
+ }
+}
+
+/**
+ * 덕키의 원형 CheckBox 를 구현합니다.
+ * [QuackRoundCheckBox] 보다 작습니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param checked 체크되었는지 여부
+ * @param checkedText 체크시 하단에 표시할 텍스트
+ * @param onClick 체크시 호출되는 콜백
+ */
+@Composable
+public fun QuackSmallRoundCheckBox(
+ modifier: Modifier = Modifier,
+ checked: Boolean,
+ checkedText: String,
+ onClick: (() -> Unit)? = null,
+): Unit = with(QuackToggleDefaults.RoundCheck) {
+ AnimatedContent(
+ targetState = checked,
+ label = "AnimatedContent",
+ ) { showUnderText ->
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ QuackSurface(
+ modifier = modifier.size(DpSize(width = 18.dp, height = 18.dp)),
+ shape = ContainerShape,
+ backgroundColor = backgroundColorFor(
+ isChecked = checked,
+ ),
+ border = borderFor(
+ isChecked = checked,
+ ),
+ onClick = onClick,
+ ) {
+ Check(
+ value = ToggleableState(
+ value = checked,
+ ),
+ checkColor = CheckColor,
+ size = DpSize(width = 12.dp, height = 12.dp),
+ )
+ }
+ if (showUnderText) {
+ QuackText(
+ modifier = Modifier.padding(top = 2.dp),
+ text = checkedText,
+ typography = QuackTypography.Body3.change(color = QuackColor.DuckieOrange),
+ )
+ }
+ }
+ }
+}
+
+/**
+ * 덕키의 사각형 CheckBox 를 구현합니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param checked 체크되었는지 여부
+ * @param onClick 체크시 호출되는 콜백
+ */
+@Composable
+public fun QuackSquareCheckBox(
+ modifier: Modifier = Modifier,
+ checked: Boolean,
+ onClick: (() -> Unit)? = null,
+): Unit = with(
+ receiver = QuackToggleDefaults.SquareCheck,
+) {
+ QuackSurface(
+ modifier = modifier.size(
+ size = ContainerSize,
+ ),
+ shape = ContainerShape,
+ backgroundColor = backgroundColorFor(
+ isChecked = checked,
+ ),
+ onClick = onClick,
+ ) {
+ Check(
+ value = ToggleableState(
+ value = checked,
+ ),
+ checkColor = CheckColor,
+ size = CheckSize,
+ )
+ }
+}
+
+/**
+ * Checked 여부에 따라 조건에 맞는 아이콘을 표시합니다.
+ *
+ * @param modifier 이 컴포넌트에 적용할 [Modifier]
+ * @param checkedIcon 체크 상태에서 표시할 아이콘
+ * @param uncheckedIcon 체크 상태가 아닐 때 표시할 아이콘
+ * @param iconSize 아이콘을 표시할 크기.
+ * 기본값은 [QuackToggleIconSize.Normal] 이며, [trailingText] 이 존재할 때는
+ * [QuackToggleIconSize.Small] 로 강제됩니다.
+ * @param checked 현재 체크 상태에 있는지 여부
+ * @param trailingText trailing content 로 배치될 텍스트.
+ * 만약 null 이 입력될 시 trailing content 를 배치하지 않습니다.
+ * @param onClick 아이콘을 클릭했을 때 실행될 람다
+ */
+@Composable
+fun QuackToggleButton(
+ modifier: Modifier = Modifier,
+ checkedIcon: QuackIcon,
+ uncheckedIcon: QuackIcon,
+ iconSize: QuackToggleIconSize = Normal,
+ checked: Boolean,
+ trailingText: String? = null,
+ onClick: (() -> Unit)? = null,
+): Unit = with(
+ receiver = QuackToggleDefaults.ToggleButton,
+) {
+ Row(
+ modifier = modifier,
+ horizontalArrangement = Arrangement.spacedBy(
+ space = ItemSpacedBy,
+ ),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ QuackImage(
+ src = when (checked) {
+ true -> checkedIcon
+ else -> uncheckedIcon
+ },
+ modifier = Modifier
+ .quackClickable(
+ rippleEnabled = false,
+ onClick = onClick,
+ )
+ .size((iconSize.takeIf { trailingText == null } ?: IconSize).size),
+ )
+ trailingText?.let {
+ QuackText(
+ text = trailingText,
+ typography = Typography,
+ singleLine = true,
+ )
+ }
+ }
+}
+
+/**
+ * [Canvas] 에 Check 모양을 그립니다.
+ *
+ * animationSpec 으로 항상 [QuackAnimationSpec] 을 사용합니다.
+ *
+ * @param value 현재 토글 상태를 의미하는 [ToggleableState]
+ * @param checkColor Check 색상
+ * @param size Check 의 크기
+ */
+@Composable
+private fun Check(
+ value: ToggleableState,
+ checkColor: QuackColor,
+ size: DpSize,
+): Unit = with(
+ receiver = QuackToggleDefaults.DrawConstaints,
+) {
+ val transition = updateTransition(
+ targetState = value,
+ label = TransitionLabel,
+ )
+ val checkDrawFraction by transition.animateFloat(
+ transitionSpec = {
+ QuackAnimationSpec()
+ },
+ label = TransitionLabel,
+ ) { toggleableState ->
+ when (toggleableState) {
+ ToggleableState.On -> 1f
+ ToggleableState.Off -> 0f
+ ToggleableState.Indeterminate -> 1f
+ }
+ }
+ val checkCenterGravitationShiftFraction by transition.animateFloat(
+ transitionSpec = {
+ when {
+ initialState == ToggleableState.Off -> snap()
+ targetState == ToggleableState.Off -> snap(
+ delayMillis = BoxOutDuration,
+ )
+
+ else -> QuackAnimationSpec()
+ }
+ },
+ label = TransitionLabel,
+ ) { toggleableState ->
+ when (toggleableState) {
+ ToggleableState.On -> 0f
+ ToggleableState.Off -> 0f
+ ToggleableState.Indeterminate -> 1f
+ }
+ }
+ val checkCache = remember {
+ CheckDrawingCache()
+ }
+ Canvas(
+ modifier = Modifier
+ .wrapContentSize(
+ align = Alignment.Center,
+ )
+ .requiredSize(
+ size = size,
+ ),
+ ) {
+ val strokeWidthPx = floor(
+ x = StrokeWidth.toPx(),
+ )
+ drawCheck(
+ checkColor = checkColor.value,
+ checkFraction = checkDrawFraction,
+ crossCenterGravitation = checkCenterGravitationShiftFraction,
+ strokeWidthPx = strokeWidthPx,
+ drawingCache = checkCache,
+ )
+ }
+}
+
+// Copied from AOSP
+// TODO: documentation
+@Immutable
+private class CheckDrawingCache(
+ val checkPath: Path = Path(),
+ val pathMeasure: PathMeasure = PathMeasure(),
+ val pathToDraw: Path = Path(),
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+
+ if (javaClass != other?.javaClass) return false
+
+ other as CheckDrawingCache
+
+ if (checkPath != other.checkPath) return false
+ if (pathMeasure != other.pathMeasure) return false
+ if (pathToDraw != other.pathToDraw) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = checkPath.hashCode()
+ result = 31 * result + pathMeasure.hashCode()
+ result = 31 * result + pathToDraw.hashCode()
+ return result
+ }
+}
+
+/**
+ * [start], [stop] 사이의 임의의 지점 f(p) 를 반환합니다.
+ * [fraction] 은 임의의 점 p 까지의 거리를 의미합니다.
+ *
+ * @param start 시작 지점
+ * @param stop 도착 지점
+ * @param fraction 임의의 점 p 까지의 거리
+ *
+ * @return [start], [stop] 사이의 임의의 지점 f(p)
+ */
+@Suppress("SameParameterValue")
+private fun linearInterpolation(
+ start: Float,
+ stop: Float,
+ fraction: Float,
+) = (1 - fraction) * start + fraction * stop
+
+/**
+ * [DrawScope] 에 체크 표시를 그립니다.
+ * [crossCenterGravitation] 의 값이 0f -> 1f -> 0f 이므로,
+ * 중심지점일수록 그려지는 속도가 빨라집니다.
+ *
+ * @param checkColor 체크 표시의 색상
+ * @param checkFraction 체크 표시가 끝나는 지점까지의 거리
+ * @param crossCenterGravitation 중심지점의 중력
+ * @param strokeWidthPx 선의 굵기
+ * @param drawingCache 그려질 선의 경로를 저장하는 캐시
+ */
+@Stable
+private fun DrawScope.drawCheck(
+ checkColor: Color,
+ checkFraction: Float,
+ crossCenterGravitation: Float,
+ strokeWidthPx: Float,
+ drawingCache: CheckDrawingCache,
+): Unit = with(
+ receiver = QuackToggleDefaults.DrawConstaints,
+) {
+ val stroke = Stroke(
+ width = strokeWidthPx,
+ cap = StrokeCap.Round,
+ )
+ val width = size.width
+
+ val gravitatedCrossX = linearInterpolation(
+ start = CheckCrossX,
+ stop = StopLocation,
+ fraction = crossCenterGravitation,
+ )
+ val gravitatedCrossY = linearInterpolation(
+ start = CheckCrossY,
+ stop = StopLocation,
+ fraction = crossCenterGravitation,
+ )
+
+ val gravitatedLeftY = linearInterpolation(
+ start = LeftY,
+ stop = StopLocation,
+ fraction = crossCenterGravitation,
+ )
+ val gravitatedRightY = linearInterpolation(
+ start = RightY,
+ stop = StopLocation,
+ fraction = crossCenterGravitation,
+ )
+
+ with(
+ receiver = drawingCache,
+ ) {
+ checkPath.reset()
+ checkPath.moveTo(
+ x = width * LeftX,
+ y = width * gravitatedLeftY,
+ )
+ checkPath.lineTo(
+ x = width * gravitatedCrossX,
+ y = width * gravitatedCrossY,
+ )
+ checkPath.lineTo(
+ x = width * RightX,
+ y = width * gravitatedRightY,
+ )
+ pathMeasure.setPath(
+ path = checkPath,
+ forceClosed = false,
+ )
+ pathToDraw.reset()
+ pathMeasure.getSegment(
+ startDistance = 0f,
+ stopDistance = pathMeasure.length * checkFraction,
+ destination = pathToDraw,
+ startWithMoveTo = true,
+ )
+ }
+ drawPath(
+ path = drawingCache.pathToDraw,
+ color = checkColor,
+ style = stroke,
+ )
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/screen/SearchTagScreen.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/screen/SearchTagScreen.kt
index 335b95c40..3abd32170 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/screen/SearchTagScreen.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/screen/SearchTagScreen.kt
@@ -8,6 +8,7 @@
package team.duckie.app.android.common.compose.ui.screen
import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
@@ -32,14 +33,14 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
import team.duckie.app.android.common.compose.R
import team.duckie.app.android.common.compose.ui.ImeSpacer
-import team.duckie.app.android.common.compose.ui.icon.v1.CloseId
import team.duckie.app.android.common.compose.ui.icon.v1.SearchId
import team.duckie.app.android.common.compose.ui.quack.QuackNoUnderlineTextField
import team.duckie.app.android.common.compose.ui.quack.todo.QuackLazyVerticalGridTag
import team.duckie.app.android.common.compose.ui.quack.todo.QuackTopAppBar
-import team.duckie.quackquack.animation.QuackAnimatedVisibility
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackText
@@ -86,7 +87,7 @@ fun SearchTagScreen(
) {
QuackTopAppBar(
leadingText = title,
- trailingIconResId = QuackIcon.CloseId,
+ trailingIcon = QuackIcon.Outlined.Close,
onTrailingIconClick = onCloseClick,
)
@@ -100,7 +101,6 @@ fun SearchTagScreen(
),
horizontalSpace = 4.dp,
items = tags,
- tagTypeResId = QuackIcon.CloseId,
onClick = { onTagClick(it) },
itemChunkedSize = 3,
)
@@ -129,7 +129,7 @@ fun SearchTagScreen(
),
)
- QuackAnimatedVisibility(
+ AnimatedVisibility(
modifier = Modifier.padding(
top = 8.dp,
),
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/temp/TempFlexiblePrimaryLargeButton.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/temp/TempFlexiblePrimaryLargeButton.kt
new file mode 100644
index 000000000..03dce6e9e
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/temp/TempFlexiblePrimaryLargeButton.kt
@@ -0,0 +1,66 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.temp
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.unit.dp
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.modifier.quackClickable
+
+/**
+ * QuackPrimaryLargeButton의 크키가 조정되지 않는 오류를 대처하기 위한 임시 버튼
+ *
+ * TODO(limsaehyun): 추후 QuackQuackV2 로 대체되어야 함
+ */
+@Composable
+fun TempFlexiblePrimaryLargeButton(
+ modifier: Modifier = Modifier,
+ text: String,
+ enabled: Boolean = true,
+ onClick: () -> Unit,
+) {
+ Box(
+ modifier = modifier
+ .heightIn(44.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .background(backgroundFor(enabled))
+ .quackClickable(onClick = onClickFor(enabled, onClick)),
+ contentAlignment = Alignment.Center,
+ ) {
+ QuackText(
+ text = text,
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.White,
+ ),
+ )
+ }
+}
+
+private fun backgroundFor(enabled: Boolean) = if (enabled) {
+ QuackColor.DuckieOrange.value
+} else {
+ QuackColor.Gray2.value
+}
+
+private fun onClickFor(
+ enabled: Boolean,
+ onClick: () -> Unit,
+) = if (enabled) {
+ onClick
+} else {
+ null
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/temp/TempFlexibleSecondaryLargButton.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/temp/TempFlexibleSecondaryLargButton.kt
new file mode 100644
index 000000000..860c79e5e
--- /dev/null
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/ui/temp/TempFlexibleSecondaryLargButton.kt
@@ -0,0 +1,56 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.compose.ui.temp
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.heightIn
+import androidx.compose.foundation.shape.RoundedCornerShape
+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.unit.dp
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.modifier.quackClickable
+
+/**
+ * QuackPrimaryLargeButton의 크키가 조정되지 않는 오류를 대처하기 위한 임시 버튼
+ *
+ * TODO(limsaehyun): 추후 QuackQuackV2 로 대체되어야 함
+ */
+@Composable
+fun TempFlexibleSecondaryLargeButton(
+ modifier: Modifier = Modifier,
+ text: String,
+ onClick: () -> Unit,
+) {
+ Box(
+ modifier = modifier
+ .heightIn(44.dp)
+ .quackBorder(
+ border = QuackBorder(color = QuackColor.Gray3),
+ shape = RoundedCornerShape(8.dp),
+ )
+ .clip(RoundedCornerShape(8.dp))
+ .background(QuackColor.White.value)
+ .quackClickable(onClick = onClick),
+ contentAlignment = Alignment.Center,
+ ) {
+ QuackText(
+ text = text,
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.Black,
+ ),
+ )
+ }
+}
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/ExpendedClickable.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/ExpendedClickable.kt
index 5a3d9715e..902f70dfa 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/ExpendedClickable.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/ExpendedClickable.kt
@@ -7,60 +7,13 @@
package team.duckie.app.android.common.compose.util
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.layout
-import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.quackClickable
-@Preview
-@Composable
-fun PreviewExpandedClickable() {
- Column(
- modifier = Modifier
- .fillMaxSize()
- .background(
- color = Color.White,
- )
- .padding(
- horizontal = 16.dp,
- ),
- ) {
- Box(
- modifier = Modifier
- .background(Color.Black)
- .size(50.dp),
- )
- Box(
- modifier = Modifier
- .background(QuackColor.Gray4.value)
- .size(50.dp)
- .expendedQuackClickable {
- },
- )
- Box(
- modifier = Modifier
- .background(QuackColor.Gray4.value)
- .size(50.dp)
- .expendedQuackClickable(
- verticalExpendedSize = 24.dp,
- horizontalExpendedSize = 24.dp,
- ) {
- },
- )
- }
-}
-
fun Modifier.expendedQuackClickable(
verticalExpendedSize: Dp = 12.dp,
horizontalExpendedSize: Dp = 12.dp,
diff --git a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/HandleKeyBoard.kt b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/HandleKeyBoard.kt
index 52f4ed6df..b2b5dac7d 100644
--- a/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/HandleKeyBoard.kt
+++ b/common/compose/src/main/kotlin/team/duckie/app/android/common/compose/util/HandleKeyBoard.kt
@@ -9,12 +9,17 @@
package team.duckie.app.android.common.compose.util
+import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.composed
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import team.duckie.app.android.common.compose.rememberKeyboardVisible
@@ -29,3 +34,15 @@ fun HandleKeyboardVisibilityWithSheet(sheetState: ModalBottomSheetState) {
}
}
}
+
+fun Modifier.addFocusCleaner(
+ doOnClear: () -> Unit = {},
+): Modifier = composed {
+ val focusManager = LocalFocusManager.current
+ pointerInput(Unit) {
+ detectTapGestures(onTap = {
+ doOnClear()
+ focusManager.clearFocus()
+ },)
+ }
+}
diff --git a/common/compose/src/main/res/drawable/ic_clock_12.xml b/common/compose/src/main/res/drawable/ic_clock_12.xml
deleted file mode 100644
index 60d169843..000000000
--- a/common/compose/src/main/res/drawable/ic_clock_12.xml
+++ /dev/null
@@ -1,9 +0,0 @@
-
-
-
diff --git a/common/compose/src/main/res/values/strings.xml b/common/compose/src/main/res/values/strings.xml
index 637a17642..7c07eef4b 100644
--- a/common/compose/src/main/res/values/strings.xml
+++ b/common/compose/src/main/res/values/strings.xml
@@ -18,6 +18,7 @@
응시자
추천
좋아요
+ 차단해제
해당 기능은 개발 예정입니다.\n기대해주세요
최소 2 이상의 columns가 필요합니다.
@@ -42,5 +43,5 @@
네트워크 연결이 불안정해요\n잠시 후 다시 시도해 주세요.
추가한 태그
완료
- 태그 입력하기
+ 태그 입력하기 (최대 10자)
diff --git a/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/exception/constant.kt b/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/exception/constant.kt
index c73e39f30..cc9584e2d 100644
--- a/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/exception/constant.kt
+++ b/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/exception/constant.kt
@@ -44,6 +44,8 @@ object ExceptionCode {
"KAKAOTALK_NOT_SUPPORT_EXCEPTION"
const val HEART_NOT_FOUND = "HEART_NOT_FOUND"
+
+ const val KAKAO_CANCELLED = "KAKAO_CANCELLED"
}
val Throwable.isHeartNotFound: Boolean
@@ -83,3 +85,6 @@ val Throwable.isKakaoTalkNotConnectedAccount: Boolean
val Throwable.isKakaoTalkNotSupportAccount: Boolean
get() = (this as? DuckieThirdPartyException)?.code == ExceptionCode.KAKAOTALK_NOT_SUPPORT_EXCEPTION
+
+val Throwable.isKakaoCancelled: Boolean
+ get() = (this as? DuckieThirdPartyException)?.code == ExceptionCode.KAKAO_CANCELLED
diff --git a/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/number.kt b/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/number.kt
new file mode 100644
index 000000000..acb582cd9
--- /dev/null
+++ b/common/kotlin/src/main/kotlin/team/duckie/app/android/common/kotlin/number.kt
@@ -0,0 +1,14 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.common.kotlin
+
+import java.text.DecimalFormat
+
+private val decimalFormat = DecimalFormat("#,###")
+
+fun Int.toDecimalFormat() = decimalFormat.format(this)
diff --git a/core/datastore/src/main/kotlin/team/duckie/app/android/core/datastore/datastore.kt b/core/datastore/src/main/kotlin/team/duckie/app/android/core/datastore/datastore.kt
index 1c323cbfd..b0bcf9ccb 100644
--- a/core/datastore/src/main/kotlin/team/duckie/app/android/core/datastore/datastore.kt
+++ b/core/datastore/src/main/kotlin/team/duckie/app/android/core/datastore/datastore.kt
@@ -40,6 +40,10 @@ object PreferenceKey {
object DevMode {
val IsStage = booleanPreferencesKey(buildPreferenceKey(type = "devMode", token = "isStage"))
}
+
+ object FeatureFlag {
+ val IsProceedEnable = booleanPreferencesKey(buildPreferenceKey(type = "feature", token = "isProceedEnable"))
+ }
}
val Context.dataStore by preferencesDataStore(
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt
index 791cffd95..3026b449f 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/mapper/mapper.kt
@@ -10,16 +10,20 @@ package team.duckie.app.android.data.exam.mapper
import kotlinx.collections.immutable.toImmutableList
import team.duckie.app.android.common.kotlin.AllowCyclomaticComplexMethod
import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
+import team.duckie.app.android.common.kotlin.exception.duckieSimpleResponseFieldNpe
import team.duckie.app.android.common.kotlin.fastMap
import team.duckie.app.android.data.category.mapper.toDomain
import team.duckie.app.android.data.exam.model.AnswerData
import team.duckie.app.android.data.exam.model.ChoiceData
+import team.duckie.app.android.data.exam.model.ExamBlockDetailResponse
+import team.duckie.app.android.data.exam.model.ExamBlockResponse
import team.duckie.app.android.data.exam.model.ExamBodyData
import team.duckie.app.android.data.exam.model.ExamData
import team.duckie.app.android.data.exam.model.ExamInfoEntity
import team.duckie.app.android.data.exam.model.ExamInstanceBodyData
import team.duckie.app.android.data.exam.model.ExamInstanceSubmitBodyData
import team.duckie.app.android.data.exam.model.ExamInstanceSubmitData
+import team.duckie.app.android.data.exam.model.ExamMeBlocksResponse
import team.duckie.app.android.data.exam.model.ExamMeFollowingResponseData
import team.duckie.app.android.data.exam.model.ExamThumbnailBodyData
import team.duckie.app.android.data.exam.model.ExamsData
@@ -37,6 +41,8 @@ import team.duckie.app.android.data.user.mapper.toDomain
import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.domain.exam.model.ChoiceModel
import team.duckie.app.android.domain.exam.model.Exam
+import team.duckie.app.android.domain.exam.model.ExamBlock
+import team.duckie.app.android.domain.exam.model.IgnoreExam
import team.duckie.app.android.domain.exam.model.ExamBody
import team.duckie.app.android.domain.exam.model.ExamInfo
import team.duckie.app.android.domain.exam.model.ExamInstanceBody
@@ -282,3 +288,20 @@ internal fun SolutionData.toDomain() = Solution(
title = title,
description = description,
)
+
+internal fun ExamMeBlocksResponse.toDomain(): List =
+ exams?.fastMap { it.toDomain() } ?: duckieSimpleResponseFieldNpe("exams")
+
+internal fun ExamBlockDetailResponse.toDomain() = IgnoreExam(
+ id = id ?: duckieSimpleResponseFieldNpe("exams"),
+ title = title ?: duckieSimpleResponseFieldNpe("title"),
+ thumbnailUrl = thumbnailUrl
+ ?: duckieSimpleResponseFieldNpe("thumbnailUrl"),
+ user = user?.toDomain() ?: duckieSimpleResponseFieldNpe("user"),
+ examBlock = examBlock?.toDomain()
+ ?: duckieSimpleResponseFieldNpe("examBlock"),
+)
+
+internal fun ExamBlockResponse.toDomain() = ExamBlock(
+ id = id ?: duckieSimpleResponseFieldNpe("id"),
+)
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamMeBlocksResponse.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamMeBlocksResponse.kt
new file mode 100644
index 000000000..4d7d9eb3e
--- /dev/null
+++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/model/ExamMeBlocksResponse.kt
@@ -0,0 +1,27 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.data.exam.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import team.duckie.app.android.data.user.model.UserResponse
+
+data class ExamMeBlocksResponse(
+ @field:JsonProperty("exams") val exams: List?,
+)
+
+data class ExamBlockDetailResponse(
+ @field:JsonProperty("id") val id: Int?,
+ @field:JsonProperty("title") val title: String?,
+ @field:JsonProperty("thumbnailUrl") val thumbnailUrl: String?,
+ @field:JsonProperty("user") val user: UserResponse?,
+ @field:JsonProperty("examBlock") val examBlock: ExamBlockResponse?,
+)
+
+data class ExamBlockResponse(
+ @field:JsonProperty("id") val id: Int?,
+)
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/paging/ProfileExamPagingSource.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/paging/ProfileExamPagingSource.kt
index cfee2e23f..9f8c95098 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/exam/paging/ProfileExamPagingSource.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/paging/ProfileExamPagingSource.kt
@@ -35,7 +35,7 @@ class ProfileExamPagingSource(
}
override fun getRefreshKey(state: PagingState): Int? {
- return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2)
- .coerceAtLeast(0)
+ return ((state.anchorPosition ?: STARTING_KEY) - state.config.initialLoadSize / 2)
+ .coerceAtLeast(STARTING_KEY)
}
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/exam/repository/ExamRepositoryImpl.kt b/data/src/main/kotlin/team/duckie/app/android/data/exam/repository/ExamRepositoryImpl.kt
index f834dd1f4..92bc3f173 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/exam/repository/ExamRepositoryImpl.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/exam/repository/ExamRepositoryImpl.kt
@@ -11,6 +11,7 @@ import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import com.github.kittinunf.fuel.Fuel
+import io.ktor.client.request.delete
import io.ktor.client.request.get
import io.ktor.client.request.parameter
import io.ktor.client.request.post
@@ -25,17 +26,20 @@ import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
import team.duckie.app.android.data._datasource.client
import team.duckie.app.android.data._exception.util.responseCatching
import team.duckie.app.android.data._exception.util.responseCatchingFuel
+import team.duckie.app.android.data._util.jsonBody
import team.duckie.app.android.data._util.toStringJsonMap
import team.duckie.app.android.data.exam.datasource.ExamInfoDataSource
import team.duckie.app.android.data.exam.mapper.toData
import team.duckie.app.android.data.exam.mapper.toDomain
import team.duckie.app.android.data.exam.model.ExamData
import team.duckie.app.android.data.exam.model.ExamInfoEntity
+import team.duckie.app.android.data.exam.model.ExamMeBlocksResponse
import team.duckie.app.android.data.exam.model.ExamMeFollowingResponseData
import team.duckie.app.android.data.exam.model.ProfileExamDatas
import team.duckie.app.android.data.exam.paging.ExamMeFollowingPagingSource
import team.duckie.app.android.data.exam.paging.ProfileExamPagingSource
import team.duckie.app.android.domain.exam.model.Exam
+import team.duckie.app.android.domain.exam.model.IgnoreExam
import team.duckie.app.android.domain.exam.model.ExamBody
import team.duckie.app.android.domain.exam.model.ExamInfo
import team.duckie.app.android.domain.exam.model.ExamThumbnailBody
@@ -176,6 +180,27 @@ class ExamRepositoryImpl @Inject constructor(
).flow
}
+ override suspend fun getIgnoreExams(): List {
+ val response = client.get("exams/me/blocks")
+ return responseCatching(
+ response = response,
+ parse = ExamMeBlocksResponse::toDomain,
+ )
+ }
+
+ override suspend fun cancelExamIgnore(examId: Int): Boolean {
+ val response = client.delete("exam-block") {
+ jsonBody {
+ "targetId" withInt examId
+ }
+ }
+
+ return responseCatching(response.status.value, response.bodyAsText()) { body ->
+ val json = body.toStringJsonMap()
+ json["success"]?.toBoolean() ?: duckieResponseFieldNpe("success")
+ }
+ }
+
override suspend fun getMadeExams(): List {
return examInfoDataSource.getMadeExams().map(ExamInfoEntity::toDomain)
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/examInstance/paging/ProfileExamInstancePagingSource.kt b/data/src/main/kotlin/team/duckie/app/android/data/examInstance/paging/ProfileExamInstancePagingSource.kt
index 2e4c57b8e..abb35d36b 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/examInstance/paging/ProfileExamInstancePagingSource.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/examInstance/paging/ProfileExamInstancePagingSource.kt
@@ -36,7 +36,7 @@ class ProfileExamInstancePagingSource(
}
override fun getRefreshKey(state: PagingState): Int? {
- return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2)
- .coerceAtLeast(0)
+ return ((state.anchorPosition ?: STARTING_KEY) - state.config.initialLoadSize / 2)
+ .coerceAtLeast(STARTING_KEY)
}
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/kakao/repository/KakaoRepositoryImpl.kt b/data/src/main/kotlin/team/duckie/app/android/data/kakao/repository/KakaoRepositoryImpl.kt
index 672a26f0e..d061962d3 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/kakao/repository/KakaoRepositoryImpl.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/kakao/repository/KakaoRepositoryImpl.kt
@@ -9,13 +9,14 @@ package team.duckie.app.android.data.kakao.repository
import android.content.Context
import com.kakao.sdk.common.model.AuthError
+import com.kakao.sdk.common.model.ClientError
+import com.kakao.sdk.common.model.ClientErrorCause
import com.kakao.sdk.user.UserApiClient
import dagger.hilt.android.qualifiers.ActivityContext
import kotlinx.coroutines.suspendCancellableCoroutine
import team.duckie.app.android.common.kotlin.exception.DuckieThirdPartyException
import team.duckie.app.android.common.kotlin.exception.ExceptionCode
import team.duckie.app.android.domain.kakao.repository.KakaoRepository
-import java.lang.ref.WeakReference
import javax.inject.Inject
import kotlin.Result.Companion.failure
import kotlin.Result.Companion.success
@@ -23,6 +24,10 @@ import kotlin.coroutines.resume
private val KakaoLoginException = IllegalStateException("Kakao API response is nothing.")
+private val KakaoCancelledException = DuckieThirdPartyException(
+ code = ExceptionCode.KAKAO_CANCELLED,
+)
+
private val KakaoTalkNotSupportException = DuckieThirdPartyException(
code = ExceptionCode.KAKAOTALK_NOT_SUPPORT_EXCEPTION,
)
@@ -34,11 +39,9 @@ private const val KakaoNotSupportStatusCode: Int = 302
class KakaoRepositoryImpl @Inject constructor(
@ActivityContext private val activityContext: Context,
) : KakaoRepository {
- private val _activity = WeakReference(activityContext)
- private val activity get() = _activity.get()!!
override suspend fun getAccessToken(): String {
- return if (UserApiClient.instance.isKakaoTalkLoginAvailable(activity)) {
+ return if (UserApiClient.instance.isKakaoTalkLoginAvailable(activityContext)) {
loginWithKakaoTalk()
} else {
loginWithWebView()
@@ -47,7 +50,7 @@ class KakaoRepositoryImpl @Inject constructor(
private suspend fun loginWithKakaoTalk(): String {
return suspendCancellableCoroutine { continuation ->
- UserApiClient.instance.loginWithKakaoTalk(activity) { token, error ->
+ UserApiClient.instance.loginWithKakaoTalk(activityContext) { token, error ->
continuation.resume(
when {
error != null -> {
@@ -60,6 +63,10 @@ class KakaoRepositoryImpl @Inject constructor(
}
}
+ is ClientError -> {
+ failure(filterKakaoClientError(error))
+ }
+
else -> failure(error)
}
}
@@ -74,10 +81,13 @@ class KakaoRepositoryImpl @Inject constructor(
override suspend fun loginWithWebView(): String {
return suspendCancellableCoroutine { continuation ->
- UserApiClient.instance.loginWithKakaoAccount(activity) { token, error ->
+ UserApiClient.instance.loginWithKakaoAccount(activityContext) { token, error ->
continuation.resume(
when {
- error != null -> failure(error)
+ error != null -> when (error) {
+ is ClientError -> failure(filterKakaoClientError(error))
+ else -> failure(error)
+ }
token != null -> success(token.accessToken)
else -> failure(KakaoLoginException)
},
@@ -85,4 +95,11 @@ class KakaoRepositoryImpl @Inject constructor(
}
}.getOrThrow()
}
+
+ private fun filterKakaoClientError(clientError: ClientError): RuntimeException {
+ return when (clientError.reason) {
+ ClientErrorCause.Cancelled -> KakaoCancelledException
+ else -> clientError
+ }
+ }
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/me/repository/MeRepositoryImpl.kt b/data/src/main/kotlin/team/duckie/app/android/data/me/repository/MeRepositoryImpl.kt
index e2c89183c..765369ab7 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/me/repository/MeRepositoryImpl.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/me/repository/MeRepositoryImpl.kt
@@ -31,6 +31,7 @@ class MeRepositoryImpl @Inject constructor(
private val dataStore: DataStore,
) : MeRepository {
private var isStageChecked: Boolean = false
+ private var isProceedEnabled: Boolean? = null
private var me: User? = null
override suspend fun getMe(): User {
// 0. DevMode 에서 API
@@ -39,6 +40,9 @@ class MeRepositoryImpl @Inject constructor(
devModeDataSource.setApiEnvironment(getIsStage())
}
+ // 1. 피처 플래그 갱신
+ featureFlagCheck()
+
// 1. DataStore 에 토큰 값이 있는지 체크
val meToken = getMeToken() ?: duckieClientLogicProblemException(code = ClientMeTokenNull)
@@ -67,6 +71,15 @@ class MeRepositoryImpl @Inject constructor(
}
}
+ /** featureFlag 값을 체크하여, 각 플래그 항목들을 갱신한다. */
+ private suspend fun featureFlagCheck() {
+ isProceedEnabled = if (isProceedEnabled == null) {
+ dataStore.data.first()[PreferenceKey.FeatureFlag.IsProceedEnable] ?: false
+ } else {
+ false
+ }
+ }
+
override suspend fun setMe(newMe: User) {
me = newMe
}
@@ -94,4 +107,10 @@ class MeRepositoryImpl @Inject constructor(
// ref: https://medium.com/androiddevelopers/datastore-and-synchronous-work-576f3869ec4c
return dataStore.data.first()[PreferenceKey.DevMode.IsStage] ?: false
}
+
+ override suspend fun getIsProceedEnable(): Boolean {
+ // TODO(riflockle7): 더 좋은 구현 방법이 있을까?
+ // ref: https://medium.com/androiddevelopers/datastore-and-synchronous-work-576f3869ec4c
+ return dataStore.data.first()[PreferenceKey.FeatureFlag.IsProceedEnable] ?: false
+ }
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/notification/mapper/mapper.kt b/data/src/main/kotlin/team/duckie/app/android/data/notification/mapper/mapper.kt
index f0d0a6f32..309490182 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/notification/mapper/mapper.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/notification/mapper/mapper.kt
@@ -7,7 +7,6 @@
package team.duckie.app.android.data.notification.mapper
-import team.duckie.app.android.data._util.toDate
import team.duckie.app.android.data.notification.model.NotificationResponse
import team.duckie.app.android.data.notification.model.NotificationsResponse
import team.duckie.app.android.domain.notification.model.Notification
@@ -20,10 +19,10 @@ internal fun NotificationsResponse.toDomain() =
internal fun NotificationResponse.toDomain() = Notification(
id = id ?: duckieResponseFieldNpe("${this::class.java.simpleName}.id"),
- title = title ?: duckieResponseFieldNpe("${this::class.java.simpleName}.title"),
+ title = title,
body = body ?: duckieResponseFieldNpe("${this::class.java.simpleName}.body"),
thumbnailUrl = thumbnailUrl
?: duckieResponseFieldNpe("${this::class.java.simpleName}.thumbnailUrl"),
- createdAt = createdAt?.toDate()
+ createdAt = createdAt
?: duckieResponseFieldNpe("${this::class.java.simpleName}.createdAt"),
)
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/search/paging/SearchExamPagingSource.kt b/data/src/main/kotlin/team/duckie/app/android/data/search/paging/SearchExamPagingSource.kt
index 596567a5e..947a7b660 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/search/paging/SearchExamPagingSource.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/search/paging/SearchExamPagingSource.kt
@@ -36,7 +36,7 @@ internal class SearchExamPagingSource(
}
override fun getRefreshKey(state: PagingState): Int {
- return ((state.anchorPosition ?: 0) - state.config.initialLoadSize / 2)
- .coerceAtLeast(0)
+ return ((state.anchorPosition ?: SearchExamStartingKey) - state.config.initialLoadSize / 2)
+ .coerceAtLeast(SearchExamStartingKey)
}
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/terms/mapper/data2domain.kt b/data/src/main/kotlin/team/duckie/app/android/data/terms/mapper/data2domain.kt
index 0314bb13c..b30994c0c 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/terms/mapper/data2domain.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/terms/mapper/data2domain.kt
@@ -7,10 +7,10 @@
package team.duckie.app.android.data.terms.mapper
+import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
import team.duckie.app.android.data._util.toDate
import team.duckie.app.android.data.terms.model.TermsResponseData
import team.duckie.app.android.domain.terms.model.Terms
-import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
internal fun TermsResponseData.toDomain() = Terms(
id = id ?: duckieResponseFieldNpe("id"),
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserDataSource.kt b/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserDataSource.kt
index b88682ee5..1bc3fc96b 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserDataSource.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserDataSource.kt
@@ -9,6 +9,7 @@ package team.duckie.app.android.data.user.datasource
import team.duckie.app.android.domain.category.model.Category
import team.duckie.app.android.domain.tag.model.Tag
+import team.duckie.app.android.domain.user.model.IgnoreUser
import team.duckie.app.android.domain.user.model.User
import team.duckie.app.android.domain.user.model.UserFollowings
import team.duckie.app.android.domain.user.model.UserProfile
@@ -39,4 +40,6 @@ interface UserDataSource {
suspend fun fetchUserFollowings(userId: Int): List
suspend fun fetchUserFollowers(userId: Int): List
+
+ suspend fun fetchIgnoreUsers(): List
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserRemoteDataSourceImpl.kt b/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserRemoteDataSourceImpl.kt
index 183e3c200..82cc18c45 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserRemoteDataSourceImpl.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/user/datasource/UserRemoteDataSourceImpl.kt
@@ -35,6 +35,8 @@ import team.duckie.app.android.common.kotlin.ExperimentalApi
import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
import team.duckie.app.android.common.kotlin.fastMap
import team.duckie.app.android.common.kotlin.runtimeCheck
+import team.duckie.app.android.data.user.model.UserMeIgnoreResponse
+import team.duckie.app.android.domain.user.model.IgnoreUser
import javax.inject.Inject
class UserRemoteDataSourceImpl @Inject constructor(
@@ -155,4 +157,12 @@ class UserRemoteDataSourceImpl @Inject constructor(
parse = UsersResponse::toDomain,
)
}
+
+ override suspend fun fetchIgnoreUsers(): List {
+ val response = client.get("users/me/blocks")
+ return responseCatching(
+ response = response.body(),
+ parse = UserMeIgnoreResponse::toDomain,
+ )
+ }
}
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/user/mapper/data2domain.kt b/data/src/main/kotlin/team/duckie/app/android/data/user/mapper/data2domain.kt
index 8875a4899..8b173841a 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/user/mapper/data2domain.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/user/mapper/data2domain.kt
@@ -32,7 +32,14 @@ import team.duckie.app.android.domain.user.model.UserFollowings
import team.duckie.app.android.domain.user.model.UserProfile
import team.duckie.app.android.domain.user.model.toUserAuthStatus
import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
+import team.duckie.app.android.common.kotlin.exception.duckieSimpleResponseFieldNpe
+import team.duckie.app.android.common.kotlin.exception.getFieldName
import team.duckie.app.android.common.kotlin.fastMap
+import team.duckie.app.android.data.user.model.IgnoreUserResponse
+import team.duckie.app.android.data.user.model.UserBlockResponse
+import team.duckie.app.android.data.user.model.UserMeIgnoreResponse
+import team.duckie.app.android.domain.user.model.IgnoreUser
+import team.duckie.app.android.domain.user.model.UserBlock
import kotlin.random.Random
private const val NicknameSuffixMaxLength = 10_000
@@ -96,3 +103,18 @@ internal fun UserProfileData.toDomain() = UserProfile(
heartExams = heartExams?.map(ProfileExamData::toDomain),
user = user?.toDomain(),
)
+
+internal fun UserBlockResponse.toDomain() = UserBlock(
+ id = id ?: duckieResponseFieldNpe(getFieldName("id")),
+)
+
+internal fun IgnoreUserResponse.toDomain() = IgnoreUser(
+ id = id ?: duckieSimpleResponseFieldNpe("id"),
+ nickName = nickName ?: duckieSimpleResponseFieldNpe("nickName"),
+ profileImageUrl = profileImageUrl ?: duckieSimpleResponseFieldNpe("profileImageUrl"),
+ duckPower = duckPower?.toDomain(),
+ userBlock = userBlock?.toDomain() ?: duckieSimpleResponseFieldNpe("userBlock"),
+)
+
+internal fun UserMeIgnoreResponse.toDomain(): List =
+ users?.fastMap { it.toDomain() } ?: duckieSimpleResponseFieldNpe("users")
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/user/model/UserBlockResponse.kt b/data/src/main/kotlin/team/duckie/app/android/data/user/model/UserBlockResponse.kt
new file mode 100644
index 000000000..b763282c9
--- /dev/null
+++ b/data/src/main/kotlin/team/duckie/app/android/data/user/model/UserBlockResponse.kt
@@ -0,0 +1,15 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.data.user.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class UserBlockResponse(
+ @field:JsonProperty("id")
+ val id: Int? = null,
+)
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/user/model/UserMeIgnoreResponse.kt b/data/src/main/kotlin/team/duckie/app/android/data/user/model/UserMeIgnoreResponse.kt
new file mode 100644
index 000000000..4047c7c42
--- /dev/null
+++ b/data/src/main/kotlin/team/duckie/app/android/data/user/model/UserMeIgnoreResponse.kt
@@ -0,0 +1,24 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.data.user.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class UserMeIgnoreResponse(
+ @field:JsonProperty("users")
+ val users: List? = null,
+)
+
+data class IgnoreUserResponse(
+ @field:JsonProperty("id") val id: Int? = null,
+ @field:JsonProperty("nickName") val nickName: String? = null,
+ @field:JsonProperty("profileImageUrl") val profileImageUrl: String? = null,
+ @field:JsonProperty("duckPower") val duckPower: DuckPowerResponse? = null,
+ @field:JsonProperty("userBlock") val userBlock: UserBlockResponse? = null,
+ @field:JsonProperty("permissions") val permissions: List? = null,
+)
diff --git a/data/src/main/kotlin/team/duckie/app/android/data/user/repository/UserRepositoryImpl.kt b/data/src/main/kotlin/team/duckie/app/android/data/user/repository/UserRepositoryImpl.kt
index 3c11d1ee2..b506b0e66 100644
--- a/data/src/main/kotlin/team/duckie/app/android/data/user/repository/UserRepositoryImpl.kt
+++ b/data/src/main/kotlin/team/duckie/app/android/data/user/repository/UserRepositoryImpl.kt
@@ -12,6 +12,7 @@ import team.duckie.app.android.common.kotlin.ExperimentalApi
import team.duckie.app.android.data.user.datasource.UserDataSource
import team.duckie.app.android.domain.category.model.Category
import team.duckie.app.android.domain.tag.model.Tag
+import team.duckie.app.android.domain.user.model.IgnoreUser
import team.duckie.app.android.domain.user.model.User
import team.duckie.app.android.domain.user.model.UserFollowings
import team.duckie.app.android.domain.user.model.UserProfile
@@ -66,4 +67,8 @@ class UserRepositoryImpl @Inject constructor(
override suspend fun fetchUserFollowers(userId: Int): List {
return userDataSource.fetchUserFollowers(userId)
}
+
+ override suspend fun fetchIgnoreUsers(): List {
+ return userDataSource.fetchIgnoreUsers()
+ }
}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/DeleteExamBlockResponse.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/DeleteExamBlockResponse.kt
new file mode 100644
index 000000000..88613b4cb
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/DeleteExamBlockResponse.kt
@@ -0,0 +1,15 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.exam.model
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class DeleteExamBlockResponse(
+ @field:JsonProperty("success")
+ val success: Boolean,
+)
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/ExamBlock.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/ExamBlock.kt
new file mode 100644
index 000000000..099bd7529
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/ExamBlock.kt
@@ -0,0 +1,12 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.exam.model
+
+data class ExamBlock(
+ val id: Int,
+)
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/IgnoreExam.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/IgnoreExam.kt
new file mode 100644
index 000000000..cf6f385db
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/model/IgnoreExam.kt
@@ -0,0 +1,18 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.exam.model
+
+import team.duckie.app.android.domain.user.model.User
+
+data class IgnoreExam(
+ val id: Int,
+ val title: String,
+ val thumbnailUrl: String,
+ val user: User,
+ val examBlock: ExamBlock,
+)
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/repository/ExamRepository.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/repository/ExamRepository.kt
index 098b50f9e..5d9733d6b 100644
--- a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/repository/ExamRepository.kt
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/repository/ExamRepository.kt
@@ -11,6 +11,7 @@ import androidx.compose.runtime.Immutable
import androidx.paging.PagingData
import kotlinx.coroutines.flow.Flow
import team.duckie.app.android.domain.exam.model.Exam
+import team.duckie.app.android.domain.exam.model.IgnoreExam
import team.duckie.app.android.domain.exam.model.ExamBody
import team.duckie.app.android.domain.exam.model.ExamInfo
import team.duckie.app.android.domain.exam.model.ExamThumbnailBody
@@ -37,4 +38,8 @@ interface ExamRepository {
suspend fun getHeartExam(userId: Int): Flow>
suspend fun getSubmittedExam(userId: Int): Flow>
+
+ suspend fun getIgnoreExams(): List
+
+ suspend fun cancelExamIgnore(examId: Int): Boolean
}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/usecase/CancelExamIgnoreUseCase.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/usecase/CancelExamIgnoreUseCase.kt
new file mode 100644
index 000000000..be25c1cd7
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/usecase/CancelExamIgnoreUseCase.kt
@@ -0,0 +1,22 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.exam.usecase
+
+import androidx.compose.runtime.Immutable
+import team.duckie.app.android.domain.exam.repository.ExamRepository
+import javax.inject.Inject
+
+@Immutable
+class CancelExamIgnoreUseCase @Inject constructor(
+ private val examRepository: ExamRepository,
+) {
+
+ suspend operator fun invoke(examId: Int) = runCatching {
+ examRepository.cancelExamIgnore(examId = examId)
+ }
+}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/exam/usecase/GetExamIgnoresUseCase.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/usecase/GetExamIgnoresUseCase.kt
new file mode 100644
index 000000000..9a5a247cc
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/exam/usecase/GetExamIgnoresUseCase.kt
@@ -0,0 +1,22 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.exam.usecase
+
+import androidx.compose.runtime.Immutable
+import team.duckie.app.android.domain.exam.repository.ExamRepository
+import javax.inject.Inject
+
+@Immutable
+class GetExamIgnoresUseCase @Inject constructor(
+ private val examRepository: ExamRepository,
+) {
+
+ suspend operator fun invoke() = runCatching {
+ examRepository.getIgnoreExams()
+ }
+}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/me/MeRepository.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/me/MeRepository.kt
index af0e45c5f..1c2853996 100644
--- a/domain/src/main/kotlin/team/duckie/app/android/domain/me/MeRepository.kt
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/me/MeRepository.kt
@@ -18,4 +18,6 @@ interface MeRepository {
suspend fun clearMeToken()
suspend fun getIsStage(): Boolean
+
+ suspend fun getIsProceedEnable(): Boolean
}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/me/usecase/GetAllFeatureFlagsUseCase.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/me/usecase/GetAllFeatureFlagsUseCase.kt
new file mode 100644
index 000000000..805c8d312
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/me/usecase/GetAllFeatureFlagsUseCase.kt
@@ -0,0 +1,22 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.me.usecase
+
+import androidx.compose.runtime.Immutable
+import team.duckie.app.android.domain.me.MeRepository
+import javax.inject.Inject
+
+// TODO(riflockle7): 그냥 모든 피처 플래그를 Map 형태로 받아내는 건 어떨까?
+@Immutable
+class GetIsProceedEnableUseCase @Inject constructor(
+ private val repository: MeRepository,
+) {
+ suspend operator fun invoke(): Result {
+ return runCatching { repository.getIsProceedEnable() }
+ }
+}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/notification/model/Notification.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/notification/model/Notification.kt
index 989aae21f..143321a7c 100644
--- a/domain/src/main/kotlin/team/duckie/app/android/domain/notification/model/Notification.kt
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/notification/model/Notification.kt
@@ -8,15 +8,14 @@
package team.duckie.app.android.domain.notification.model
import androidx.compose.runtime.Immutable
-import java.util.Date
@Immutable
data class Notification(
val id: Int,
- val title: String,
+ val title: String?,
val body: String,
val thumbnailUrl: String,
- val createdAt: Date,
+ val createdAt: String,
) {
companion object {
fun empty(id: Int = 0) = Notification(
@@ -24,7 +23,7 @@ data class Notification(
title = "",
body = "",
thumbnailUrl = "",
- createdAt = Date(0),
+ createdAt = "",
)
}
}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/user/model/IgnoreUser.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/user/model/IgnoreUser.kt
new file mode 100644
index 000000000..c283a9096
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/user/model/IgnoreUser.kt
@@ -0,0 +1,16 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.user.model
+
+data class IgnoreUser(
+ val id: Int,
+ val nickName: String,
+ val profileImageUrl: String,
+ val duckPower: DuckPower?,
+ val userBlock: UserBlock,
+)
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/user/model/UserBlock.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/user/model/UserBlock.kt
new file mode 100644
index 000000000..6fe083bfd
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/user/model/UserBlock.kt
@@ -0,0 +1,12 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.user.model
+
+data class UserBlock(
+ val id: Int,
+)
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/user/repository/UserRepository.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/user/repository/UserRepository.kt
index 0e504533b..26c187584 100644
--- a/domain/src/main/kotlin/team/duckie/app/android/domain/user/repository/UserRepository.kt
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/user/repository/UserRepository.kt
@@ -10,6 +10,7 @@ package team.duckie.app.android.domain.user.repository
import androidx.compose.runtime.Immutable
import team.duckie.app.android.domain.category.model.Category
import team.duckie.app.android.domain.tag.model.Tag
+import team.duckie.app.android.domain.user.model.IgnoreUser
import team.duckie.app.android.domain.user.model.User
import team.duckie.app.android.domain.user.model.UserFollowings
import team.duckie.app.android.domain.user.model.UserProfile
@@ -41,4 +42,6 @@ interface UserRepository {
suspend fun fetchUserFollowings(userId: Int): List
suspend fun fetchUserFollowers(userId: Int): List
+
+ suspend fun fetchIgnoreUsers(): List
}
diff --git a/domain/src/main/kotlin/team/duckie/app/android/domain/user/usecase/FetchIgnoreUsersUseCase.kt b/domain/src/main/kotlin/team/duckie/app/android/domain/user/usecase/FetchIgnoreUsersUseCase.kt
new file mode 100644
index 000000000..17dd0891d
--- /dev/null
+++ b/domain/src/main/kotlin/team/duckie/app/android/domain/user/usecase/FetchIgnoreUsersUseCase.kt
@@ -0,0 +1,23 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.domain.user.usecase
+
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.toImmutableList
+import team.duckie.app.android.domain.user.repository.UserRepository
+import javax.inject.Inject
+
+@Immutable
+class FetchIgnoreUsersUseCase @Inject constructor(
+ private val userRepository: UserRepository,
+) {
+
+ suspend operator fun invoke() = runCatching {
+ userRepository.fetchIgnoreUsers().toImmutableList()
+ }
+}
diff --git a/feature/create-problem/build.gradle.kts b/feature/create-exam/build.gradle.kts
similarity index 87%
rename from feature/create-problem/build.gradle.kts
rename to feature/create-exam/build.gradle.kts
index 7b5a69b6c..3af91becb 100644
--- a/feature/create-problem/build.gradle.kts
+++ b/feature/create-exam/build.gradle.kts
@@ -14,7 +14,7 @@ plugins {
}
android {
- namespace = "team.duckie.app.android.feature.create.problem"
+ namespace = "team.duckie.app.android.feature.create.exam"
}
dependencies {
@@ -32,7 +32,8 @@ dependencies {
libs.ktx.lifecycle.runtime,
libs.compose.lifecycle.runtime,
libs.compose.ui.material, // needs for Scaffold
- libs.quack.ui.components,
+ libs.quack.v2.ui,
+ libs.kotlin.collections.immutable,
libs.firebase.crashlytics,
)
}
diff --git a/feature/create-problem/src/main/AndroidManifest.xml b/feature/create-exam/src/main/AndroidManifest.xml
similarity index 89%
rename from feature/create-problem/src/main/AndroidManifest.xml
rename to feature/create-exam/src/main/AndroidManifest.xml
index d606d1dd0..632826cd7 100644
--- a/feature/create-problem/src/main/AndroidManifest.xml
+++ b/feature/create-exam/src/main/AndroidManifest.xml
@@ -10,7 +10,7 @@
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/CreateProblemActivity.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/CreateExamActivity.kt
similarity index 77%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/CreateProblemActivity.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/CreateExamActivity.kt
index ed9e78e1a..822ada064 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/CreateProblemActivity.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/CreateExamActivity.kt
@@ -5,17 +5,19 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem
+package team.duckie.app.android.feature.create.exam
import android.os.Bundle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.activity.viewModels
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.LaunchedEffect
@@ -30,27 +32,27 @@ import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import org.orbitmvi.orbit.compose.collectAsState
-import team.duckie.app.android.feature.create.problem.screen.AdditionalInformationScreen
-import team.duckie.app.android.feature.create.problem.screen.CreateProblemScreen
-import team.duckie.app.android.feature.create.problem.screen.ExamInformationScreen
-import team.duckie.app.android.feature.create.problem.screen.SearchTagScreen
-import team.duckie.app.android.feature.create.problem.viewmodel.CreateProblemViewModel
-import team.duckie.app.android.feature.create.problem.viewmodel.sideeffect.CreateProblemSideEffect
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemStep
-import team.duckie.app.android.common.compose.ui.ErrorScreen
-import team.duckie.app.android.common.compose.ui.LoadingScreen
import team.duckie.app.android.common.android.exception.handling.reporter.reportToToast
-import team.duckie.app.android.common.kotlin.exception.DuckieResponseException
import team.duckie.app.android.common.android.ui.BaseActivity
import team.duckie.app.android.common.android.ui.finishWithAnimation
-import team.duckie.quackquack.ui.animation.QuackAnimatedContent
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackTitle1
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.app.android.common.compose.ui.ErrorScreen
+import team.duckie.app.android.common.compose.ui.LoadingScreen
+import team.duckie.app.android.common.kotlin.exception.DuckieResponseException
+import team.duckie.app.android.feature.create.exam.screen.AdditionalInformationScreen
+import team.duckie.app.android.feature.create.exam.screen.CreateExamScreen
+import team.duckie.app.android.feature.create.exam.screen.ExamInformationScreen
+import team.duckie.app.android.feature.create.exam.screen.SearchTagScreen
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.app.android.feature.create.exam.viewmodel.sideeffect.CreateProblemSideEffect
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemStep
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.material.theme.QuackTheme
+import team.duckie.quackquack.ui.QuackText
@AndroidEntryPoint
-class CreateProblemActivity : BaseActivity() {
+class CreateExamActivity : BaseActivity() {
private val viewModel: CreateProblemViewModel by viewModels()
@@ -58,13 +60,13 @@ class CreateProblemActivity : BaseActivity() {
super.onCreate(savedInstanceState)
setContent {
val rootState = viewModel.collectAsState().value
- val createProblemStep = rootState.createProblemStep
+ val createExamStep = rootState.createExamStep
val isMakeExamUploading = remember(rootState.isMakeExamUploading) {
rootState.isMakeExamUploading
}
BackHandler {
- when (createProblemStep) {
+ when (createExamStep) {
CreateProblemStep.Loading, CreateProblemStep.Error -> finishWithAnimation()
else -> {}
}
@@ -77,11 +79,12 @@ class CreateProblemActivity : BaseActivity() {
}
QuackTheme {
- QuackAnimatedContent(
+ AnimatedContent(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor),
- targetState = createProblemStep,
+ .background(color = QuackColor.White.value),
+ targetState = createExamStep,
+ label = "AnimatedContent",
) { step: CreateProblemStep ->
when (step) {
CreateProblemStep.Loading -> LoadingScreen(
@@ -103,7 +106,7 @@ class CreateProblemActivity : BaseActivity() {
.statusBarsPadding(),
)
- CreateProblemStep.CreateProblem -> CreateProblemScreen(
+ CreateProblemStep.CreateExam -> CreateExamScreen(
modifier = Modifier
.fillMaxSize()
.statusBarsPadding(),
@@ -131,20 +134,20 @@ class CreateProblemActivity : BaseActivity() {
modifier = Modifier
.quackClickable(rippleEnabled = false) {}
.fillMaxSize()
- .background(color = QuackColor.Dimmed.composeColor),
+ .background(color = QuackColor.Dimmed.value),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
// 로딩 바
CircularProgressIndicator(
- color = QuackColor.DuckieOrange.composeColor,
+ color = QuackColor.DuckieOrange.value,
)
// 제목
- QuackTitle1(
+ QuackText(
+ modifier = Modifier.padding(PaddingValues(top = 8.dp)),
text = stringResource(id = R.string.make_exam_loading),
- color = QuackColor.White,
- padding = PaddingValues(top = 8.dp),
+ typography = QuackTypography.Title1.change(QuackColor.White),
)
}
}
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/CreateProblemBottomLayout.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/CreateExamBottomLayout.kt
similarity index 76%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/CreateProblemBottomLayout.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/CreateExamBottomLayout.kt
index 5e7ecabd4..00b751ce0 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/CreateProblemBottomLayout.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/CreateExamBottomLayout.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
@@ -13,27 +13,28 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
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.vector.ImageVector
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import team.duckie.app.android.common.compose.asLoose
+import team.duckie.app.android.common.compose.ui.QuackDivider
import team.duckie.app.android.common.kotlin.fastFirstOrNull
import team.duckie.app.android.common.kotlin.npe
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.border.applyAnimatedQuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackDivider
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackSubtitle2
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackText
internal fun getCreateProblemMeasurePolicy(
topLayoutId: String,
@@ -55,7 +56,7 @@ internal fun getCreateProblemMeasurePolicy(
// TODO(riflockle7): 왜 이걸 더해야하는지 모르겠음.. padding 을 더하면 이렇게 됨...
val bottomLayoutHeight = topAppBarMeasurable.height + 72.toDp().toPx().toInt()
- // 3. createProblemButton 높이값 측정
+ // 3. createExamButton 높이값 측정
val contentThresholdHeight = constraints.maxHeight - topAppBarHeight - bottomLayoutHeight
val contentConstraints = constraints.copy(
minHeight = contentThresholdHeight,
@@ -88,7 +89,7 @@ internal fun getCreateProblemMeasurePolicy(
@Composable
internal fun CreateProblemBottomLayout(
modifier: Modifier,
- leftButtonLeadingIcon: QuackIcon? = null,
+ leftButtonLeadingIcon: ImageVector? = null,
leftButtonText: String? = null,
leftButtonClick: (() -> Unit)? = null,
tempSaveButtonText: String? = null,
@@ -99,7 +100,7 @@ internal fun CreateProblemBottomLayout(
isValidateCheck: () -> Boolean,
) {
val isValidate = isValidateCheck()
- Column(modifier = modifier.background(QuackColor.White.composeColor)) {
+ Column(modifier = modifier.background(QuackColor.White.value)) {
QuackDivider()
Row(
modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp),
@@ -119,10 +120,21 @@ internal fun CreateProblemBottomLayout(
.padding(4.dp),
) {
// TODO(riflockle7): 추후 비활성화 될 때의 resouce 이미지 필요
- leftButtonLeadingIcon?.let { QuackImage(src = it, size = DpSize(16.dp, 16.dp)) }
- QuackSubtitle2(
+ leftButtonLeadingIcon?.let {
+ QuackIcon(
+ modifier = modifier.size(DpSize(16.dp, 16.dp)),
+ icon = it,
+ )
+ }
+ QuackText(
text = leftButtonText,
- color = if (isCreateProblemValidate) QuackColor.Black else QuackColor.Gray2,
+ typography = QuackTypography.Subtitle2.change(
+ if (isCreateProblemValidate) {
+ QuackColor.Black
+ } else {
+ QuackColor.Gray2
+ },
+ ),
)
}
}
@@ -132,30 +144,30 @@ internal fun CreateProblemBottomLayout(
// 임시저장 버튼
tempSaveButtonClick?.let {
requireNotNull(tempSaveButtonText)
- QuackSubtitle(
+ QuackText(
modifier = Modifier
.clip(RoundedCornerShape(size = 8.dp))
- .background(QuackColor.White.composeColor)
+ .background(QuackColor.White.value)
.quackClickable(onClick = tempSaveButtonClick)
- .applyAnimatedQuackBorder(
+ .quackBorder(
QuackBorder(1.dp, QuackColor.Gray3),
shape = RoundedCornerShape(size = 8.dp),
)
.padding(vertical = 12.dp, horizontal = 19.dp),
- color = QuackColor.Black,
+ typography = QuackTypography.Subtitle.change(QuackColor.Black),
text = tempSaveButtonText,
)
}
// 다음 버튼
- QuackSubtitle(
+ QuackText(
modifier = Modifier
.clip(RoundedCornerShape(size = 8.dp))
.background(
if (isValidate) {
- QuackColor.DuckieOrange.composeColor
+ QuackColor.DuckieOrange.value
} else {
- QuackColor.Gray2.composeColor
+ QuackColor.Gray2.value
},
)
.quackClickable {
@@ -163,7 +175,7 @@ internal fun CreateProblemBottomLayout(
nextButtonClick()
}
}
- .applyAnimatedQuackBorder(
+ .quackBorder(
QuackBorder(
1.dp,
if (isValidate) {
@@ -178,7 +190,7 @@ internal fun CreateProblemBottomLayout(
vertical = 12.dp,
horizontal = 19.dp,
),
- color = QuackColor.White,
+ typography = QuackTypography.Subtitle.change(QuackColor.White),
text = nextButtonText,
)
}
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/FadeAnimatedVisibility.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/FadeAnimatedVisibility.kt
similarity index 91%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/FadeAnimatedVisibility.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/FadeAnimatedVisibility.kt
index 1c0cf2ddd..7bf284fec 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/FadeAnimatedVisibility.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/FadeAnimatedVisibility.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/FindTagItem.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/FindTagItem.kt
similarity index 59%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/FindTagItem.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/FindTagItem.kt
index 0dd633f7e..0edf05886 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/FindTagItem.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/FindTagItem.kt
@@ -5,22 +5,25 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import team.duckie.quackquack.ui.component.QuackBody1
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.sugar.QuackBody1
@Composable
internal fun SearchResultText(
text: String,
onClick: () -> Unit,
) = QuackBody1(
- modifier = Modifier.fillMaxWidth(),
- padding = PaddingValues(vertical = 12.dp),
+ modifier = Modifier
+ .quackClickable(onClick = onClick)
+ .fillMaxWidth()
+ .padding(PaddingValues(vertical = 12.dp)),
text = text,
- onClick = onClick,
)
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/NoLazyGridItems.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/NoLazyGridItems.kt
similarity index 98%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/NoLazyGridItems.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/NoLazyGridItems.kt
index 21ba7a498..e3108b3eb 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/NoLazyGridItems.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/NoLazyGridItems.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TextfieldOptions.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TextfieldOptions.kt
similarity index 92%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TextfieldOptions.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TextfieldOptions.kt
index 4197ca464..e51ec7afa 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TextfieldOptions.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TextfieldOptions.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TitleAndComponent.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TitleAndComponent.kt
similarity index 92%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TitleAndComponent.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TitleAndComponent.kt
index ceb6798f2..ed552774f 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TitleAndComponent.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TitleAndComponent.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
@@ -15,7 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import team.duckie.quackquack.ui.component.QuackHeadLine2
+import team.duckie.quackquack.ui.sugar.QuackHeadLine2
@Suppress("FunctionName")
internal fun LazyListScope.TitleAndComponent(
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TopAppBar.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TopAppBar.kt
similarity index 71%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TopAppBar.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TopAppBar.kt
index 8f293b57b..14423fede 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/common/TopAppBar.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/TopAppBar.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.common
+package team.duckie.app.android.feature.create.exam.common
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
@@ -16,11 +16,14 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import team.duckie.app.android.feature.create.problem.R
-import team.duckie.quackquack.ui.component.QuackHeadLine2
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackTopAppBar
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowBack
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.sugar.QuackHeadLine2
@Composable
internal fun PrevAndNextTopAppBar(
@@ -31,8 +34,8 @@ internal fun PrevAndNextTopAppBar(
trailingTextEnabled: Boolean = false,
) { // TODO(EvergreenTree97): enabled 속성 필요
QuackTopAppBar(
- modifier = modifier,
- leadingIcon = QuackIcon.ArrowBack,
+ modifier = modifier.padding(12.dp),
+ leadingIcon = OutlinedGroup.ArrowBack,
leadingText = stringResource(id = R.string.create_problem),
onLeadingIconClick = onLeadingIconClick,
trailingText = trailingText,
@@ -64,9 +67,9 @@ internal fun ExitAppBar(
horizontalArrangement = Arrangement.SpaceBetween,
) {
QuackHeadLine2(text = leadingText)
- QuackImage(
- src = QuackIcon.Close,
- onClick = onTrailingIconClick,
+ QuackIcon(
+ modifier = Modifier.quackClickable(onClick = onTrailingIconClick),
+ icon = OutlinedGroup.Close,
)
}
}
diff --git a/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/Constant.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/Constant.kt
new file mode 100644
index 000000000..ca0e2fa36
--- /dev/null
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/Constant.kt
@@ -0,0 +1,10 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.feature.create.exam.common.type
+
+internal const val MaximumChoice = 5
diff --git a/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/ImageChoiceLayout.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/ImageChoiceLayout.kt
new file mode 100644
index 000000000..7305dc4df
--- /dev/null
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/ImageChoiceLayout.kt
@@ -0,0 +1,196 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+@file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class)
+
+package team.duckie.app.android.feature.create.exam.common.type
+
+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.PaddingValues
+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.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackRoundCheckBox
+import team.duckie.app.android.domain.exam.model.Answer
+import team.duckie.app.android.domain.exam.model.Question
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.app.android.feature.create.exam.common.NoLazyGridItems
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.icon.quackicon.outlined.Image
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.sugar.QuackSubtitle
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+
+/**
+ * 객관식/사진 Layout
+ * // TODO(riflockle7): 정답 체크 연동 필요
+ */
+@Composable
+internal fun ImageChoiceLayout(
+ questionIndex: Int,
+ question: Question?,
+ titleChanged: (String) -> Unit,
+ imageClick: () -> Unit,
+ onDropdownItemClick: (Int) -> Unit,
+ answers: Answer.ImageChoice,
+ answerTextChanged: (String, Int) -> Unit,
+ answerImageClick: (Int) -> Unit,
+ addAnswerClick: () -> Unit,
+ correctAnswers: String?,
+ setCorrectAnswerClick: (String) -> Unit,
+ deleteLongClick: (Int?) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .quackClickable(
+ onLongClick = { deleteLongClick(null) },
+ ) {},
+ ) {
+ TitleView(
+ questionIndex,
+ question,
+ titleChanged,
+ imageClick,
+ answers.type.title,
+ onDropdownItemClick,
+ )
+
+ NoLazyGridItems(
+ count = answers.imageChoice.size,
+ nColumns = 2,
+ paddingValues = PaddingValues(top = 12.dp),
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ itemContent = { answerIndex ->
+ val answerNo = answerIndex + 1
+ val answerItem = answers.imageChoice[answerIndex]
+ val isChecked = correctAnswers == "$answerIndex"
+
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .quackBorder(border = QuackBorder(color = QuackColor.Gray4))
+ .quackBorder(
+ border = QuackBorder(
+ color = if (isChecked) {
+ QuackColor.DuckieOrange
+ } else {
+ QuackColor.Gray4
+ },
+ ),
+ )
+ .padding(12.dp),
+ ) {
+ Row(
+ modifier = Modifier.padding(vertical = 12.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ QuackRoundCheckBox(
+ modifier = Modifier.quackClickable(
+ onClick = {
+ setCorrectAnswerClick(if (isChecked) "" else "$answerIndex")
+ },
+ ),
+ checked = isChecked,
+ )
+
+ if (isChecked) {
+ QuackText(
+ modifier = Modifier.padding(start = 2.dp),
+ typography = QuackTypography.Body3.change(QuackColor.DuckieOrange),
+ text = stringResource(id = R.string.answer),
+ )
+ }
+
+ Spacer(Modifier.weight(1f))
+
+ QuackIcon(
+ icon = OutlinedGroup.Close,
+ modifier = Modifier
+ .quackClickable(
+ onClick = { deleteLongClick(answerIndex) },
+ )
+ .size(DpSize(20.dp, 20.dp)),
+ )
+ }
+
+ if (answerItem.imageUrl.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .quackClickable { answerImageClick(answerIndex) }
+ .background(color = QuackColor.Gray4.value)
+ .padding(52.dp),
+ ) {
+ QuackIcon(
+ modifier = Modifier.size(DpSize(32.dp, 32.dp)),
+ icon = OutlinedGroup.Image,
+ )
+ }
+ } else {
+ QuackImage(
+ modifier = Modifier
+ .quackClickable(
+ onClick = { answerImageClick(answerIndex) },
+ onLongClick = { deleteLongClick(answerIndex) },
+ )
+ .size(DpSize(136.dp, 136.dp)),
+ src = answerItem.imageUrl,
+ )
+ }
+
+ // TODO(riflockle7): 동작 확인 필요
+ QuackDefaultTextField(
+ value = answers.imageChoice[answerIndex].text,
+ onValueChange = { newAnswer ->
+ answerTextChanged(newAnswer, answerIndex)
+ },
+ placeholderText = stringResource(
+ id = R.string.create_problem_answer_placeholder,
+ "$answerNo",
+ ),
+ style = QuackTextFieldStyle.Default,
+ )
+ }
+ },
+ )
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ if (answers.imageChoice.size < MaximumChoice) {
+ QuackSubtitle(
+ modifier = Modifier
+ .quackClickable(onClick = addAnswerClick)
+ .padding(vertical = 2.dp, horizontal = 4.dp),
+ text = stringResource(id = R.string.create_problem_add_button),
+ )
+ }
+ }
+}
diff --git a/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/ShortAnswerLayout.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/ShortAnswerLayout.kt
new file mode 100644
index 000000000..6cbd28087
--- /dev/null
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/ShortAnswerLayout.kt
@@ -0,0 +1,65 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+@file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class)
+
+package team.duckie.app.android.feature.create.exam.common.type
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.domain.exam.model.Answer
+import team.duckie.app.android.domain.exam.model.Question
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+
+/** 주관식 Layout */
+@Composable
+internal fun ShortAnswerLayout(
+ questionIndex: Int,
+ question: Question?,
+ titleChanged: (String) -> Unit,
+ imageClick: () -> Unit,
+ onDropdownItemClick: (Int) -> Unit,
+ answer: String,
+ answerTextChanged: (String, Int) -> Unit,
+ deleteLongClick: () -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .quackClickable(
+ onLongClick = { deleteLongClick() },
+ ) {},
+ ) {
+ TitleView(
+ questionIndex,
+ question,
+ titleChanged,
+ imageClick,
+ Answer.Type.ShortAnswer.title,
+ onDropdownItemClick,
+ )
+
+ // TODO(riflockle7): 동작 확인 필요
+ QuackDefaultTextField(
+ value = answer,
+ onValueChange = { newAnswer -> answerTextChanged(newAnswer, 0) },
+ placeholderText = stringResource(id = R.string.create_problem_short_answer_placeholder),
+ style = QuackTextFieldStyle.Default,
+ )
+ }
+}
diff --git a/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/TextChoiceLayout.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/TextChoiceLayout.kt
new file mode 100644
index 000000000..348b80da0
--- /dev/null
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/TextChoiceLayout.kt
@@ -0,0 +1,129 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+@file:OptIn(
+ ExperimentalDesignToken::class,
+ ExperimentalDesignToken::class,
+ ExperimentalQuackQuackApi::class,
+)
+
+package team.duckie.app.android.feature.create.exam.common.type
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.kotlin.fastForEachIndexed
+import team.duckie.app.android.domain.exam.model.Answer
+import team.duckie.app.android.domain.exam.model.Question
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.sugar.QuackSubtitle
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+
+/** 객관식/글 Layout */
+@Composable
+@Suppress("unused")
+internal fun TextChoiceLayout(
+ questionIndex: Int,
+ question: Question?,
+ titleChanged: (String) -> Unit,
+ imageClick: () -> Unit,
+ onDropdownItemClick: (Int) -> Unit,
+ answers: Answer.Choice,
+ answerTextChanged: (String, Int) -> Unit,
+ addAnswerClick: () -> Unit,
+ correctAnswers: String?,
+ setCorrectAnswerClick: (String) -> Unit,
+ deleteLongClick: (Int?) -> Unit,
+) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(16.dp)
+ .quackClickable(
+ onLongClick = { deleteLongClick(null) },
+ ) {},
+ ) {
+ TitleView(
+ questionIndex,
+ question,
+ titleChanged,
+ imageClick,
+ answers.type.title,
+ onDropdownItemClick,
+ )
+
+ answers.choices.fastForEachIndexed { answerIndex, choiceModel ->
+ val answerNo = answerIndex + 1
+ val isChecked = correctAnswers == "$answerIndex"
+ QuackDefaultTextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 12.dp)
+ .quackBorder(
+ border = QuackBorder(
+ color = if (isChecked) QuackColor.DuckieOrange else QuackColor.Gray4,
+ ),
+ )
+ .quackClickable(
+ onLongClick = { deleteLongClick(answerIndex) },
+ ) {},
+ value = choiceModel.text,
+ onValueChange = { newAnswer -> answerTextChanged(newAnswer, answerIndex) },
+ placeholderText = stringResource(
+ id = R.string.create_problem_answer_placeholder,
+ "$answerNo",
+ ),
+ style = QuackTextFieldStyle.Default,
+ // TODO(riflockle7): 꽥꽥 기능 제공 안함
+ // trailingContent = {
+ // Column(
+ // modifier = Modifier.quackClickable(
+ // onClick = {
+ // setCorrectAnswerClick(if (isChecked) "" else "$answerIndex")
+ // },
+ // ),
+ // horizontalAlignment = Alignment.CenterHorizontally,
+ // ) {
+ // QuackRoundCheckBox(checked = isChecked)
+ //
+ // if (isChecked) {
+ // QuackText(
+ // modifier = Modifier.padding(top = 2.dp),
+ // typography = QuackTypography.Body3.change(QuackColor.DuckieOrange),
+ // text = stringResource(id = R.string.answer),
+ // )
+ // }
+ // }
+ // },
+ )
+ }
+
+ Spacer(modifier = Modifier.height(12.dp))
+
+ if (answers.choices.size < MaximumChoice) {
+ QuackSubtitle(
+ modifier = Modifier
+ .quackClickable(onClick = addAnswerClick)
+ .padding(vertical = 2.dp, horizontal = 4.dp),
+ text = stringResource(id = R.string.create_problem_add_button),
+ )
+ }
+ }
+}
diff --git a/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/TitleView.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/TitleView.kt
new file mode 100644
index 000000000..49c2761e1
--- /dev/null
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/common/type/TitleView.kt
@@ -0,0 +1,74 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+@file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class)
+
+package team.duckie.app.android.feature.create.exam.common.type
+
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackDropDownCard
+import team.duckie.app.android.domain.exam.model.Question
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Image
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.trailingIcon
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+
+/** 문제 항목 Layout 내 공통 제목 Layout */
+@Composable
+internal fun TitleView(
+ questionIndex: Int,
+ question: Question?,
+ titleChanged: (String) -> Unit,
+ imageClick: () -> Unit,
+ dropDownTitle: String,
+ onDropdownItemClick: (Int) -> Unit,
+) {
+ // TODO(riflockle7): 동작 확인 필요
+ // TODO(riflockle7): 최상단 Line 없는 TextField 필요
+ QuackDefaultTextField(
+ modifier = Modifier.trailingIcon(
+ icon = OutlinedGroup.Image,
+ onClick = imageClick,
+ ),
+ value = question?.text ?: "",
+ onValueChange = titleChanged,
+ style = QuackTextFieldStyle.Default,
+ placeholderText = stringResource(
+ id = R.string.create_problem_question_placeholder,
+ "${questionIndex + 1}",
+ ),
+ )
+
+ (question as? Question.Image)?.imageUrl?.let {
+ QuackImage(
+ modifier = Modifier
+ .padding(top = 24.dp)
+ .size(DpSize(200.dp, 200.dp)),
+ src = it,
+ )
+ }
+
+ // TODO(riflockle7): border 없는 DropDownCard 필요
+ QuackDropDownCard(
+ modifier = Modifier.padding(top = 24.dp),
+ text = dropDownTitle,
+ onClick = {
+ onDropdownItemClick(questionIndex)
+ },
+ )
+}
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/navigator/impl/CreateProblemNavigatorImpl.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/navigator/impl/CreateProblemNavigatorImpl.kt
similarity index 77%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/navigator/impl/CreateProblemNavigatorImpl.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/navigator/impl/CreateProblemNavigatorImpl.kt
index 88ba06a47..ee75b324b 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/navigator/impl/CreateProblemNavigatorImpl.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/navigator/impl/CreateProblemNavigatorImpl.kt
@@ -5,11 +5,11 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.navigator.impl
+package team.duckie.app.android.feature.create.exam.navigator.impl
import android.app.Activity
import android.content.Intent
-import team.duckie.app.android.feature.create.problem.CreateProblemActivity
+import team.duckie.app.android.feature.create.exam.CreateExamActivity
import team.duckie.app.android.navigator.feature.createproblem.CreateProblemNavigator
import team.duckie.app.android.common.android.ui.startActivityWithAnimation
import javax.inject.Inject
@@ -20,7 +20,7 @@ internal class CreateProblemNavigatorImpl @Inject constructor() : CreateProblemN
intentBuilder: Intent.() -> Intent,
withFinish: Boolean,
) {
- activity.startActivityWithAnimation(
+ activity.startActivityWithAnimation(
intentBuilder = intentBuilder,
withFinish = withFinish,
)
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/navigator/module/CreateProblemNavigatorModule.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/navigator/module/CreateProblemNavigatorModule.kt
similarity index 79%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/navigator/module/CreateProblemNavigatorModule.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/navigator/module/CreateProblemNavigatorModule.kt
index 474e2bb9d..a2d57397d 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/navigator/module/CreateProblemNavigatorModule.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/navigator/module/CreateProblemNavigatorModule.kt
@@ -5,13 +5,13 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.navigator.module
+package team.duckie.app.android.feature.create.exam.navigator.module
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
-import team.duckie.app.android.feature.create.problem.navigator.impl.CreateProblemNavigatorImpl
+import team.duckie.app.android.feature.create.exam.navigator.impl.CreateProblemNavigatorImpl
import team.duckie.app.android.navigator.feature.createproblem.CreateProblemNavigator
import javax.inject.Singleton
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/AdditionalInfoScreen.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/AdditionalInfoScreen.kt
similarity index 82%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/AdditionalInfoScreen.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/AdditionalInfoScreen.kt
index 2f8baf7bc..9ccd46b1c 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/AdditionalInfoScreen.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/AdditionalInfoScreen.kt
@@ -6,10 +6,11 @@
*/
@file:OptIn(
ExperimentalMaterialApi::class,
- ExperimentalComposeUiApi::class,
+ ExperimentalQuackQuackApi::class,
+ ExperimentalDesignToken::class,
)
-package team.duckie.app.android.feature.create.problem.screen
+package team.duckie.app.android.feature.create.exam.screen
import android.Manifest
import android.content.Context
@@ -28,6 +29,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -37,16 +39,13 @@ import androidx.compose.material.ModalBottomSheetLayout
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
@@ -62,35 +61,41 @@ import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
-import team.duckie.app.android.domain.exam.model.ThumbnailType
-import team.duckie.app.android.domain.tag.model.Tag
-import team.duckie.app.android.common.compose.ui.PhotoPicker
-import team.duckie.app.android.feature.create.problem.R
-import team.duckie.app.android.feature.create.problem.common.CreateProblemBottomLayout
-import team.duckie.app.android.feature.create.problem.common.FadeAnimatedVisibility
-import team.duckie.app.android.feature.create.problem.common.ImeActionNext
-import team.duckie.app.android.feature.create.problem.common.PrevAndNextTopAppBar
-import team.duckie.app.android.feature.create.problem.common.TitleAndComponent
-import team.duckie.app.android.feature.create.problem.common.getCreateProblemMeasurePolicy
-import team.duckie.app.android.feature.create.problem.viewmodel.CreateProblemViewModel
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemPhotoState
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemStep
import team.duckie.app.android.common.compose.GetHeightRatioW328H240
+import team.duckie.app.android.common.compose.HideKeyboardWhenBottomSheetHidden
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.rememberToast
import team.duckie.app.android.common.compose.systemBarPaddings
+import team.duckie.app.android.common.compose.ui.PhotoPicker
+import team.duckie.app.android.common.compose.ui.icon.v1.AreaId
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackLazyVerticalGridTag
import team.duckie.app.android.common.kotlin.fastMap
import team.duckie.app.android.common.kotlin.takeBy
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBasicTextField
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.component.QuackLazyVerticalGridTag
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackTagType
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.app.android.domain.exam.model.ThumbnailType
+import team.duckie.app.android.domain.tag.model.Tag
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.app.android.feature.create.exam.common.CreateProblemBottomLayout
+import team.duckie.app.android.feature.create.exam.common.FadeAnimatedVisibility
+import team.duckie.app.android.feature.create.exam.common.ImeActionNext
+import team.duckie.app.android.feature.create.exam.common.PrevAndNextTopAppBar
+import team.duckie.app.android.feature.create.exam.common.TitleAndComponent
+import team.duckie.app.android.feature.create.exam.common.getCreateProblemMeasurePolicy
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemPhotoState
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemStep
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackButton
+import team.duckie.quackquack.ui.QuackButtonStyle
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.sugar.QuackSubtitle
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private const val TopAppBarLayoutId = "AdditionalInfoScreenTopAppBarLayoutId"
private const val ContentLayoutId = "AdditionalInfoScreenContentLayoutId"
@@ -153,24 +158,17 @@ internal fun AdditionalInformationScreen(
sheetState.hide()
}
} else {
- vm.navigateStep(CreateProblemStep.CreateProblem)
+ vm.navigateStep(CreateProblemStep.CreateExam)
}
}
- LaunchedEffect(Unit) {
- val sheetStateFlow = snapshotFlow { sheetState.currentValue }
- sheetStateFlow.collect { state ->
- if (state == ModalBottomSheetValue.Hidden) {
- keyboard?.hide()
- }
- }
- }
+ HideKeyboardWhenBottomSheetHidden(sheetState)
ModalBottomSheetLayout(
modifier = modifier,
sheetState = sheetState,
- sheetBackgroundColor = QuackColor.White.composeColor,
- scrimColor = QuackColor.Dimmed.composeColor,
+ sheetBackgroundColor = QuackColor.White.value,
+ scrimColor = QuackColor.Dimmed.value,
sheetShape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
@@ -189,7 +187,7 @@ internal fun AdditionalInformationScreen(
.width(40.dp)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
- .background(QuackColor.Gray2.composeColor),
+ .background(QuackColor.Gray2.value),
)
// 선택 목록
@@ -219,7 +217,7 @@ internal fun AdditionalInformationScreen(
// 갤러리에서 선택
AdditionalBottomSheetThumbnailLayout(
title = "",
- src = team.duckie.quackquack.ui.R.drawable.quack_ic_area_24,
+ src = QuackIcon.AreaId,
onClick = {
val result = imagePermission.check(context)
if (result) {
@@ -247,7 +245,7 @@ internal fun AdditionalInformationScreen(
// 상단 탭바
PrevAndNextTopAppBar(
modifier = Modifier.layoutId(TopAppBarLayoutId),
- onLeadingIconClick = { vm.navigateStep(CreateProblemStep.CreateProblem) },
+ onLeadingIconClick = { vm.navigateStep(CreateProblemStep.CreateExam) },
)
// 컨텐츠 Layout
@@ -297,7 +295,7 @@ internal fun AdditionalInformationScreen(
modifier = Modifier
.padding(top = systemBarPaddings.calculateTopPadding())
.fillMaxSize()
- .background(color = QuackColor.White.composeColor),
+ .background(color = QuackColor.White.value),
imageUris = galleryImages,
imageSelections = galleryImagesSelections,
onCameraClick = {},
@@ -330,6 +328,7 @@ internal fun AdditionalInformationScreen(
}
/** 썸네일 선택 (어떤 카테고리를 좋아하나요?) Layout */
+@OptIn(ExperimentalQuackQuackApi::class)
@Composable
private fun AdditionalThumbnailLayout(
thumbnail: Any?,
@@ -346,27 +345,29 @@ private fun AdditionalThumbnailLayout(
stringResource = R.string.category_title,
) {
QuackImage(
- size = DpSize(
- thumbnailWidthDp,
- thumbnailWidthDp * GetHeightRatioW328H240,
+ modifier = Modifier.size(
+ DpSize(
+ thumbnailWidthDp,
+ thumbnailWidthDp * GetHeightRatioW328H240,
+ ),
),
contentScale = ContentScale.FillWidth,
src = thumbnail,
)
// 썸네일 종류 선택 버튼
- // TODO(riflockle7): trailingIcon 추가 필요
- QuackLargeButton(
- modifier = Modifier.padding(top = 4.dp),
- type = QuackLargeButtonType.Border,
+ // TODO(riflockle7): 동작 확인 필요
+ QuackButton(
text = stringResource(id = R.string.additional_information_thumbnail_select),
- leadingIcon = QuackIcon.ArrowRight,
+ style = QuackButtonStyle.PrimaryLarge,
+ modifier = Modifier.padding(top = 4.dp),
onClick = onClick,
)
}
}
/** 시험 응시 텍스트 선택 (시험 응시하기 버튼) Layout */
+@OptIn(ExperimentalDesignToken::class)
@Composable
private fun AdditionalTakeLayout(vm: CreateProblemViewModel = activityViewModel()) {
val state = vm.collectAsState().value.additionalInfo
@@ -375,9 +376,10 @@ private fun AdditionalTakeLayout(vm: CreateProblemViewModel = activityViewModel(
modifier = Modifier.padding(top = 48.dp),
stringResource = R.string.additional_information_take_title,
) {
- QuackBasicTextField(
- text = state.takeTitle,
- onTextChanged = {
+ // TODO(riflockle7): 동작 확인 필요
+ QuackDefaultTextField(
+ value = state.takeTitle,
+ onValueChange = {
vm.setButtonTitle(
it.takeBy(
TakeTitleMaxLength,
@@ -385,6 +387,7 @@ private fun AdditionalTakeLayout(vm: CreateProblemViewModel = activityViewModel(
),
)
},
+ style = QuackTextFieldStyle.Default,
placeholderText = stringResource(
id = R.string.additional_information_take_input_hint,
TakeTitleMaxLength,
@@ -403,12 +406,14 @@ private fun AdditionalSubTagsLayout(vm: CreateProblemViewModel = activityViewMod
modifier = Modifier.padding(top = 48.dp),
stringResource = R.string.additional_information_sub_tags_title,
) {
- QuackBasicTextField(
+ // TODO(riflockle7): 동작 확인 필요
+ QuackDefaultTextField(
modifier = Modifier.quackClickable {
vm.goToSearchSubTags()
},
- text = "",
- onTextChanged = {},
+ value = "",
+ onValueChange = { },
+ style = QuackTextFieldStyle.Default,
placeholderText = stringResource(id = R.string.additional_information_sub_tags_placeholder),
enabled = false,
)
@@ -419,7 +424,7 @@ private fun AdditionalSubTagsLayout(vm: CreateProblemViewModel = activityViewMod
QuackLazyVerticalGridTag(
horizontalSpace = 4.dp,
items = state.subTags.fastMap(Tag::name),
- tagType = QuackTagType.Circle(QuackIcon.Close),
+ trailingIcon = OutlinedGroup.Close,
onClick = { vm.onClickCloseTag(it) },
itemChunkedSize = 3,
)
@@ -445,9 +450,11 @@ private fun AdditionalBottomSheetThumbnailLayout(
),
) {
QuackImage(
- size = DpSize(
- thumbnailWidthDp,
- thumbnailWidthDp * GetHeightRatioW328H240,
+ modifier = Modifier.size(
+ DpSize(
+ thumbnailWidthDp,
+ thumbnailWidthDp * GetHeightRatioW328H240,
+ ),
),
contentScale = ContentScale.FillWidth,
src = src,
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/CreateProblemScreen.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/CreateExamScreen.kt
similarity index 64%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/CreateProblemScreen.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/CreateExamScreen.kt
index d2153d38b..8d3eff819 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/CreateProblemScreen.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/CreateExamScreen.kt
@@ -7,10 +7,11 @@
@file:OptIn(
ExperimentalMaterialApi::class,
- ExperimentalComposeUiApi::class,
+ ExperimentalDesignToken::class,
+ ExperimentalQuackQuackApi::class,
)
-package team.duckie.app.android.feature.create.problem.screen
+package team.duckie.app.android.feature.create.exam.screen
import android.Manifest
import android.content.Context
@@ -21,12 +22,9 @@ import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
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.PaddingValues
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -41,16 +39,13 @@ import androidx.compose.material.ModalBottomSheetState
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.Layout
@@ -59,57 +54,52 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.platform.SoftwareKeyboardController
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.compose.HideKeyboardWhenBottomSheetHidden
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.rememberToast
import team.duckie.app.android.common.compose.systemBarPaddings
import team.duckie.app.android.common.compose.ui.PhotoPicker
import team.duckie.app.android.common.compose.ui.dialog.DuckieDialog
import team.duckie.app.android.common.kotlin.fastForEach
-import team.duckie.app.android.common.kotlin.fastForEachIndexed
import team.duckie.app.android.common.kotlin.takeBy
import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.domain.exam.model.Question
-import team.duckie.app.android.feature.create.problem.R
-import team.duckie.app.android.feature.create.problem.common.CreateProblemBottomLayout
-import team.duckie.app.android.feature.create.problem.common.NoLazyGridItems
-import team.duckie.app.android.feature.create.problem.common.PrevAndNextTopAppBar
-import team.duckie.app.android.feature.create.problem.common.getCreateProblemMeasurePolicy
-import team.duckie.app.android.feature.create.problem.viewmodel.CreateProblemViewModel
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemPhotoState
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemStep
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.border.applyAnimatedQuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBasic2TextField
-import team.duckie.quackquack.ui.component.QuackBasicTextField
-import team.duckie.quackquack.ui.component.QuackBody3
-import team.duckie.quackquack.ui.component.QuackBorderTextField
-import team.duckie.quackquack.ui.component.QuackDropDownCard
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackRoundCheckBox
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.app.android.feature.create.exam.common.CreateProblemBottomLayout
+import team.duckie.app.android.feature.create.exam.common.PrevAndNextTopAppBar
+import team.duckie.app.android.feature.create.exam.common.getCreateProblemMeasurePolicy
+import team.duckie.app.android.feature.create.exam.common.type.ImageChoiceLayout
+import team.duckie.app.android.feature.create.exam.common.type.ShortAnswerLayout
+import team.duckie.app.android.feature.create.exam.common.type.TextChoiceLayout
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemPhotoState
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemStep
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Plus
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private const val TopAppBarLayoutId = "CreateProblemScreenTopAppBarLayoutId"
private const val ContentLayoutId = "CreateProblemScreenContentLayoutId"
private const val BottomLayoutId = "CreateProblemScreenBottomLayoutId"
private const val GalleryListLayoutId = "CreateProblemScreenGalleryListLayoutId"
-private const val MaximumChoice = 5
private const val MaximumProblem = 10
private const val TextFieldMaxLength = 20
/** 문제 만들기 2단계 (문제 만들기) Screen */
@Composable
-internal fun CreateProblemScreen(
+internal fun CreateExamScreen(
modifier: Modifier,
vm: CreateProblemViewModel = activityViewModel(),
) {
@@ -117,7 +107,7 @@ internal fun CreateProblemScreen(
val coroutineShape = rememberCoroutineScope()
val rootState = vm.collectAsState().value
- val state = rootState.createProblem
+ val state = rootState.createExam
val keyboard = LocalSoftwareKeyboardController.current
val sheetState = rememberModalBottomSheetState(
ModalBottomSheetValue.Hidden,
@@ -181,20 +171,13 @@ internal fun CreateProblemScreen(
}
}
- LaunchedEffect(sheetState) {
- val sheetStateFlow = snapshotFlow { sheetState.currentValue }
- sheetStateFlow.collect { state ->
- if (state == ModalBottomSheetValue.Hidden) {
- keyboard?.hide()
- }
- }
- }
+ HideKeyboardWhenBottomSheetHidden(sheetState)
ModalBottomSheetLayout(
modifier = modifier,
sheetState = sheetState,
- sheetBackgroundColor = QuackColor.White.composeColor,
- scrimColor = QuackColor.Dimmed.composeColor,
+ sheetBackgroundColor = QuackColor.White.value,
+ scrimColor = QuackColor.Dimmed.value,
sheetShape = RoundedCornerShape(
topStart = 16.dp,
topEnd = 16.dp,
@@ -213,7 +196,7 @@ internal fun CreateProblemScreen(
.width(40.dp)
.height(4.dp)
.clip(RoundedCornerShape(2.dp))
- .background(QuackColor.Gray2.composeColor),
+ .background(QuackColor.Gray2.value),
)
// 선택 목록
@@ -223,26 +206,30 @@ internal fun CreateProblemScreen(
.padding(top = 16.dp),
) {
buttonNames.fastForEach {
- QuackSubtitle(
- modifier = Modifier.fillMaxWidth(),
- padding = PaddingValues(
- vertical = 12.dp,
- horizontal = 16.dp,
- ),
- text = it.first,
- onClick = {
- coroutineShape.launch {
- selectedQuestionIndex?.let { questionIndex ->
- // 특정 문제의 답안 유형 수정
- vm.editAnswersType(questionIndex, it.second)
- selectedQuestionIndex = null
- } ?: run {
- // 문제 추가
- vm.addProblem(it.second)
+ QuackText(
+ modifier = Modifier
+ .quackClickable {
+ coroutineShape.launch {
+ selectedQuestionIndex?.let { questionIndex ->
+ // 특정 문제의 답안 유형 수정
+ vm.editAnswersType(questionIndex, it.second)
+ selectedQuestionIndex = null
+ } ?: run {
+ // 문제 추가
+ vm.addProblem(it.second)
+ }
+ hideBottomSheet(sheetState) { selectedQuestionIndex = null }
}
- hideBottomSheet(sheetState) { selectedQuestionIndex = null }
}
- },
+ .fillMaxWidth()
+ .padding(
+ PaddingValues(
+ vertical = 12.dp,
+ horizontal = 16.dp,
+ ),
+ ),
+ text = it.first,
+ typography = QuackTypography.Subtitle,
)
}
}
@@ -285,7 +272,7 @@ internal fun CreateProblemScreen(
val correctAnswer = state.correctAnswers[questionIndex]
when (answers) {
- is Answer.Short -> ShortAnswerProblemLayout(
+ is Answer.Short -> ShortAnswerLayout(
questionIndex = questionIndex,
question = question,
titleChanged = { newTitle ->
@@ -334,7 +321,7 @@ internal fun CreateProblemScreen(
},
)
- is Answer.Choice -> ChoiceProblemLayout(
+ is Answer.Choice -> TextChoiceLayout(
questionIndex = questionIndex,
question = question,
titleChanged = { newTitle ->
@@ -396,7 +383,7 @@ internal fun CreateProblemScreen(
},
)
- is Answer.ImageChoice -> ImageChoiceProblemLayout(
+ is Answer.ImageChoice -> ImageChoiceLayout(
questionIndex = questionIndex,
question = question,
titleChanged = { newTitle ->
@@ -487,7 +474,7 @@ internal fun CreateProblemScreen(
modifier = Modifier
.fillMaxWidth()
.layoutId(BottomLayoutId),
- leftButtonLeadingIcon = QuackIcon.Plus,
+ leftButtonLeadingIcon = OutlinedGroup.Plus,
leftButtonText = stringResource(id = R.string.create_problem_add_problem_button),
leftButtonClick = {
coroutineShape.launch {
@@ -504,7 +491,7 @@ internal fun CreateProblemScreen(
}
},
isCreateProblemValidate = problemCount < MaximumProblem,
- isValidateCheck = vm::createProblemIsValidate,
+ isValidateCheck = vm::createExamIsValidate,
)
},
)
@@ -516,7 +503,7 @@ internal fun CreateProblemScreen(
modifier = Modifier
.padding(top = systemBarPaddings.calculateTopPadding())
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
.layoutId(GalleryListLayoutId),
imageUris = galleryImages,
imageSelections = galleryImagesSelections,
@@ -605,327 +592,20 @@ private fun CoroutineScope.hideBottomSheet(
private fun CoroutineScope.openPhotoPicker(
context: Context,
vm: CreateProblemViewModel,
- createProblemPhotoState: CreateProblemPhotoState,
+ createExamPhotoState: CreateProblemPhotoState,
keyboard: SoftwareKeyboardController?,
launcher: ManagedActivityResultLauncher,
) = launch {
val result = imagePermission.check(context)
if (result) {
vm.loadGalleryImages()
- vm.updatePhotoState(createProblemPhotoState)
+ vm.updatePhotoState(createExamPhotoState)
keyboard?.hide()
} else {
launcher.launch(imagePermission)
}
}
-/** 문제 항목 Layout 내 공통 제목 Layout */
-@Composable
-private fun CreateProblemTitleLayout(
- questionIndex: Int,
- question: Question?,
- titleChanged: (String) -> Unit,
- imageClick: () -> Unit,
- dropDownTitle: String,
- onDropdownItemClick: (Int) -> Unit,
-) {
- // TODO(riflockle7): 최상단 Line 없는 TextField 필요
- QuackBasic2TextField(
- text = question?.text ?: "",
- onTextChanged = titleChanged,
- placeholderText = stringResource(
- id = R.string.create_problem_question_placeholder,
- "${questionIndex + 1}",
- ),
- trailingIcon = QuackIcon.Image,
- trailingIconOnClick = imageClick,
- )
-
- (question as? Question.Image)?.imageUrl?.let {
- QuackImage(
- modifier = Modifier.padding(top = 24.dp),
- src = it,
- size = DpSize(200.dp, 200.dp),
- )
- }
-
- // TODO(riflockle7): border 없는 DropDownCard 필요
- QuackDropDownCard(
- modifier = Modifier.padding(top = 24.dp),
- text = dropDownTitle,
- onClick = {
- onDropdownItemClick(questionIndex)
- },
- )
-}
-
-/** 객관식/글 Layout */
-@Composable
-private fun ChoiceProblemLayout(
- questionIndex: Int,
- question: Question?,
- titleChanged: (String) -> Unit,
- imageClick: () -> Unit,
- onDropdownItemClick: (Int) -> Unit,
- answers: Answer.Choice,
- answerTextChanged: (String, Int) -> Unit,
- addAnswerClick: () -> Unit,
- correctAnswers: String?,
- setCorrectAnswerClick: (String) -> Unit,
- deleteLongClick: (Int?) -> Unit,
-) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- .quackClickable(
- onLongClick = { deleteLongClick(null) },
- ) {},
- ) {
- CreateProblemTitleLayout(
- questionIndex,
- question,
- titleChanged,
- imageClick,
- answers.type.title,
- onDropdownItemClick,
- )
-
- answers.choices.fastForEachIndexed { answerIndex, choiceModel ->
- val answerNo = answerIndex + 1
- val isChecked = correctAnswers == "$answerIndex"
- QuackBorderTextField(
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 12.dp)
- .applyAnimatedQuackBorder(
- border = QuackBorder(
- width = 1.dp,
- color = if (isChecked) QuackColor.DuckieOrange else QuackColor.Gray4,
- ),
- )
- .quackClickable(
- onLongClick = { deleteLongClick(answerIndex) },
- ) {},
- text = choiceModel.text,
- onTextChanged = { newAnswer -> answerTextChanged(newAnswer, answerIndex) },
- placeholderText = stringResource(
- id = R.string.create_problem_answer_placeholder,
- "$answerNo",
- ),
- trailingContent = {
- Column(
- modifier = Modifier.quackClickable(
- onClick = {
- setCorrectAnswerClick(if (isChecked) "" else "$answerIndex")
- },
- ),
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- QuackRoundCheckBox(checked = isChecked)
-
- if (isChecked) {
- QuackBody3(
- modifier = Modifier.padding(top = 2.dp),
- color = QuackColor.DuckieOrange,
- text = stringResource(id = R.string.answer),
- )
- }
- }
- },
- )
- }
-
- Spacer(modifier = Modifier.height(12.dp))
-
- if (answers.choices.size < MaximumChoice) {
- QuackSubtitle(
- modifier = Modifier.padding(vertical = 2.dp, horizontal = 4.dp),
- text = stringResource(id = R.string.create_problem_add_button),
- onClick = { addAnswerClick() },
- )
- }
- }
-}
-
-/**
- * 객관식/사진 Layout
- * // TODO(riflockle7): 정답 체크 연동 필요
- */
-@Composable
-private fun ImageChoiceProblemLayout(
- questionIndex: Int,
- question: Question?,
- titleChanged: (String) -> Unit,
- imageClick: () -> Unit,
- onDropdownItemClick: (Int) -> Unit,
- answers: Answer.ImageChoice,
- answerTextChanged: (String, Int) -> Unit,
- answerImageClick: (Int) -> Unit,
- addAnswerClick: () -> Unit,
- correctAnswers: String?,
- setCorrectAnswerClick: (String) -> Unit,
- deleteLongClick: (Int?) -> Unit,
-) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- .quackClickable(
- onLongClick = { deleteLongClick(null) },
- ) {},
- ) {
- CreateProblemTitleLayout(
- questionIndex,
- question,
- titleChanged,
- imageClick,
- answers.type.title,
- onDropdownItemClick,
- )
-
- NoLazyGridItems(
- count = answers.imageChoice.size,
- nColumns = 2,
- paddingValues = PaddingValues(top = 12.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp),
- itemContent = { answerIndex ->
- val answerNo = answerIndex + 1
- val answerItem = answers.imageChoice[answerIndex]
- val isChecked = correctAnswers == "$answerIndex"
-
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .applyAnimatedQuackBorder(border = QuackBorder(color = QuackColor.Gray4))
- .applyAnimatedQuackBorder(
- border = QuackBorder(
- width = 1.dp,
- color = if (isChecked) {
- QuackColor.DuckieOrange
- } else {
- QuackColor.Gray4
- },
- ),
- )
- .padding(12.dp),
- ) {
- Row(
- modifier = Modifier.padding(vertical = 12.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- QuackRoundCheckBox(
- modifier = Modifier.quackClickable(
- onClick = {
- setCorrectAnswerClick(if (isChecked) "" else "$answerIndex")
- },
- ),
- checked = isChecked,
- )
-
- if (isChecked) {
- QuackBody3(
- modifier = Modifier.padding(start = 2.dp),
- color = QuackColor.DuckieOrange,
- text = stringResource(id = R.string.answer),
- )
- }
-
- Spacer(Modifier.weight(1f))
-
- QuackImage(
- modifier = Modifier.quackClickable(
- onClick = { deleteLongClick(answerIndex) },
- ),
- src = QuackIcon.Close,
- size = DpSize(20.dp, 20.dp),
- )
- }
-
- if (answerItem.imageUrl.isEmpty()) {
- Box(
- modifier = Modifier
- .quackClickable { answerImageClick(answerIndex) }
- .background(color = QuackColor.Gray4.composeColor)
- .padding(52.dp),
- ) {
- QuackImage(
- src = QuackIcon.Image,
- size = DpSize(32.dp, 32.dp),
- )
- }
- } else {
- QuackImage(
- src = answerItem.imageUrl,
- size = DpSize(136.dp, 136.dp),
- onClick = { answerImageClick(answerIndex) },
- onLongClick = { deleteLongClick(answerIndex) },
- )
- }
-
- QuackBasicTextField(
- text = answers.imageChoice[answerIndex].text,
- onTextChanged = { newAnswer ->
- answerTextChanged(newAnswer, answerIndex)
- },
- placeholderText = stringResource(
- id = R.string.create_problem_answer_placeholder,
- "$answerNo",
- ),
- )
- }
- },
- )
-
- Spacer(modifier = Modifier.height(12.dp))
-
- if (answers.imageChoice.size < MaximumChoice) {
- QuackSubtitle(
- modifier = Modifier.padding(vertical = 2.dp, horizontal = 4.dp),
- text = stringResource(id = R.string.create_problem_add_button),
- onClick = { addAnswerClick() },
- )
- }
- }
-}
-
-/** 주관식 Layout */
-@Composable
-private fun ShortAnswerProblemLayout(
- questionIndex: Int,
- question: Question?,
- titleChanged: (String) -> Unit,
- imageClick: () -> Unit,
- onDropdownItemClick: (Int) -> Unit,
- answer: String,
- answerTextChanged: (String, Int) -> Unit,
- deleteLongClick: () -> Unit,
-) {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp)
- .quackClickable(
- onLongClick = { deleteLongClick() },
- ) {},
- ) {
- CreateProblemTitleLayout(
- questionIndex,
- question,
- titleChanged,
- imageClick,
- Answer.Type.ShortAnswer.title,
- onDropdownItemClick,
- )
-
- QuackBasicTextField(
- text = answer,
- onTextChanged = { newAnswer -> answerTextChanged(newAnswer, 0) },
- placeholderText = stringResource(id = R.string.create_problem_short_answer_placeholder),
- )
- }
-}
-
/**
* 이미지 권한 체크시 사용해야하는 permission
* TODO(riflockle7): 권한 로직은 추후 PermissionViewModel 과 같이 쓰면서 지워질 예정
diff --git a/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/CreateProblemScreen.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/CreateProblemScreen.kt
new file mode 100644
index 000000000..894183ecf
--- /dev/null
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/CreateProblemScreen.kt
@@ -0,0 +1,42 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.feature.create.exam.screen
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import kotlinx.coroutines.launch
+import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.app.android.feature.create.exam.common.PrevAndNextTopAppBar
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemStep
+
+/** 단일 문제 만들기 Screen */
+@Composable
+internal fun CreateProblemScreen(
+ modifier: Modifier,
+ vm: CreateProblemViewModel = activityViewModel(),
+) {
+ val coroutineScope = rememberCoroutineScope()
+
+ Column(modifier = modifier) {
+ PrevAndNextTopAppBar(
+ modifier = Modifier.fillMaxWidth(),
+ onLeadingIconClick = {
+ coroutineScope.launch { vm.navigateStep(CreateProblemStep.ExamInformation) }
+ },
+ trailingText = stringResource(id = R.string.next),
+ onTrailingTextClick = {},
+ trailingTextEnabled = true,
+ )
+ }
+}
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/ExamInformationScreen.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/ExamInformationScreen.kt
similarity index 68%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/ExamInformationScreen.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/ExamInformationScreen.kt
index 08c9cffa6..5ba6dc160 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/ExamInformationScreen.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/ExamInformationScreen.kt
@@ -5,11 +5,14 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.screen
+@file:OptIn(ExperimentalQuackQuackApi::class, ExperimentalDesignToken::class)
+
+package team.duckie.app.android.feature.create.exam.screen
import android.app.Activity
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
@@ -36,7 +39,6 @@ import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
@@ -44,27 +46,31 @@ import org.orbitmvi.orbit.compose.collectAsState
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.ui.DuckieGridLayout
import team.duckie.app.android.common.compose.ui.dialog.DuckieDialog
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSurface
import team.duckie.app.android.common.kotlin.takeBy
-import team.duckie.app.android.feature.create.problem.R
-import team.duckie.app.android.feature.create.problem.common.CreateProblemBottomLayout
-import team.duckie.app.android.feature.create.problem.common.ImeActionNext
-import team.duckie.app.android.feature.create.problem.common.PrevAndNextTopAppBar
-import team.duckie.app.android.feature.create.problem.common.TitleAndComponent
-import team.duckie.app.android.feature.create.problem.common.getCreateProblemMeasurePolicy
-import team.duckie.app.android.feature.create.problem.common.moveDownFocus
-import team.duckie.app.android.feature.create.problem.viewmodel.CreateProblemViewModel
-import team.duckie.quackquack.ui.animation.QuackAnimatedVisibility
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBasicTextField
-import team.duckie.quackquack.ui.component.QuackCircleTag
-import team.duckie.quackquack.ui.component.QuackGrayscaleTextField
-import team.duckie.quackquack.ui.component.QuackReviewTextArea
-import team.duckie.quackquack.ui.component.QuackSurface
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.app.android.feature.create.exam.common.CreateProblemBottomLayout
+import team.duckie.app.android.feature.create.exam.common.ImeActionNext
+import team.duckie.app.android.feature.create.exam.common.PrevAndNextTopAppBar
+import team.duckie.app.android.feature.create.exam.common.TitleAndComponent
+import team.duckie.app.android.feature.create.exam.common.getCreateProblemMeasurePolicy
+import team.duckie.app.android.feature.create.exam.common.moveDownFocus
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackTag
+import team.duckie.quackquack.ui.QuackTagStyle
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.QuackTextArea
+import team.duckie.quackquack.ui.QuackTextAreaStyle
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.trailingIcon
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private const val TopAppBarLayoutId = "ExamInformationScreenTopAppBarLayoutId"
private const val ContentLayoutId = "ExamInformationScreenContentLayoutId"
@@ -84,12 +90,12 @@ internal fun ExamInformationScreen(
val coroutineScope = rememberCoroutineScope()
val focusManager = LocalFocusManager.current
val lazyListState = rememberLazyListState()
- var createProblemExitDialogVisible by remember { mutableStateOf(false) }
+ var createExamExitDialogVisible by remember { mutableStateOf(false) }
val focusRequester = remember { FocusRequester() }
BackHandler {
- if (!createProblemExitDialogVisible) {
- createProblemExitDialogVisible = true
+ if (!createExamExitDialogVisible) {
+ createExamExitDialogVisible = true
}
}
@@ -115,8 +121,8 @@ internal fun ExamInformationScreen(
modifier = Modifier.layoutId(TopAppBarLayoutId),
trailingText = stringResource(id = R.string.next),
onLeadingIconClick = {
- if (!createProblemExitDialogVisible) {
- createProblemExitDialogVisible = true
+ if (!createExamExitDialogVisible) {
+ createExamExitDialogVisible = true
}
},
)
@@ -147,30 +153,34 @@ internal fun ExamInformationScreen(
}
TitleAndComponent(stringResource = R.string.main_tag) {
if (state.isMainTagSelected) {
- QuackCircleTag(
+ QuackTag(
text = state.mainTag,
- trailingIcon = QuackIcon.Close,
- isSelected = false,
- onClick = { viewModel.onClickCloseTag() },
- )
+ style = QuackTagStyle.Outlined,
+ modifier = Modifier.trailingIcon(OutlinedGroup.Close) { viewModel.onClickCloseTag() },
+ selected = false,
+ ) {}
}
- QuackAnimatedVisibility(visible = !state.isMainTagSelected) {
- QuackBasicTextField(
- modifier = Modifier.quackClickable {
- viewModel.goToSearchMainTag(lazyListState.firstVisibleItemIndex)
- },
- leadingIcon = QuackIcon.Search,
- text = state.mainTag,
- onTextChanged = {},
+ AnimatedVisibility(visible = !state.isMainTagSelected) {
+ // TODO(riflockle7): 동작 확인 필요
+ QuackDefaultTextField(
+ modifier = Modifier.quackClickable(
+ onClick = {
+ viewModel.goToSearchMainTag(lazyListState.firstVisibleItemIndex)
+ },
+ ),
+ // leadingIcon = QuackIcon.Search,
+ value = state.mainTag,
+ onValueChange = {},
placeholderText = stringResource(id = R.string.search_main_tag_placeholder),
+ style = QuackTextFieldStyle.Default,
enabled = false,
)
}
}
TitleAndComponent(stringResource = R.string.exam_title) {
- QuackBasicTextField(
- text = state.examTitle,
- onTextChanged = {
+ QuackDefaultTextField(
+ value = state.examTitle,
+ onValueChange = {
viewModel.setExamTitle(
it.takeBy(
ExamTitleMaxLength,
@@ -178,6 +188,7 @@ internal fun ExamInformationScreen(
),
)
},
+ style = QuackTextFieldStyle.Default,
placeholderText = stringResource(
id = R.string.input_exam_title,
ExamTitleMaxLength,
@@ -187,15 +198,17 @@ internal fun ExamInformationScreen(
)
}
TitleAndComponent(stringResource = R.string.exam_description) {
- QuackReviewTextArea(
+ // TODO(riflockle7): 동작 확인 필요
+ QuackTextArea(
modifier = Modifier
.heightIn(140.dp)
.focusRequester(focusRequester = focusRequester)
.onFocusChanged { state ->
viewModel.onSearchTextFocusChanged(state.isFocused)
},
- text = state.examDescription,
- onTextChanged = {
+ value = state.examDescription,
+ style = QuackTextAreaStyle.Default,
+ onValueChange = {
viewModel.setExamDescription(
it.takeBy(
ExamDescriptionMaxLength,
@@ -207,16 +220,17 @@ internal fun ExamInformationScreen(
id = R.string.input_exam_description,
ExamDescriptionMaxLength,
),
- imeAction = ImeAction.Next,
- keyboardActions = moveDownFocus(focusManager),
- focused = state.examDescriptionFocused,
+ // TODO(riflockle7): 꽥꽥에서 기능 제공 안함
+ // imeAction = ImeAction.Next,
+ // keyboardActions = moveDownFocus(focusManager),
+ // focused = state.examDescriptionFocused,
)
}
TitleAndComponent(stringResource = R.string.certifying_statement) {
- QuackGrayscaleTextField(
+ QuackDefaultTextField(
modifier = Modifier.padding(bottom = 16.dp),
- text = state.certifyingStatement,
- onTextChanged = {
+ value = state.certifyingStatement,
+ onValueChange = {
viewModel.setCertifyingStatement(
it.takeBy(
CertifyingStatementMaxLength,
@@ -224,6 +238,7 @@ internal fun ExamInformationScreen(
),
)
},
+ style = QuackTextFieldStyle.Default,
placeholderText = stringResource(
id = R.string.input_certifying_statement,
CertifyingStatementMaxLength,
@@ -236,8 +251,9 @@ internal fun ExamInformationScreen(
}
},
),
- maxLength = CertifyingStatementMaxLength,
- showCounter = true,
+ // TODO(riflockle7): 꽥꽥에서 기능 제공 안함
+ // maxLength = CertifyingStatementMaxLength,
+ // showCounter = true,
)
}
}
@@ -264,15 +280,15 @@ internal fun ExamInformationScreen(
DuckieDialog(
title = stringResource(id = R.string.create_problem_exit_dialog_title),
message = stringResource(id = R.string.create_problem_exit_dialog_message),
- visible = createProblemExitDialogVisible,
+ visible = createExamExitDialogVisible,
leftButtonText = stringResource(id = R.string.cancel),
- leftButtonOnClick = { createProblemExitDialogVisible = false },
+ leftButtonOnClick = { createExamExitDialogVisible = false },
rightButtonText = stringResource(id = R.string.ok),
rightButtonOnClick = {
- createProblemExitDialogVisible = false
+ createExamExitDialogVisible = false
activity.finish()
},
- onDismissRequest = { createProblemExitDialogVisible = false },
+ onDismissRequest = { createExamExitDialogVisible = false },
)
}
@@ -289,11 +305,12 @@ private fun MediumButton(
height = 40.dp,
),
backgroundColor = QuackColor.White,
- border = QuackBorder(
- color = when (selected) {
+ border = BorderStroke(
+ width = 1.dp,
+ brush = when (selected) {
true -> QuackColor.DuckieOrange
else -> QuackColor.Gray3
- },
+ }.toBrush(),
),
shape = RoundedCornerShape(size = 8.dp),
onClick = onClick,
@@ -301,13 +318,13 @@ private fun MediumButton(
QuackText(
modifier = Modifier.padding(all = 10.dp),
text = text,
- style = when (selected) {
- true -> QuackTextStyle.Title2.change(
+ typography = when (selected) {
+ true -> QuackTypography.Title2.change(
color = QuackColor.DuckieOrange,
textAlign = TextAlign.Center,
)
- else -> QuackTextStyle.Body1.change(
+ else -> QuackTypography.Body1.change(
color = QuackColor.Black,
textAlign = TextAlign.Center,
)
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/SearchTagScreen.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/SearchTagScreen.kt
similarity index 81%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/SearchTagScreen.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/SearchTagScreen.kt
index 4898ca53a..ea771418f 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/screen/SearchTagScreen.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/screen/SearchTagScreen.kt
@@ -5,9 +5,12 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.screen
+@file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class)
+
+package team.duckie.app.android.feature.create.exam.screen
import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
@@ -30,21 +33,23 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
-import team.duckie.app.android.feature.create.problem.R
-import team.duckie.app.android.feature.create.problem.common.CreateProblemBottomLayout
-import team.duckie.app.android.feature.create.problem.common.ExitAppBar
-import team.duckie.app.android.feature.create.problem.common.SearchResultText
-import team.duckie.app.android.feature.create.problem.viewmodel.CreateProblemViewModel
-import team.duckie.app.android.feature.create.problem.viewmodel.state.FindResultType
-import team.duckie.app.android.feature.create.problem.viewmodel.state.SearchScreenData
-import team.duckie.app.android.common.compose.ui.ImeSpacer
import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.ui.ImeSpacer
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackLazyVerticalGridTag
import team.duckie.app.android.common.kotlin.fastMap
-import team.duckie.quackquack.ui.animation.QuackAnimatedVisibility
-import team.duckie.quackquack.ui.component.QuackBasicTextField
-import team.duckie.quackquack.ui.component.QuackLazyVerticalGridTag
-import team.duckie.quackquack.ui.component.QuackTagType
-import team.duckie.quackquack.ui.icon.QuackIcon
+import team.duckie.app.android.feature.create.exam.R
+import team.duckie.app.android.feature.create.exam.common.CreateProblemBottomLayout
+import team.duckie.app.android.feature.create.exam.common.ExitAppBar
+import team.duckie.app.android.feature.create.exam.common.SearchResultText
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.app.android.feature.create.exam.viewmodel.state.FindResultType
+import team.duckie.app.android.feature.create.exam.viewmodel.state.SearchScreenData
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.ui.QuackDefaultTextField
+import team.duckie.quackquack.ui.QuackTextFieldStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private const val MaximumSubTagCount = 5
@@ -111,13 +116,16 @@ internal fun SearchTagScreen(
),
horizontalSpace = 4.dp,
items = state.results.fastMap { it.name },
- tagType = QuackTagType.Circle(QuackIcon.Close),
+ trailingIcon = OutlinedGroup.Close,
onClick = { viewModel.onClickCloseTag(it) },
itemChunkedSize = 3,
)
}
- QuackBasicTextField(
+ // TODO(riflockle7): 동작 확인 필요
+ QuackDefaultTextField(
+ // TODO(riflockle7): 꽥꽥 기능 제공 안함
+ // leadingIcon = QuackIcon.Search,
modifier = Modifier
.padding(
top = 16.dp,
@@ -125,11 +133,11 @@ internal fun SearchTagScreen(
end = 16.dp,
)
.focusRequester(focusRequester),
- leadingIcon = QuackIcon.Search,
- text = searchTextFieldValue,
- onTextChanged = { textFieldValue ->
+ value = searchTextFieldValue,
+ onValueChange = { textFieldValue ->
viewModel.setTextFieldValue(textFieldValue = textFieldValue)
},
+ style = QuackTextFieldStyle.Default,
placeholderText = placeholderText,
keyboardActions = KeyboardActions(
onDone = {
@@ -140,7 +148,7 @@ internal fun SearchTagScreen(
),
)
- QuackAnimatedVisibility(
+ AnimatedVisibility(
modifier = Modifier.padding(
top = 8.dp,
start = 16.dp,
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/CreateProblemViewModel.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/CreateProblemViewModel.kt
similarity index 92%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/CreateProblemViewModel.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/CreateProblemViewModel.kt
index 38ad139ad..b530ee80f 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/CreateProblemViewModel.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/CreateProblemViewModel.kt
@@ -7,7 +7,7 @@
@file:Suppress("ConstPropertyName", "PrivatePropertyName")
-package team.duckie.app.android.feature.create.problem.viewmodel
+package team.duckie.app.android.feature.create.exam.viewmodel
import android.app.Application
import android.content.Context
@@ -31,6 +31,16 @@ import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
+import team.duckie.app.android.common.android.image.MediaUtil
+import team.duckie.app.android.common.android.network.NetworkUtil
+import team.duckie.app.android.common.android.ui.const.Debounce
+import team.duckie.app.android.common.android.ui.const.Extras
+import team.duckie.app.android.common.android.viewmodel.context
+import team.duckie.app.android.common.kotlin.copy
+import team.duckie.app.android.common.kotlin.exception.duckieClientLogicProblemException
+import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
+import team.duckie.app.android.common.kotlin.exception.isTagAlreadyExist
+import team.duckie.app.android.common.kotlin.fastMapIndexed
import team.duckie.app.android.domain.category.usecase.GetCategoriesUseCase
import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.domain.exam.model.ChoiceModel
@@ -55,21 +65,11 @@ import team.duckie.app.android.domain.search.usecase.GetSearchUseCase
import team.duckie.app.android.domain.tag.model.Tag
import team.duckie.app.android.domain.tag.repository.TagRepository
import team.duckie.app.android.domain.user.usecase.GetMeUseCase
-import team.duckie.app.android.feature.create.problem.viewmodel.sideeffect.CreateProblemSideEffect
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemPhotoState
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemState
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemStep
-import team.duckie.app.android.feature.create.problem.viewmodel.state.FindResultType
-import team.duckie.app.android.common.android.image.MediaUtil
-import team.duckie.app.android.common.android.network.NetworkUtil
-import team.duckie.app.android.common.android.viewmodel.context
-import team.duckie.app.android.common.kotlin.copy
-import team.duckie.app.android.common.kotlin.exception.duckieClientLogicProblemException
-import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
-import team.duckie.app.android.common.kotlin.exception.isTagAlreadyExist
-import team.duckie.app.android.common.kotlin.fastMapIndexed
-import team.duckie.app.android.common.android.ui.const.Debounce
-import team.duckie.app.android.common.android.ui.const.Extras
+import team.duckie.app.android.feature.create.exam.viewmodel.sideeffect.CreateProblemSideEffect
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemPhotoState
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemState
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemStep
+import team.duckie.app.android.feature.create.exam.viewmodel.state.FindResultType
import javax.inject.Inject
private const val TagsMaximumCount = 10
@@ -111,7 +111,7 @@ internal class CreateProblemViewModel @Inject constructor(
state.copy(
me = me,
isEditMode = false,
- createProblemStep = CreateProblemStep.ExamInformation,
+ createExamStep = CreateProblemStep.ExamInformation,
)
}
}.onFailure {
@@ -130,7 +130,7 @@ internal class CreateProblemViewModel @Inject constructor(
private suspend fun loadErrorPage(isNetworkError: Boolean = false) = intent {
reduce {
state.copy(
- createProblemStep = CreateProblemStep.Error,
+ createExamStep = CreateProblemStep.Error,
isNetworkError = isNetworkError,
)
}
@@ -141,7 +141,7 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
state.copy(
isNetworkError = false,
- createProblemStep = CreateProblemStep.Loading,
+ createExamStep = CreateProblemStep.Loading,
)
}
}
@@ -236,21 +236,21 @@ internal class CreateProblemViewModel @Inject constructor(
/** request 를 위해 필요한 [ExamBody] 를 생성한다. */
private fun generateExamBody(): ExamBody {
val examInformationState = container.stateFlow.value.examInformation
- val createProblemState = container.stateFlow.value.createProblem
+ val createExamState = container.stateFlow.value.createExam
val additionalInfoState = container.stateFlow.value.additionalInfo
val serverCorrectAnswers =
- createProblemState.correctAnswers.fastMapIndexed { index, correctAnswer ->
- correctAnswer.toCorrectAnswerData(createProblemState.answers[index])
+ createExamState.correctAnswers.fastMapIndexed { index, correctAnswer ->
+ correctAnswer.toCorrectAnswerData(createExamState.answers[index])
}
- val problems = createProblemState.questions.fastMapIndexed { index, question ->
+ val problems = createExamState.questions.fastMapIndexed { index, question ->
Problem(
index,
question,
- createProblemState.answers[index],
+ createExamState.answers[index],
serverCorrectAnswers[index],
- createProblemState.hints[index],
- createProblemState.memos[index],
+ createExamState.hints[index],
+ createExamState.memos[index],
null,
)
}.toPersistentList()
@@ -284,7 +284,7 @@ internal class CreateProblemViewModel @Inject constructor(
/** 특정 태그의 닫기 버튼을 클릭한다. 대체로 삭제 로직이 실행된다. */
internal fun onClickCloseTag(index: Int = 0) = intent {
reduce {
- when (state.createProblemStep) {
+ when (state.createExamStep) {
CreateProblemStep.ExamInformation -> {
state.copy(
examInformation = state.examInformation.run {
@@ -358,7 +358,7 @@ internal class CreateProblemViewModel @Inject constructor(
/** 특정 화면으로 이동한다. */
internal fun navigateStep(step: CreateProblemStep) = intent {
reduce {
- state.copy(createProblemStep = step)
+ state.copy(createExamStep = step)
}
}
@@ -394,7 +394,7 @@ internal class CreateProblemViewModel @Inject constructor(
internal fun goToSearchMainTag(scrollPosition: Int) = intent {
reduce {
state.copy(
- createProblemStep = CreateProblemStep.Search,
+ createExamStep = CreateProblemStep.Search,
findResultType = FindResultType.MainTag,
examInformation = state.examInformation.copy(
scrollPosition = scrollPosition,
@@ -453,7 +453,7 @@ internal class CreateProblemViewModel @Inject constructor(
// 이전에 등록된 제목과 동일한 경우 별다른 처리를 하지 않는다.
if (state.examInformation.prevExamTitle == state.examInformation.examTitle) {
reduce {
- state.copy(createProblemStep = CreateProblemStep.CreateProblem)
+ state.copy(createExamStep = CreateProblemStep.CreateExam)
}
return@intent
}
@@ -463,7 +463,7 @@ internal class CreateProblemViewModel @Inject constructor(
).onSuccess { thumbnail ->
reduce {
state.copy(
- createProblemStep = CreateProblemStep.CreateProblem,
+ createExamStep = CreateProblemStep.CreateExam,
examInformation = state.examInformation.copy(
prevExamTitle = state.examInformation.examTitle,
),
@@ -490,7 +490,7 @@ internal class CreateProblemViewModel @Inject constructor(
val newQuestion = Question.Text(text = "")
val newAnswer = answerType.getDefaultAnswer()
- with(state.createProblem) {
+ with(state.createExam) {
val newQuestions = questions.copy { add(newQuestion) }
val newAnswers = answers.copy { add(newAnswer) }
val newCorrectAnswers = correctAnswers.copy { add("") }
@@ -499,7 +499,7 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
questions = newQuestions.toPersistentList(),
answers = newAnswers.toPersistentList(),
correctAnswers = newCorrectAnswers.toPersistentList(),
@@ -513,7 +513,7 @@ internal class CreateProblemViewModel @Inject constructor(
/** [questionIndex + 1] 번 문제를 삭제한다. */
internal fun removeProblem(questionIndex: Int) = intent {
- with(state.createProblem) {
+ with(state.createExam) {
val newQuestions = questions.copy { removeAt(questionIndex) }
val newAnswers = answers.copy { removeAt(questionIndex) }
val newCorrectAnswers = correctAnswers.copy { removeAt(questionIndex) }
@@ -522,7 +522,7 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
questions = newQuestions.toPersistentList(),
answers = newAnswers.toPersistentList(),
correctAnswers = newCorrectAnswers.toPersistentList(),
@@ -546,7 +546,7 @@ internal class CreateProblemViewModel @Inject constructor(
title: String? = null,
urlSource: String? = null,
) = intent {
- val newQuestions = state.createProblem.questions.toMutableList()
+ val newQuestions = state.createExam.questions.toMutableList()
val prevQuestion = newQuestions[questionIndex]
val newQuestion = when (questionType) {
Question.Type.Text -> Question.Text(
@@ -574,7 +574,7 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
questions = newQuestions.toPersistentList(),
),
)
@@ -617,7 +617,7 @@ internal class CreateProblemViewModel @Inject constructor(
questionIndex: Int,
answerType: Answer.Type,
) = intent {
- val newAnswers = state.createProblem.answers.toMutableList()
+ val newAnswers = state.createExam.answers.toMutableList()
newAnswers[questionIndex] = when (answerType) {
Answer.Type.ShortAnswer -> newAnswers[questionIndex].toShort()
Answer.Type.Choice -> newAnswers[questionIndex].toChoice()
@@ -626,7 +626,7 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
answers = newAnswers.toPersistentList(),
),
)
@@ -647,7 +647,7 @@ internal class CreateProblemViewModel @Inject constructor(
answer: String? = null,
urlSource: String? = null,
) = intent {
- val newAnswers = state.createProblem.answers.toMutableList()
+ val newAnswers = state.createExam.answers.toMutableList()
newAnswers[questionIndex].getEditedAnswers(
answerIndex,
@@ -656,14 +656,14 @@ internal class CreateProblemViewModel @Inject constructor(
urlSource,
).let { newAnswers[questionIndex] = it }
- val newCorrectAnswers = state.createProblem.correctAnswers.toMutableList()
+ val newCorrectAnswers = state.createExam.correctAnswers.toMutableList()
if (answerType == Answer.Type.ShortAnswer) {
newCorrectAnswers[questionIndex] = answer ?: ""
}
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
answers = newAnswers.toPersistentList(),
correctAnswers = newCorrectAnswers.toPersistentList(),
),
@@ -699,7 +699,7 @@ internal class CreateProblemViewModel @Inject constructor(
questionIndex: Int,
answerIndex: Int,
) = intent {
- val newAnswers = state.createProblem.answers.toMutableList()
+ val newAnswers = state.createExam.answers.toMutableList()
val newAnswer = newAnswers[questionIndex]
newAnswers[questionIndex] = when (newAnswer) {
is Answer.Short -> duckieClientLogicProblemException(message = "주관식 답변은 삭제할 수 없습니다.")
@@ -714,14 +714,14 @@ internal class CreateProblemViewModel @Inject constructor(
}
// 만약 정답 처리 된 내용을 삭제하는 경우 정답내용(여기에서는 정답 index)를 초기화 시킨다.
- val newCorrectAnswers = state.createProblem.correctAnswers.toMutableList()
+ val newCorrectAnswers = state.createExam.correctAnswers.toMutableList()
if ("$answerIndex" == newCorrectAnswers[questionIndex]) {
newCorrectAnswers[questionIndex] = ""
}
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
answers = newAnswers.toPersistentList(),
correctAnswers = newCorrectAnswers.toPersistentList(),
),
@@ -752,12 +752,12 @@ internal class CreateProblemViewModel @Inject constructor(
questionIndex: Int,
correctAnswer: String,
) = intent {
- val newCorrectAnswers = state.createProblem.correctAnswers.toMutableList()
+ val newCorrectAnswers = state.createExam.correctAnswers.toMutableList()
newCorrectAnswers[questionIndex] = correctAnswer
reduce {
state.copy(
- createProblem = state.createProblem.copy(
+ createExam = state.createExam.copy(
correctAnswers = newCorrectAnswers.toPersistentList(),
),
)
@@ -772,7 +772,7 @@ internal class CreateProblemViewModel @Inject constructor(
questionIndex: Int,
answerType: Answer.Type,
) = intent {
- val newAnswers = state.createProblem.answers.toMutableList()
+ val newAnswers = state.createExam.answers.toMutableList()
val newAnswer = newAnswers[questionIndex]
when (answerType) {
@@ -796,8 +796,8 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
state.copy(
- createProblem = state.createProblem.copy(
- questions = state.createProblem.questions,
+ createExam = state.createExam.copy(
+ questions = state.createExam.questions,
answers = newAnswers.toPersistentList(),
),
)
@@ -805,8 +805,8 @@ internal class CreateProblemViewModel @Inject constructor(
}
/** 문제 만들기 2단계 화면의 유효성을 체크한다. */
- internal fun createProblemIsValidate(): Boolean {
- return with(container.stateFlow.value.createProblem) {
+ internal fun createExamIsValidate(): Boolean {
+ return with(container.stateFlow.value.createExam) {
val examCountValidate = this.questions.size in MinimumProblem..MaximumProblem
val questionsValidate = this.questions.asSequence()
.map { it.validate() }
@@ -878,14 +878,14 @@ internal class CreateProblemViewModel @Inject constructor(
/** 문제 만들기 전체 화면의 유효성을 체크한다. */
internal fun isAllFieldsNotEmpty(): Boolean {
- return examInformationIsValidate() && createProblemIsValidate() && additionInfoIsValidate()
+ return examInformationIsValidate() && createExamIsValidate() && additionInfoIsValidate()
}
/** 태그 항목들을 등록하기 위한 검색화면으로 진입한다. */
internal fun goToSearchSubTags() = intent {
reduce {
state.copy(
- createProblemStep = CreateProblemStep.Search,
+ createExamStep = CreateProblemStep.Search,
findResultType = FindResultType.SubTags,
)
}
@@ -970,7 +970,7 @@ internal class CreateProblemViewModel @Inject constructor(
when (state.findResultType) {
FindResultType.MainTag -> {
state.copy(
- createProblemStep = CreateProblemStep.ExamInformation,
+ createExamStep = CreateProblemStep.ExamInformation,
examInformation = state.examInformation.run {
copy(
isMainTagSelected = true,
@@ -1019,7 +1019,7 @@ internal class CreateProblemViewModel @Inject constructor(
reduce {
when (state.findResultType) {
FindResultType.MainTag -> state.copy(
- createProblemStep = CreateProblemStep.ExamInformation,
+ createExamStep = CreateProblemStep.ExamInformation,
examInformation = state.examInformation.run {
copy(
isMainTagSelected = false,
@@ -1032,7 +1032,7 @@ internal class CreateProblemViewModel @Inject constructor(
)
FindResultType.SubTags -> state.copy(
- createProblemStep = CreateProblemStep.AdditionalInformation,
+ createExamStep = CreateProblemStep.AdditionalInformation,
additionalInfo = state.additionalInfo.run {
copy(
isSubTagsAdded = isComplete,
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/sideeffect/CreateProblemSideEffect.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/sideeffect/CreateProblemSideEffect.kt
similarity index 84%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/sideeffect/CreateProblemSideEffect.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/sideeffect/CreateProblemSideEffect.kt
index 5486b0ac4..811d4dce0 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/sideeffect/CreateProblemSideEffect.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/sideeffect/CreateProblemSideEffect.kt
@@ -5,12 +5,12 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.viewmodel.sideeffect
+package team.duckie.app.android.feature.create.exam.viewmodel.sideeffect
import com.google.firebase.crashlytics.FirebaseCrashlytics
import team.duckie.app.android.domain.gallery.usecase.LoadGalleryImagesUseCase
-import team.duckie.app.android.feature.create.problem.viewmodel.CreateProblemViewModel
-import team.duckie.app.android.feature.create.problem.viewmodel.state.CreateProblemState
+import team.duckie.app.android.feature.create.exam.viewmodel.CreateProblemViewModel
+import team.duckie.app.android.feature.create.exam.viewmodel.state.CreateProblemState
internal sealed class CreateProblemSideEffect {
object FinishActivity : CreateProblemSideEffect()
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/state/CreateProblemState.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/state/CreateProblemState.kt
similarity index 95%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/state/CreateProblemState.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/state/CreateProblemState.kt
index 21599268e..56768593c 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/state/CreateProblemState.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/state/CreateProblemState.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.viewmodel.state
+package team.duckie.app.android.feature.create.exam.viewmodel.state
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -23,9 +23,9 @@ internal data class CreateProblemState(
val me: User? = null,
val isNetworkError: Boolean = false,
val isEditMode: Boolean = false,
- val createProblemStep: CreateProblemStep = CreateProblemStep.Loading,
+ val createExamStep: CreateProblemStep = CreateProblemStep.Loading,
val examInformation: ExamInformation = ExamInformation(),
- val createProblem: CreateProblem = CreateProblem(),
+ val createExam: CreateProblem = CreateProblem(),
val additionalInfo: AdditionInfo = AdditionInfo(),
val findResultType: FindResultType = FindResultType.MainTag,
val photoState: CreateProblemPhotoState? = null,
diff --git a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/state/CreateProblemStep.kt b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/state/CreateProblemStep.kt
similarity index 87%
rename from feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/state/CreateProblemStep.kt
rename to feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/state/CreateProblemStep.kt
index bb568dda2..3ff0d672d 100644
--- a/feature/create-problem/src/main/kotlin/team/duckie/app/android/feature/create/problem/viewmodel/state/CreateProblemStep.kt
+++ b/feature/create-exam/src/main/kotlin/team/duckie/app/android/feature/create/exam/viewmodel/state/CreateProblemStep.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-package team.duckie.app.android.feature.create.problem.viewmodel.state
+package team.duckie.app.android.feature.create.exam.viewmodel.state
import team.duckie.app.android.common.kotlin.AllowMagicNumber
@@ -13,7 +13,7 @@ import team.duckie.app.android.common.kotlin.AllowMagicNumber
enum class CreateProblemStep(private val index: Int) {
Loading(0),
ExamInformation(1),
- CreateProblem(2),
+ CreateExam(2),
AdditionalInformation(3),
Search(4),
Error(5),
diff --git a/feature/create-problem/src/main/res/values/strings.xml b/feature/create-exam/src/main/res/values/strings.xml
similarity index 100%
rename from feature/create-problem/src/main/res/values/strings.xml
rename to feature/create-exam/src/main/res/values/strings.xml
diff --git a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/BottomContent.kt b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/BottomContent.kt
index 05748e915..9c6f813a9 100644
--- a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/BottomContent.kt
+++ b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/BottomContent.kt
@@ -29,7 +29,7 @@ import team.duckie.quackquack.material.icon.quackicon.outlined.Heart
import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackButton
import team.duckie.quackquack.ui.QuackButtonStyle
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
/** 상세 화면 최하단 Layout */
@@ -54,11 +54,11 @@ internal fun DetailBottomLayout(
horizontalArrangement = Arrangement.SpaceBetween,
) {
// 좋아요 버튼
- QuackImage(
+ QuackIcon(
modifier = Modifier
.size(DpSize(24.dp, 24.dp))
.quackClickable(onClick = onHeartClick),
- src = if (state.isHeart) {
+ icon = if (state.isHeart) {
QuackIcon.FilledHeart
} else {
QuackIcon.Outlined.Heart
diff --git a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/DetailContent.kt b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/DetailContent.kt
index a67c8147b..dbb6eac53 100644
--- a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/DetailContent.kt
+++ b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/DetailContent.kt
@@ -18,7 +18,6 @@ import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@@ -46,7 +45,7 @@ import team.duckie.quackquack.material.icon.QuackIcon
import team.duckie.quackquack.material.icon.quackicon.Outlined
import team.duckie.quackquack.material.icon.quackicon.outlined.More
import team.duckie.quackquack.material.quackClickable
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.QuackTag
import team.duckie.quackquack.ui.QuackTagStyle
import team.duckie.quackquack.ui.QuackText
@@ -101,14 +100,14 @@ internal fun DetailContentLayout(
)
// 더보기 아이콘
- QuackImage(
+ QuackIcon(
modifier = Modifier
.width(24.dp)
.height(24.dp)
.quackClickable(
onClick = moreButtonClick,
),
- src = QuackIcon.Outlined.More,
+ icon = QuackIcon.Outlined.More,
)
}
@@ -173,11 +172,12 @@ private fun DetailProfileLayout(
profileClick: (Int) -> Unit,
) {
val isFollowed = remember(state.isFollowing) { state.isFollowing }
+ val onProfileClick = { profileClick(state.exam.user?.id ?: 0) }
Row(
modifier = Modifier
.quackClickable(
- onClick = { profileClick(state.exam.user?.id ?: 0) },
+ onClick = onProfileClick,
rippleEnabled = false,
)
.padding(
@@ -190,6 +190,7 @@ private fun DetailProfileLayout(
QuackProfileImage(
profileUrl = state.profileImageUrl,
size = DpSize(36.dp, 36.dp),
+ onClick = onProfileClick,
)
// 공백
diff --git a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/TopCustomBar.kt b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/TopCustomBar.kt
index 2b622901f..494ec85cd 100644
--- a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/TopCustomBar.kt
+++ b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/common/TopCustomBar.kt
@@ -29,7 +29,7 @@ import team.duckie.quackquack.material.icon.quackicon.Outlined
import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowBack
import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowRight
import team.duckie.quackquack.material.quackClickable
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.QuackTag
import team.duckie.quackquack.ui.QuackTagStyle
import team.duckie.quackquack.ui.trailingIcon
@@ -54,14 +54,14 @@ internal fun TopAppCustomBar(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
- QuackImage(
+ QuackIcon(
modifier = Modifier
.padding(6.dp)
.size(DpSize(24.dp, 24.dp))
.quackClickable(
onClick = { activity.finish() },
),
- src = QuackIcon.Outlined.ArrowBack,
+ icon = QuackIcon.Outlined.ArrowBack,
)
QuackTag(
diff --git a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/screen/quiz/QuizDetailScreen.kt b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/screen/quiz/QuizDetailScreen.kt
index 519e7e6d2..f5736dab4 100644
--- a/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/screen/quiz/QuizDetailScreen.kt
+++ b/feature/detail/src/main/kotlin/team/duckie/app/android/feature/detail/screen/quiz/QuizDetailScreen.kt
@@ -262,6 +262,7 @@ private fun RankingContent(
}
}
}
+
Divider(color = QuackColor.Gray4.value)
}
}
diff --git a/feature/exam-result/build.gradle.kts b/feature/exam-result/build.gradle.kts
index 24520d71e..59de6901e 100644
--- a/feature/exam-result/build.gradle.kts
+++ b/feature/exam-result/build.gradle.kts
@@ -32,8 +32,8 @@ dependencies {
libs.compose.ui.coil,
libs.compose.lifecycle.runtime,
libs.compose.ui.material, // needs for Scaffold
- libs.quack.ui.components,
libs.quack.v2.ui,
+ libs.kotlin.collections.immutable,
libs.firebase.crashlytics,
)
}
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/ExamResultActivity.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/ExamResultActivity.kt
index a8bdffcdc..603663392 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/ExamResultActivity.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/ExamResultActivity.kt
@@ -23,7 +23,7 @@ import team.duckie.app.android.feature.exam.result.screen.ExamResultScreen
import team.duckie.app.android.feature.exam.result.viewmodel.ExamResultSideEffect
import team.duckie.app.android.feature.exam.result.viewmodel.ExamResultViewModel
import team.duckie.app.android.navigator.feature.startexam.StartExamNavigator
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.theme.QuackTheme
import javax.inject.Inject
@AndroidEntryPoint
@@ -78,6 +78,7 @@ class ExamResultActivity : BaseActivity() {
withFinish = true,
)
}
+
is ExamResultSideEffect.SendReactionSuccessToast -> {
ToastWrapper(this).invoke(getString(R.string.exam_result_post_reaction_success))
}
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/LoadingIndicator.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/LoadingIndicator.kt
deleted file mode 100644
index 5e6bb06a3..000000000
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/LoadingIndicator.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-/*
- * Designed and developed by Duckie Team, 2022
- *
- * Licensed under the MIT.
- * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
- */
-
-package team.duckie.app.android.feature.exam.result.common
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material.CircularProgressIndicator
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import team.duckie.quackquack.ui.color.QuackColor
-
-// TODO(EvergreenTree97): QuackLoadingIndicator로 통합 필요
-@Composable
-internal fun LoadingIndicator() {
- Box(
- modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center,
- ) {
- CircularProgressIndicator(color = QuackColor.DuckieOrange.composeColor)
- }
-}
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/ResultBottomBar.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/ResultBottomBar.kt
index 9e10a4535..de40e41fe 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/ResultBottomBar.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/common/ResultBottomBar.kt
@@ -5,8 +5,11 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.feature.exam.result.common
+import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@@ -18,13 +21,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSurface
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.feature.exam.result.R
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackSmallButton
-import team.duckie.quackquack.ui.component.QuackSmallButtonType
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackSurface
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.ui.sugar.QuackSubtitle
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
@Composable
internal fun ResultBottomBar(
@@ -51,13 +53,9 @@ internal fun ResultBottomBar(
onClick = onClickRetryButton,
)
}
- QuackSmallButton(
- modifier = Modifier
- .heightIn(44.dp)
- .weight(1f),
- type = QuackSmallButtonType.Fill,
+ TempFlexiblePrimaryLargeButton(
+ modifier = Modifier.weight(1f),
text = stringResource(id = R.string.exam_result_exit_exam),
- enabled = true,
onClick = onClickExitButton,
)
}
@@ -72,7 +70,7 @@ private fun GrayBorderSmallButton(
QuackSurface(
modifier = modifier,
backgroundColor = QuackColor.White,
- border = QuackBorder(color = QuackColor.Gray3),
+ border = BorderStroke(width = 1.dp, color = QuackColor.Gray3.value),
shape = RoundedCornerShape(size = 8.dp),
onClick = onClick,
) {
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultScreen.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultScreen.kt
index b794d44c0..2c324395a 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultScreen.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultScreen.kt
@@ -29,7 +29,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment.Companion.CenterHorizontally
+import androidx.compose.ui.Alignment
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
@@ -47,6 +47,7 @@ import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.common.compose.ui.dialog.DuckieBottomSheetDialog
import team.duckie.app.android.common.compose.ui.dialog.DuckieDialog
import team.duckie.app.android.common.compose.ui.quack.todo.QuackReactionTextArea
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackTopAppBar
import team.duckie.app.android.common.compose.util.HandleKeyboardVisibilityWithSheet
import team.duckie.app.android.feature.exam.result.R
import team.duckie.app.android.feature.exam.result.common.ResultBottomBar
@@ -57,8 +58,9 @@ import team.duckie.app.android.feature.exam.result.viewmodel.ExamResultScreen
import team.duckie.app.android.feature.exam.result.viewmodel.ExamResultState
import team.duckie.app.android.feature.exam.result.viewmodel.ExamResultViewModel
import team.duckie.quackquack.material.QuackColor
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.icon.quackicon.outlined.Share
import team.duckie.quackquack.ui.span
import team.duckie.quackquack.ui.sugar.QuackHeadLine1
@@ -231,9 +233,9 @@ private fun ExamResultSuccessScreen(
modifier = Modifier
.padding(vertical = 8.dp)
.padding(horizontal = 16.dp),
- leadingIcon = QuackIcon.Close,
+ leadingIcon = team.duckie.quackquack.material.icon.QuackIcon.Outlined.Close,
onLeadingIconClick = viewModel::exitExam,
- trailingIcon = QuackIcon.Share,
+ trailingIcon = team.duckie.quackquack.material.icon.QuackIcon.Outlined.Share,
onTrailingIconClick = {
viewModel.updateExamResultScreen(ExamResultScreen.SHARE_EXAM_RESULT)
},
@@ -252,11 +254,12 @@ private fun ExamResultSuccessScreen(
PullRefreshIndicator(
modifier = Modifier
.fillMaxWidth()
- .wrapContentWidth(CenterHorizontally)
+ .wrapContentWidth(Alignment.CenterHorizontally)
.zIndex(1f),
refreshing = state.isRefreshing,
state = pullRefreshState,
)
+
Column(
modifier = Modifier
.fillMaxSize()
@@ -282,9 +285,6 @@ private fun ExamResultSuccessScreen(
nickname = nickname,
myAnswer = myAnswer,
profileImg = profileImg,
- onHeartComment = { isLike ->
- viewModel.heartWrongComment(isLike)
- },
initialState = {
viewModel.initialQuizState()
},
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultShareScreen.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultShareScreen.kt
index 9190766cc..4625f27f0 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultShareScreen.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/ExamResultShareScreen.kt
@@ -43,22 +43,22 @@ import coil.compose.AsyncImagePainter
import coil.compose.rememberAsyncImagePainter
import kotlinx.coroutines.launch
import team.duckie.app.android.common.android.image.saveImageInGallery
-import team.duckie.app.android.common.compose.util.ComposeToBitmap
import team.duckie.app.android.common.compose.GetHeightRatioW328H240
import team.duckie.app.android.common.compose.rememberToast
import team.duckie.app.android.common.compose.ui.BackPressedTopAppBar
+import team.duckie.app.android.common.compose.ui.QuackDivider
import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.common.compose.ui.icon.v2.Download
import team.duckie.app.android.common.compose.ui.icon.v2.DuckieTextLogo
+import team.duckie.app.android.common.compose.util.ComposeToBitmap
import team.duckie.app.android.common.kotlin.toHourMinuteSecond
import team.duckie.app.android.feature.exam.result.R
import team.duckie.app.android.feature.exam.result.viewmodel.ExamResultState
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.icon.QuackIcon
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.component.QuackDivider
import team.duckie.quackquack.ui.icons
import team.duckie.quackquack.ui.span
import team.duckie.quackquack.ui.sugar.QuackBody1
@@ -251,9 +251,9 @@ private fun ExamResultImage(
Spacer(space = 4.dp)
QuackBody1(text = "${state.solvedCount}명 중 ${round(state.percent)}%!")
Spacer(space = 24.dp)
- QuackImage(
+ QuackIcon(
modifier = Modifier.size(48.dp, 16.dp),
- src = QuackIcon.DuckieTextLogo,
+ icon = QuackIcon.DuckieTextLogo,
)
}
}
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/quiz/QuizResultContent.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/quiz/QuizResultContent.kt
index 04147218c..b63c3e0b6 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/quiz/QuizResultContent.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/quiz/QuizResultContent.kt
@@ -10,7 +10,6 @@
package team.duckie.app.android.feature.exam.result.screen.quiz
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.PaddingValues
@@ -51,6 +50,7 @@ import team.duckie.quackquack.ui.QuackText
import team.duckie.quackquack.ui.span
import team.duckie.quackquack.ui.sugar.QuackHeadLine1
import team.duckie.quackquack.ui.sugar.QuackHeadLine2
+import team.duckie.quackquack.ui.sugar.QuackQuote
import java.util.Locale
@Composable
@@ -70,7 +70,6 @@ internal fun QuizResultContent(
isPerfectChallenge: Boolean,
profileImg: String,
myAnswer: String,
- onHeartComment: (Int) -> Unit,
comments: ImmutableList,
commentsTotal: Int,
showCommentSheet: () -> Unit,
@@ -136,22 +135,7 @@ internal fun QuizResultContent(
Spacer(space = 16.dp)
// TODO(limsaehyun): QuackText Quote의 버그가 픽스된 후 아래 코드로 변경해야 함
// https://duckie-team.slack.com/archives/C054HU0CKMY/p1688278156256779
- // QuackText(text = message, typography = QuackTypography.Quote)
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 30.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp),
- ) {
- QuackHeadLine1(text = "\"")
- Box(
- modifier = Modifier.weight(1f),
- contentAlignment = Alignment.Center,
- ) {
- QuackHeadLine1(text = message)
- }
- QuackHeadLine1(text = "\"")
- }
+ QuackQuote(text = message)
Spacer(space = 24.dp)
QuackMaxWidthDivider()
Row(
@@ -223,7 +207,6 @@ internal fun QuizResultContent(
ChallengeCommentSection(
profileUrl = profileImg,
myAnswer = myAnswer,
- onHeartComment = onHeartComment,
comments = comments,
commentsTotal = commentsTotal,
showCommentSheet = showCommentSheet,
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeComment.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeComment.kt
index 68291f0c9..576436ff2 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeComment.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeComment.kt
@@ -43,9 +43,9 @@ import team.duckie.quackquack.material.icon.QuackIcon
import team.duckie.quackquack.material.icon.quackicon.Outlined
import team.duckie.quackquack.material.icon.quackicon.outlined.Flag
import team.duckie.quackquack.material.icon.quackicon.outlined.Heart
+import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.modifier.quackClickable
import team.duckie.quackquack.ui.sugar.QuackBody1
@Composable
@@ -118,12 +118,12 @@ internal fun ChallengeComment(
modifier: Modifier = Modifier,
wrongComment: ExamResultState.Success.ChallengeCommentUiModel,
innerPaddingValues: PaddingValues = PaddingValues(),
- onHeartClick: (Int) -> Unit,
+ onHeartClick: ((Int) -> Unit)?,
visibleHeart: Boolean,
showCommentSheet: () -> Unit,
) {
val animateHeartColor =
- animateQuackColorAsState(targetValue = if (wrongComment.isHeart) QuackColor.Gray1 else QuackColor.Gray2)
+ animateQuackColorAsState(targetValue = if (wrongComment.isHeart)QuackColor.Gray1 else QuackColor.Gray2)
Row(
modifier = modifier
@@ -131,7 +131,7 @@ internal fun ChallengeComment(
.background(QuackColor.White.value)
.padding(innerPaddingValues)
.quackClickable(
- rippleEnabled = true,
+ rippleEnabled = false,
onClick = showCommentSheet,
)
.padding(vertical = 8.dp),
@@ -175,7 +175,9 @@ internal fun ChallengeComment(
.size(24.dp)
.quackClickable(
onClick = {
- onHeartClick(wrongComment.id)
+ if (onHeartClick != null) {
+ onHeartClick(wrongComment.id)
+ }
},
),
icon = if (wrongComment.isHeart) {
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentBottomSheetContent.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentBottomSheetContent.kt
index 6de752df0..550d2b898 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentBottomSheetContent.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentBottomSheetContent.kt
@@ -26,6 +26,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import team.duckie.app.android.common.compose.WrapScaffoldLayout
+import team.duckie.app.android.common.compose.ui.QuackDivider
import team.duckie.app.android.common.compose.ui.QuackIconWrapper
import team.duckie.app.android.common.compose.ui.icon.v1.ArrowSendId
import team.duckie.app.android.common.compose.ui.icon.v2.Order18
@@ -39,7 +40,6 @@ import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.icon.QuackIcon
import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.component.QuackDivider
@Composable
internal fun ChallengeCommentBottomSheetContent(
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentSection.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentSection.kt
index ffd7810ab..0217ffaf3 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentSection.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/screen/wronganswer/ChallengeCommentSection.kt
@@ -57,7 +57,6 @@ private val PaleOrange: Color = Color(0xFFFFEFCF)
internal fun ColumnScope.ChallengeCommentSection(
profileUrl: String,
myAnswer: String,
- onHeartComment: (Int) -> Unit,
commentsTotal: Int,
comments: ImmutableList,
showCommentSheet: () -> Unit,
@@ -155,10 +154,12 @@ internal fun ColumnScope.ChallengeCommentSection(
ChallengeComment(
modifier = Modifier.fillMaxScreenWidth(),
wrongComment = item,
- onHeartClick = onHeartComment,
innerPaddingValues = PaddingValues(horizontal = 16.dp),
visibleHeart = true,
showCommentSheet = showCommentSheet,
+ onHeartClick = {
+ showCommentSheet() // 하트 클릭 미지원
+ },
)
Spacer(space = 8.dp)
}
diff --git a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/viewmodel/ExamResultViewModel.kt b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/viewmodel/ExamResultViewModel.kt
index 37e9e22fd..323459441 100644
--- a/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/viewmodel/ExamResultViewModel.kt
+++ b/feature/exam-result/src/main/java/team/duckie/app/android/feature/exam/result/viewmodel/ExamResultViewModel.kt
@@ -158,6 +158,7 @@ class ExamResultViewModel @Inject constructor(
updateRefreshState(true)
getChallengeCommentList()
state.updateCommentCreateAt()
+ refreshQuiz()
if (forceLoading) delay(pullToRefreshMinLoadingDelay)
updateRefreshState(false)
}
@@ -183,10 +184,19 @@ class ExamResultViewModel @Inject constructor(
}
}
+ private var isWriteSending: Boolean = false
+ private val existSendingMessage: String = "열심히 댓글을 등록하고 있어요! 조금만 기다려 주세요."
+
fun writeChallengeComment() = intent {
val state = state as ExamResultState.Success
val myComment = state.myWrongComment
+ if (isWriteSending) {
+ postSideEffect(ExamResultSideEffect.SendErrorToast(existSendingMessage))
+ return@intent
+ }
+ isWriteSending = true
+
if (myComment.isNotEmpty()) {
writeChallengeCommentUseCase(
challengeId = state.examId,
@@ -205,6 +215,8 @@ class ExamResultViewModel @Inject constructor(
}
}.onFailure { exception ->
postSideEffect(ExamResultSideEffect.ReportError(exception))
+ }.also {
+ isWriteSending = false
}
}
}
@@ -459,6 +471,28 @@ class ExamResultViewModel @Inject constructor(
}
}
+ private fun refreshQuiz() = intent {
+ val state = state as ExamResultState.Success
+ getQuizUseCase(state.examId).onSuccess { quizResult ->
+ reduce {
+ with(quizResult) {
+ state.copy(
+ popularComments = popularComments?.fastMap(ChallengeComment::toUiModel)
+ ?.toImmutableList() ?: persistentListOf(),
+ commentsTotal = commentsTotal ?: 0,
+ equalAnswerCount = wrongAnswer?.meTotal ?: 0,
+ )
+ }
+ }
+ }.onFailure {
+ it.printStackTrace()
+ reduce {
+ ExamResultState.Error(exception = it)
+ }
+ postSideEffect(ExamResultSideEffect.ReportError(it))
+ }
+ }
+
fun updateReaction(reaction: String) = intent {
val state = state as ExamResultState.Success
diff --git a/feature/friends/build.gradle.kts b/feature/friends/build.gradle.kts
index 60c50cfbd..9146e1e10 100644
--- a/feature/friends/build.gradle.kts
+++ b/feature/friends/build.gradle.kts
@@ -30,6 +30,7 @@ dependencies {
libs.ktx.lifecycle.runtime,
libs.compose.ui.material, // needs for Scaffold
libs.compose.lifecycle.runtime,
- libs.quack.ui.components,
+ libs.quack.v2.ui,
+ libs.kotlin.collections.immutable,
)
}
diff --git a/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsActivity.kt b/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsActivity.kt
index 2b379a561..ebce07de5 100644
--- a/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsActivity.kt
+++ b/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsActivity.kt
@@ -14,14 +14,14 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import dagger.hilt.android.AndroidEntryPoint
import org.orbitmvi.orbit.viewmodel.observe
-import team.duckie.app.android.feature.friends.viewmodel.FriendsViewModel
-import team.duckie.app.android.feature.friends.viewmodel.sideeffect.FriendsSideEffect
-import team.duckie.app.android.navigator.feature.profile.ProfileNavigator
import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
import team.duckie.app.android.common.android.ui.BaseActivity
import team.duckie.app.android.common.android.ui.const.Extras
import team.duckie.app.android.common.android.ui.finishWithAnimation
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.app.android.feature.friends.viewmodel.FriendsViewModel
+import team.duckie.app.android.feature.friends.viewmodel.sideeffect.FriendsSideEffect
+import team.duckie.app.android.navigator.feature.profile.ProfileNavigator
+import team.duckie.quackquack.material.theme.QuackTheme
import javax.inject.Inject
@AndroidEntryPoint
diff --git a/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsScreen.kt b/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsScreen.kt
index 4481cd03b..f2de31a07 100644
--- a/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsScreen.kt
+++ b/feature/friends/src/main/java/team/duckie/app/android/feature/friends/FriendsScreen.kt
@@ -13,7 +13,9 @@ import androidx.compose.foundation.ExperimentalFoundationApi
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.foundation.layout.padding
+import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
@@ -23,6 +25,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
+import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -32,19 +35,21 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.launch
-import team.duckie.app.android.feature.friends.viewmodel.FriendsViewModel
-import team.duckie.app.android.feature.friends.viewmodel.state.FriendsState
+import team.duckie.app.android.common.compose.systemBarPaddings
import team.duckie.app.android.common.compose.ui.BackPressedHeadLine2TopAppBar
import team.duckie.app.android.common.compose.ui.ErrorScreen
import team.duckie.app.android.common.compose.ui.NoItemScreen
import team.duckie.app.android.common.compose.ui.Spacer
-import team.duckie.app.android.common.compose.ui.UserFollowingLayout
+import team.duckie.app.android.common.compose.ui.content.UserFollowingLayout
import team.duckie.app.android.common.compose.ui.skeleton
-import team.duckie.app.android.common.compose.systemBarPaddings
import team.duckie.app.android.common.kotlin.FriendsType
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackHeadLine2
-import team.duckie.quackquack.ui.component.QuackMainTab
+import team.duckie.app.android.feature.friends.viewmodel.FriendsViewModel
+import team.duckie.app.android.feature.friends.viewmodel.state.FriendsState
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackTab
+import team.duckie.quackquack.ui.QuackTabColors
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun FriendScreen(
@@ -59,31 +64,36 @@ internal fun FriendScreen(
)
}
val state by viewModel.container.stateFlow.collectAsStateWithLifecycle()
- val pagerState = rememberPagerState(initialPage = state.friendType.index)
+ val pagerState = rememberPagerState(
+ initialPage = state.friendType.index,
+ pageCount = { FriendsType.values().size },
+ )
val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
.padding(systemBarPaddings),
) {
BackPressedHeadLine2TopAppBar(
title = state.targetName,
onBackPressed = onPrevious,
)
- QuackMainTab(
- titles = tabs,
- selectedTabIndex = pagerState.currentPage,
- onTabSelected = { index ->
- coroutineScope.launch {
- pagerState.animateScrollToPage(index)
+ QuackTab(
+ index = pagerState.currentPage,
+ colors = QuackTabColors.defaultTabColors(),
+ ) {
+ tabs.forEach { label ->
+ tab(label) { index ->
+ coroutineScope.launch {
+ pagerState.animateScrollToPage(index)
+ }
}
- },
- )
+ }
+ }
HorizontalPager(
modifier = Modifier.fillMaxSize(),
- pageCount = FriendsType.values().size,
state = pagerState,
) { index ->
if (state.isError) {
@@ -198,7 +208,9 @@ private fun FriendNotFoundScreen(
)
} else {
NoItemScreen(
- modifier = Modifier.padding(top = 72.dp),
+ modifier = Modifier
+ .fillMaxWidth()
+ .wrapContentWidth(CenterHorizontally),
title = stringResource(
id = if (isFollower) {
R.string.follower_not_found_title
@@ -228,11 +240,13 @@ private fun MyPageFriendNotFoundScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Spacer(space = 74.5.dp)
- QuackHeadLine2(
+ QuackText(
modifier = Modifier.skeleton(isLoading),
text = title,
- color = QuackColor.Gray1,
- align = TextAlign.Center,
+ typography = QuackTypography.HeadLine2.change(
+ color = QuackColor.Gray1,
+ textAlign = TextAlign.Center,
+ ),
)
}
}
@@ -257,10 +271,10 @@ private fun FriendListScreen(
favoriteTag = item.favoriteTag,
tier = item.tier,
isFollowing = item.isFollowing,
- onClickFollow = { follow ->
+ onClickTrailingButton = { follow ->
onClickFollow(item.userId, follow)
},
- isMine = myUserId == item.userId,
+ visibleTrailingButton = myUserId != item.userId,
onClickUserProfile = onClickUserProfile,
)
}
diff --git a/feature/home/build.gradle.kts b/feature/home/build.gradle.kts
index f2a35bd98..60b93a5fd 100644
--- a/feature/home/build.gradle.kts
+++ b/feature/home/build.gradle.kts
@@ -32,7 +32,7 @@ dependencies {
projects.navigator,
libs.orbit.viewmodel,
libs.orbit.compose,
- libs.quack.ui.components,
+ libs.kotlin.collections.immutable,
libs.quack.v2.ui,
libs.compose.lifecycle.runtime,
libs.compose.ui.navigation,
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/BaseBottomLayout.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/BaseBottomLayout.kt
index 257eb72ae..3aa960314 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/BaseBottomLayout.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/BaseBottomLayout.kt
@@ -20,8 +20,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackDivider
+import team.duckie.app.android.common.compose.ui.DuckieDivider
+import team.duckie.quackquack.material.QuackColor
@Composable
internal fun BaseBottomLayout(
@@ -36,11 +36,11 @@ internal fun BaseBottomLayout(
.fillMaxWidth()
.height(48.dp),
) {
- QuackDivider()
+ DuckieDivider()
Row(
modifier = Modifier
.fillMaxSize()
- .background(QuackColor.White.composeColor)
+ .background(QuackColor.White.value)
.padding(contentPadding),
verticalAlignment = Alignment.CenterVertically,
) {
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/DuckTestBottomNavigation.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/DuckTestBottomNavigation.kt
index 3d3faceb6..d5e164824 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/DuckTestBottomNavigation.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/DuckTestBottomNavigation.kt
@@ -18,21 +18,23 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import okhttp3.internal.immutableListOf
-import team.duckie.app.android.feature.home.R
import team.duckie.app.android.common.kotlin.fastForEachIndexed
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.app.android.feature.home.R
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
/**
* [DuckTestBottomNavigation] 를 그리는데 필요한 리소스들을 정의합니다.
@@ -41,7 +43,7 @@ private object DuckTestBottomNavigationDefaults {
val Height = 52.dp
val BackgroundColor = QuackColor.White
- val IconSize = DpSize(all = 24.dp)
+ val IconSize = DpSize(width = 24.dp, height = 24.dp)
}
/**
@@ -71,7 +73,7 @@ internal fun DuckTestBottomNavigation(
height = Height,
)
.background(
- color = BackgroundColor.composeColor,
+ color = BackgroundColor.value,
),
) {
rememberBottomNavigationIcons().fastForEachIndexed { index, icons ->
@@ -83,25 +85,29 @@ internal fun DuckTestBottomNavigation(
.fillMaxSize()
.quackClickable(
rippleEnabled = false,
- ) {
- onClick(
- /* index = */
- index,
- )
- },
+ onClick = {
+ onClick(index)
+ },
+ ),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
QuackImage(
+ modifier = Modifier.size(IconSize),
src = icons.pick(
isSelected = index == selectedIndex,
),
- size = IconSize,
)
Spacer(modifier = Modifier.height(8.dp))
- QuackBody2(
+ QuackText(
text = stringResource(id = icons.title),
- color = if (index == selectedIndex) QuackColor.Black else QuackColor.Gray1,
+ typography = QuackTypography.Body2.change(
+ color = if (index == selectedIndex) {
+ QuackColor.Black
+ } else {
+ QuackColor.Gray1
+ },
+ ),
)
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HeadLineTopAppBar.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HeadLineTopAppBar.kt
index 75e752aae..bd890976f 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HeadLineTopAppBar.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HeadLineTopAppBar.kt
@@ -15,7 +15,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
-import team.duckie.quackquack.ui.component.QuackHeadLine1
+import team.duckie.quackquack.ui.sugar.QuackHeadLine1
@Composable
internal fun HeadLineTopAppBar(
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HomeTopAppBar.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HomeTopAppBar.kt
index 9ba9647dd..fb6274483 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HomeTopAppBar.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/component/HomeTopAppBar.kt
@@ -22,24 +22,23 @@ import okhttp3.internal.immutableListOf
import team.duckie.app.android.common.compose.ui.TextTabLayout
import team.duckie.app.android.feature.home.R
import team.duckie.quackquack.material.QuackTypography
-import team.duckie.quackquack.ui.util.DpSize
import team.duckie.quackquack.material.QuackColor as QuackV2Color
-internal val HomeIconSize = DpSize(24.dp)
-
-@Suppress("UnusedPrivateMember") // 시험 생성하기를 추후에 다시 활용하기 위함
+@Suppress("unused") // 진행중 API 추가 작업을 위함
@Composable
internal fun HomeTopAppBar(
modifier: Modifier = Modifier,
selectedTabIndex: Int,
onTabSelected: (Int) -> Unit,
onClickedCreate: () -> Unit,
+ onClickedNotice: () -> Unit,
) {
val context = LocalContext.current
val homeTextTabTitles = remember {
immutableListOf(
context.getString(R.string.recommend),
+ // context.getString(R.string.proceed),
context.getString(R.string.following),
)
}
@@ -53,15 +52,25 @@ internal fun HomeTopAppBar(
) {
TextTabLayout(
titles = homeTextTabTitles.toImmutableList(),
+ selectedTabStyle = QuackTypography.HeadLine1,
selectedTabIndex = selectedTabIndex,
onTabSelected = onTabSelected,
- tabStyle = QuackTypography.Title2.change(color = QuackV2Color.Gray2),
+ tabStyle = QuackTypography.HeadLine1.change(color = QuackV2Color.Gray2),
)
-// TODO(limsaehyun): 시험 생성하기가 가능한 스펙에서 활용
-// QuackImage(
-// src = QuackIcon.Create,
-// onClick = onClickedCreate,
-// size = HomeIconSize,
-// )
+
+ // QuackImage(
+ // src = R.drawable.home_ic_notice,
+ // modifier = Modifier
+ // .quackClickable(onClick = onClickedNotice)
+ // .size(DpSize(24.dp, 24.dp)),
+ // )
+ //
+ // // TODO(riflockle7): 임시로 활성화
+ // QuackIcon(
+ // modifier = Modifier
+ // .quackClickable(onClick = onClickedCreate)
+ // .size(DpSize(24.dp, 24.dp)),
+ // icon = OutlinedGroup.Create,
+ // )
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/constants/HomeStep.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/constants/HomeStep.kt
index e4dc92a1b..1200d7d42 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/constants/HomeStep.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/constants/HomeStep.kt
@@ -25,6 +25,11 @@ internal enum class HomeStep(
HomeFollowingScreen(
index = 1,
),
+
+ // TODO(riflockle7): 추후 index = 1
+ HomeProceedScreen(
+ index = 2,
+ ),
;
companion object {
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainActivity.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainActivity.kt
index 59bab7cce..01243bca0 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainActivity.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainActivity.kt
@@ -43,7 +43,7 @@ import team.duckie.app.android.navigator.feature.profile.ViewAllNavigator
import team.duckie.app.android.navigator.feature.search.SearchNavigator
import team.duckie.app.android.navigator.feature.setting.SettingNavigator
import team.duckie.app.android.navigator.feature.tagedit.TagEditNavigator
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.theme.QuackTheme
import javax.inject.Inject
@AllowMagicNumber("앱 종료 시간에 대해서 매직 넘버 처리")
@@ -57,7 +57,7 @@ class MainActivity : BaseActivity() {
private val homeViewModel: HomeViewModel by viewModels()
@Inject
- lateinit var createProblemNavigator: CreateProblemNavigator
+ lateinit var createExamNavigator: CreateProblemNavigator
@Inject
lateinit var notificationNavigator: NotificationNavigator
@@ -162,11 +162,6 @@ class MainActivity : BaseActivity() {
}
}
- override fun onStart() {
- super.onStart()
- myPageViewModel.getUserProfile()
- }
-
private fun startedGuide(intent: Intent) {
intent.getBooleanExtra(Extras.StartGuide, false).also { start ->
if (start) {
@@ -186,6 +181,7 @@ class MainActivity : BaseActivity() {
startActivityWithAnimation(
intentBuilder = {
putExtra(Extras.SearchTag, sideEffect.searchTag)
+ putExtra(Extras.AutoFocusing, sideEffect.autoFocusing)
},
)
}
@@ -200,7 +196,7 @@ class MainActivity : BaseActivity() {
}
is MainSideEffect.NavigateToCreateProblem -> {
- createProblemNavigator.navigateFrom(activity = this)
+ createExamNavigator.navigateFrom(activity = this)
}
is MainSideEffect.NavigateToSetting -> {
@@ -233,6 +229,15 @@ class MainActivity : BaseActivity() {
is MainSideEffect.CopyExamIdDynamicLink -> {
DynamicLinkHelper.createAndShareLink(this, sideEffect.examId)
}
+
+ is MainSideEffect.NavigateToProfile -> {
+ profileNavigator.navigateFrom(
+ activity = this,
+ intentBuilder = {
+ putExtra(Extras.UserId, sideEffect.userId)
+ },
+ )
+ }
}
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainScreen.kt
index e2b092dc5..f168c6a7b 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/MainScreen.kt
@@ -11,6 +11,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
@@ -37,8 +38,7 @@ import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
import team.duckie.app.android.feature.home.viewmodel.mypage.MyPageViewModel
import team.duckie.app.android.feature.home.viewmodel.ranking.RankingViewModel
import team.duckie.app.android.feature.profile.viewmodel.state.ExamType
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackDivider
+import team.duckie.quackquack.material.QuackColor
private const val MainCrossFacadeLayoutId = "MainCrossFacade"
private const val MainBottomNavigationDividerLayoutId = "MainBottomNavigationDivider"
@@ -114,7 +114,7 @@ internal fun MainScreen(
Layout(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
.padding(systemBarPaddings),
content = {
HomeGuideScreen(
@@ -141,6 +141,7 @@ internal fun MainScreen(
navigateToCreateProblem = {
mainViewModel.navigateToCreateProblem()
},
+ navigateToProfile = mainViewModel::navigateToProfile,
setTargetExamId = { examId ->
mainViewModel.setTargetExamId(examId)
},
@@ -209,7 +210,7 @@ internal fun MainScreen(
}
}
- QuackDivider(
+ Divider(
modifier = Modifier.layoutId(MainBottomNavigationDividerLayoutId),
)
DuckTestBottomNavigation(
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideFeatureScrren.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideFeatureScrren.kt
index fc4cad724..6bd5e8c98 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideFeatureScrren.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideFeatureScrren.kt
@@ -28,15 +28,14 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.DuckieHorizontalPagerIndicator
import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.component.BaseBottomLayout
import team.duckie.app.android.feature.home.constants.GuideStep
-import team.duckie.app.android.common.compose.ui.DuckieHorizontalPagerIndicator
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackHeadLine2
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun HomeGuideFeatureScreen(
@@ -46,17 +45,21 @@ internal fun HomeGuideFeatureScreen(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
content = {
- QuackHeadLine2(
+ QuackText(
modifier = Modifier
.padding(top = 41.dp),
text = stringResource(id = guideStep.subtitle),
- color = QuackColor.White,
- align = TextAlign.Center,
+ typography = QuackTypography.HeadLine2.change(
+ color = QuackColor.White,
+ textAlign = TextAlign.Center,
+ ),
)
- QuackHeadLine1(
+ QuackText(
text = stringResource(id = guideStep.title),
- color = QuackColor.DuckieOrange,
- align = TextAlign.Center,
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.DuckieOrange,
+ textAlign = TextAlign.Center,
+ ),
)
Image(
modifier = Modifier
@@ -84,15 +87,13 @@ internal fun HomeGuideFeatureBottomLayout(
modifier = Modifier
.fillMaxWidth()
.height(48.dp)
- .quackClickable {
- onNext()
- }
- .background(color = QuackColor.DuckieOrange.composeColor),
+ .quackClickable(onClick = onNext)
+ .background(color = QuackColor.DuckieOrange.value),
contentAlignment = Alignment.Center,
) {
- QuackHeadLine2(
+ QuackText(
text = stringResource(id = R.string.start_duckie),
- color = QuackColor.White,
+ typography = QuackTypography.HeadLine2.change(color = QuackColor.White),
)
}
} else {
@@ -105,10 +106,12 @@ internal fun HomeGuideFeatureBottomLayout(
)
},
trailingContent = {
- QuackSubtitle(
+ QuackText(
+ modifier = Modifier.quackClickable(onClick = onNext),
text = stringResource(id = R.string.next),
- color = QuackColor.DuckieOrange,
- onClick = onNext,
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.DuckieOrange,
+ ),
)
},
)
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideScreen.kt
index 736b433ad..c16a63376 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/guide/HomeGuideScreen.kt
@@ -5,7 +5,7 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-@file:OptIn(ExperimentalFoundationApi::class)
+@file:OptIn(ExperimentalFoundationApi::class, ExperimentalQuackQuackApi::class)
package team.duckie.app.android.feature.home.screen.guide
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -34,17 +35,17 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.constants.GuideStep
import team.duckie.app.android.feature.home.viewmodel.guide.HomeGuideViewModel
-import team.duckie.app.android.common.compose.ui.Spacer
-import team.duckie.app.android.common.compose.activityViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackSmallButton
-import team.duckie.quackquack.ui.component.QuackSmallButtonType
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
@Composable
internal fun HomeGuideScreen(
@@ -53,14 +54,17 @@ internal fun HomeGuideScreen(
onClose: () -> Unit,
) {
val state = vm.collectAsState().value
- val pagerState = rememberPagerState()
- val pageCount = GuideStep.values().size
+ val pageCount = remember { GuideStep.values().size }
+ val pagerState = rememberPagerState(
+
+ pageCount = { pageCount },
+ )
val coroutineScope = rememberCoroutineScope()
Box(
modifier = modifier
.fillMaxSize()
- .background(color = QuackColor.Black.composeColor.copy(alpha = 0.9F)),
+ .background(color = QuackColor.Black.value.copy(alpha = 0.9F)),
contentAlignment = Alignment.BottomCenter,
) {
if (state.isGuideStarted) {
@@ -82,18 +86,17 @@ internal fun HomeGuideScreen(
.navigationBarsPadding(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- QuackBody2(
+ QuackText(
modifier = Modifier
+ .quackClickable(onClick = onClose)
.fillMaxWidth()
.wrapContentWidth(Alignment.End)
.padding(top = 40.dp, end = 16.dp),
text = stringResource(id = R.string.skip),
- color = QuackColor.Gray3,
- onClick = onClose,
+ typography = QuackTypography.Body2.change(color = QuackColor.Gray3),
)
HorizontalPager(
modifier = Modifier.fillMaxSize(),
- pageCount = pageCount,
state = pagerState,
) { index ->
HomeGuideFeatureScreen(
@@ -136,25 +139,26 @@ private fun HomeGuideStartScreen(
contentScale = ContentScale.Fit,
)
Spacer(space = 12.dp)
- QuackHeadLine1(
+ QuackText(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
text = stringResource(id = R.string.guide_start_message),
- color = QuackColor.White,
- align = TextAlign.Center,
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.White,
+ textAlign = TextAlign.Center,
+ ),
)
Spacer(space = 20.dp)
- QuackSmallButton(
- modifier = Modifier
- .size(118.dp, 44.dp),
- type = QuackSmallButtonType.Fill,
+ TempFlexiblePrimaryLargeButton(
text = stringResource(id = R.string.guide_start_accept_message),
+ modifier = Modifier.size(118.dp, 44.dp),
enabled = true,
onClick = onNext,
)
Spacer(space = 16.dp)
- QuackBody2(
+ QuackText(
+ modifier = Modifier.quackClickable(onClick = onClosed),
text = stringResource(id = R.string.guide_start_deny_message),
- color = QuackColor.Gray2,
- onClick = onClosed,
+ typography = QuackTypography.Body2.change(color = QuackColor.Gray2),
)
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeProceedScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeProceedScreen.kt
new file mode 100644
index 000000000..28038ede3
--- /dev/null
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeProceedScreen.kt
@@ -0,0 +1,526 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+@file:OptIn(
+ ExperimentalMaterialApi::class,
+ ExperimentalQuackQuackApi::class,
+)
+
+@file:Suppress("unused") // 더미 값, 미구현 된 내용
+@file:AllowMagicNumber("더미 값, 미구현 된 내용")
+
+package team.duckie.app.android.feature.home.screen.home
+
+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.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.pullrefresh.PullRefreshIndicator
+import androidx.compose.material.pullrefresh.pullRefresh
+import androidx.compose.material.pullrefresh.rememberPullRefreshState
+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.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import team.duckie.app.android.common.compose.GetHeightRatioW129H84
+import team.duckie.app.android.common.compose.GetHeightRatioW328H240
+import team.duckie.app.android.common.compose.GetHeightRatioW360H90
+import team.duckie.app.android.common.compose.GetHeightRatioW85H63
+import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.icon.v1.ArrowRightId
+import team.duckie.app.android.common.compose.ui.quack.QuackProfileImage
+import team.duckie.app.android.common.kotlin.AllowMagicNumber
+import team.duckie.app.android.feature.home.R
+import team.duckie.app.android.feature.home.component.HomeTopAppBar
+import team.duckie.app.android.feature.home.constants.HomeStep
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedCategory.categories
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedCategory.categoryThumbnailUrl
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedCategory.items
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.currentExamCount
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.joinCount
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.maximumExamCount
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.nickname
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.profileImageUrl
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.remainCount
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.thumbnailUrl
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.ProceedItemView.title
+import team.duckie.app.android.feature.home.screen.home.HomeProceedTempConstants.username
+import team.duckie.app.android.feature.home.viewmodel.home.HomeState
+import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackTag
+import team.duckie.quackquack.ui.QuackTagStyle
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackBody1
+import team.duckie.quackquack.ui.sugar.QuackSubtitle2
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
+
+object HomeProceedTempConstants {
+ const val username = "무지개양말"
+ object ProceedItemView {
+ const val title = "투바투 덕력고사"
+ const val thumbnailUrl =
+ "https://duckie-resource.s3.ap-northeast-2.amazonaws.com/exam/thumbnail/1684545439537"
+ const val maximumExamCount = 10
+ const val currentExamCount = 9
+ val remainCount: Int
+ get() = maximumExamCount - currentExamCount
+ const val profileImageUrl =
+ "https://duckie-resource.s3.ap-northeast-2.amazonaws.com/profile/1686068260083"
+ const val nickname = "킹도로"
+ const val joinCount = 5
+ }
+
+ object ProceedCategory {
+ val categories = listOf("전체", "애니", "아이돌", "영화", "운동", "트렌드")
+ const val categoryThumbnailUrl =
+ "https://duckie-resource.s3.ap-northeast-2.amazonaws.com/exam/thumbnail/1684545439537"
+ val items = listOf("투바투 덕력고사", "베스킨라빈스 31 덕력고사", "예능 덕력고사", "코난 극장판 덕력고사", "아따맘마 덕력고사")
+ }
+}
+
+@Suppress("unused") // 더미 값
+@Composable
+internal fun HomeProceedScreen(
+ modifier: Modifier = Modifier,
+ state: HomeState,
+ homeViewModel: HomeViewModel = activityViewModel(),
+ navigateToCreateProblem: () -> Unit,
+ navigateToHomeDetail: (Int) -> Unit,
+ navigateToSearch: (String) -> Unit,
+ openExamBottomSheet: (Int) -> Unit,
+) {
+ val pullRefreshState = rememberPullRefreshState(
+ refreshing = state.isHomeProceedPullRefreshLoading,
+ onRefresh = {
+ homeViewModel.refreshProceeds(forceLoading = true)
+ },
+ )
+
+ Box(modifier = Modifier.pullRefresh(pullRefreshState)) {
+ LazyColumn(modifier = modifier.fillMaxSize()) {
+ item {
+ HomeTopAppBar(
+ modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
+ selectedTabIndex = state.homeSelectedIndex.index,
+ onTabSelected = { step ->
+ homeViewModel.changedHomeScreen(HomeStep.toStep(step))
+ },
+ onClickedCreate = navigateToCreateProblem,
+ onClickedNotice = {},
+ )
+ }
+
+ // 공백
+ item {
+ Spacer(Modifier.height(12.dp))
+ }
+
+ // 제목
+ item {
+ QuackText(
+ modifier = Modifier.padding(start = 16.dp),
+ text = stringResource(id = R.string.home_proceed_title),
+ typography = QuackTypography.HeadLine1,
+ )
+ }
+
+ // 공백
+ item {
+ Spacer(Modifier.height(12.dp))
+ }
+
+ // 진행중인 덕력고사 목록 뷰
+ items(10) {
+ ProceedItemView()
+ }
+
+ // 공백
+ item {
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+
+ // 전체보기 버튼
+ item {
+ ProceedViewAllButton(onViewAllClick = {})
+ }
+
+ // 공백
+ item {
+ Spacer(Modifier.height(48.dp))
+ }
+
+ // 덕력고사 진행중 배너 뷰
+ item {
+ ProceedBannerView()
+ }
+
+ // 공백
+ item {
+ Spacer(Modifier.height(48.dp))
+ }
+
+ // 덕력고사 진행 중 카테고리 영역 뷰
+ item {
+ ProceedCategorySection(
+ selectedTagIndex = 0,
+ tagItemClick = {},
+ categories = categories,
+ items = items,
+ )
+ }
+ }
+ PullRefreshIndicator(
+ modifier = Modifier.align(Alignment.TopCenter),
+ refreshing = state.isHomeRecommendPullRefreshLoading,
+ state = pullRefreshState,
+ )
+ }
+}
+
+/** 진행중인 덕력고사 Item 뷰 */
+@Composable
+fun ProceedItemView() {
+ Column(
+ modifier = Modifier
+ .padding(vertical = 8.dp, horizontal = 16.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .border(
+ width = 1.dp,
+ color = QuackColor.Gray3.value,
+ shape = RoundedCornerShape(8.dp),
+ )
+ .quackClickable(
+ onClick = {},
+ ),
+ ) {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .clip(
+ RoundedCornerShape(
+ topStart = 8.dp,
+ topEnd = 8.dp,
+ bottomEnd = 0.dp,
+ bottomStart = 0.dp,
+ ),
+ ),
+ ) {
+ // 덕퀴즈/덕질고사 썸네일 이미지
+ AsyncImage(
+ model = thumbnailUrl,
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(ratio = GetHeightRatioW328H240),
+ contentScale = ContentScale.FillBounds,
+ contentDescription = null,
+ )
+
+ // 오픈까지 x문제
+ QuackText(
+ modifier = Modifier
+ .clip(RoundedCornerShape(bottomEnd = 4.dp))
+ .background(
+ if (remainCount == 1) {
+ QuackColor.DuckieOrange.value
+ } else {
+ Color(0xFF222222)
+ },
+ )
+ .padding(vertical = 4.dp, horizontal = 8.dp),
+ text = stringResource(
+ id = R.string.home_proceed_item_count_down,
+ remainCount,
+ ),
+ typography = QuackTypography.Body3.change(color = QuackColor.White),
+ )
+ }
+
+ // 만들어진 문제 개수 / 최대 문제 개수 비율 막대 그래프
+ Row(modifier = Modifier.height(8.dp)) {
+ // 첫 번째 막대 (8:2 비율)
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(currentExamCount.toFloat())
+ .background(QuackColor.DuckieOrange.value),
+ )
+
+ // 두 번째 막대 (8:2 비율)
+ Box(
+ modifier = Modifier
+ .fillMaxHeight()
+ .weight(remainCount.toFloat())
+ .background(QuackColor.Gray3.value),
+ )
+ }
+
+ // 덕력고사 정보 & 참여율
+ Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp)) {
+ // 덕력고사 이름
+ QuackText(text = title, typography = QuackTypography.HeadLine2)
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Row(horizontalArrangement = Arrangement.Center) {
+ // 프로필 이미지
+ QuackProfileImage(
+ modifier = Modifier.size(DpSize(width = 16.dp, height = 16.dp)),
+ profileUrl = profileImageUrl,
+ )
+
+ // 닉네임 + 참여 인원수
+ QuackText(
+ modifier = Modifier.padding(start = 4.dp),
+ text = stringResource(
+ R.string.home_proceed_item_info,
+ nickname,
+ stringResource(id = R.string.home_proceed_item_join_count, joinCount),
+ ),
+ typography = QuackTypography.Body2.change(color = QuackColor.Gray1),
+ )
+
+ // 너비
+ Spacer(weight = 1f)
+
+ // 만들어진 문제 개수 / 최대 문제 개수
+ QuackText(
+ modifier = Modifier.padding(start = 4.dp),
+ text = stringResource(
+ R.string.home_proceed_item_problem_count,
+ currentExamCount,
+ maximumExamCount,
+ ),
+ typography = QuackTypography.Body2.change(color = QuackColor.Gray1),
+ )
+ }
+ }
+ }
+}
+
+/** 배너 뷰 */
+@Composable
+fun ProceedBannerView() {
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .aspectRatio(ratio = GetHeightRatioW360H90)
+ .background(Color(0xFFFFF8CF)),
+ ) {
+ Column(
+ modifier = Modifier
+ .padding(start = 16.dp)
+ .align(Alignment.CenterStart),
+ verticalArrangement = Arrangement.SpaceBetween,
+ ) {
+ // 직접 덕력고사를 열고 싶다면
+ QuackText(
+ modifier = Modifier.padding(start = 8.dp),
+ text = stringResource(id = R.string.home_proceed_banner_title),
+ typography = QuackTypography.HeadLine2,
+ )
+
+ Spacer(modifier = Modifier.height(4.dp))
+
+ // 출제 제안하기 버튼
+ Row(
+ modifier = Modifier
+ .clip(RoundedCornerShape(100.dp))
+ .background(QuackColor.White.value)
+ .padding(vertical = 6.dp, horizontal = 12.dp),
+ ) {
+ // 텍스트
+ QuackSubtitle2(
+ text = stringResource(id = R.string.home_proceed_banner_submit_button_title),
+ )
+
+ // 공백
+ Spacer(space = 4.dp)
+
+ // 우측 방향 화살표
+ QuackImage(
+ modifier = Modifier.size(DpSize(width = 16.dp, height = 16.dp)),
+ src = QuackIcon.ArrowRightId,
+ )
+ }
+ }
+
+ // 배너 우측 이미지
+ QuackImage(
+ modifier = Modifier
+ .padding(top = 6.dp)
+ .aspectRatio(GetHeightRatioW129H84)
+ .align(Alignment.CenterEnd),
+ src = R.drawable.home_proceed_banner_right,
+ )
+ }
+}
+
+/** 진행중인 덕력고사 카테고리 섹션 */
+@Composable
+fun ProceedCategorySection(
+ selectedTagIndex: Int = 0,
+ tagItemClick: (String) -> Unit,
+ categories: List,
+ items: List,
+) {
+ // 제목
+ QuackText(
+ modifier = Modifier.padding(start = 16.dp),
+ text = stringResource(
+ id = R.string.home_proceed_category_title,
+ username,
+ ),
+ typography = QuackTypography.HeadLine1,
+ )
+
+ // 공백
+ Spacer(modifier = Modifier.height(14.dp))
+
+ // 카테고리 목록
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ contentPadding = PaddingValues(horizontal = 16.dp),
+ ) {
+ itemsIndexed(items = categories) { index, tagName ->
+ QuackTag(
+ text = tagName,
+ style = QuackTagStyle.Outlined,
+ onClick = {
+ tagItemClick(tagName)
+ },
+ selected = index == selectedTagIndex,
+ )
+ }
+ }
+
+ // 공백
+ Spacer(modifier = Modifier.height(14.dp))
+
+ // 카테고리에 해당하는 덕력고사 목록
+ Column {
+ items.forEach { item ->
+ ProceedCategoryItemView(categoryItem = item)
+ }
+ }
+
+ // 전체보기 버튼
+ ProceedViewAllButton(onViewAllClick = {})
+
+ // 공백
+ Spacer(modifier = Modifier.height(20.dp))
+}
+
+/** 카테고리별 뷰[ProceedCategorySection]에 보이는 Item 뷰 */
+@Composable
+fun ProceedCategoryItemView(categoryItem: String) {
+ Row(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ // 덕퀴즈/덕질고사 썸네일 이미지
+ AsyncImage(
+ model = categoryThumbnailUrl,
+ modifier = Modifier
+ .width(85.dp)
+ .clip(RoundedCornerShape(8.dp))
+ .aspectRatio(ratio = GetHeightRatioW85H63),
+ contentScale = ContentScale.FillBounds,
+ contentDescription = null,
+ )
+
+ // 덕력고사 정보 & 참여율
+ Column(modifier = Modifier.padding(start = 8.dp)) {
+ // 덕력고사 이름
+ QuackText(text = title, typography = QuackTypography.HeadLine2)
+
+ // 공백
+ Spacer(modifier = Modifier.height(4.dp))
+
+ Row(horizontalArrangement = Arrangement.Center) {
+ // 프로필 이미지
+ QuackProfileImage(
+ modifier = Modifier.size(DpSize(width = 16.dp, height = 16.dp)),
+ profileUrl = profileImageUrl,
+ )
+
+ // 닉네임 + 참여 인원수
+ QuackText(
+ modifier = Modifier.padding(start = 4.dp),
+ text = stringResource(
+ R.string.home_proceed_item_info,
+ nickname,
+ stringResource(id = R.string.home_proceed_item_join_count, joinCount),
+ ),
+ typography = QuackTypography.Body2.change(color = QuackColor.Gray1),
+ )
+ }
+ }
+ }
+
+ // 공백
+ Spacer(modifier = Modifier.height(16.dp))
+}
+
+/** 전체 보기 버튼 */
+@Composable
+private fun ProceedViewAllButton(onViewAllClick: () -> Unit) {
+ Row(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .fillMaxWidth()
+ .clip(RoundedCornerShape(size = 8.dp))
+ .quackClickable(onClick = onViewAllClick)
+ .border(
+ width = 1.dp,
+ color = QuackColor.Gray3.value,
+ shape = RoundedCornerShape(8.dp),
+ )
+ .padding(vertical = 12.dp),
+ horizontalArrangement = Arrangement.Center,
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ QuackBody1(
+ text = stringResource(id = R.string.home_proceed_view_all_button_title),
+ )
+
+ Spacer(space = 4.dp)
+
+ QuackImage(
+ modifier = Modifier.size(DpSize(width = 24.dp, height = 24.dp)),
+ src = QuackIcon.ArrowRightId,
+ )
+ }
+}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingExamScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingExamScreen.kt
index 9d2f6fbb4..3236f9ece 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingExamScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingExamScreen.kt
@@ -40,29 +40,30 @@ import androidx.compose.ui.unit.dp
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.itemsIndexed
import coil.compose.AsyncImage
+import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.collectAndHandleState
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.quack.QuackProfileImage
+import team.duckie.app.android.common.compose.ui.skeleton
import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.component.HomeTopAppBar
import team.duckie.app.android.feature.home.constants.HomeStep
+import team.duckie.app.android.feature.home.constants.MainScreenType
import team.duckie.app.android.feature.home.viewmodel.home.HomeState
import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
-import team.duckie.app.android.common.compose.ui.Spacer
-import team.duckie.app.android.common.compose.ui.skeleton
-import team.duckie.app.android.common.compose.activityViewModel
-import team.duckie.app.android.common.compose.collectAndHandleState
-import team.duckie.app.android.common.compose.ui.quack.QuackProfileImage
-import team.duckie.app.android.feature.home.constants.MainScreenType
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackBody3
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackSubtitle2
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackBody2
+import team.duckie.quackquack.ui.sugar.QuackBody3
+import team.duckie.quackquack.ui.sugar.QuackSubtitle2
internal const val ThumbnailRatio = 4f / 3f
private val HomeProfileSize: DpSize = DpSize(
- all = 24.dp,
+ width = 32.dp,
+ height = 32.dp,
)
@Composable
@@ -73,6 +74,7 @@ internal fun HomeRecommendFollowingExamScreen(
state: HomeState,
navigateToCreateProblem: () -> Unit,
navigateToHomeDetail: (Int) -> Unit,
+ navigateToProfile: (Int) -> Unit,
) {
val followingExam =
vm.followingExam.collectAndHandleState(vm::handleLoadRecommendFollowingState)
@@ -105,6 +107,7 @@ internal fun HomeRecommendFollowingExamScreen(
followingExam = followingExam,
navigateToCreateProblem = navigateToCreateProblem,
navigateToHomeDetail = navigateToHomeDetail,
+ navigateToProfile = navigateToProfile,
)
}
}
@@ -118,6 +121,7 @@ private fun HomeRecommendFollowingSuccessScreen(
followingExam: LazyPagingItems,
navigateToCreateProblem: () -> Unit,
navigateToHomeDetail: (Int) -> Unit,
+ navigateToProfile: (Int) -> Unit,
) {
Box(
modifier = Modifier
@@ -136,6 +140,7 @@ private fun HomeRecommendFollowingSuccessScreen(
vm.changedHomeScreen(HomeStep.toStep(step))
},
onClickedCreate = navigateToCreateProblem,
+ onClickedNotice = {},
)
}
@@ -148,10 +153,10 @@ private fun HomeRecommendFollowingSuccessScreen(
title = maker?.title ?: "",
tier = maker?.owner?.tier ?: "",
favoriteTag = maker?.owner?.favoriteTag ?: "",
- onClickUserProfile = {
- // TODO(limsaehyun): 마이페이지로 이동
+ onUserProfileClick = {
+ navigateToProfile(maker?.owner?.userId ?: 0)
},
- onClickTestCover = {
+ onTestClick = {
navigateToHomeDetail(maker?.examId ?: 0)
},
cover = maker?.coverUrl ?: "",
@@ -176,9 +181,9 @@ private fun TestCoverWithMaker(
name: String,
tier: String,
favoriteTag: String,
- onClickTestCover: () -> Unit,
- onClickUserProfile: () -> Unit,
+ onTestClick: () -> Unit,
isLoading: Boolean,
+ onUserProfileClick: () -> Unit,
) {
Column(
modifier = modifier,
@@ -188,9 +193,7 @@ private fun TestCoverWithMaker(
.aspectRatio(ThumbnailRatio)
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
- .quackClickable {
- onClickTestCover()
- }
+ .quackClickable(onClick = onTestClick)
.skeleton(isLoading),
model = cover,
contentDescription = null,
@@ -203,8 +206,9 @@ private fun TestCoverWithMaker(
name = name,
tier = tier,
favoriteTag = favoriteTag,
- onClickUserProfile = onClickUserProfile,
isLoading = isLoading,
+ onUserProfileClick = onUserProfileClick,
+ onLayoutClick = onTestClick,
)
}
}
@@ -218,17 +222,22 @@ private fun TestMakerLayout(
tier: String,
favoriteTag: String,
isLoading: Boolean,
- onClickUserProfile: (() -> Unit)? = null,
+ onLayoutClick: () -> Unit,
+ onUserProfileClick: () -> Unit,
) {
Row(
- modifier = modifier,
+ modifier = modifier.quackClickable(
+ rippleEnabled = false,
+ ) {
+ onLayoutClick()
+ },
verticalAlignment = Alignment.CenterVertically,
) {
QuackProfileImage(
modifier = Modifier.skeleton(isLoading),
profileUrl = profileImageUrl,
size = HomeProfileSize,
- onClick = onClickUserProfile,
+ onClick = onUserProfileClick,
)
Column(modifier = Modifier.padding(start = 8.dp)) {
QuackSubtitle2(
@@ -241,10 +250,10 @@ private fun TestMakerLayout(
text = name,
)
Spacer(modifier = Modifier.width(8.dp))
- QuackBody3(
+ QuackText(
modifier = Modifier.skeleton(isLoading),
text = "$tier · $favoriteTag",
- color = QuackColor.Gray2,
+ typography = QuackTypography.Body3.change(color = QuackColor.Gray2),
)
}
}
@@ -271,11 +280,12 @@ private fun HomeFollowingExamNotFoundScreen(
onClickedCreate = {
navigateToCreateProblem()
},
+ onClickedNotice = {},
)
Spacer(space = 60.dp)
- QuackSubtitle(
+ QuackText(
text = stringResource(id = R.string.home_following_exam_not_found_title),
- align = TextAlign.Center,
+ typography = QuackTypography.Subtitle.change(textAlign = TextAlign.Center),
)
Spacer(space = 12.dp)
QuackBody2(text = stringResource(id = R.string.home_following_exam_not_found_subtitle))
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingScreen.kt
index e4eda5707..53e64dedc 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendFollowingScreen.kt
@@ -24,18 +24,19 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.ui.QuackMaxWidthDivider
+import team.duckie.app.android.common.kotlin.fastForEach
import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.component.HomeTopAppBar
import team.duckie.app.android.feature.home.constants.HomeStep
+import team.duckie.app.android.feature.home.constants.MainScreenType
import team.duckie.app.android.feature.home.viewmodel.home.HomeState
import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
-import team.duckie.app.android.common.compose.ui.QuackMaxWidthDivider
-import team.duckie.app.android.common.compose.ui.UserFollowingLayout
-import team.duckie.app.android.common.compose.activityViewModel
-import team.duckie.app.android.common.kotlin.fastForEach
-import team.duckie.app.android.feature.home.constants.MainScreenType
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackTitle2
+import team.duckie.app.android.common.compose.ui.content.UserFollowingLayout
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackTitle2
@Composable
internal fun HomeRecommendFollowingScreen(
@@ -60,6 +61,7 @@ internal fun HomeRecommendFollowingScreen(
vm.changedHomeScreen(HomeStep.toStep(step))
},
onClickedCreate = navigateToCreateProblem,
+ onClickedNotice = {},
)
}
@@ -70,9 +72,9 @@ internal fun HomeRecommendFollowingScreen(
.height(213.dp),
contentAlignment = Alignment.Center,
) {
- QuackSubtitle(
+ QuackText(
text = stringResource(id = R.string.home_following_initial_title),
- align = TextAlign.Center,
+ typography = QuackTypography.Subtitle.change(textAlign = TextAlign.Center),
)
}
}
@@ -115,13 +117,13 @@ private fun HomeFollowingInitialRecommendUsers(
recommendUser.fastForEach { user ->
UserFollowingLayout(
userId = user.userId,
- isMine = myUserId == user.userId,
+ visibleTrailingButton = myUserId != user.userId,
profileImgUrl = user.profileImgUrl,
nickname = user.nickname,
favoriteTag = user.favoriteTag,
tier = user.tier,
isFollowing = user.isFollowing,
- onClickFollow = {
+ onClickTrailingButton = {
onClickFollowing(user.userId, it)
},
)
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendScreen.kt
index cf7469fcd..1da5fde76 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeRecommendScreen.kt
@@ -8,6 +8,7 @@
@file:OptIn(
ExperimentalMaterialApi::class,
ExperimentalFoundationApi::class,
+ ExperimentalQuackQuackApi::class,
)
package team.duckie.app.android.feature.home.screen.home
@@ -27,6 +28,7 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
@@ -34,6 +36,10 @@ import androidx.compose.material.pullrefresh.PullRefreshIndicator
import androidx.compose.material.pullrefresh.pullRefresh
import androidx.compose.material.pullrefresh.rememberPullRefreshState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.derivedStateOf
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -47,12 +53,16 @@ import coil.compose.AsyncImage
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.NonCancellable
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.withContext
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.ui.DuckExamSmallCover
import team.duckie.app.android.common.compose.ui.DuckTestCoverItem
import team.duckie.app.android.common.compose.ui.DuckieHorizontalPagerIndicator
import team.duckie.app.android.common.compose.ui.quack.todo.QuackAnnotatedText
import team.duckie.app.android.common.compose.ui.skeleton
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.common.kotlin.addHashTag
import team.duckie.app.android.domain.exam.model.Exam
import team.duckie.app.android.domain.recommendation.model.ExamType
@@ -61,13 +71,14 @@ import team.duckie.app.android.feature.home.component.HomeTopAppBar
import team.duckie.app.android.feature.home.constants.HomeStep
import team.duckie.app.android.feature.home.viewmodel.home.HomeState
import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
-import team.duckie.quackquack.ui.component.QuackBody1
-import team.duckie.quackquack.ui.component.QuackBody3
-import team.duckie.quackquack.ui.component.QuackLarge1
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackBody3
+import team.duckie.quackquack.ui.sugar.QuackLarge1
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private val HomeHorizontalPadding = PaddingValues(horizontal = 16.dp)
+private const val JumbotronSwipeInterval = 3000L
@Composable
internal fun HomeRecommendScreen(
@@ -79,7 +90,33 @@ internal fun HomeRecommendScreen(
navigateToSearch: (String) -> Unit,
openExamBottomSheet: (Int) -> Unit,
) {
- val pageState = rememberPagerState()
+ val pageState = rememberPagerState(
+ pageCount = { state.jumbotrons.size },
+ initialPage = state.jumbotronPage,
+ )
+
+ val isLocateMiddle by remember {
+ derivedStateOf {
+ pageState.currentPageOffsetFraction != 0.0f
+ }
+ }
+
+ LaunchedEffect(key1 = Unit) {
+ if (isLocateMiddle) {
+ pageState.scrollToPage(state.jumbotronPage)
+ }
+ }
+
+ LaunchedEffect(key1 = pageState.currentPage) {
+ delay(JumbotronSwipeInterval)
+ withContext(NonCancellable) {
+ if (pageState.isLastPage) {
+ pageState.animateScrollToPage(0.also(homeViewModel::saveJumbotronPage))
+ } else {
+ pageState.animateScrollToPage((pageState.currentPage + 1).also(homeViewModel::saveJumbotronPage))
+ }
+ }
+ }
val lazyRecommendations = homeViewModel.recommendations.collectAsLazyPagingItems()
@@ -91,8 +128,7 @@ internal fun HomeRecommendScreen(
)
Box(
- modifier = Modifier
- .pullRefresh(pullRefreshState),
+ modifier = Modifier.pullRefresh(pullRefreshState),
) {
LazyColumn(
modifier = modifier
@@ -110,18 +146,16 @@ internal fun HomeRecommendScreen(
onClickedCreate = {
navigateToCreateProblem()
},
+ onClickedNotice = {},
)
}
item {
- HorizontalPager(
- pageCount = state.jumbotrons.size,
- state = pageState,
- ) { page ->
+ HorizontalPager(state = pageState) { page ->
HomeRecommendJumbotronLayout(
modifier = Modifier
.padding(HomeHorizontalPadding),
- recommendItem = state.jumbotrons[page],
+ item = state.jumbotrons.getOrNull(page % state.jumbotrons.size),
onStartClicked = { examId ->
navigateToHomeDetail(examId)
},
@@ -167,13 +201,16 @@ internal fun HomeRecommendScreen(
}
}
+private val PagerState.isLastPage: Boolean
+ get() = currentPage == pageCount - 1
+
@Composable
private fun HomeRecommendJumbotronLayout(
modifier: Modifier = Modifier,
- recommendItem: HomeState.HomeRecommendJumbotron,
+ item: HomeState.HomeRecommendJumbotron?,
onStartClicked: (Int) -> Unit,
isLoading: Boolean,
-) {
+) = item?.let { recommendItem ->
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
@@ -194,18 +231,19 @@ private fun HomeRecommendJumbotronLayout(
text = recommendItem.title,
)
Spacer(modifier = Modifier.height(12.dp))
- QuackBody1(
+ QuackText(
modifier = Modifier.skeleton(isLoading),
text = recommendItem.content,
- align = TextAlign.Center,
+ typography = QuackTypography.Body1.change(textAlign = TextAlign.Center),
)
Spacer(modifier = Modifier.height(24.dp))
- QuackLargeButton(
- modifier = Modifier.skeleton(isLoading),
- type = QuackLargeButtonType.Fill,
+
+ TempFlexiblePrimaryLargeButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .skeleton(isLoading),
text = recommendItem.buttonContent,
onClick = { onStartClicked(recommendItem.examId) },
- enabled = true,
)
Spacer(modifier = Modifier.height(8.dp))
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeScreen.kt
index 2c78cb521..95b13e3e6 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/home/HomeScreen.kt
@@ -25,15 +25,15 @@ import androidx.compose.ui.unit.dp
import kotlinx.coroutines.launch
import okhttp3.internal.immutableListOf
import org.orbitmvi.orbit.compose.collectAsState
-import team.duckie.app.android.feature.home.constants.HomeStep
-import team.duckie.app.android.feature.home.viewmodel.home.HomeSideEffect
-import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
+import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
import team.duckie.app.android.common.compose.ui.ErrorScreen
import team.duckie.app.android.common.compose.ui.dialog.DuckieSelectableBottomSheetDialog
-import team.duckie.app.android.common.compose.ui.quack.QuackCrossfade
-import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
import team.duckie.app.android.common.compose.ui.dialog.DuckieSelectableType
+import team.duckie.app.android.common.compose.ui.quack.QuackCrossfade
+import team.duckie.app.android.feature.home.constants.HomeStep
import team.duckie.app.android.feature.home.constants.MainScreenType
+import team.duckie.app.android.feature.home.viewmodel.home.HomeSideEffect
+import team.duckie.app.android.feature.home.viewmodel.home.HomeViewModel
private val HomeHorizontalPadding = PaddingValues(horizontal = 16.dp)
@@ -47,6 +47,7 @@ internal fun HomeScreen(
navigateToCreateProblem: () -> Unit,
navigateToHomeDetail: (Int) -> Unit,
navigateToSearch: (String) -> Unit,
+ navigateToProfile: (Int) -> Unit,
) {
val state = vm.collectAsState().value
val bottomSheetDialogState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
@@ -106,6 +107,20 @@ internal fun HomeScreen(
},
)
+ page == HomeStep.HomeProceedScreen -> HomeProceedScreen(
+ state = state,
+ homeViewModel = vm,
+ navigateToCreateProblem = navigateToCreateProblem,
+ navigateToHomeDetail = navigateToHomeDetail,
+ navigateToSearch = navigateToSearch,
+ openExamBottomSheet = { exam ->
+ setTargetExamId(exam)
+ coroutineScope.launch {
+ bottomSheetDialogState.show()
+ }
+ },
+ )
+
page == HomeStep.HomeFollowingScreen -> if (state.isFollowingExist) {
HomeRecommendFollowingExamScreen(
initState = initState,
@@ -113,6 +128,7 @@ internal fun HomeScreen(
state = state,
navigateToHomeDetail = navigateToHomeDetail,
navigateToCreateProblem = navigateToCreateProblem,
+ navigateToProfile = navigateToProfile,
)
} else {
HomeRecommendFollowingScreen(
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/mypage/MypageScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/mypage/MypageScreen.kt
index 0f05aa9db..9d8e1ec4c 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/mypage/MypageScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/mypage/MypageScreen.kt
@@ -44,7 +44,7 @@ import team.duckie.app.android.feature.home.viewmodel.mypage.MyPageViewModel
import team.duckie.app.android.feature.profile.screen.MyProfileScreen
import team.duckie.app.android.feature.profile.viewmodel.state.ExamType
import team.duckie.app.android.feature.profile.viewmodel.state.ProfileStep
-import team.duckie.quackquack.ui.color.QuackColor
+import team.duckie.quackquack.material.QuackColor
@Composable
internal fun MyPageScreen(
@@ -147,7 +147,7 @@ internal fun MyPageScreen(
MyProfileScreen(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor),
+ .background(color = QuackColor.White.value),
userProfile = state.userProfile,
isLoading = state.isLoading,
onClickSetting = viewModel::clickSetting,
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamSection.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamSection.kt
index 00ddd7eb4..ce6415206 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamSection.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamSection.kt
@@ -41,15 +41,15 @@ import team.duckie.app.android.common.compose.ui.DuckExamSmallCoverForColumn
import team.duckie.app.android.common.compose.ui.DuckTestCoverItem
import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.common.compose.ui.TextTabLayout
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackOutLinedSingeLazyRowTag
import team.duckie.app.android.common.compose.ui.skeleton
import team.duckie.app.android.common.kotlin.fastMap
import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.viewmodel.ranking.RankingViewModel
+import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackSingeLazyRowTag
-import team.duckie.quackquack.ui.component.QuackTagType
-import team.duckie.quackquack.ui.component.QuackTitle2
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackTitle2
import team.duckie.quackquack.material.QuackColor as QuackV2Color
@Composable
@@ -78,13 +78,13 @@ internal fun ExamSection(
.padding(top = 10.dp),
) {
// TODO:(EvergreenTree97) 태그의 inner padding이 바뀌어야 함
- QuackSingeLazyRowTag(
+ QuackOutLinedSingeLazyRowTag(
modifier = Modifier
.fillMaxWidth()
.skeleton(state.isTagLoading),
items = tagNames,
itemSelections = state.tagSelections,
- tagType = QuackTagType.Circle(),
+ trailingIcon = null,
onClick = viewModel::changeSelectedTags,
)
Spacer(space = 28.dp)
@@ -196,13 +196,13 @@ private fun RankingEdge(rank: Int) {
),
contentAlignment = Alignment.Center,
) {
- QuackTitle2(
+ QuackText(
modifier = Modifier.padding(
horizontal = 9.dp,
vertical = 2.dp,
),
- color = QuackColor.White,
text = rank.toString(),
+ typography = QuackTypography.Title2.change(color = QuackColor.White),
)
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamineeSection.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamineeSection.kt
index 1628f6013..1975f0442 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamineeSection.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/ExamineeSection.kt
@@ -16,28 +16,28 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.material.Divider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import team.duckie.app.android.common.compose.itemsIndexedPagingKey
+import team.duckie.app.android.common.compose.ui.DuckieDivider
import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.common.compose.ui.quack.QuackProfileImage
import team.duckie.app.android.common.compose.ui.skeleton
import team.duckie.app.android.domain.user.model.User
import team.duckie.app.android.feature.home.viewmodel.ranking.RankingViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackTitle2
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.sugar.QuackBody2
+import team.duckie.quackquack.ui.sugar.QuackSubtitle
+import team.duckie.quackquack.ui.sugar.QuackTitle2
@Composable
internal fun ExamineeSection(
@@ -104,7 +104,10 @@ private fun ExamineeContent(
QuackProfileImage(
modifier = Modifier.skeleton(isLoading),
profileUrl = profileImageUrl,
- size = DpSize(all = 44.dp),
+ size = DpSize(
+ width = 44.dp,
+ height = 44.dp,
+ ),
)
Spacer(space = 8.dp)
QuackTitle2(
@@ -127,6 +130,6 @@ private fun ExamineeContent(
}
}
}
- Divider(color = QuackColor.Gray4.composeColor)
+ DuckieDivider(color = QuackColor.Gray4)
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/RankingScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/RankingScreen.kt
index 7e8fd4622..44d582b19 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/RankingScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/ranking/RankingScreen.kt
@@ -44,16 +44,17 @@ import com.google.firebase.ktx.Firebase
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
+import team.duckie.app.android.common.compose.ui.ErrorScreen
+import team.duckie.app.android.common.compose.ui.dialog.DuckieSelectableBottomSheetDialog
+import team.duckie.app.android.common.kotlin.AllowMagicNumber
+import team.duckie.app.android.common.kotlin.fastForEach
import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.component.HeadLineTopAppBar
+import team.duckie.app.android.feature.home.constants.MainScreenType
import team.duckie.app.android.feature.home.constants.RankingPage
import team.duckie.app.android.feature.home.viewmodel.ranking.RankingSideEffect
import team.duckie.app.android.feature.home.viewmodel.ranking.RankingViewModel
-import team.duckie.app.android.common.compose.ui.ErrorScreen
-import team.duckie.app.android.common.compose.ui.dialog.DuckieSelectableBottomSheetDialog
-import team.duckie.app.android.common.kotlin.AllowMagicNumber
-import team.duckie.app.android.feature.home.constants.MainScreenType
-import team.duckie.quackquack.ui.component.QuackMainTab
+import team.duckie.quackquack.ui.QuackTab
@Composable
internal fun RankingScreen(
@@ -73,7 +74,10 @@ internal fun RankingScreen(
context.getString(R.string.exam),
)
}
- val pagerState = rememberPagerState(initialPage = state.selectedTab)
+ val pagerState = rememberPagerState(
+ initialPage = state.selectedTab,
+ pageCount = { tabs.size },
+ )
val coroutineScope = rememberCoroutineScope()
val lazyListState = rememberLazyListState()
val lazyGridState = rememberLazyGridState()
@@ -171,20 +175,19 @@ internal fun RankingScreen(
onRetryClick = viewModel::refresh,
)
} else {
- QuackMainTab(
- titles = tabs,
- selectedTabIndex = state.selectedTab,
- onTabSelected = {
- viewModel.setSelectedTab(it)
- coroutineScope.launch {
- pagerState.animateScrollToPage(it)
+ QuackTab(index = state.selectedTab) {
+ tabs.fastForEach { label ->
+ tab(label) { index ->
+ viewModel.setSelectedTab(index)
+ coroutineScope.launch {
+ pagerState.animateScrollToPage(index)
+ }
}
- },
- )
+ }
+ }
HorizontalPager(
modifier = Modifier.fillMaxSize(),
state = pagerState,
- pageCount = tabs.size,
key = { tabs[it] },
) { page ->
when (page) {
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/search/SearchMainScreen.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/search/SearchMainScreen.kt
index 29d91f7dd..6efa25ac0 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/search/SearchMainScreen.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/screen/search/SearchMainScreen.kt
@@ -5,19 +5,27 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-@file:OptIn(ExperimentalMaterialApi::class)
+@file:OptIn(
+ ExperimentalMaterialApi::class,
+ ExperimentalLayoutApi::class,
+ ExperimentalQuackQuackApi::class,
+)
package team.duckie.app.android.feature.home.screen.search
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.ExperimentalLayoutApi
+import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi
@@ -34,26 +42,27 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
-import team.duckie.app.android.feature.home.R
-import team.duckie.app.android.feature.home.viewmodel.MainViewModel
import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.ui.icon.v1.SearchId
import team.duckie.app.android.common.kotlin.AllowMagicNumber
+import team.duckie.app.android.feature.home.R
import team.duckie.app.android.feature.home.constants.MainScreenType
+import team.duckie.app.android.feature.home.viewmodel.MainViewModel
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackTag
+import team.duckie.quackquack.ui.QuackTagStyle
import team.duckie.quackquack.ui.QuackText
-
import team.duckie.quackquack.ui.sugar.QuackTitle2
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackLazyVerticalGridTag
-import team.duckie.quackquack.ui.component.QuackTagType
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private val SearchScreenHorizontalPaddingDp = 16.dp
@@ -102,12 +111,16 @@ internal fun SearchMainScreen(
verticalAlignment = Alignment.CenterVertically,
) {
QuackImage(
- src = QuackIcon.Search,
- size = DpSize(24.dp),
+ modifier = Modifier.size(DpSize(width = 24.dp, height = 24.dp)),
+ src = QuackIcon.SearchId,
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
+ .quackClickable(
+ rippleEnabled = false,
+ onClick = vm::navigateToSearch,
+ )
.fillMaxWidth()
.height(36.dp)
.background(
@@ -116,11 +129,6 @@ internal fun SearchMainScreen(
size = 8.dp,
),
)
- .quackClickable(
- rippleEnabled = false,
- ) {
- vm.navigateToSearch()
- }
.padding(start = 12.dp),
contentAlignment = Alignment.CenterStart,
) {
@@ -132,22 +140,30 @@ internal fun SearchMainScreen(
)
}
}
- Spacer(modifier = Modifier.height(22.dp))
+ Spacer(modifier = Modifier.height(16.dp))
QuackTitle2(
modifier = Modifier.padding(start = SearchScreenHorizontalPaddingDp),
text = stringResource(id = R.string.popular_tag),
)
- Spacer(modifier = Modifier.height(8.dp))
- // TODO(limsaehyun): 추후 꽥꽥에서, 전체 너비만큼 태그 Composable 을 넣을 수 있는 Composable 적용 필요
- QuackLazyVerticalGridTag(
+ Spacer(modifier = Modifier.height(12.dp))
+ FlowRow(
modifier = Modifier.padding(horizontal = SearchScreenHorizontalPaddingDp),
- items = state.popularTags.map { it.name },
- tagType = QuackTagType.Round,
- onClick = { index ->
- vm.navigateToSearch(searchTag = state.popularTags[index].name)
- },
- itemChunkedSize = 5,
- )
+ horizontalArrangement = Arrangement.spacedBy(8.dp),
+ verticalArrangement = Arrangement.spacedBy(12.dp),
+ ) {
+ state.popularTags.forEach { tag ->
+ QuackTag(
+ text = tag.name,
+ style = QuackTagStyle.Filled,
+ selected = false,
+ ) {
+ vm.navigateToSearch(
+ searchTag = tag.name,
+ autoFocusing = false,
+ )
+ }
+ }
+ }
}
PullRefreshIndicator(
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainSideEffect.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainSideEffect.kt
index d73866cca..17462f146 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainSideEffect.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainSideEffect.kt
@@ -22,7 +22,7 @@ internal sealed class MainSideEffect {
/**
* [SearchResultActivity] 로 이동하는 SideEffect 입니다.
*/
- class NavigateToSearch(val searchTag: String?) : MainSideEffect()
+ class NavigateToSearch(val searchTag: String?, val autoFocusing: Boolean) : MainSideEffect()
/**
* [HomeDetailActivity] 로 이동하는 SideEffect 입니다.
@@ -35,12 +35,14 @@ internal sealed class MainSideEffect {
object NavigateToSetting : MainSideEffect()
/**
- * [CreateProblemActivity] 로 이동하는 SideEffect 입니다.
+ * [CreateExamActivity] 로 이동하는 SideEffect 입니다.
*/
object NavigateToCreateProblem : MainSideEffect()
object NavigateToNotification : MainSideEffect()
+ data class NavigateToProfile(val userId: Int) : MainSideEffect()
+
object ClickRankingRetry : MainSideEffect()
class NavigateToFriends(val friendType: FriendsType, val myUserId: Int, val nickname: String) :
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainViewModel.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainViewModel.kt
index 670f1540a..d00d66599 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainViewModel.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/MainViewModel.kt
@@ -16,12 +16,12 @@ import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import team.duckie.app.android.common.android.ui.const.Extras
-import team.duckie.app.android.domain.report.usecase.ReportUseCase
-import team.duckie.app.android.domain.tag.usecase.FetchPopularTagsUseCase
-import team.duckie.app.android.feature.home.constants.BottomNavigationStep
import team.duckie.app.android.common.compose.ui.dialog.ReportAlreadyExists
import team.duckie.app.android.common.kotlin.FriendsType
import team.duckie.app.android.common.kotlin.exception.isReportAlreadyExists
+import team.duckie.app.android.domain.report.usecase.ReportUseCase
+import team.duckie.app.android.domain.tag.usecase.FetchPopularTagsUseCase
+import team.duckie.app.android.feature.home.constants.BottomNavigationStep
import team.duckie.app.android.feature.home.constants.MainScreenType
import javax.inject.Inject
@@ -136,8 +136,9 @@ internal class MainViewModel @Inject constructor(
/** 검색 화면으로 이동한다 */
fun navigateToSearch(
searchTag: String? = null,
+ autoFocusing: Boolean = true,
) = intent {
- postSideEffect(MainSideEffect.NavigateToSearch(searchTag))
+ postSideEffect(MainSideEffect.NavigateToSearch(searchTag, autoFocusing))
}
/** 홈 디테일 화면으로 이동한다 */
@@ -171,6 +172,10 @@ internal class MainViewModel @Inject constructor(
postSideEffect(MainSideEffect.NavigateToNotification)
}
+ fun navigateToProfile(userId: Int) = intent {
+ postSideEffect(MainSideEffect.NavigateToProfile(userId))
+ }
+
/** 온보딩(가이드) 활성화 여부를 업데이트한다 */
fun updateGuideVisible(visible: Boolean) = intent {
reduce { state.copy(guideVisible = visible) }
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeState.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeState.kt
index 38f294840..b74a5f056 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeState.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeState.kt
@@ -21,14 +21,17 @@ internal data class HomeState(
val isHomeRecommendLoading: Boolean = false,
val isHomeRecommendFollowingExamLoading: Boolean = false,
+ val isHomeProceedLoading: Boolean = false,
val isHomeRecommendFollowingExamRefreshLoading: Boolean = false,
val isHomeRecommendPullRefreshLoading: Boolean = false,
+ val isHomeProceedPullRefreshLoading: Boolean = false,
val homeSelectedIndex: HomeStep = HomeStep.HomeRecommendScreen,
val jumbotrons: ImmutableList = skeletonJumbotrons,
val recommendTopics: ImmutableList = persistentListOf(),
+ val jumbotronPage: Int = 0,
val isFollowingExist: Boolean = true,
val recommendFollowing: ImmutableList = persistentListOf(),
@@ -60,13 +63,14 @@ internal data class HomeState(
val profileImgUrl: String,
val favoriteTag: String,
val tier: String,
+ val userId: Int,
) {
/**
* [User] 의 Empty Model 입니다.
* 초기화 혹은 Skeleton UI 등에 필요한 Mock Data 로 쓰입니다.
*/
companion object {
- fun empty() = User("", "", "", "")
+ fun empty() = User("", "", "", "", 0)
}
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeViewModel.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeViewModel.kt
index abe5d29d8..a098a0f55 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeViewModel.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/home/HomeViewModel.kt
@@ -26,6 +26,9 @@ import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
+import team.duckie.app.android.common.kotlin.exception.isFollowingAlreadyExists
+import team.duckie.app.android.common.kotlin.exception.isFollowingNotFound
+import team.duckie.app.android.common.kotlin.fastMap
import team.duckie.app.android.domain.exam.model.Exam
import team.duckie.app.android.domain.follow.model.FollowBody
import team.duckie.app.android.domain.follow.usecase.FollowUseCase
@@ -40,9 +43,6 @@ import team.duckie.app.android.feature.home.constants.HomeStep
import team.duckie.app.android.feature.home.viewmodel.mapper.toFollowingModel
import team.duckie.app.android.feature.home.viewmodel.mapper.toJumbotronModel
import team.duckie.app.android.feature.home.viewmodel.mapper.toUiModel
-import team.duckie.app.android.common.kotlin.exception.isFollowingAlreadyExists
-import team.duckie.app.android.common.kotlin.exception.isFollowingNotFound
-import team.duckie.app.android.common.kotlin.fastMap
import javax.inject.Inject
@HiltViewModel
@@ -110,6 +110,12 @@ internal class HomeViewModel @Inject constructor(
}
}
+ fun saveJumbotronPage(page: Int) = intent {
+ reduce {
+ state.copy(jumbotronPage = page)
+ }
+ }
+
/**
* 팔로잉 추천 탭을 새로고침한다.
* [forceLoading] - PullRefresh 를 할 경우 사용자에게 새로고침이 됐음을 알리기 위한 최소한의 로딩 시간을 부여한다.
@@ -125,6 +131,21 @@ internal class HomeViewModel @Inject constructor(
}
}
+ /**
+ * 진행중 탭을 새로고침한다.
+ *
+ * [forceLoading] - PullRefresh 를 할 경우 사용자에게 새로고침이 됐음을 알리기 위한 최소한의 로딩 시간을 부여한다.
+ */
+ fun refreshProceeds(forceLoading: Boolean = false) {
+ viewModelScope.launch {
+ updateHomeProceedRefreshLoading(true)
+ // TODO(riflockle7): 진행중 시험 API 로직 필요
+ // fetchRecommendFollowingExam()
+ if (forceLoading) delay(pullToRefreshMinLoadingDelay)
+ updateHomeProceedRefreshLoading(false)
+ }
+ }
+
/** 홈 화면의 jumbotron을 가져온다. */
internal fun fetchJumbotrons() = intent {
startHomeRecommendLoading()
@@ -135,7 +156,7 @@ internal class HomeViewModel @Inject constructor(
isHomeRecommendLoading = false,
jumbotrons = jumbotrons
.fastMap(Exam::toJumbotronModel)
- .toPersistentList(),
+ .toImmutableList(),
)
}
}.onFailure { exception ->
@@ -288,4 +309,17 @@ internal class HomeViewModel @Inject constructor(
)
}
}
+
+ /** 홈 화면의 진행중 탭의 pull refresh 로딩 상태를 [loading]으로 바꾼다. */
+ private fun updateHomeProceedRefreshLoading(
+ loading: Boolean,
+ ) = intent {
+ reduce {
+ state.copy(
+ isHomeProceedPullRefreshLoading = loading,
+ isHomeProceedLoading = loading, // for skeleton UI
+ isError = false,
+ )
+ }
+ }
}
diff --git a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/mapper/mapper.kt b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/mapper/mapper.kt
index c7e7aa680..bf59c773d 100644
--- a/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/mapper/mapper.kt
+++ b/feature/home/src/main/kotlin/team/duckie/app/android/feature/home/viewmodel/mapper/mapper.kt
@@ -50,5 +50,6 @@ internal fun Exam.toFollowingModel() =
profileImgUrl = user?.profileImageUrl ?: "",
favoriteTag = user?.duckPower?.tag?.name ?: "",
tier = user?.duckPower?.tier ?: "",
+ userId = user?.id ?: 0,
),
)
diff --git a/feature/home/src/main/res/drawable/home_ic_notice.png b/feature/home/src/main/res/drawable/home_ic_notice.png
new file mode 100644
index 000000000..9b34af7d1
Binary files /dev/null and b/feature/home/src/main/res/drawable/home_ic_notice.png differ
diff --git a/feature/home/src/main/res/drawable/home_proceed_banner_right.png b/feature/home/src/main/res/drawable/home_proceed_banner_right.png
new file mode 100644
index 000000000..9c725dd1e
Binary files /dev/null and b/feature/home/src/main/res/drawable/home_proceed_banner_right.png differ
diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml
index ecb90714c..f29d0b502 100644
--- a/feature/home/src/main/res/values/strings.xml
+++ b/feature/home/src/main/res/values/strings.xml
@@ -19,6 +19,7 @@
팔로우
응시자
추천
+ 진행중
"관심있는 키워드를 검색해보세요!"
인기 태그
@@ -66,4 +67,15 @@
팔로우한 유저 중에\n아직 올라온 덕력고사가 없어요.
다른 유저들을 더 찾아보세요!
+
+
+ 출제 임박 덕력고사
+ 오픈까지 %s문제!
+ %s | %s
+ %s명 참여
+ %s/%s
+ 전체보기
+ 직접 덕력고사를 열고 싶다면?
+ 출제 제안하기
+ %s,\n이런 덕력고사도 있덕
diff --git a/feature/notification/build.gradle.kts b/feature/notification/build.gradle.kts
index 947c7415e..51df2b689 100644
--- a/feature/notification/build.gradle.kts
+++ b/feature/notification/build.gradle.kts
@@ -28,9 +28,10 @@ dependencies {
projects.common.compose,
libs.orbit.viewmodel,
libs.orbit.compose,
- libs.quack.ui.components,
libs.compose.lifecycle.runtime,
libs.compose.ui.material, // needs for CircularProgressIndicator
libs.firebase.crashlytics,
+ libs.quack.v2.ui,
+ libs.kotlin.collections.immutable,
)
}
diff --git a/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/NotificationActivity.kt b/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/NotificationActivity.kt
index c39f56244..e0d03fb23 100644
--- a/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/NotificationActivity.kt
+++ b/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/NotificationActivity.kt
@@ -22,13 +22,13 @@ import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import dagger.hilt.android.AndroidEntryPoint
import org.orbitmvi.orbit.viewmodel.observe
+import team.duckie.app.android.common.android.ui.BaseActivity
+import team.duckie.app.android.common.android.ui.finishWithAnimation
import team.duckie.app.android.feature.notification.screen.NotificationScreen
import team.duckie.app.android.feature.notification.viewmodel.NotificationSideEffect
import team.duckie.app.android.feature.notification.viewmodel.NotificationViewModel
import team.duckie.app.android.navigator.feature.home.HomeNavigator
-import team.duckie.app.android.common.android.ui.BaseActivity
-import team.duckie.app.android.common.android.ui.finishWithAnimation
-import team.duckie.quackquack.ui.color.QuackColor
+import team.duckie.quackquack.material.QuackColor
import javax.inject.Inject
@AndroidEntryPoint
@@ -48,11 +48,10 @@ class NotificationActivity : BaseActivity() {
NotificationScreen(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
.systemBarsPadding()
.navigationBarsPadding()
- .padding(top = 12.dp)
- .padding(horizontal = 12.dp),
+ .padding(horizontal = 16.dp),
)
}
viewModel.observe(
diff --git a/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/screen/NoticationScreen.kt b/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/screen/NoticationScreen.kt
index 5be73745a..db0f5f865 100644
--- a/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/screen/NoticationScreen.kt
+++ b/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/screen/NoticationScreen.kt
@@ -8,55 +8,46 @@
package team.duckie.app.android.feature.notification.screen
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.layout.statusBarsPadding
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import team.duckie.app.android.feature.notification.R
-import team.duckie.app.android.feature.notification.viewmodel.NotificationViewModel
-import team.duckie.app.android.common.compose.ui.ErrorScreen
import team.duckie.app.android.common.compose.ui.NoItemScreen
-import team.duckie.app.android.common.compose.ui.quack.QuackCrossfade
+import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.common.compose.ui.skeleton
-import team.duckie.app.android.common.compose.activityViewModel
-import team.duckie.app.android.common.kotlin.getDiffDayFromToday
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackBody3
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.shape.SquircleShape
-import team.duckie.quackquack.ui.util.DpSize
-import java.util.Date
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.material.shape.SquircleShape
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.sugar.QuackBody2
+import team.duckie.quackquack.ui.sugar.QuackBody3
@Composable
internal fun NotificationScreen(
modifier: Modifier = Modifier,
- viewModel: NotificationViewModel = activityViewModel(),
+ // viewModel: NotificationViewModel = activityViewModel(),
) {
- val state by viewModel.container.stateFlow.collectAsStateWithLifecycle()
-
- LaunchedEffect(Unit) {
- viewModel.getNotifications()
- }
+ // val state by viewModel.container.stateFlow.collectAsStateWithLifecycle()
Column(modifier = modifier) {
- QuackTopAppBar(
- leadingIcon = QuackIcon.ArrowBack,
+ // TODO(EvergreenTree97) : 알림 화면 구현 후 제거
+ Column(
+ modifier = Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Spacer(space = 120.dp)
+ NoItemScreen(
+ title = "아직 알림기능이 준비중입니다",
+ description = "조금만 기다려주세요:)",
+ )
+ }
+ /*QuackTopAppBar(
+ leadingIcon = QuackIcon.Outlined.ArrowBack,
leadingText = stringResource(id = R.string.notification),
onLeadingIconClick = viewModel::clickBackPress,
)
@@ -74,10 +65,12 @@ internal fun NotificationScreen(
}
isEmpty -> {
- Box(
+ Column(
modifier = Modifier.fillMaxSize(),
- contentAlignment = Alignment.Center,
+ verticalArrangement = Arrangement.Top,
+ horizontalAlignment = Alignment.CenterHorizontally,
) {
+ Spacer(space = 120.dp)
NoItemScreen(
title = stringResource(id = R.string.empty_notfications),
description = stringResource(id = R.string.check_notifications_after_activity),
@@ -110,15 +103,16 @@ internal fun NotificationScreen(
}
}
}
- }
+ }*/
}
}
+@Suppress("UnusedPrivateMember")
@Composable
private fun NotificationItem(
thumbnailUrl: String,
body: String,
- createdAt: Date,
+ createdAt: String,
isLoading: Boolean,
onClick: () -> Unit,
) {
@@ -129,12 +123,13 @@ private fun NotificationItem(
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
QuackImage(
- modifier = Modifier.skeleton(
- visible = isLoading,
- shape = SquircleShape,
- ),
+ modifier = Modifier
+ .size(36.dp)
+ .skeleton(
+ visible = isLoading,
+ shape = SquircleShape,
+ ),
src = thumbnailUrl,
- size = DpSize(all = 36.dp),
)
Column(
modifier = Modifier.fillMaxWidth(),
@@ -146,7 +141,7 @@ private fun NotificationItem(
)
QuackBody3(
modifier = Modifier.skeleton(visible = isLoading),
- text = createdAt.getDiffDayFromToday(),
+ text = createdAt,
)
}
}
diff --git a/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/viewmodel/NotificationViewModel.kt b/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/viewmodel/NotificationViewModel.kt
index 27e21187e..bb17fb18a 100644
--- a/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/viewmodel/NotificationViewModel.kt
+++ b/feature/notification/src/main/kotlin/team/duckie/app/android/feature/notification/viewmodel/NotificationViewModel.kt
@@ -9,6 +9,7 @@ package team.duckie.app.android.feature.notification.viewmodel
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
@@ -29,27 +30,40 @@ internal class NotificationViewModel @Inject constructor(
override val container =
container(NotificationState())
- fun getNotifications() = intent {
- startLoading()
- getNotificationsUseCase().onSuccess { notifications ->
+ init {
+ // TODO(EvergreenTree97) : 알림 기능 구현 후 제거
+ intent {
reduce {
state.copy(
isLoading = false,
- isError = false,
- notifications = notifications.toImmutableList(),
- )
- }
- }.onFailure {
- reduce {
- state.copy(
- isLoading = false,
- isError = true,
- notifications = emptyList().toImmutableList(),
+ notifications = persistentListOf(),
)
}
}
}
+ fun getNotifications() = intent {
+ startLoading()
+ getNotificationsUseCase()
+ .onSuccess { notifications ->
+ reduce {
+ state.copy(
+ isLoading = false,
+ isError = false,
+ notifications = notifications.toImmutableList(),
+ )
+ }
+ }.onFailure {
+ reduce {
+ state.copy(
+ isLoading = false,
+ isError = true,
+ notifications = emptyList().toImmutableList(),
+ )
+ }
+ }
+ }
+
fun clickBackPress() = intent { postSideEffect(NotificationSideEffect.FinishActivity) }
fun clickNotification(id: Int) = intent {
diff --git a/feature/onboard/build.gradle.kts b/feature/onboard/build.gradle.kts
index 0b04a5fc5..9b980e926 100644
--- a/feature/onboard/build.gradle.kts
+++ b/feature/onboard/build.gradle.kts
@@ -33,8 +33,10 @@ dependencies {
libs.orbit.compose,
libs.ktx.lifecycle.runtime,
libs.compose.ui.material, // needs for ModalBottomSheet
+ libs.compose.ui.foundation,
libs.compose.lifecycle.runtime,
libs.compose.ui.accompanist.flowlayout,
- libs.quack.ui.components,
+ libs.kotlin.collections.immutable,
+ libs.quack.v2.ui,
)
}
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/OnboardActivity.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/OnboardActivity.kt
index 2569c5e4c..97553a60d 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/OnboardActivity.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/OnboardActivity.kt
@@ -14,6 +14,7 @@ import androidx.activity.addCallback
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.getValue
@@ -49,9 +50,8 @@ import team.duckie.app.android.feature.onboard.screen.TagScreen
import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
import team.duckie.app.android.feature.onboard.viewmodel.sideeffect.OnboardSideEffect
import team.duckie.app.android.feature.onboard.viewmodel.state.OnboardState
-import team.duckie.quackquack.ui.animation.QuackAnimatedContent
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.theme.QuackTheme
import javax.inject.Inject
@AndroidEntryPoint
@@ -129,11 +129,12 @@ class OnboardActivity : BaseActivity() {
setContent {
QuackTheme {
- QuackAnimatedContent(
+ AnimatedContent(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor),
+ .background(color = QuackColor.White.value),
targetState = onboardStepState,
+ label = "AnimatedContent",
) { onboardStep ->
when (onboardStep) {
OnboardStep.Activity, OnboardStep.Login -> LoginScreen()
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/OnboardTopAppBar.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/OnboardTopAppBar.kt
index 9bbadcd32..2f58439c4 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/OnboardTopAppBar.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/OnboardTopAppBar.kt
@@ -10,27 +10,24 @@ package team.duckie.app.android.feature.onboard.common
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import team.duckie.app.android.feature.onboard.constant.OnboardStep
-import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.systemBarPaddings
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackTopAppBar
+import team.duckie.app.android.feature.onboard.constant.OnboardStep
+import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowBack
@Composable
internal fun OnboardTopAppBar(
modifier: Modifier = Modifier,
currentStep: OnboardStep,
- horizontalPadding: Dp = 20.dp,
vm: OnboardViewModel = activityViewModel(),
) {
QuackTopAppBar(
- modifier = modifier
- .padding(top = systemBarPaddings.calculateTopPadding())
- .padding(horizontal = horizontalPadding - 8.dp), // 내부에서 8.dp 가 들어감
- leadingIcon = QuackIcon.ArrowBack,
+ modifier = modifier.padding(top = systemBarPaddings.calculateTopPadding()),
+ leadingIcon = QuackIcon.Outlined.ArrowBack,
onLeadingIconClick = { vm.navigateStep(currentStep - 1) },
)
}
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/TitleAndDescription.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/TitleAndDescription.kt
index 9017b098b..47ee9a00b 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/TitleAndDescription.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/common/TitleAndDescription.kt
@@ -15,8 +15,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import team.duckie.quackquack.ui.component.QuackBody1
-import team.duckie.quackquack.ui.component.QuackHeadLine1
+import team.duckie.quackquack.ui.sugar.QuackBody1
+import team.duckie.quackquack.ui.sugar.QuackHeadLine1
@Composable
internal fun TitleAndDescription(
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/0_LoginScreen.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/0_LoginScreen.kt
index 2e82a3f23..4768fc819 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/0_LoginScreen.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/0_LoginScreen.kt
@@ -20,9 +20,7 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -35,23 +33,20 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
-import kotlinx.coroutines.launch
-import team.duckie.app.android.feature.onboard.R
-import team.duckie.app.android.feature.onboard.constant.OnboardStep
-import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.asLoose
import team.duckie.app.android.common.compose.systemBarPaddings
import team.duckie.app.android.common.kotlin.fastFirstOrNull
import team.duckie.app.android.common.kotlin.npe
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody3
-import team.duckie.quackquack.ui.component.QuackHeadLine2
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.app.android.feature.onboard.R
+import team.duckie.app.android.feature.onboard.constant.OnboardStep
+import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
@Suppress("UnusedPrivateMember", "unused")
private val currentStep = OnboardStep.Login
@@ -125,21 +120,25 @@ private fun LoginScreenWelcome() {
),
) {
QuackImage(
- src = R.drawable.img_duckie_talk,
- size = DpSize(
+ modifier = Modifier.size(
width = 180.dp,
height = 248.dp,
),
+ src = R.drawable.img_duckie_talk,
)
- QuackHeadLine2(
+ QuackText(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
text = stringResource(R.string.kakaologin_welcome_message),
- align = TextAlign.Center,
+ typography = QuackTypography.HeadLine2.change(
+ textAlign = TextAlign.Center,
+ ),
)
}
}
private const val LoginScreenLoginAreaKakaoSymbolLayoutId = "LoginScreenLoginAreaKakaoSymbol"
-private const val LoginScreenLoginAreaKakaoLoginLabelLayoutId = "LoginScreenLoginAreaKakaoLoginLabel"
+private const val LoginScreenLoginAreaKakaoLoginLabelLayoutId =
+ "LoginScreenLoginAreaKakaoLoginLabel"
private val LoginScreenLoginAreaMeasurePolicy = MeasurePolicy { measurables, constraints ->
val extraLooseConstraints = constraints.asLoose(width = true)
@@ -180,7 +179,6 @@ private val LoginScreenLoginAreaMeasurePolicy = MeasurePolicy { measurables, con
@Composable
private fun LoginScreenLoginArea(vm: OnboardViewModel = activityViewModel()) {
val context = LocalContext.current
- val coroutineScope = rememberCoroutineScope()
Column(
modifier = Modifier.layoutId(LoginScreenLoginAreaLayoutId),
@@ -203,9 +201,7 @@ private fun LoginScreenLoginArea(vm: OnboardViewModel = activityViewModel()) {
),
)
.clickable {
- coroutineScope.launch {
- vm.getKakaoAccessTokenAndJoin()
- }
+ vm.getKakaoAccessTokenAndJoin()
},
content = {
Image(
@@ -224,25 +220,25 @@ private fun LoginScreenLoginArea(vm: OnboardViewModel = activityViewModel()) {
contentDescription = null,
)
// QuackColor 생성자가 internal 이라 BasicText 사용
- BasicText(
+ QuackText(
modifier = Modifier.layoutId(LoginScreenLoginAreaKakaoLoginLabelLayoutId),
text = stringResource(R.string.kakaologin_button_label),
- style = QuackTextStyle.HeadLine2.asComposeStyle().copy(
- color = Color(
- ContextCompat.getColor(
- context,
- R.color.kakao_login_button_label,
- ),
+ typography = QuackTypography.HeadLine2.change(
+ color = QuackColor(
+ Color(ContextCompat.getColor(context, R.color.kakao_login_button_label)),
),
),
)
},
measurePolicy = LoginScreenLoginAreaMeasurePolicy,
)
- QuackBody3(
+ QuackText(
+ modifier = Modifier.align(Alignment.CenterHorizontally),
text = stringResource(R.string.kakaologin_login_terms),
- color = QuackColor.Gray2,
- align = TextAlign.Center,
+ typography = QuackTypography.Body3.change(
+ color = QuackColor.Gray2,
+ textAlign = TextAlign.Center,
+ ),
)
}
}
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/1_ProfileScreen.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/1_ProfileScreen.kt
index ee77f9d61..cc3098cd8 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/1_ProfileScreen.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/1_ProfileScreen.kt
@@ -5,7 +5,12 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-@file:OptIn(FlowPreview::class, ExperimentalComposeUiApi::class)
+@file:OptIn(
+ FlowPreview::class,
+ ExperimentalComposeUiApi::class,
+ ExperimentalQuackQuackApi::class,
+ ExperimentalDesignToken::class,
+)
@file:Suppress("ConstPropertyName", "PrivatePropertyName")
package team.duckie.app.android.feature.onboard.screen
@@ -14,9 +19,12 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.launch
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.KeyboardActions
@@ -35,13 +43,11 @@ import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.layout.MeasurePolicy
-import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.core.net.toUri
@@ -53,102 +59,34 @@ import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.android.ui.const.Debounce
+import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.rememberToast
+import team.duckie.app.android.common.compose.systemBarPaddings
+import team.duckie.app.android.common.compose.ui.ImeSpacer
import team.duckie.app.android.common.compose.ui.PhotoPicker
import team.duckie.app.android.common.compose.ui.PhotoPickerConstants
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.constant.SharedIcon
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackErrorableTextField
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackErrorableTextFieldState
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
+import team.duckie.app.android.common.kotlin.runIf
import team.duckie.app.android.feature.onboard.R
import team.duckie.app.android.feature.onboard.common.OnboardTopAppBar
import team.duckie.app.android.feature.onboard.common.TitleAndDescription
import team.duckie.app.android.feature.onboard.constant.OnboardStep
import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
import team.duckie.app.android.feature.onboard.viewmodel.state.ProfileScreenState
-import team.duckie.app.android.common.compose.ui.constant.SharedIcon
-import team.duckie.app.android.common.compose.activityViewModel
-import team.duckie.app.android.common.compose.asLoose
-import team.duckie.app.android.common.compose.rememberToast
-import team.duckie.app.android.common.compose.systemBarPaddings
-import team.duckie.app.android.common.kotlin.fastFirstOrNull
-import team.duckie.app.android.common.kotlin.npe
-import team.duckie.app.android.common.kotlin.runIf
-import team.duckie.app.android.common.android.ui.const.Debounce
-import team.duckie.quackquack.ui.animation.QuackAnimatedContent
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackErrorableTextField
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.shape.SquircleShape
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.material.shape.SquircleShape
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private val currentStep = OnboardStep.Profile
-private const val ProfileScreenTopAppBarLayoutId = "ProfileScreenTopAppBar"
-private const val ProfileScreenTitleAndDescriptionLayoutId = "ProfileScreenTitleAndDescription"
-private const val ProfileScreenProfileImageLayoutId = "ProfileScreenProfileImage"
-private const val ProfileScreenNicknameTextFieldLayoutId = "ProfileScreenNicknameTextField"
-private const val ProfileScreenNextButtonLayoutId = "ProfileScreenNextButton"
-
-private val ProfileScreenMeasurePolicy = MeasurePolicy { measurables, constraints ->
- val looseConstraints = constraints.asLoose()
- val extraLooseConstraints = constraints.asLoose(width = true)
-
- val topAppBarPlaceable = measurables.fastFirstOrNull { measurable ->
- measurable.layoutId == ProfileScreenTopAppBarLayoutId
- }?.measure(looseConstraints) ?: npe()
-
- val titileAndDescriptionPlaceable = measurables.fastFirstOrNull { measurable ->
- measurable.layoutId == ProfileScreenTitleAndDescriptionLayoutId
- }?.measure(looseConstraints) ?: npe()
-
- val profileImagePlaceable = measurables.fastFirstOrNull { measurable ->
- measurable.layoutId == ProfileScreenProfileImageLayoutId
- }?.measure(extraLooseConstraints) ?: npe()
-
- val nicknameTextFieldPlaceable = measurables.fastFirstOrNull { measurable ->
- measurable.layoutId == ProfileScreenNicknameTextFieldLayoutId
- }?.measure(looseConstraints) ?: npe()
-
- val nextButtonPlaceable = measurables.fastFirstOrNull { measurable ->
- measurable.layoutId == ProfileScreenNextButtonLayoutId
- }?.measure(looseConstraints) ?: npe()
-
- val topAppBarHeight = topAppBarPlaceable.height
- val titleAndDescriptionHeight = titileAndDescriptionPlaceable.height
- val profileImageHeight = profileImagePlaceable.height
- val nextButtonHeight = nextButtonPlaceable.height
-
- layout(
- width = constraints.maxWidth,
- height = constraints.maxHeight,
- ) {
- topAppBarPlaceable.place(
- x = 0,
- y = 0,
- )
- titileAndDescriptionPlaceable.place(
- x = 0,
- y = topAppBarHeight,
- )
- profileImagePlaceable.place(
- x = Alignment.CenterHorizontally.align(
- size = profileImagePlaceable.width,
- space = constraints.maxWidth,
- layoutDirection = layoutDirection,
- ),
- y = topAppBarHeight + titleAndDescriptionHeight,
- )
- nicknameTextFieldPlaceable.place(
- x = 0,
- y = topAppBarHeight + titleAndDescriptionHeight + profileImageHeight,
- )
- nextButtonPlaceable.place(
- x = 0,
- y = constraints.maxHeight - nextButtonHeight,
- )
- }
-}
-
private const val MaxNicknameLength = 10
@Composable
@@ -204,11 +142,11 @@ internal fun ProfileScreen(vm: OnboardViewModel = activityViewModel()) {
},
)
}
- var lastErrorText by remember { mutableStateOf("") }
LaunchedEffect(vm) {
val nicknameInputFlow = snapshotFlow { nickname }
nicknameInputFlow
+ .onEach { vm.nicknameChecking() }
.onEach { vm.readyToScreenCheck(currentStep) }
.debounce(Debounce.SearchSecond)
.collect { nickname ->
@@ -221,39 +159,31 @@ internal fun ProfileScreen(vm: OnboardViewModel = activityViewModel()) {
}
Box(modifier = Modifier.fillMaxSize()) {
- Layout(
+ Column(
modifier = Modifier
.zIndex(1f)
.fillMaxSize()
+ .padding(horizontal = 16.dp)
.padding(bottom = systemBarPaddings.calculateBottomPadding() + 16.dp),
- content = {
- val profileScreenState = vm.collectAsState().value.profileState
- OnboardTopAppBar(
- modifier = Modifier.layoutId(ProfileScreenTopAppBarLayoutId),
- currentStep = currentStep,
- )
- TitleAndDescription(
- modifier = Modifier
- .layoutId(ProfileScreenTitleAndDescriptionLayoutId)
- .padding(
- top = 12.dp,
- start = 20.dp,
- end = 20.dp,
- ),
- titleRes = R.string.profile_title,
- descriptionRes = R.string.profile_description,
- )
+ ) {
+ val profileScreenState = vm.collectAsState().value.profileState
+ OnboardTopAppBar(currentStep = currentStep)
+ TitleAndDescription(
+ modifier = Modifier
+ .padding(top = 12.dp),
+ titleRes = R.string.profile_title,
+ descriptionRes = R.string.profile_description,
+ )
+ Box(modifier = Modifier.align(Alignment.CenterHorizontally)) {
ProfilePhoto(
modifier = Modifier
- .layoutId(ProfileScreenProfileImageLayoutId)
.padding(
- // 항상 center 에 배치돼서 horizontal padding 불필요
top = 32.dp,
bottom = 20.dp,
),
profilePhoto = profilePhoto,
resetProfilePhoto = {
- profilePhoto = QuackIcon.Profile
+ profilePhoto = ""
profilePhotoLastSelectionIndex?.let { lastSelectionIndex ->
profilePhotoSelections[lastSelectionIndex] = false
}
@@ -262,50 +192,57 @@ internal fun ProfileScreen(vm: OnboardViewModel = activityViewModel()) {
photoPickerVisible = true
}.takeIf { vm.isImagePermissionGranted == true },
)
- // TODO(sungbin): https://github.com/duckie-team/quack-quack-android/issues/438
- QuackErrorableTextField(
- modifier = Modifier
- .layoutId(ProfileScreenNicknameTextFieldLayoutId)
- // 패딩이 왜 2배로 들어가지??
- .padding(horizontal = 10.dp),
- text = nickname,
- onTextChanged = { text ->
- if (text.length <= MaxNicknameLength) {
- nickname = text
- }
- },
- placeholderText = stringResource(R.string.profile_nickname_placeholder),
- isError = ProfileScreenState.errorState.contains(profileScreenState),
- maxLength = MaxNicknameLength,
- errorText = when (profileScreenState) {
- ProfileScreenState.NicknameRuleError -> stringResource(R.string.profile_nickname_rule_error)
- ProfileScreenState.NicknameDuplicateError -> stringResource(R.string.profile_nickname_duplicate_error)
- ProfileScreenState.NicknameEmpty -> stringResource(R.string.profile_nickname_empty)
- else -> lastErrorText // 안하면 invisible 될 때 갑자기 텍스트가 사라짐 (애니메이션 X)
- }.also { errorText ->
- lastErrorText = errorText
- },
- keyboardActions = KeyboardActions {
- keyboard?.hide()
- },
- )
- QuackLargeButton(
- modifier = Modifier
- .layoutId(ProfileScreenNextButtonLayoutId)
- .padding(horizontal = 20.dp),
- text = stringResource(R.string.button_next),
- type = QuackLargeButtonType.Fill,
- imeAnimation = true,
- enabled = profileScreenState == ProfileScreenState.Valid && nickname.isNotEmpty(),
- ) {
- navigateNextStep(
- vm = vm,
- nickname = nickname,
+ }
+
+ QuackErrorableTextField(
+ modifier = Modifier,
+ text = nickname,
+ onTextChanged = { text ->
+ if (text.length <= MaxNicknameLength) {
+ nickname = text
+ }
+ },
+ placeholderText = stringResource(R.string.profile_nickname_placeholder),
+ maxLength = MaxNicknameLength,
+ textFieldState = when (profileScreenState) {
+ ProfileScreenState.NicknameRuleError -> QuackErrorableTextFieldState.Error(
+ errorText = stringResource(R.string.profile_nickname_rule_error),
)
- }
- },
- measurePolicy = ProfileScreenMeasurePolicy,
- )
+
+ ProfileScreenState.NicknameDuplicateError -> QuackErrorableTextFieldState.Error(
+ errorText = stringResource(R.string.profile_nickname_duplicate_error),
+ )
+
+ ProfileScreenState.NicknameEmpty -> QuackErrorableTextFieldState.Error(
+ errorText = stringResource(R.string.profile_nickname_empty),
+ )
+
+ ProfileScreenState.Valid -> QuackErrorableTextFieldState.Success(
+ successText = stringResource(R.string.profile_nickname_valid),
+ )
+
+ else -> QuackErrorableTextFieldState.Normal
+ },
+ keyboardActions = KeyboardActions {
+ keyboard?.hide()
+ },
+ )
+
+ Spacer(weight = 1f)
+ TempFlexiblePrimaryLargeButton(
+ modifier = Modifier.fillMaxWidth(),
+ text = stringResource(R.string.button_next),
+ enabled = profileScreenState == ProfileScreenState.Valid &&
+ nickname.isNotEmpty(),
+ ) {
+ navigateNextStep(
+ vm = vm,
+ nickname = nickname,
+ )
+ }
+
+ ImeSpacer()
+ }
// TODO(sungbin): 효율적인 애니메이션 (카메라가 로드되면서 생기는 프라임드랍 때문에 애니메이션 제거)
if (photoPickerVisible) {
@@ -318,7 +255,7 @@ internal fun ProfileScreen(vm: OnboardViewModel = activityViewModel()) {
modifier = Modifier
.padding(top = systemBarPaddings.calculateTopPadding())
.fillMaxSize()
- .background(color = QuackColor.White.composeColor),
+ .background(color = QuackColor.White.value),
imageUris = galleryImages,
imageSelections = profilePhotoSelections,
onCameraClick = {
@@ -350,9 +287,6 @@ internal fun ProfileScreen(vm: OnboardViewModel = activityViewModel()) {
}
}
-private val ProfilePhotoShape = SquircleShape
-private val ProfilePhotoSize = DpSize(all = 80.dp)
-
@Composable
private fun ProfilePhoto(
modifier: Modifier = Modifier,
@@ -360,20 +294,37 @@ private fun ProfilePhoto(
resetProfilePhoto: () -> Unit,
openPhotoPicker: (() -> Unit)?,
) {
- QuackAnimatedContent(
+ AnimatedContent(
modifier = modifier
- .size(ProfilePhotoSize)
- .clip(ProfilePhotoShape)
- .quackClickable(onClick = openPhotoPicker),
+ .quackClickable(onClick = openPhotoPicker)
+ .size(DpSize(width = 80.dp, height = 80.dp))
+ .clip(SquircleShape),
targetState = profilePhoto,
+ label = "AnimatedContent",
) { photo ->
- QuackImage(
- src = if (photo == "") SharedIcon.ic_default_profile else photo,
- size = ProfilePhotoSize,
- contentScale = ContentScale.Crop,
- onClick = openPhotoPicker ?: {}, // required when onLongClick is used
- onLongClick = resetProfilePhoto,
- )
+ if ("$photo".isEmpty()) {
+ QuackImage(
+ modifier = Modifier
+ .quackClickable(
+ onClick = openPhotoPicker ?: {}, // required when onLongClick is used
+ onLongClick = resetProfilePhoto,
+ )
+ .size(DpSize(width = 80.dp, height = 80.dp)),
+ src = SharedIcon.ic_default_profile,
+ contentScale = ContentScale.Crop,
+ )
+ } else {
+ QuackImage(
+ modifier = Modifier
+ .quackClickable(
+ onClick = openPhotoPicker ?: {}, // required when onLongClick is used
+ onLongClick = resetProfilePhoto,
+ )
+ .size(DpSize(width = 80.dp, height = 80.dp)),
+ src = photo,
+ contentScale = ContentScale.Crop,
+ )
+ }
}
}
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/2_CategoryScreen.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/2_CategoryScreen.kt
index 7ddea5fce..6eca2de60 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/2_CategoryScreen.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/2_CategoryScreen.kt
@@ -25,29 +25,27 @@ import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.toImmutableList
import org.orbitmvi.orbit.compose.collectAsState
-import team.duckie.app.android.feature.onboard.R
-import team.duckie.app.android.feature.onboard.common.OnboardTopAppBar
-import team.duckie.app.android.feature.onboard.common.TitleAndDescription
-import team.duckie.app.android.feature.onboard.constant.OnboardStep
-import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.asLoose
import team.duckie.app.android.common.compose.systemBarPaddings
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackGridLayout
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSelectableImage
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSelectableImageType
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.common.kotlin.fastAny
import team.duckie.app.android.common.kotlin.fastFirstOrNull
import team.duckie.app.android.common.kotlin.fastMapIndexedNotNull
import team.duckie.app.android.common.kotlin.npe
-import team.duckie.quackquack.ui.animation.QuackAnimatedVisibility
-import team.duckie.quackquack.ui.component.QuackGridLayout
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.component.QuackSelectableImage
-import team.duckie.quackquack.ui.component.QuackSelectableImageType
-import team.duckie.quackquack.ui.component.QuackTitle2
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.app.android.feature.onboard.R
+import team.duckie.app.android.feature.onboard.common.OnboardTopAppBar
+import team.duckie.app.android.feature.onboard.common.TitleAndDescription
+import team.duckie.app.android.feature.onboard.constant.OnboardStep
+import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
+import team.duckie.quackquack.ui.sugar.QuackTitle2
private val currentStep = OnboardStep.Category
@@ -102,16 +100,17 @@ private val CategoryScreenMeasurePolicy = MeasurePolicy { measurables, constrain
internal fun CategoryScreen(vm: OnboardViewModel = activityViewModel()) {
val onboardState by vm.collectAsState()
- val categoriesSelectedIndex = remember(onboardState.categories, onboardState.selectedCategories) {
- mutableStateListOf(
- elements = Array(
- size = onboardState.categories.size,
- init = { index ->
- onboardState.selectedCategories.contains(onboardState.categories[index])
- },
- ),
- )
- }
+ val categoriesSelectedIndex =
+ remember(onboardState.categories, onboardState.selectedCategories) {
+ mutableStateListOf(
+ elements = Array(
+ size = onboardState.categories.size,
+ init = { index ->
+ onboardState.selectedCategories.contains(onboardState.categories[index])
+ },
+ ),
+ )
+ }
Layout(
modifier = Modifier
@@ -158,35 +157,26 @@ internal fun CategoryScreen(vm: OnboardViewModel = activityViewModel()) {
)
}
}
- QuackAnimatedVisibility(
+ TempFlexiblePrimaryLargeButton(
modifier = Modifier
.layoutId(CategoryScreenNextButtonLayoutId)
- .padding(horizontal = 20.dp)
- .fillMaxWidth(),
- visible = categoriesSelectedIndex.fastAny { it },
+ .fillMaxWidth()
+ .padding(horizontal = 20.dp),
+ text = stringResource(R.string.button_next),
+ enabled = categoriesSelectedIndex.fastAny { it },
) {
- QuackLargeButton(
- type = QuackLargeButtonType.Fill,
- enabled = true,
- text = stringResource(R.string.button_next),
- ) {
- vm.updateUserSelectCategories(
- categories = categoriesSelectedIndex.fastMapIndexedNotNull { index, selected ->
- onboardState.categories[index].takeIf { selected }
- },
- )
- vm.navigateStep(currentStep + 1)
- }
+ vm.updateUserSelectCategories(
+ categories = categoriesSelectedIndex.fastMapIndexedNotNull { index, selected ->
+ onboardState.categories[index].takeIf { selected }
+ },
+ )
+ vm.navigateStep(currentStep + 1)
}
},
measurePolicy = CategoryScreenMeasurePolicy,
)
}
-// FIXME(sungbin): 디자인상 100.dp 가 맞는데, 100 을 주면 디바이스 너비에 압축됨
-private val CategoryImageSize = DpSize(all = 80.dp)
-private val CategoryItemShape = RoundedCornerShape(size = 12.dp)
-
@Composable
private fun CategoryItem(
imageUrl: String,
@@ -203,12 +193,13 @@ private fun CategoryItem(
) {
QuackSelectableImage(
src = imageUrl,
- size = CategoryImageSize,
- shape = CategoryItemShape,
+ size = DpSize(width = 80.dp, height = 80.dp),
+ shape = RoundedCornerShape(size = 12.dp),
selectableType = QuackSelectableImageType.CheckOverlay,
isSelected = isSelected,
onClick = onClick,
)
+
QuackTitle2(text = name)
}
}
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/3_TagScreen.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/3_TagScreen.kt
index 1bb42eb3f..29b1d0b3b 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/3_TagScreen.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/screen/3_TagScreen.kt
@@ -5,7 +5,11 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-@file:OptIn(ExperimentalMaterialApi::class, ExperimentalComposeUiApi::class)
+@file:OptIn(
+ ExperimentalMaterialApi::class,
+ ExperimentalComposeUiApi::class,
+ ExperimentalQuackQuackApi::class,
+)
@file:Suppress(
"ConstPropertyName",
"PrivatePropertyName",
@@ -42,7 +46,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.layoutId
-import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.google.accompanist.flowlayout.FlowRow
@@ -50,10 +53,14 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.compose.HideKeyboardWhenBottomSheetHidden
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.asLoose
import team.duckie.app.android.common.compose.systemBarPaddings
import team.duckie.app.android.common.compose.ui.domain.DuckieTagAddBottomSheet
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackCircleTag
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackOutLinedSingeLazyRowTag
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.common.kotlin.AllowMagicNumber
import team.duckie.app.android.common.kotlin.fastAny
import team.duckie.app.android.common.kotlin.fastFirstOrNull
@@ -68,15 +75,12 @@ import team.duckie.app.android.feature.onboard.common.TitleAndDescription
import team.duckie.app.android.feature.onboard.constant.OnboardStep
import team.duckie.app.android.feature.onboard.viewmodel.OnboardViewModel
import team.duckie.app.android.feature.onboard.viewmodel.state.OnboardState
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackCircleTag
-import team.duckie.quackquack.ui.component.QuackHeadLine2
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.component.QuackSingeLazyRowTag
-import team.duckie.quackquack.ui.component.QuackTagType
-import team.duckie.quackquack.ui.component.QuackTitle2
-import team.duckie.quackquack.ui.icon.QuackIcon
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackTitle2
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
private val currentStep = OnboardStep.Tag
@@ -124,7 +128,6 @@ private val TagScreenMeasurePolicy = MeasurePolicy { measurables, constraints ->
@Composable
internal fun TagScreen(vm: OnboardViewModel = activityViewModel()) {
- val keyboard = LocalSoftwareKeyboardController.current
val coroutineScope = rememberCoroutineScope()
var isLoadingToFinish by remember { mutableStateOf(false) }
@@ -137,14 +140,7 @@ internal fun TagScreen(vm: OnboardViewModel = activityViewModel()) {
skipHalfExpanded = true,
)
- LaunchedEffect(sheetState) {
- val sheetStateFlow = snapshotFlow { sheetState.currentValue }
- sheetStateFlow.collect { state ->
- if (state == ModalBottomSheetValue.Hidden) {
- keyboard?.hide()
- }
- }
- }
+ HideKeyboardWhenBottomSheetHidden(sheetState)
BackHandler(sheetState.isVisible) {
coroutineScope.launch {
@@ -182,14 +178,17 @@ internal fun TagScreen(vm: OnboardViewModel = activityViewModel()) {
addedTags.remove(addedTags[index])
},
)
- QuackLargeButton(
+
+ // TODO(riflockle7): 문제 있으므로 꽥꽥 이슈 해결할 때까지 주석 제거하지 않음
+ // type = QuackLargeButtonType.Fill,
+ // isLoading = isLoadingToFinish,
+ TempFlexiblePrimaryLargeButton(
modifier = Modifier
.layoutId(TagScreenQuackLargeButtonLayoutId)
+ .fillMaxWidth()
.padding(horizontal = 20.dp),
text = stringResource(id = R.string.button_start_duckie),
- type = QuackLargeButtonType.Fill,
enabled = true,
- isLoading = isLoadingToFinish,
) {
updateUserAndFinishOnboard(
coroutineScope = coroutineScope,
@@ -308,31 +307,29 @@ private fun TagSelection(
QuackCircleTag(
text = tag.name,
isSelected = false,
- trailingIcon = QuackIcon.Close,
) {
requestRemoveAddedTag(index)
}
}
}
}
- QuackHeadLine2(
- modifier = Modifier.padding(
- top = if (addedTags.isNotEmpty()) 0.dp else 4.dp,
- start = 10.dp,
- ),
+ QuackText(
+ modifier = Modifier
+ .quackClickable(
+ onClick = {
+ coroutineScope.launch {
+ sheetState.show()
+ }
+ },
+ )
+ .padding(
+ top = if (addedTags.isNotEmpty()) 0.dp else 8.dp,
+ start = 20.dp,
+ end = 10.dp,
+ bottom = 8.dp,
+ ),
text = stringResource(R.string.tag_add_manual),
- padding = PaddingValues(
- top = if (addedTags.isNotEmpty()) 0.dp else 4.dp,
- start = 10.dp,
- end = 10.dp,
- bottom = 8.dp,
- ),
- color = QuackColor.DuckieOrange,
- onClick = {
- coroutineScope.launch {
- sheetState.show()
- }
- },
+ typography = QuackTypography.HeadLine2.change(color = QuackColor.DuckieOrange),
)
}
@AllowMagicNumber(because = "(34 - 8).dp")
@@ -344,11 +341,11 @@ private fun TagSelection(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
onboardState.selectedCategories.fastForEachIndexed { categoryIndex, category ->
- QuackSingeLazyRowTag(
+ QuackOutLinedSingeLazyRowTag(
title = stringResource(R.string.tag_hottest_tag, category.name),
items = hottestTags[categoryIndex],
itemSelections = hottestTagSelections[categoryIndex],
- tagType = QuackTagType.Circle(),
+ trailingIcon = null,
contentPadding = PaddingValues(horizontal = 20.dp),
onClick = { tagIndex ->
hottestTagSelections[categoryIndex][tagIndex] =
diff --git a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/viewmodel/OnboardViewModel.kt b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/viewmodel/OnboardViewModel.kt
index f9724af35..56e4cf676 100644
--- a/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/viewmodel/OnboardViewModel.kt
+++ b/feature/onboard/src/main/kotlin/team/duckie/app/android/feature/onboard/viewmodel/OnboardViewModel.kt
@@ -35,6 +35,7 @@ import org.orbitmvi.orbit.viewmodel.container
import team.duckie.app.android.common.android.permission.PermissionCompat
import team.duckie.app.android.common.android.savedstate.SaveableMutableStateFlow
import team.duckie.app.android.common.android.viewmodel.context
+import team.duckie.app.android.common.kotlin.exception.isKakaoCancelled
import team.duckie.app.android.common.kotlin.seconds
import team.duckie.app.android.domain.auth.usecase.AttachAccessTokenToHeaderUseCase
import team.duckie.app.android.domain.auth.usecase.JoinUseCase
@@ -182,6 +183,10 @@ internal class OnboardViewModel @AssistedInject constructor(
}
}
+ fun nicknameChecking() = intent {
+ reduce { state.copy(profileState = ProfileScreenState.Checking) }
+ }
+
/** 닉네임을 체크한다. */
fun checkNickname(nickname: String) {
val isNicknameRuleError = checkNicknameRule(nickname)
@@ -267,7 +272,7 @@ internal class OnboardViewModel @AssistedInject constructor(
/* ----- Api ----- */
- suspend fun getKakaoAccessTokenAndJoin() = intent {
+ fun getKakaoAccessTokenAndJoin() = intent {
getKakaoAccessTokenUseCase()
.onSuccess { token ->
postSideEffect(OnboardSideEffect.DelegateJoin(token))
@@ -275,7 +280,7 @@ internal class OnboardViewModel @AssistedInject constructor(
.attachExceptionHandling()
}
- suspend fun join(kakaoAccessToken: String) = intent {
+ fun join(kakaoAccessToken: String) = intent {
joinUseCase(kakaoAccessToken)
.onSuccess { response ->
reduce {
@@ -371,7 +376,10 @@ internal class OnboardViewModel @AssistedInject constructor(
additinal: suspend (exception: Throwable) -> Unit = {},
) = intent {
onFailure { exception ->
- postSideEffect(OnboardSideEffect.ReportError(exception))
+ when {
+ exception.isKakaoCancelled -> return@onFailure // this is not error
+ else -> postSideEffect(OnboardSideEffect.ReportError(exception))
+ }
additinal(exception)
}
}
diff --git a/feature/onboard/src/main/res/values/strings.xml b/feature/onboard/src/main/res/values/strings.xml
index e2ae0d65f..b74b0361a 100644
--- a/feature/onboard/src/main/res/values/strings.xml
+++ b/feature/onboard/src/main/res/values/strings.xml
@@ -29,6 +29,7 @@
문자, 숫자, 밑줄, 마침표만 사용할 수 있어요.
이미 있는 이름이에요 :(
닉네임을 입력해주세요.
+ 사용 가능한 닉네임이에요!
어떤 분야를 좋아하나요?
1개 이상 골라보세요.\n취향에 맞춰 피드를 추천해드려요 :)
@@ -37,7 +38,7 @@
태그 할수록 더키의 추천이 정확해져요!
추가한 태그
+ 직접 태그 추가하기
- 태그 입력하기
+ 태그 입력하기 (최대 10자)
%s 분야 인기 태그
이미 추가한 태그예요.
태그 생성에 실패했어요. 실패한 태그: %s
diff --git a/feature/profile/build.gradle.kts b/feature/profile/build.gradle.kts
index 5f22a464a..e4d272466 100644
--- a/feature/profile/build.gradle.kts
+++ b/feature/profile/build.gradle.kts
@@ -31,7 +31,6 @@ dependencies {
libs.ktx.lifecycle.runtime,
libs.compose.ui.material, // needs for Scaffold
libs.compose.lifecycle.runtime,
- libs.quack.ui.components,
libs.quack.v2.ui,
libs.kotlin.collections.immutable,
libs.paging.runtime,
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/ProfileActivity.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/ProfileActivity.kt
index 1c713d17b..9066b93b9 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/ProfileActivity.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/ProfileActivity.kt
@@ -55,7 +55,7 @@ class ProfileActivity : BaseActivity() {
private val viewModel: ProfileViewModel by viewModels()
@Inject
- lateinit var createProblemNavigator: CreateProblemNavigator
+ lateinit var createExamNavigator: CreateProblemNavigator
@Inject
lateinit var notificationNavigator: NotificationNavigator
@@ -170,7 +170,7 @@ class ProfileActivity : BaseActivity() {
}
ProfileSideEffect.NavigateToMakeExam -> {
- createProblemNavigator.navigateFrom(this@ProfileActivity)
+ createExamNavigator.navigateFrom(this@ProfileActivity)
}
is ProfileSideEffect.NavigateToExamDetail -> {
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/component/EditTopAppBar.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/component/EditTopAppBar.kt
index c2dd032aa..53addbedc 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/component/EditTopAppBar.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/component/EditTopAppBar.kt
@@ -23,7 +23,7 @@ import team.duckie.quackquack.material.icon.QuackIcon
import team.duckie.quackquack.material.icon.quackicon.Outlined
import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowBack
import team.duckie.quackquack.material.quackClickable
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.QuackText
import team.duckie.quackquack.ui.sugar.QuackHeadLine2
@@ -47,9 +47,9 @@ internal fun EditTopAppBar(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- QuackImage(
+ QuackIcon(
modifier = Modifier.quackClickable(onClick = onBackPressed),
- src = QuackIcon.Outlined.ArrowBack,
+ icon = QuackIcon.Outlined.ArrowBack,
)
QuackHeadLine2(text = title)
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/MyProfileScreen.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/MyProfileScreen.kt
index b2d67e090..370a3c823 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/MyProfileScreen.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/MyProfileScreen.kt
@@ -4,6 +4,7 @@
* Licensed under the MIT.
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+
@file:OptIn(ExperimentalQuackQuackApi::class)
package team.duckie.app.android.feature.profile.screen
@@ -12,7 +13,7 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@@ -23,7 +24,7 @@ import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
import team.duckie.app.android.common.compose.ui.BackPressedHeadLineTopAppBar
import team.duckie.app.android.common.compose.ui.DuckTestCoverItem
-import team.duckie.app.android.common.compose.ui.icon.v1.Notice
+import team.duckie.app.android.common.compose.ui.temp.TempFlexibleSecondaryLargeButton
import team.duckie.app.android.common.kotlin.FriendsType
import team.duckie.app.android.domain.exam.model.ProfileExam
import team.duckie.app.android.domain.user.model.UserProfile
@@ -38,13 +39,11 @@ import team.duckie.app.android.feature.profile.viewmodel.state.mapper.toUiModel
import team.duckie.quackquack.material.icon.QuackIcon
import team.duckie.quackquack.material.icon.quackicon.Outlined
import team.duckie.quackquack.material.icon.quackicon.outlined.Create
+import team.duckie.quackquack.material.icon.quackicon.outlined.Notice
import team.duckie.quackquack.material.icon.quackicon.outlined.Setting
import team.duckie.quackquack.material.quackClickable
-import team.duckie.quackquack.ui.QuackImage
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
-import team.duckie.quackquack.ui.icon.QuackIcon as QuackV1Icon
@Suppress("UnusedPrivateMember") // 시험 생성하기를 추후에 다시 활용하기 위함
@Composable
@@ -67,17 +66,13 @@ fun MyProfileScreen(
@Composable
fun BackPressedHeadLineTopBarInternal() {
Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- QuackImage(
- modifier = Modifier
- .size(24.dp, 24.dp)
- .quackClickable(onClick = onClickNotification),
- src = QuackV1Icon.Companion.Notice,
+ QuackIcon(
+ modifier = Modifier.quackClickable(onClick = onClickNotification),
+ icon = QuackIcon.Outlined.Notice,
)
- QuackImage(
- modifier = Modifier
- .size(24.dp, 24.dp)
- .quackClickable(onClick = onClickSetting),
- src = QuackIcon.Outlined.Setting,
+ QuackIcon(
+ modifier = Modifier.quackClickable(onClick = onClickSetting),
+ icon = QuackIcon.Outlined.Setting,
)
}
}
@@ -125,8 +120,8 @@ fun MyProfileScreen(
title = stringResource(id = R.string.my_favorite_tag),
tags = tags,
emptySection = {
- QuackLargeButton(
- type = QuackLargeButtonType.Compact,
+ TempFlexibleSecondaryLargeButton(
+ modifier = Modifier.fillMaxWidth(),
text = stringResource(id = R.string.add_favorite_tag),
onClick = onClickEditTag,
)
@@ -150,8 +145,8 @@ fun MyProfileScreen(
EmptyText(message = stringResource(id = R.string.not_yet_submit_exam))
// TODO(limsaehyun): 시험 생성하기가 가능한 스펙에서 활용
// Spacer(modifier = Modifier.padding(8.dp))
-// QuackLargeButton(
-// type = QuackLargeButtonType.Compact,
+// QuackButton(
+// style = QuackButtonStyle.SecondaryLarge,
// text = stringResource(id = R.string.make_exam),
// onClick = onClickMakeExam,
// )
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/OtherProfileScreen.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/OtherProfileScreen.kt
index 338870ff2..c35e65b51 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/OtherProfileScreen.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/OtherProfileScreen.kt
@@ -11,7 +11,8 @@ package team.duckie.app.android.feature.profile.screen
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.ModalBottomSheetValue
import androidx.compose.material.rememberModalBottomSheetState
@@ -22,7 +23,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.collections.immutable.persistentListOf
@@ -48,7 +48,7 @@ import team.duckie.quackquack.material.icon.quackicon.Outlined
import team.duckie.quackquack.material.icon.quackicon.outlined.Create
import team.duckie.quackquack.material.icon.quackicon.outlined.More
import team.duckie.quackquack.material.quackClickable
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
@Composable
internal fun OtherProfileScreen(
@@ -102,6 +102,9 @@ internal fun OtherProfileScreen(
isLoading = state.isLoading,
editSection = {
FollowSection(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
enabled = state.follow,
onClick = viewModel::clickFollow,
)
@@ -112,9 +115,8 @@ internal fun OtherProfileScreen(
isLoading = state.isLoading,
onBackPressed = viewModel::clickBackPress,
trailingContent = {
- QuackImage(
+ QuackIcon(
modifier = Modifier
- .size(DpSize(24.dp, 24.dp))
.quackClickable(
onClick = {
viewModel.updateBottomSheetDialogType(DuckieSelectableType.Ignore)
@@ -123,7 +125,7 @@ internal fun OtherProfileScreen(
}
},
),
- src = QuackIcon.Outlined.More,
+ icon = QuackIcon.Outlined.More,
)
},
)
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/ProfileScreen.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/ProfileScreen.kt
index 8a50191e3..6a9462b99 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/ProfileScreen.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/ProfileScreen.kt
@@ -65,16 +65,13 @@ fun ProfileScreen(
Column(
modifier = Modifier
.verticalScroll(scrollState)
- .padding(
- horizontal = 16.dp,
- vertical = 24.dp,
- ),
+ .padding(vertical = 24.dp),
) {
with(userProfile) {
ProfileSection(
userId = user?.id ?: 0,
profile = user?.profileImageUrl ?: "",
- duckPower = user?.duckPower?.tier ?: "",
+ duckPower = user?.duckPower?.tier ?: "0덕",
follower = followerCount,
following = followingCount,
introduce = user?.introduction
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/edit/ProfileEditScreen.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/edit/ProfileEditScreen.kt
index 341fd257e..688a739ca 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/edit/ProfileEditScreen.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/edit/ProfileEditScreen.kt
@@ -5,7 +5,11 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
-@file:OptIn(ExperimentalComposeUiApi::class)
+@file:OptIn(
+ ExperimentalComposeUiApi::class,
+ ExperimentalDesignToken::class,
+ ExperimentalQuackQuackApi::class,
+)
package team.duckie.app.android.feature.profile.screen.edit
@@ -14,6 +18,9 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.result.launch
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
@@ -23,6 +30,7 @@ import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
@@ -43,7 +51,10 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import kotlinx.coroutines.launch
import team.duckie.app.android.common.compose.ui.PhotoPicker
import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackErrorableTextField
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackErrorableTextFieldState
import team.duckie.app.android.common.compose.ui.skeleton
+import team.duckie.app.android.common.compose.util.addFocusCleaner
import team.duckie.app.android.feature.profile.R
import team.duckie.app.android.feature.profile.component.EditTopAppBar
import team.duckie.app.android.feature.profile.component.GrayBorderButton
@@ -54,10 +65,13 @@ import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.shape.SquircleShape
import team.duckie.quackquack.ui.QuackImage
import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.component.QuackErrorableTextField
-import team.duckie.quackquack.ui.component.QuackReviewTextArea
+import team.duckie.quackquack.ui.QuackTextArea
+import team.duckie.quackquack.ui.QuackTextAreaStyle
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.textAreaCounter
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
-private const val MaxNicknameLength = 12
+private const val MaxNicknameLength = 10
private const val MaxIntroductionLength = 60
@Composable
@@ -67,6 +81,10 @@ internal fun ProfileEditScreen(
val context = LocalContext.current.applicationContext
val state by vm.container.stateFlow.collectAsStateWithLifecycle()
+ // keyboard focused
+ val interactionSource = remember { MutableInteractionSource() }
+ val introduceTextFieldFocused = interactionSource.collectIsFocusedAsState().value
+
val galleryState = remember(state) { state.galleryState }
val keyboardController = LocalSoftwareKeyboardController.current
val coroutineScope = rememberCoroutineScope()
@@ -118,6 +136,7 @@ internal fun ProfileEditScreen(
modifier = Modifier
.fillMaxSize()
.background(QuackColor.White.value)
+ .addFocusCleaner()
.navigationBarsPadding()
.systemBarsPadding(),
) {
@@ -136,14 +155,14 @@ internal fun ProfileEditScreen(
Spacer(space = 40.dp)
ProfileEditSection(
profile = state.profile,
- onClickEditProfile = vm::clickEditProfile,
+ onClickEditProfile = vm::loadGalleryImages,
)
Spacer(space = 40.dp)
QuackText(
text = stringResource(R.string.nickname),
typography = QuackTypography.Body1.change(color = QuackColor.Gray1),
)
- // TODO(riflockle7): quack v1 -> quack v2
+ Spacer(space = 16.dp)
QuackErrorableTextField(
modifier = Modifier.skeleton(state.isLoading),
text = state.nickname,
@@ -153,12 +172,21 @@ internal fun ProfileEditScreen(
}
},
placeholderText = stringResource(R.string.profile_nickname_placeholder),
- isError = state.nicknameState.isInValid(),
maxLength = MaxNicknameLength,
- errorText = when (state.nicknameState) {
- NicknameState.NicknameRuleError -> stringResource(R.string.profile_nickname_rule_error)
- NicknameState.NicknameDuplicateError -> stringResource(R.string.profile_nickname_duplicate_error)
- else -> ""
+ textFieldState = when (state.nicknameState) {
+ NicknameState.NicknameRuleError -> QuackErrorableTextFieldState.Error(
+ errorText = stringResource(R.string.profile_nickname_rule_error),
+ )
+
+ NicknameState.NicknameDuplicateError -> QuackErrorableTextFieldState.Error(
+ errorText = stringResource(R.string.profile_nickname_duplicate_error),
+ )
+
+ NicknameState.Valid -> QuackErrorableTextFieldState.Success(
+ successText = stringResource(R.string.profile_nickname_valid),
+ )
+
+ else -> QuackErrorableTextFieldState.Normal
},
keyboardActions = KeyboardActions {
keyboardController?.hide()
@@ -170,18 +198,28 @@ internal fun ProfileEditScreen(
typography = QuackTypography.Body1.change(color = QuackColor.Gray1),
)
Spacer(space = 8.dp)
- // TODO(riflockle7): quack v1 -> quack v2
- QuackReviewTextArea(
- // TODO(evergreenTree97) 배포 후 글자 수 제한있는 텍스트필드 구현
- modifier = Modifier.skeleton(state.isLoading),
- text = state.introduce,
- onTextChanged = {
+ QuackTextArea(
+ modifier = Modifier
+ .fillMaxWidth()
+ .border(
+ width = 1.dp,
+ color = if (introduceTextFieldFocused) {
+ QuackColor.DuckieOrange.value
+ } else {
+ QuackColor.Gray3.value
+ },
+ shape = RoundedCornerShape(8.dp),
+ )
+ .textAreaCounter(maxLength = 60),
+ style = QuackTextAreaStyle.Default,
+ value = state.introduce,
+ onValueChange = {
if (it.length <= MaxIntroductionLength) {
vm.inputIntroduce(it)
}
},
- focused = state.introduceFocused,
placeholderText = stringResource(id = R.string.please_input_introduce),
+ interactionSource = interactionSource,
)
}
}
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ButtonSection.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ButtonSection.kt
index b367ecf46..56314c7b1 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ButtonSection.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ButtonSection.kt
@@ -7,29 +7,30 @@
package team.duckie.app.android.feature.profile.screen.section
+import androidx.compose.foundation.background
import androidx.compose.foundation.border
-import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import team.duckie.app.android.feature.profile.R
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackSurface
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun FollowSection(
+ modifier: Modifier = Modifier,
enabled: Boolean,
onClick: () -> Unit,
) {
EditButton(
- modifier = Modifier.fillMaxWidth(),
+ modifier = modifier,
text = if (enabled) {
stringResource(id = R.string.follow)
} else {
@@ -48,39 +49,39 @@ fun EditButton(
enabled: Boolean = false,
plainTextColor: QuackColor = QuackColor.Gray1,
) {
- QuackSurface(
- modifier = modifier.fillMaxWidth(),
- backgroundColor = if (enabled) {
- QuackColor.White
- } else {
- QuackColor.Gray4
- },
- border = if (enabled) {
- QuackBorder(
+ QuackText(
+ modifier = modifier
+ .clip(RoundedCornerShape(size = 8.dp))
+ .quackClickable(onClick = onClick)
+ .background(
+ if (enabled) {
+ QuackColor.White.toBrush()
+ } else {
+ QuackColor.Gray4.toBrush()
+ },
+ )
+ .border(
width = 1.dp,
- color = QuackColor.DuckieOrange,
+ brush = if (enabled) {
+ QuackColor.DuckieOrange.toBrush()
+ } else {
+ QuackColor.Unspecified.toBrush()
+ },
+ shape = RoundedCornerShape(size = 8.dp),
)
- } else {
- null
- },
- shape = RoundedCornerShape(size = 8.dp),
- onClick = onClick,
- ) {
- QuackText(
- modifier = Modifier.padding(
+ .padding(
vertical = 8.dp,
horizontal = 12.dp,
),
- text = text,
- style = QuackTextStyle.Body1.change(
- color = if (enabled) {
- QuackColor.DuckieOrange
- } else {
- plainTextColor
- },
- textAlign = TextAlign.Center,
- ),
- singleLine = true,
- )
- }
+ text = text,
+ typography = QuackTypography.Body1.change(
+ color = if (enabled) {
+ QuackColor.DuckieOrange
+ } else {
+ plainTextColor
+ },
+ textAlign = TextAlign.Center,
+ ),
+ singleLine = true,
+ )
}
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/EditSection.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/EditSection.kt
index b1292bf8c..dfe32bbaf 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/EditSection.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/EditSection.kt
@@ -10,6 +10,7 @@ package team.duckie.app.android.feature.profile.screen.section
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -17,7 +18,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.feature.profile.R
-import team.duckie.quackquack.ui.color.QuackColor
+import team.duckie.quackquack.material.QuackColor
@Composable
internal fun EditSection(
@@ -25,7 +26,9 @@ internal fun EditSection(
onClickEditTag: () -> Unit,
) {
Row(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
) {
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ExamSection.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ExamSection.kt
index 3743a9f68..316a317aa 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ExamSection.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ExamSection.kt
@@ -11,6 +11,7 @@ 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.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
@@ -30,7 +31,7 @@ import team.duckie.app.android.feature.profile.R
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.material.quackClickable
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackIcon
import team.duckie.quackquack.ui.QuackText
import team.duckie.quackquack.ui.sugar.QuackTitle2
@@ -47,7 +48,9 @@ fun ExamSection(
) {
Column(modifier = Modifier.fillMaxWidth()) {
Row(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
@@ -55,11 +58,11 @@ fun ExamSection(
horizontalArrangement = Arrangement.spacedBy(2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
- QuackImage(
+ QuackIcon(
modifier = Modifier
.size(DpSize(24.dp, 24.dp))
.skeleton(isLoading),
- src = icon,
+ icon = icon,
)
QuackTitle2(
modifier = Modifier.skeleton(isLoading),
@@ -81,6 +84,9 @@ fun ExamSection(
LazyRow(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
+ item {
+ Spacer(space = 16.dp)
+ }
items(exams) { item ->
DuckExamSmallCover(
isLoading = isLoading,
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/FavoriteSection.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/FavoriteSection.kt
index 07bde2d58..322aa8526 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/FavoriteSection.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/FavoriteSection.kt
@@ -9,37 +9,44 @@ package team.duckie.app.android.feature.profile.screen.section
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackOutLinedSingeLazyRowTag
import team.duckie.app.android.common.compose.ui.skeleton
import team.duckie.app.android.domain.tag.model.Tag
-import team.duckie.quackquack.ui.component.QuackSingeLazyRowTag
-import team.duckie.quackquack.ui.component.QuackTagType
import team.duckie.quackquack.ui.sugar.QuackTitle2
@Composable
internal fun FavoriteTagSection(
isLoading: Boolean,
title: String,
- emptySection: @Composable () -> Unit,
+ emptySection: @Composable ColumnScope.() -> Unit,
tags: ImmutableList,
onClickTag: (String) -> Unit,
) {
val tagList = remember(tags) { tags.map { it.name }.toList() }
- Column(verticalArrangement = Arrangement.spacedBy(13.5.dp)) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ verticalArrangement = Arrangement.spacedBy(13.5.dp),
+ ) {
QuackTitle2(text = title)
if (tags.isEmpty()) {
emptySection()
} else {
// TODO(riflockle7): quack v2 에서 대체할 수 있는 내용 찾기
- QuackSingeLazyRowTag(
+ QuackOutLinedSingeLazyRowTag(
modifier = Modifier.skeleton(isLoading),
items = tagList,
- tagType = QuackTagType.Circle(),
+ trailingIcon = null,
onClick = { index -> onClickTag(tagList[index]) },
)
}
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ProfileSection.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ProfileSection.kt
index 8722cbfa4..54afc228c 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ProfileSection.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/section/ProfileSection.kt
@@ -11,7 +11,7 @@ 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.size
+import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -40,7 +40,11 @@ internal fun ProfileSection(
isLoading: Boolean,
onClickFriend: (FriendsType, Int) -> Unit,
) {
- Column(modifier = Modifier.fillMaxWidth()) {
+ Column(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ ) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/viewall/ViewAllScreen.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/viewall/ViewAllScreen.kt
index 8a6f4bb70..3c6dca280 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/viewall/ViewAllScreen.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/screen/viewall/ViewAllScreen.kt
@@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridScope
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
@@ -21,6 +22,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
+import team.duckie.app.android.common.compose.getUniqueKey
import team.duckie.app.android.common.compose.itemsPagingKey
import team.duckie.app.android.common.compose.ui.BackPressedHeadLine2TopAppBar
import team.duckie.app.android.common.compose.ui.DuckExamSmallCoverForColumn
@@ -33,6 +35,21 @@ import team.duckie.app.android.feature.profile.viewmodel.state.ExamType
import team.duckie.app.android.feature.profile.viewmodel.state.mapper.toUiModel
import team.duckie.quackquack.material.QuackColor
+private const val GRID_COUNT: Int = 2
+
+/**
+ * [GRID_COUNT]만큼 item 을 생성하여 [Arrangement.spacedBy]로 스크롤에 여백을 만듭니다.
+ */
+private fun LazyGridScope.spaceItem(
+ maxIndex: Int,
+ content: @Composable () -> Unit = {},
+) {
+ val count = maxIndex % GRID_COUNT + 1
+ repeat(count) {
+ item { content() }
+ }
+}
+
@Composable
fun ViewAllScreen(
examType: ExamType,
@@ -52,26 +69,31 @@ fun ViewAllScreen(
title = getViewAllTitle(examType = examType),
onBackPressed = onBackPressed,
)
- Spacer(space = 20.dp)
LazyVerticalGrid(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
- columns = GridCells.Fixed(2),
+ columns = GridCells.Fixed(GRID_COUNT),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
+ repeat(GRID_COUNT) {
+ item {
+ Spacer(space = 20.dp)
+ }
+ }
when (examType) {
ExamType.Heart, ExamType.Created -> {
items(
count = profileExams.itemCount,
key = itemsPagingKey(
items = profileExams,
- key = { profileExams[it]?.id },
+ key = { profileExams[it]?.id?.getUniqueKey(it) },
),
) { index ->
profileExams[index]?.let { item ->
val duckTestCoverItem = item.toUiModel()
DuckExamSmallCoverForColumn(
+ modifier = Modifier.padding(bottom = 40.dp),
duckTestCoverItem = duckTestCoverItem,
onItemClick = { onItemClick(duckTestCoverItem) },
isLoading = profileExamInstances.loadState.append == LoadState.Loading,
@@ -79,6 +101,7 @@ fun ViewAllScreen(
)
}
}
+ spaceItem(profileExams.itemCount)
}
ExamType.Solved -> {
@@ -86,7 +109,7 @@ fun ViewAllScreen(
count = profileExamInstances.itemCount,
key = itemsPagingKey(
items = profileExamInstances,
- key = { profileExamInstances[it]?.id },
+ key = { profileExams[it]?.id?.getUniqueKey(it) },
),
) { index ->
profileExamInstances[index]?.let { item ->
@@ -98,7 +121,11 @@ fun ViewAllScreen(
onMoreClick = onMoreClick, // 추후 신고하기 구현 필요
)
}
+ if (index == profileExams.itemCount) {
+ Spacer(space = 40.dp)
+ }
}
+ spaceItem(profileExamInstances.itemCount)
}
}
}
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileEditViewModel.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileEditViewModel.kt
index cf0d26c87..ac124c84e 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileEditViewModel.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileEditViewModel.kt
@@ -15,7 +15,9 @@ import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.launch
import org.orbitmvi.orbit.Container
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
@@ -111,10 +113,6 @@ class ProfileEditViewModel @Inject constructor(
}
}
- fun clickEditProfile() = intent {
- loadGalleryImages()
- }
-
private fun initUser() = intent {
updateLoading(true)
getUserUseCase(state.userId).onSuccess { user ->
@@ -135,17 +133,19 @@ class ProfileEditViewModel @Inject constructor(
}
}
- private fun loadGalleryImages() = intent {
- loadGalleryImagesUseCase().onSuccess { images ->
- changeGalleryState(
- galleryState = state.galleryState.copy(
- images = persistentListOf(*images.toTypedArray()),
- imagesSelections = images.fastMap { false }.toImmutableList(),
- ),
- )
- changePhotoPickerVisible(true)
- }.onFailure {
- postSideEffect(ProfileEditSideEffect.ReportError(it))
+ fun loadGalleryImages() = intent {
+ viewModelScope.launch(Dispatchers.IO) {
+ loadGalleryImagesUseCase().onSuccess { images ->
+ changeGalleryState(
+ galleryState = state.galleryState.copy(
+ images = persistentListOf(*images.toTypedArray()),
+ imagesSelections = images.fastMap { false }.toImmutableList(),
+ ),
+ )
+ changePhotoPickerVisible(true)
+ }.onFailure {
+ postSideEffect(ProfileEditSideEffect.ReportError(it))
+ }
}
}
@@ -200,8 +200,6 @@ class ProfileEditViewModel @Inject constructor(
}
fun clickEditComplete(applicationContext: Context?) = intent {
- updateLoading(true)
-
getUploadableFileUrl(
state.profile.toString(),
applicationContext,
@@ -217,14 +215,11 @@ class ProfileEditViewModel @Inject constructor(
nickname = state.nickname,
introduction = state.introduce,
).onSuccess {
- updateLoading(false)
postSideEffect(ProfileEditSideEffect.NavigateBack)
}.onFailure {
- updateLoading(false)
postSideEffect(ProfileEditSideEffect.ReportError(it))
}
}.onFailure {
- updateLoading(false)
postSideEffect(ProfileEditSideEffect.ReportError(it))
}
}
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileViewModel.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileViewModel.kt
index e35a2b60c..3441b238a 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileViewModel.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/ProfileViewModel.kt
@@ -21,6 +21,7 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.ContainerHost
+import org.orbitmvi.orbit.syntax.simple.SimpleSyntax
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
@@ -87,7 +88,7 @@ internal class ProfileViewModel @Inject constructor(
fun init() = intent {
val userId = savedStateHandle.getStateFlow(Extras.UserId, 0).value
- startLoading()
+ updateLoading(true)
val job = viewModelScope.launch {
getMeUseCase()
@@ -97,7 +98,7 @@ internal class ProfileViewModel @Inject constructor(
reduce { state.copy(step = ProfileStep.Error) }
postSideEffect(ProfileSideEffect.ReportError(it))
}.also {
- stopLoading()
+ updateLoading(false)
}
}.apply { join() }
@@ -118,7 +119,7 @@ internal class ProfileViewModel @Inject constructor(
reduce { state.copy(step = ProfileStep.Error) }
postSideEffect(ProfileSideEffect.ReportError(it))
}.also {
- stopLoading()
+ updateLoading(false)
}
}
}
@@ -156,7 +157,6 @@ internal class ProfileViewModel @Inject constructor(
}
fun getUserProfile() = intent {
- startLoading()
viewModelScope.launch {
fetchUserProfileUseCase(state.userId)
.onSuccess { profile ->
@@ -170,8 +170,6 @@ internal class ProfileViewModel @Inject constructor(
.onFailure {
reduce { state.copy(step = ProfileStep.Error) }
postSideEffect(ProfileSideEffect.ReportError(it))
- }.also {
- stopLoading()
}
}
}
@@ -241,15 +239,9 @@ internal class ProfileViewModel @Inject constructor(
postSideEffect(ProfileSideEffect.NavigateToMakeExam)
}
- private fun startLoading() = intent {
- reduce {
- state.copy(isLoading = true)
- }
- }
-
- private fun stopLoading() = intent {
+ private suspend fun SimpleSyntax.updateLoading(isLoading: Boolean) {
reduce {
- state.copy(isLoading = false)
+ state.copy(isLoading = isLoading)
}
}
}
diff --git a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/state/ProfileEditState.kt b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/state/ProfileEditState.kt
index 4084f905c..6b84e6ae0 100644
--- a/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/state/ProfileEditState.kt
+++ b/feature/profile/src/main/kotlin/team/duckie/app/android/feature/profile/viewmodel/state/ProfileEditState.kt
@@ -19,7 +19,6 @@ data class ProfileEditState(
val nicknameState: NicknameState = NicknameState.Checking,
val galleryState: GalleryState = GalleryState(),
val introduce: String = "",
- val introduceFocused: Boolean = false,
val userId: Int = 0,
)
diff --git a/feature/profile/src/main/res/values/strings.xml b/feature/profile/src/main/res/values/strings.xml
index 06a1d2778..657bc2edb 100644
--- a/feature/profile/src/main/res/values/strings.xml
+++ b/feature/profile/src/main/res/values/strings.xml
@@ -38,6 +38,7 @@
이름에는 문자, 숫자, 밑줄, 마침표만 사용할 수 있어요.
이미 있는 이름이에요
전체보기
+ 사용 가능한 닉네임이에요!
닉네임
소개
diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts
index 3b189d08f..ec18da48a 100644
--- a/feature/search/build.gradle.kts
+++ b/feature/search/build.gradle.kts
@@ -28,10 +28,13 @@ dependencies {
projects.common.compose,
libs.orbit.viewmodel,
libs.orbit.compose,
- libs.quack.ui.components,
libs.compose.lifecycle.runtime,
+ libs.compose.ui.material,
libs.firebase.crashlytics,
libs.paging.runtime,
libs.paging.compose,
+ libs.quack.v2.ui,
+ libs.quack.v2.ui.plugin.interceptor.textfield,
+ libs.kotlin.collections.immutable,
)
}
diff --git a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchActivity.kt b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchActivity.kt
index aeb7a2eee..37322b074 100644
--- a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchActivity.kt
+++ b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchActivity.kt
@@ -5,37 +5,74 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalMaterialApi::class)
+// TODO(limsaehyun): The function onCreate is too long (127).
+// The maximum length is 100. [LongMethod] 대응 필요
+@file:Suppress("LongMethod")
+
package team.duckie.app.android.feature.search.screen
import android.os.Bundle
+import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.systemBarsPadding
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.material.ExperimentalMaterialApi
+import androidx.compose.material.ModalBottomSheetValue
+import androidx.compose.material.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.paging.compose.LazyPagingItems
import com.google.firebase.crashlytics.ktx.crashlytics
import com.google.firebase.ktx.Firebase
import dagger.hilt.android.AndroidEntryPoint
+import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.android.deeplink.DynamicLinkHelper
import team.duckie.app.android.common.android.ui.BaseActivity
import team.duckie.app.android.common.android.ui.const.Extras
+import team.duckie.app.android.common.android.ui.finishWithAnimation
+import team.duckie.app.android.common.android.ui.popStringExtra
+import team.duckie.app.android.common.compose.collectAndHandleState
+import team.duckie.app.android.common.compose.systemBarPaddings
+import team.duckie.app.android.common.compose.ui.DuckieCircularProgressIndicator
+import team.duckie.app.android.common.compose.ui.ErrorScreen
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.constant.SharedIcon
+import team.duckie.app.android.common.compose.ui.dialog.DuckieSelectableBottomSheetDialog
+import team.duckie.app.android.common.compose.ui.dialog.DuckieSelectableType
+import team.duckie.app.android.common.compose.ui.dialog.ReportDialog
+import team.duckie.app.android.common.compose.ui.quack.QuackNoUnderlineTextField
+import team.duckie.app.android.common.compose.util.addFocusCleaner
+import team.duckie.app.android.domain.exam.model.Exam
import team.duckie.app.android.feature.search.R
import team.duckie.app.android.feature.search.constants.SearchResultStep
import team.duckie.app.android.feature.search.constants.SearchStep
@@ -43,20 +80,15 @@ import team.duckie.app.android.feature.search.viewmodel.SearchViewModel
import team.duckie.app.android.feature.search.viewmodel.sideeffect.SearchSideEffect
import team.duckie.app.android.navigator.feature.detail.DetailNavigator
import team.duckie.app.android.navigator.feature.profile.ProfileNavigator
-import team.duckie.app.android.common.compose.ui.DuckieCircularProgressIndicator
-import team.duckie.app.android.common.compose.ui.ErrorScreen
-import team.duckie.app.android.common.compose.ui.constant.SharedIcon
-import team.duckie.app.android.common.compose.ui.quack.QuackNoUnderlineTextField
-import team.duckie.app.android.common.compose.collectAndHandleState
-import team.duckie.app.android.common.android.ui.finishWithAnimation
-import team.duckie.app.android.common.android.ui.popStringExtra
-import team.duckie.app.android.common.compose.ui.Spacer
-import team.duckie.quackquack.ui.animation.QuackAnimatedContent
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.theme.QuackTheme
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.ArrowBack
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.material.theme.QuackTheme
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.plugin.interceptor.textfield.QuackTextFieldFontFamilyRemovalPlugin
+import team.duckie.quackquack.ui.plugin.rememberQuackPlugins
import javax.inject.Inject
internal val SearchHorizontalPadding = PaddingValues(horizontal = 16.dp)
@@ -84,88 +116,152 @@ class SearchActivity : BaseActivity() {
setContent {
val state = vm.collectAsState().value
+ val focusRequester = remember { FocusRequester() }
+ val bottomSheetDialogState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
+ val coroutineScope = rememberCoroutineScope()
+ val keyboardController = LocalSoftwareKeyboardController.current
+ val searchText by vm.searchText.collectAsStateWithLifecycle()
+ val focusManager = LocalFocusManager.current
vm.searchUsers.collectAndHandleState(handleLoadStates = vm::checkError)
- vm.searchExams.collectAndHandleState(handleLoadStates = vm::checkError)
+ val searchExams =
+ vm.searchExams.collectAndHandleState(handleLoadStates = vm::checkError)
LaunchedEffect(key1 = vm) {
vm.container.sideEffectFlow
- .onEach(::handleSideEffect)
+ .onEach {
+ handleSideEffect(it, searchExams)
+ }
.launchIn(this)
}
- QuackTheme {
- Box(
+ LaunchedEffect(key1 = state.searchAutoFocusing) {
+ if (state.searchAutoFocusing) {
+ focusRequester.requestFocus()
+ }
+ }
+
+ QuackTheme(
+ plugins = rememberQuackPlugins {
+ +QuackTextFieldFontFamilyRemovalPlugin
+ },
+ ) {
+ ReportDialog(
+ visible = state.reportDialogVisible,
+ onClick = { vm.updateReportDialogVisible(false) },
+ onDismissRequest = { vm.updateReportDialogVisible(false) },
+ )
+ DuckieSelectableBottomSheetDialog(
modifier = Modifier
.fillMaxSize()
- .systemBarsPadding(),
- contentAlignment = Alignment.Center,
+ .systemBarsPadding()
+ .navigationBarsPadding(),
+ bottomSheetState = bottomSheetDialogState,
+ closeSheet = {
+ coroutineScope.launch {
+ bottomSheetDialogState.hide()
+ }
+ },
+ onReport = vm::report,
+ onCopyLink = vm::copyExamDynamicLink,
+ types = persistentListOf(
+ DuckieSelectableType.CopyLink,
+ DuckieSelectableType.Report,
+ ),
) {
- Column(
- modifier = Modifier.background(QuackColor.White.composeColor),
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .background(QuackColor.White.value)
+ .addFocusCleaner(),
+ contentAlignment = Alignment.Center,
) {
- SearchTextFieldTopBar(
- searchKeyword = state.searchKeyword,
- onSearchKeywordChanged = { keyword ->
- vm.updateSearchKeyword(keyword = keyword)
- },
- onPrevious = {
- finishWithAnimation()
- },
- clearSearchKeyword = {
- vm.clearSearchKeyword()
- },
- )
- QuackAnimatedContent(
- targetState = state.searchStep,
- ) { step ->
- when (step) {
- SearchStep.Search -> SearchScreen(vm = vm)
- SearchStep.SearchResult -> {
- if (state.isSearchProblemError &&
- state.tagSelectedTab == SearchResultStep.DuckExam
- ) {
- ErrorScreen(
- modifier = Modifier
- .fillMaxSize()
- .statusBarsPadding(),
- false,
- onRetryClick = {
- vm.fetchSearchExams(state.searchKeyword)
- },
- )
- } else if (state.isSearchUserError &&
- state.tagSelectedTab == SearchResultStep.User
- ) {
- ErrorScreen(
- modifier = Modifier
- .fillMaxSize()
- .statusBarsPadding(),
- false,
- onRetryClick = {
- vm.fetchSearchUsers(state.searchKeyword)
- },
- )
- } else {
- SearchResultScreen(
- navigateDetail = { examId ->
- vm.navigateToDetail(examId = examId)
- },
- )
+ Column {
+ systemBarPaddings
+ SearchTextFieldTopBar(
+ searchKeyword = searchText,
+ onSearchKeywordChanged = { keyword ->
+ vm.updateSearchKeyword(keyword = keyword)
+ },
+ onPrevious = {
+ finishWithAnimation()
+ },
+ clearSearchKeyword = {
+ vm.clearSearchKeyword()
+ },
+ onAction = {
+ focusManager.clearFocus()
+ },
+ focusRequester = focusRequester,
+ )
+ AnimatedContent(
+ targetState = state.searchStep,
+ label = "AnimatedContent",
+ ) { step ->
+ when (step) {
+ SearchStep.Search -> SearchScreen(
+ vm = vm,
+ onSearchComplete = {
+ focusManager.clearFocus()
+ },
+ )
+
+ SearchStep.SearchResult -> {
+ if (state.isSearchProblemError &&
+ state.tagSelectedTab == SearchResultStep.DuckExam
+ ) {
+ ErrorScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding(),
+ false,
+ onRetryClick = {
+ vm.fetchSearchExams(searchText)
+ },
+ )
+ } else if (state.isSearchUserError &&
+ state.tagSelectedTab == SearchResultStep.User
+ ) {
+ ErrorScreen(
+ modifier = Modifier
+ .fillMaxSize()
+ .statusBarsPadding(),
+ false,
+ onRetryClick = {
+ vm.fetchSearchUsers(searchText)
+ },
+ )
+ } else {
+ SearchResultScreen(
+ navigateDetail = { examId ->
+ vm.navigateToDetail(examId = examId)
+ },
+ openBottomSheet = { examId ->
+ vm.setTargetExamId(examId = examId)
+ coroutineScope.launch {
+ keyboardController?.hide()
+ bottomSheetDialogState.show()
+ }
+ },
+ )
+ }
}
}
}
}
- }
- if (state.isSearchLoading) {
- DuckieCircularProgressIndicator()
+ if (state.isSearchLoading) {
+ DuckieCircularProgressIndicator()
+ }
}
}
}
}
}
- private fun handleSideEffect(sideEffect: SearchSideEffect) {
+ private fun handleSideEffect(
+ sideEffect: SearchSideEffect,
+ examPagingItems: LazyPagingItems,
+ ) {
when (sideEffect) {
is SearchSideEffect.ReportError -> {
Firebase.crashlytics.recordException(sideEffect.exception)
@@ -188,6 +284,18 @@ class SearchActivity : BaseActivity() {
},
)
}
+
+ is SearchSideEffect.SendToast -> {
+ Toast.makeText(this, sideEffect.message, Toast.LENGTH_SHORT).show()
+ }
+
+ is SearchSideEffect.CopyDynamicLink -> {
+ DynamicLinkHelper.createAndShareLink(this, sideEffect.examId)
+ }
+
+ SearchSideEffect.ExamRefresh -> {
+ examPagingItems.refresh()
+ }
}
}
}
@@ -199,6 +307,8 @@ private fun SearchTextFieldTopBar(
onSearchKeywordChanged: (String) -> Unit,
clearSearchKeyword: () -> Unit,
onPrevious: () -> Unit,
+ onAction: () -> Unit,
+ focusRequester: FocusRequester,
) {
Row(
modifier = modifier
@@ -210,21 +320,23 @@ private fun SearchTextFieldTopBar(
),
verticalAlignment = Alignment.CenterVertically,
) {
- QuackImage(
- src = QuackIcon.ArrowBack,
- size = DpSize(all = 24.dp),
- onClick = onPrevious,
+ QuackIcon(
+ modifier = Modifier.quackClickable(onClick = onPrevious),
+ icon = QuackIcon.Outlined.ArrowBack,
)
Spacer(space = 8.dp)
QuackNoUnderlineTextField(
+ modifier = Modifier.focusRequester(focusRequester),
text = searchKeyword,
- onTextChanged = { keyword ->
- onSearchKeywordChanged(keyword)
- },
+ onTextChanged = onSearchKeywordChanged,
placeholderText = stringResource(id = R.string.try_search),
- trailingIcon = SharedIcon.ic_textfield_delete_16,
+ trailingIcon = if (searchKeyword.isNotEmpty()) SharedIcon.ic_textfield_delete_16 else null,
trailingIconOnClick = clearSearchKeyword,
- trailingEndPadding = 12.dp,
+ keyboardActions = KeyboardActions(
+ onDone = {
+ onAction()
+ },
+ ),
)
}
}
diff --git a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchResultScreen.kt b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchResultScreen.kt
index 4a3b2c274..b62ad035e 100644
--- a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchResultScreen.kt
+++ b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchResultScreen.kt
@@ -17,6 +17,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
@@ -32,49 +33,54 @@ import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.ui.DuckExamSmallCover
import team.duckie.app.android.common.compose.ui.DuckTestCoverItem
import team.duckie.app.android.common.compose.ui.Spacer
-import team.duckie.app.android.common.compose.ui.UserFollowingLayout
+import team.duckie.app.android.common.compose.ui.content.UserFollowingLayout
+import team.duckie.app.android.common.kotlin.fastForEach
import team.duckie.app.android.domain.exam.model.Exam
import team.duckie.app.android.feature.search.R
import team.duckie.app.android.feature.search.constants.SearchResultStep
import team.duckie.app.android.feature.search.viewmodel.SearchViewModel
import team.duckie.app.android.feature.search.viewmodel.state.SearchState
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody1
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackMainTab
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackTab
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun SearchResultScreen(
modifier: Modifier = Modifier,
vm: SearchViewModel = activityViewModel(),
navigateDetail: (Int) -> Unit,
+ openBottomSheet: (Int) -> Unit,
) {
val state = vm.collectAsState().value
val searchUsers = vm.searchUsers.collectAsLazyPagingItems()
val searchExams = vm.searchExams.collectAsLazyPagingItems()
- val tabTitles = SearchResultStep.values().map {
- it.title
- }.toPersistentList()
+ val tabTitles = remember {
+ SearchResultStep.values().map { step ->
+ step.title
+ }.toPersistentList()
+ }
Column(
modifier = modifier
.fillMaxSize()
.nestedScroll(rememberNestedScrollInteropConnection()),
) {
- QuackMainTab(
- titles = tabTitles,
- selectedTabIndex = state.tagSelectedTab.index,
- onTabSelected = { index ->
- vm.updateSearchResultTab(SearchResultStep.toStep(index))
- },
- )
+ QuackTab(index = state.tagSelectedTab.index) {
+ tabTitles.fastForEach { label ->
+ tab(label) { index ->
+ vm.updateSearchResultTab(SearchResultStep.toStep(index))
+ }
+ }
+ }
when (state.tagSelectedTab) {
SearchResultStep.DuckExam -> {
SearchResultForExam(
searchExams = searchExams,
navigateDetail = navigateDetail,
+ onMoreClick = openBottomSheet,
)
}
@@ -108,14 +114,18 @@ private fun SearchResultForUser(
.padding(top = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- QuackHeadLine1(
+ QuackText(
text = stringResource(id = R.string.no_search_user),
- color = QuackColor.Gray1,
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.Gray1,
+ ),
)
Spacer(space = 12.dp)
- QuackBody1(
+ QuackText(
text = stringResource(id = R.string.search_another_keyword),
- color = QuackColor.Gray1,
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Gray1,
+ ),
)
}
} else {
@@ -128,10 +138,10 @@ private fun SearchResultForUser(
favoriteTag = item?.favoriteTag ?: "",
tier = item?.tier ?: "",
isFollowing = item?.isFollowing ?: false,
- onClickFollow = { follow ->
+ onClickTrailingButton = { follow ->
onClickFollow(item?.userId ?: 0, follow)
},
- isMine = myUserId == item?.userId,
+ visibleTrailingButton = myUserId != item?.userId,
onClickUserProfile = onClickUserProfile,
)
}
@@ -143,6 +153,7 @@ private fun SearchResultForUser(
private fun SearchResultForExam(
searchExams: LazyPagingItems,
navigateDetail: (Int) -> Unit,
+ onMoreClick: (Int) -> Unit,
) {
if (searchExams.itemCount == 0) {
Column(
@@ -151,18 +162,23 @@ private fun SearchResultForExam(
.padding(top = 60.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
- QuackHeadLine1(
+ QuackText(
text = stringResource(id = R.string.no_search_exam),
- color = QuackColor.Gray1,
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.Gray1,
+ ),
)
Spacer(space = 12.dp)
- QuackBody1(
+ QuackText(
text = stringResource(id = R.string.search_another_keyword),
- color = QuackColor.Gray1,
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Gray1,
+ ),
)
}
} else {
LazyVerticalGrid(
+ modifier = Modifier.padding(top = 20.dp),
columns = GridCells.Fixed(2),
state = rememberLazyGridState(),
verticalArrangement = Arrangement.spacedBy(48.dp),
@@ -183,7 +199,9 @@ private fun SearchResultForExam(
onItemClick = {
navigateDetail(exam?.id ?: 0)
},
- onMoreClick = {},
+ onMoreClick = {
+ onMoreClick(exam?.id ?: 0)
+ },
)
}
}
diff --git a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchScreen.kt b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchScreen.kt
index 1949f6376..1d8d3e6d1 100644
--- a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchScreen.kt
+++ b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/screen/SearchScreen.kt
@@ -36,19 +36,22 @@ import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.domain.tag.model.Tag
import team.duckie.app.android.feature.search.R
import team.duckie.app.android.feature.search.viewmodel.SearchViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody1
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackTitle2
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.icon.quackicon.outlined.Search
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackBody1
+import team.duckie.quackquack.ui.sugar.QuackTitle2
@Composable
internal fun SearchScreen(
vm: SearchViewModel,
+ onSearchComplete: () -> Unit,
) {
LaunchedEffect(Unit) {
vm.getRecentSearch()
@@ -61,7 +64,7 @@ internal fun SearchScreen(
.fillMaxSize()
.imePadding(),
) {
- Spacer(modifier = Modifier.height(22.dp))
+ Spacer(modifier = Modifier.height(16.dp))
if (state.recentSearch.isEmpty()) {
RecentSearchNotFoundScreen()
} else {
@@ -75,6 +78,7 @@ internal fun SearchScreen(
vm.clearRecentSearch(keyword = keyword)
},
navigateToResult = { keyword ->
+ onSearchComplete()
vm.updateSearchKeyword(
keyword = keyword,
debounce = false,
@@ -98,14 +102,18 @@ private fun RecentSearchNotFoundScreen() {
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
- QuackHeadLine1(
+ QuackText(
text = stringResource(id = R.string.no_recent_search),
- color = QuackColor.Gray1,
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.Gray1,
+ ),
)
Spacer(space = 12.dp)
- QuackBody1(
+ QuackText(
text = stringResource(id = R.string.search_favorite_exam),
- color = QuackColor.Gray1,
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.Gray1,
+ ),
)
}
}
@@ -125,9 +133,9 @@ private fun LazyListScope.recommendKeywordSection(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
- .quackClickable {
- onClickedSearch()
- },
+ .quackClickable(
+ onClick = onClickedSearch,
+ ),
contentAlignment = Alignment.CenterStart,
) {
QuackTitle2(text = tag?.name ?: "") // TODO(limsaehyun): QuackAnnotationTitle2 교체 필요
@@ -154,12 +162,14 @@ private fun LazyListScope.recentKeywordSection(
) {
QuackTitle2(text = stringResource(id = R.string.recent_search))
Spacer(modifier = Modifier.weight(1f))
- QuackBody2(
+ QuackText(
+ modifier = Modifier.quackClickable(
+ onClick = onClickedClearAll,
+ ),
text = stringResource(id = R.string.clear_all),
- color = QuackColor.Gray1,
- onClick = {
- onClickedClearAll()
- },
+ typography = QuackTypography.Body2.change(
+ color = QuackColor.Gray1,
+ ),
)
}
}
@@ -189,19 +199,23 @@ private fun RecentSearchLayout(
.padding(SearchHorizontalPadding)
.padding(vertical = 12.dp),
) {
- QuackImage(
- src = QuackIcon.Search,
- size = DpSize(16.dp),
+ QuackIcon(
+ icon = QuackIcon.Outlined.Search,
+ size = 16.dp,
tint = QuackColor.Gray2,
)
Spacer(modifier = Modifier.width(8.dp))
QuackBody1(text = keyword)
Spacer(modifier = Modifier.weight(1f))
- QuackImage(
- src = QuackIcon.Close,
- size = DpSize(16.dp),
+ QuackIcon(
+ modifier = Modifier.quackClickable(
+ onClick = {
+ onCloseClick(keyword)
+ },
+ ),
+ icon = QuackIcon.Outlined.Close,
+ size = 16.dp,
tint = QuackColor.Gray2,
- onClick = { onCloseClick(keyword) },
)
}
}
diff --git a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/SearchViewModel.kt b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/SearchViewModel.kt
index c9fd10083..16b21e11b 100644
--- a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/SearchViewModel.kt
+++ b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/SearchViewModel.kt
@@ -7,6 +7,7 @@
package team.duckie.app.android.feature.search.viewmodel
+import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.LoadState
@@ -21,6 +22,7 @@ import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.map
@@ -31,9 +33,13 @@ import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import team.duckie.app.android.common.android.ui.const.Debounce
+import team.duckie.app.android.common.android.ui.const.Extras
+import team.duckie.app.android.common.compose.ui.dialog.ReportAlreadyExists
+import team.duckie.app.android.common.kotlin.exception.isReportAlreadyExists
import team.duckie.app.android.domain.exam.model.Exam
import team.duckie.app.android.domain.follow.model.FollowBody
import team.duckie.app.android.domain.follow.usecase.FollowUseCase
+import team.duckie.app.android.domain.report.usecase.ReportUseCase
import team.duckie.app.android.domain.search.usecase.ClearAllRecentSearchUseCase
import team.duckie.app.android.domain.search.usecase.ClearRecentSearchUseCase
import team.duckie.app.android.domain.search.usecase.GetRecentSearchUseCase
@@ -59,6 +65,8 @@ internal class SearchViewModel @Inject constructor(
private val clearRecentSearchUseCase: ClearRecentSearchUseCase,
private val followUseCase: FollowUseCase,
private val getMeUseCase: GetMeUseCase,
+ private val reportUseCase: ReportUseCase,
+ private val savedStateHandle: SavedStateHandle,
) : ContainerHost, ViewModel() {
override val container = container(SearchState())
@@ -70,8 +78,18 @@ internal class SearchViewModel @Inject constructor(
MutableStateFlow>(PagingData.empty())
val searchUsers: Flow> = _searchUsers
+ private val _searchText = MutableStateFlow("")
+ val searchText: StateFlow = _searchText
+
init {
initState()
+ getAutoFocusing()
+ }
+
+ private fun getAutoFocusing() = intent {
+ val autoFocusing = savedStateHandle.getStateFlow(Extras.AutoFocusing, true).value
+
+ reduce { state.copy(searchAutoFocusing = autoFocusing) }
}
/** [SearchViewModel]의 초기 상태를 설정한다. */
@@ -94,7 +112,7 @@ internal class SearchViewModel @Inject constructor(
).apply {
intent {
this@apply.debounce(Debounce.SearchSecond).collectLatest { query ->
- refreshSearchStep(keyword = state.searchKeyword)
+ refreshSearchStep(keyword = _searchText.value)
// TODO(limsaehyun): 추후 추천 검색어 비즈니스 로직을 이곳에서 작업해야 함
}
}
@@ -149,6 +167,12 @@ internal class SearchViewModel @Inject constructor(
}
}
+ fun updateReportDialogVisible(visible: Boolean) = intent {
+ reduce {
+ state.copy(reportDialogVisible = visible)
+ }
+ }
+
/** [keyword]에 따른 덕질고사 검색 결과를 가져온다. */
internal fun fetchSearchExams(keyword: String) {
intent { reduce { state.copy(isSearchProblemError = false) } }
@@ -178,6 +202,35 @@ internal class SearchViewModel @Inject constructor(
}
}
+ fun setTargetExamId(examId: Int) = intent {
+ reduce {
+ state.copy(targetExamId = examId)
+ }
+ }
+
+ fun report() = intent {
+ reportUseCase(state.targetExamId)
+ .onSuccess {
+ updateReportDialogVisible(true)
+ postSideEffect(SearchSideEffect.ExamRefresh)
+ }
+ .onFailure { exception ->
+ when {
+ exception.isReportAlreadyExists -> postSideEffect(
+ SearchSideEffect.SendToast(ReportAlreadyExists),
+ )
+
+ else -> postSideEffect(SearchSideEffect.ReportError(exception))
+ }
+ }
+ }
+
+ fun copyExamDynamicLink() = intent {
+ val examId = state.targetExamId
+
+ postSideEffect(SearchSideEffect.CopyDynamicLink(examId))
+ }
+
/** 검색 화면에서 [query] 값에 맞는 검색 결과를 가져온다. */
private suspend fun recommendKeywords(query: String) {
_getRecommendKeywords.emit(query)
@@ -187,10 +240,9 @@ internal class SearchViewModel @Inject constructor(
fun updateSearchKeyword(
keyword: String,
debounce: Boolean = true,
- ) = intent {
- reduce {
- state.copy(searchKeyword = keyword)
- }.run {
+ ) {
+ viewModelScope.launch {
+ _searchText.value = keyword
recommendKeywords(query = keyword)
if (!debounce) refreshSearchStep(keyword = keyword)
}
@@ -206,12 +258,12 @@ internal class SearchViewModel @Inject constructor(
if (keyword.isEmpty()) {
navigateSearchStep(
step = SearchStep.Search,
- keyword = state.searchKeyword,
+ keyword = _searchText.value,
)
} else {
navigateSearchStep(
step = SearchStep.SearchResult,
- keyword = state.searchKeyword,
+ keyword = _searchText.value,
)
}
}
diff --git a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/sideeffect/SearchSideEffect.kt b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/sideeffect/SearchSideEffect.kt
index 1e579a95f..f7c4a00eb 100644
--- a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/sideeffect/SearchSideEffect.kt
+++ b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/sideeffect/SearchSideEffect.kt
@@ -19,4 +19,10 @@ internal sealed class SearchSideEffect {
class NavigateToDetail(val examId: Int) : SearchSideEffect()
class NavigateToUserProfile(val userId: Int) : SearchSideEffect()
+
+ class SendToast(val message: String) : SearchSideEffect()
+
+ object ExamRefresh : SearchSideEffect()
+
+ class CopyDynamicLink(val examId: Int) : SearchSideEffect()
}
diff --git a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/state/SearchState.kt b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/state/SearchState.kt
index b1ea923d6..fce65e535 100644
--- a/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/state/SearchState.kt
+++ b/feature/search/src/main/kotlin/team/duckie/app/android/feature/search/viewmodel/state/SearchState.kt
@@ -7,6 +7,7 @@
package team.duckie.app.android.feature.search.viewmodel.state
+import androidx.compose.runtime.Immutable
import androidx.paging.PagingData
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@@ -27,6 +28,7 @@ import team.duckie.app.android.feature.search.constants.SearchStep
* [searchKeyword] 검색어
* [tagSelectedTab] 검색 결과에서 선택된 탭
*/
+@Immutable
internal data class SearchState(
val me: User? = null,
val isSearchLoading: Boolean = false,
@@ -35,8 +37,11 @@ internal data class SearchState(
val searchStep: SearchStep = SearchStep.Search,
val recentSearch: ImmutableList = persistentListOf(),
val recommendSearchs: Flow> = flow { PagingData.empty() },
- val searchKeyword: String = "",
val tagSelectedTab: SearchResultStep = SearchResultStep.DuckExam,
+ val targetExamId: Int = 0,
+ val bottomSheetVisible: Boolean = false,
+ val reportDialogVisible: Boolean = false,
+ val searchAutoFocusing: Boolean = false,
) {
data class SearchUser(
val userId: Int,
diff --git a/feature/setting/build.gradle.kts b/feature/setting/build.gradle.kts
index 4fa518d00..c754a1f31 100644
--- a/feature/setting/build.gradle.kts
+++ b/feature/setting/build.gradle.kts
@@ -41,7 +41,7 @@ dependencies {
projects.feature.devMode,
libs.orbit.viewmodel,
libs.orbit.compose,
- libs.quack.ui.components,
+ libs.kotlin.collections.immutable,
libs.quack.v2.ui,
libs.compose.lifecycle.runtime,
libs.compose.ui.accompanist.webview,
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/component/SettingContentLayout.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/component/SettingContentLayout.kt
index a8e8f77c8..fb6333187 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/component/SettingContentLayout.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/component/SettingContentLayout.kt
@@ -10,50 +10,52 @@ package team.duckie.app.android.feature.setting.component
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.util.fillMaxScreenWidth
import team.duckie.app.android.feature.setting.constans.SettingDesignToken
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.sugar.QuackBody1
-import team.duckie.quackquack.ui.sugar.QuackTitle2
@Composable
internal fun SettingContentLayout(
+ modifier: Modifier = Modifier,
title: String,
content: String? = null,
trailingText: String? = null,
onTrailingTextClick: (() -> Unit)? = null,
- isBold: Boolean,
+ typography: QuackTypography = QuackTypography.Title2,
+ horizontalArrangement: Arrangement.Horizontal = Arrangement.SpaceBetween,
onClick: (() -> Unit)? = null,
) = with(SettingDesignToken) {
Row(
- modifier = Modifier
+ modifier = modifier
.fillMaxWidth()
- .height(44.dp)
+ .fillMaxScreenWidth()
.quackClickable(
- rippleEnabled = false,
- ) {
- if (onClick != null) {
- onClick()
- }
- },
+ rippleEnabled = true,
+ onClick = {
+ if (onClick != null) {
+ onClick()
+ }
+ },
+ )
+ .padding(
+ horizontal = 16.dp,
+ vertical = 12.dp,
+ ),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween,
+ horizontalArrangement = horizontalArrangement,
) {
- if (isBold) {
- QuackTitle2(
- text = title,
- )
- } else {
- QuackBody1(
- text = title,
- )
- }
+ QuackText(
+ text = title,
+ typography = typography,
+ )
if (content != null) {
QuackText(
modifier = Modifier.padding(start = 12.dp),
@@ -61,6 +63,7 @@ internal fun SettingContentLayout(
typography = SettingHorizontalResultTypography,
)
}
+ Spacer(weight = 1f)
if (trailingText != null) {
QuackText(
modifier = Modifier
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/SettingType.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/SettingType.kt
index 1638d8d61..33cdd071e 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/SettingType.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/SettingType.kt
@@ -12,9 +12,12 @@ import kotlinx.collections.immutable.persistentListOf
import team.duckie.app.android.feature.setting.R
import team.duckie.app.android.feature.setting.constans.SettingType.AccountInfo
import team.duckie.app.android.feature.setting.constans.SettingType.Inquiry
+import team.duckie.app.android.feature.setting.constans.SettingType.ListOfIgnoreExam
+import team.duckie.app.android.feature.setting.constans.SettingType.ListOfIgnoreUser
import team.duckie.app.android.feature.setting.constans.SettingType.Main
import team.duckie.app.android.feature.setting.constans.SettingType.MainPolicy
import team.duckie.app.android.feature.setting.constans.SettingType.Notification
+import team.duckie.app.android.feature.setting.constans.SettingType.PrivacyPolicy
import team.duckie.app.android.feature.setting.constans.SettingType.Version
/**
@@ -31,6 +34,12 @@ enum class SettingType(
@StringRes
val titleRes: Int,
) {
+ ListOfIgnoreUser(
+ titleRes = R.string.list_of_ignore_user,
+ ),
+ ListOfIgnoreExam(
+ titleRes = R.string.list_of_ignore_exam,
+ ),
Main(
titleRes = R.string.app_setting,
),
@@ -67,16 +76,16 @@ enum class SettingType(
PrivacyPolicy,
)
- /** [AccountInfo] 안에 위치한 설정 */
- private val accountInfoPages = persistentListOf(
- WithDraw,
+ val userSettings = persistentListOf(
+ AccountInfo,
+ ListOfIgnoreUser,
+ ListOfIgnoreExam,
)
- /** 메인 설정 페이지에 표시될 설정 */
- val settingPages = SettingType
- .values()
- .filter { it !in listOf(Main) }
- .filter { it !in policyPages }
- .filter { it !in accountInfoPages }
+ val otherSettings = persistentListOf(
+ Inquiry,
+ MainPolicy,
+ Version,
+ )
}
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/Withdraweason.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/Withdraweason.kt
index a0c2e720e..6e1543164 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/Withdraweason.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/constans/Withdraweason.kt
@@ -40,7 +40,7 @@ internal enum class Withdraweason(
/** 기타 */
OTHERS(
- description = R.string.withdraw_others,
+ description = R.string.others,
),
;
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingAccountInfoScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingAccountInfoScreen.kt
index b1886f905..00bbcfa8f 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingAccountInfoScreen.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingAccountInfoScreen.kt
@@ -7,40 +7,29 @@
package team.duckie.app.android.feature.setting.screen
-import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import team.duckie.app.android.common.compose.ui.QuackMaxWidthDivider
-import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.feature.setting.R
import team.duckie.app.android.feature.setting.component.SettingContentLayout
import team.duckie.app.android.feature.setting.constans.SettingDesignToken
-import team.duckie.quackquack.ui.QuackImage
-import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.sugar.QuackBody1
+import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.ui.sugar.QuackSubtitle2
-private val KakaoColor: Color = Color(0xFFFEE500)
+// private val KakaoColor: Color = Color(0xFFFEE500)
+@Suppress("UnusedPrivateMember") // TODO(limsaehyun) 추후 이메일 작업 필요
@Composable
fun SettingAccountInfoScreen(
email: String,
@@ -61,36 +50,37 @@ fun SettingAccountInfoScreen(
modifier = Modifier.padding(vertical = 12.dp),
text = stringResource(id = R.string.sign_in_account),
)
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .height(44.dp),
- verticalAlignment = Alignment.CenterVertically,
- ) {
- QuackBody1(
- text = stringResource(id = R.string.email),
- )
- Spacer(space = 12.dp)
- Box(
- modifier = Modifier
- .size(18.dp)
- .background(
- color = KakaoColor,
- shape = RoundedCornerShape(2.dp),
- ),
- contentAlignment = Alignment.Center,
- ) {
- QuackImage(
- modifier = Modifier.size(12.dp, 10.dp),
- src = R.drawable.ic_setting_kakao,
- )
- }
- Spacer(space = 4.dp)
- QuackText(
- text = email,
- typography = SettingHorizontalResultTypography,
- )
- }
+// 이메일 로직: 필요시 주석 해제 후 사용
+// Row(
+// modifier = Modifier
+// .fillMaxWidth()
+// .height(44.dp),
+// verticalAlignment = Alignment.CenterVertically,
+// ) {
+// QuackBody1(
+// text = stringResource(id = R.string.email),
+// )
+// Spacer(space = 12.dp)
+// Box(
+// modifier = Modifier
+// .size(18.dp)
+// .background(
+// color = KakaoColor,
+// shape = RoundedCornerShape(2.dp),
+// ),
+// contentAlignment = Alignment.Center,
+// ) {
+// QuackImage(
+// modifier = Modifier.size(12.dp, 10.dp),
+// src = R.drawable.ic_setting_kakao,
+// )
+// }
+// Spacer(space = 4.dp)
+// QuackText(
+// text = email,
+// typography = SettingHorizontalResultTypography,
+// )
+// }
QuackMaxWidthDivider(modifier = Modifier.padding(vertical = 16.dp))
LazyColumn(
verticalArrangement = Arrangement.spacedBy(4.dp),
@@ -98,8 +88,8 @@ fun SettingAccountInfoScreen(
items(rememberAccountInfoItems) { index ->
SettingContentLayout(
title = stringResource(id = index.first),
- isBold = false,
onClick = index.second,
+ typography = QuackTypography.Body1,
)
}
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingActivity.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingActivity.kt
index 4d196cd26..3161bd410 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingActivity.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingActivity.kt
@@ -34,6 +34,7 @@ import team.duckie.app.android.common.android.ui.finishWithAnimation
import team.duckie.app.android.common.android.ui.startActivityWithAnimation
import team.duckie.app.android.common.compose.ToastWrapper
import team.duckie.app.android.common.compose.systemBarPaddings
+import team.duckie.app.android.common.compose.ui.BackPressedHeadLine2TopAppBar
import team.duckie.app.android.common.compose.ui.DuckieTodoScreen
import team.duckie.app.android.common.compose.ui.dialog.DuckieDialog
import team.duckie.app.android.common.compose.ui.quack.QuackCrossfade
@@ -47,8 +48,6 @@ import team.duckie.app.android.feature.setting.viewmodel.state.SettingState
import team.duckie.app.android.navigator.feature.intro.IntroNavigator
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.theme.QuackTheme
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
import javax.inject.Inject
@AndroidEntryPoint
@@ -84,13 +83,9 @@ class SettingActivity : BaseActivity() {
.background(QuackColor.White.value)
.padding(systemBarPaddings),
) {
- QuackTopAppBar(
- leadingIcon = QuackIcon.ArrowBack,
- leadingText = stringResource(id = state.settingType.titleRes),
- onLeadingIconClick = {
- vm.navigateBack()
- },
- )
+ BackPressedHeadLine2TopAppBar(title = stringResource(id = state.settingType.titleRes)) {
+ vm.navigateBack()
+ }
Column(
modifier = Modifier
.padding(
@@ -138,6 +133,16 @@ class SettingActivity : BaseActivity() {
state = state,
)
+ SettingType.ListOfIgnoreUser -> SettingIgnoreUserScreen(
+ state = state,
+ vm = vm,
+ )
+
+ SettingType.ListOfIgnoreExam -> SettingIgnoreExamScreen(
+ vm = vm,
+ state = state,
+ )
+
else -> Unit
}
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingIgnoreExamScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingIgnoreExamScreen.kt
new file mode 100644
index 000000000..aafced5f8
--- /dev/null
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingIgnoreExamScreen.kt
@@ -0,0 +1,63 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.feature.setting.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import team.duckie.app.android.common.compose.ui.content.ExamIgnoreLayout
+import team.duckie.app.android.feature.setting.R
+import team.duckie.app.android.feature.setting.viewmodel.SettingViewModel
+import team.duckie.app.android.feature.setting.viewmodel.state.SettingState
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+
+@Composable
+internal fun SettingIgnoreExamScreen(
+ vm: SettingViewModel,
+ state: SettingState,
+) {
+ LaunchedEffect(key1 = Unit) {
+ vm.getIgnoreExams()
+ }
+
+ if (state.ignoreExams.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ QuackText(
+ text = stringResource(id = R.string.setting_ignore_exam_not_found),
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.DuckieOrange,
+ ),
+ )
+ }
+ } else {
+ LazyColumn {
+ items(state.ignoreExams) { item ->
+ ExamIgnoreLayout(
+ examId = item.id,
+ examThumbnailUrl = item.thumbnailUrl,
+ name = item.title,
+ onClickTrailingButton = { examId ->
+ vm.cancelIgnoreExam(examId = examId)
+ },
+ rippleEnabled = false,
+ )
+ }
+ }
+ }
+}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingIgnoreUserScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingIgnoreUserScreen.kt
new file mode 100644
index 000000000..4abf51365
--- /dev/null
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingIgnoreUserScreen.kt
@@ -0,0 +1,65 @@
+/*
+ * Designed and developed by Duckie Team, 2022
+ *
+ * Licensed under the MIT.
+ * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
+ */
+
+package team.duckie.app.android.feature.setting.screen
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import team.duckie.app.android.common.compose.ui.content.UserIgnoreLayout
+import team.duckie.app.android.feature.setting.R
+import team.duckie.app.android.feature.setting.viewmodel.SettingViewModel
+import team.duckie.app.android.feature.setting.viewmodel.state.SettingState
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+
+@Composable
+internal fun SettingIgnoreUserScreen(
+ vm: SettingViewModel,
+ state: SettingState,
+) {
+ LaunchedEffect(key1 = Unit) {
+ vm.getIgnoreUsers()
+ }
+
+ if (state.ignoreUsers.isEmpty()) {
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center,
+ ) {
+ QuackText(
+ text = stringResource(id = R.string.setting_ignore_user_not_found),
+ typography = QuackTypography.HeadLine1.change(
+ color = QuackColor.DuckieOrange,
+ ),
+ )
+ }
+ } else {
+ LazyColumn {
+ items(state.ignoreUsers) { item ->
+ UserIgnoreLayout(
+ userId = item.id,
+ profileImageIrl = item.profileImageUrl,
+ nickname = item.nickName,
+ favoriteTag = item.duckPower?.tag?.name ?: "",
+ tier = item.duckPower?.tier ?: "",
+ onClickTrailingButton = { userId ->
+ vm.cancelIgnoreUser(userId)
+ },
+ rippleEnabled = false,
+ )
+ }
+ }
+ }
+}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingInquiryScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingInquiryScreen.kt
index d1a5e08bd..229f38ab9 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingInquiryScreen.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingInquiryScreen.kt
@@ -22,6 +22,7 @@ import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import team.duckie.app.android.feature.setting.R
import team.duckie.app.android.feature.setting.component.SettingContentLayout
+import team.duckie.quackquack.material.QuackTypography
import team.duckie.quackquack.ui.sugar.QuackSubtitle2
/**
@@ -57,7 +58,8 @@ fun SettingInquiryScreen() {
SettingContentLayout(
title = stringResource(id = item.first),
content = item.second,
- isBold = false,
+ typography = QuackTypography.Body1,
+ horizontalArrangement = Arrangement.Start,
)
}
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainPolicyScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainPolicyScreen.kt
index 73c8cd4f5..6308912c9 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainPolicyScreen.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainPolicyScreen.kt
@@ -20,6 +20,7 @@ import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.feature.setting.R
import team.duckie.app.android.feature.setting.component.SettingContentLayout
import team.duckie.app.android.feature.setting.constans.SettingType
+import team.duckie.quackquack.material.QuackTypography
@Composable
fun SettingMainPolicyScreen(
@@ -36,7 +37,7 @@ fun SettingMainPolicyScreen(
items(SettingType.policyPages) { page ->
SettingContentLayout(
title = stringResource(id = page.titleRes),
- isBold = false,
+ typography = QuackTypography.Body1,
) {
navigatePage(page)
}
@@ -46,7 +47,7 @@ fun SettingMainPolicyScreen(
title = stringResource(
id = R.string.open_source_license,
),
- isBold = false,
+ typography = QuackTypography.Body1,
) {
navigateOssLicense()
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainScreen.kt
index cff063028..4006ce241 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainScreen.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingMainScreen.kt
@@ -7,32 +7,68 @@
package team.duckie.app.android.feature.setting.screen
+import androidx.annotation.StringRes
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.rememberToast
+import team.duckie.app.android.common.compose.ui.QuackMaxWidthDivider
+import team.duckie.app.android.feature.setting.R
import team.duckie.app.android.feature.setting.component.SettingContentLayout
import team.duckie.app.android.feature.setting.constans.SettingType
import team.duckie.app.android.feature.setting.viewmodel.SettingViewModel
+import team.duckie.quackquack.material.QuackTypography
@Composable
internal fun SettingMainScreen(
vm: SettingViewModel,
version: String,
) {
- val settingItems = SettingType.settingPages
+ val userSettings = SettingType.userSettings
+ val otherSettings = SettingType.otherSettings
+ val toast = rememberToast()
LazyColumn {
- items(settingItems) { item ->
+ titleSection(title = R.string.user_settings)
+ items(userSettings) { item ->
SettingContentLayout(
title = stringResource(id = item.titleRes),
+ onClick = { vm.navigateStep(item) },
+ typography = QuackTypography.Body1,
+ )
+ }
+ item {
+ Spacer(modifier = Modifier.height(12.dp))
+ QuackMaxWidthDivider()
+ SettingContentLayout(
+ modifier = Modifier
+ .padding(vertical = 12.dp),
+ title = stringResource(id = R.string.notification),
+ typography = QuackTypography.Body1,
+ onClick = {
+ toast.invoke("개발 중인 기능입니다!") // TODO(limsaehyun) 알림 develop 필요
+ },
+ )
+ QuackMaxWidthDivider()
+ }
+ titleSection(title = R.string.others)
+ items(otherSettings) { item ->
+ SettingContentLayout(
+ title = stringResource(id = item.titleRes),
+ typography = QuackTypography.Body1,
trailingText = if (item == SettingType.Version) version else null,
- onTrailingTextClick = if (item == SettingType.Version) {
- {
+ onTrailingTextClick = {
+ if (item == SettingType.Version) {
vm.changeDevModeDialogVisible(true)
}
- } else {
- null
},
onClick = {
when (item) {
@@ -45,8 +81,18 @@ internal fun SettingMainScreen(
}
}
},
- isBold = true,
)
}
}
}
+
+internal fun LazyListScope.titleSection(
+ @StringRes title: Int,
+) {
+ item {
+ SettingContentLayout(
+ title = LocalContext.current.getString(title),
+ typography = QuackTypography.Title2,
+ )
+ }
+}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingNotificationScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingNotificationScreen.kt
index 03f5aa9dc..6dfbf3f00 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingNotificationScreen.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingNotificationScreen.kt
@@ -4,6 +4,7 @@
* Licensed under the MIT.
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:Suppress("UnusedPrivateMember")
package team.duckie.app.android.feature.setting.screen
@@ -21,10 +22,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
-import team.duckie.app.android.feature.setting.constans.SettingNotificationType
import team.duckie.app.android.feature.setting.constans.SettingDesignToken
+import team.duckie.app.android.feature.setting.constans.SettingNotificationType
import team.duckie.quackquack.ui.QuackText
-import team.duckie.quackquack.ui.component.QuackSwitch
import team.duckie.quackquack.ui.sugar.QuackBody1
/**
@@ -51,6 +51,7 @@ fun SettingNotificationScreen() {
}
}
+@Suppress("unused")
@Composable
private fun SettingNotificationLayout(
title: String,
@@ -75,9 +76,10 @@ private fun SettingNotificationLayout(
)
}
Spacer(modifier = Modifier.weight(1f))
- QuackSwitch(
- checked = checked,
- onCheckedChange = onCheckedChange,
- )
+// QuackSwitch(
+// checked = checked,
+// onCheckedChange = onCheckedChange,
+// )
+// TODO(limsaehyun) [QuackQuack] Switch 작업 필요!
}
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingWithdrawScreen.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingWithdrawScreen.kt
index 0ced38060..1dbe58b55 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingWithdrawScreen.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/screen/SettingWithdrawScreen.kt
@@ -5,11 +5,15 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.feature.setting.screen
import android.annotation.SuppressLint
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -30,20 +34,23 @@ import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackReactionTextArea
+import team.duckie.app.android.common.compose.ui.quack.todo.animation.QuackRoundCheckBox
+import team.duckie.app.android.common.kotlin.runIf
import team.duckie.app.android.feature.setting.R
import team.duckie.app.android.feature.setting.constans.Withdraweason
import team.duckie.app.android.feature.setting.viewmodel.SettingViewModel
import team.duckie.app.android.feature.setting.viewmodel.state.SettingState
-import team.duckie.quackquack.animation.QuackAnimatedVisibility
+import team.duckie.quackquack.animation.animateQuackColorAsState
import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
import team.duckie.quackquack.ui.QuackImage
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.component.QuackReviewTextArea
-import team.duckie.quackquack.ui.component.QuackRoundCheckBox
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.quackquack.ui.QuackText
import team.duckie.quackquack.ui.sugar.QuackBody1
import team.duckie.quackquack.ui.sugar.QuackHeadLine2
+import team.duckie.quackquack.ui.sugar.QuackSubtitle
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
@Composable
internal fun SettingWithdrawScreen(
@@ -92,8 +99,8 @@ internal fun SettingWithdrawScreen(
)
}
item {
- QuackAnimatedVisibility(visible = state.withdrawReasonSelected == Withdraweason.OTHERS) {
- QuackReviewTextArea(
+ AnimatedVisibility(visible = state.withdrawReasonSelected == Withdraweason.OTHERS) {
+ QuackReactionTextArea(
modifier = Modifier
.padding(top = 4.dp)
.height(140.dp)
@@ -101,15 +108,14 @@ internal fun SettingWithdrawScreen(
.onFocusChanged { state ->
vm.updateWithDrawFocus(state.isFocused)
},
- text = state.withdrawUserInputReason,
- onTextChanged = { text ->
+ reaction = state.withdrawUserInputReason,
+ onReactionChanged = { text ->
vm.updateWithdrawUserInputReason(text)
},
- focused = state.withdrawIsFocused,
- placeholderText = stringResource(
- // TODO(limsaehyun): placeholder의 maxline을 설정할 수 있어야 함
+ placeHolderText = stringResource(
id = R.string.withdraw_others_text_field_hint,
),
+ visibleCurrentLength = false,
)
}
}
@@ -124,20 +130,49 @@ internal fun SettingWithdrawScreen(
.weight(1f)
.height(44.dp)
- QuackLargeButton(
- modifier = buttonModifier,
- type = QuackLargeButtonType.Border,
- text = stringResource(id = R.string.withdraw_cancel_msg),
- onClick = vm::navigateBack,
- )
+ val buttonEnabled = state.withdrawReasonSelected != Withdraweason.INITIAL
+
+ val primaryButtonColor =
+ animateQuackColorAsState(targetValue = if (buttonEnabled) QuackColor.DuckieOrange else QuackColor.Gray2)
+
+ Box(
+ modifier = buttonModifier
+ .background(
+ color = QuackColor.White.value,
+ shape = RoundedCornerShape(8.dp),
+ )
+ .border(
+ width = 1.dp,
+ shape = RoundedCornerShape(8.dp),
+ color = QuackColor.Gray3.value,
+ )
+ .quackClickable(
+ onClick = vm::navigateBack,
+ ),
+ contentAlignment = Alignment.Center,
+ ) {
+ QuackSubtitle(text = stringResource(id = R.string.withdraw_cancel_msg))
+ }
Spacer(space = 8.dp)
- QuackLargeButton(
- modifier = buttonModifier,
- type = QuackLargeButtonType.Fill,
- text = stringResource(id = R.string.withdraw),
- enabled = state.withdrawReasonSelected != Withdraweason.INITIAL,
+ Box(
+ modifier = buttonModifier
+ .background(
+ color = primaryButtonColor.value.value,
+ shape = RoundedCornerShape(8.dp),
+ )
+ .runIf(buttonEnabled) {
+ quackClickable(
+ onClick = { vm.changeWithdrawDialogVisible(true) },
+ )
+ },
+ contentAlignment = Alignment.Center,
) {
- vm.changeWithdrawDialogVisible(true)
+ QuackText(
+ text = stringResource(id = R.string.withdraw),
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.White,
+ ),
+ )
}
}
}
@@ -157,9 +192,11 @@ internal fun SettingCheckBox(
Row(
modifier = modifier
.fillMaxWidth()
- .quackClickable {
- onClick(reason)
- }
+ .quackClickable(
+ onClick = {
+ onClick(reason)
+ },
+ )
.clip(RoundedCornerShape(8.dp))
.background(
color = QuackColor.Gray4.value,
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/SettingViewModel.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/SettingViewModel.kt
index 776d76ccd..2b24df1c6 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/SettingViewModel.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/SettingViewModel.kt
@@ -9,6 +9,7 @@ package team.duckie.app.android.feature.setting.viewmodel
import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.collections.immutable.toImmutableList
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
@@ -16,7 +17,11 @@ import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
import team.duckie.app.android.common.kotlin.seconds
import team.duckie.app.android.domain.auth.usecase.ClearTokenUseCase
+import team.duckie.app.android.domain.exam.usecase.CancelExamIgnoreUseCase
+import team.duckie.app.android.domain.exam.usecase.GetExamIgnoresUseCase
+import team.duckie.app.android.domain.ignore.usecase.CancelUserIgnoreUseCase
import team.duckie.app.android.domain.me.usecase.GetIsStageUseCase
+import team.duckie.app.android.domain.user.usecase.FetchIgnoreUsersUseCase
import team.duckie.app.android.domain.user.usecase.GetMeUseCase
import team.duckie.app.android.feature.setting.constans.SettingType
import team.duckie.app.android.feature.setting.constans.SettingType.Companion.policyPages
@@ -33,6 +38,10 @@ internal class SettingViewModel @Inject constructor(
private val getMeUseCase: GetMeUseCase,
private val getIsStageUseCase: GetIsStageUseCase,
private val clearTokenUseCase: ClearTokenUseCase,
+ private val fetchIgnoreUsers: FetchIgnoreUsersUseCase,
+ private val cancelUserIgnoreUseCase: CancelUserIgnoreUseCase,
+ private val getIgnoreExamsUseCase: GetExamIgnoresUseCase,
+ private val cancelExamIgnoreUseCase: CancelExamIgnoreUseCase,
) : ContainerHost, ViewModel() {
override val container = container(SettingState())
@@ -58,6 +67,48 @@ internal class SettingViewModel @Inject constructor(
}
}
+ fun getIgnoreExams() = intent {
+ getIgnoreExamsUseCase()
+ .onSuccess { exams ->
+ reduce { state.copy(ignoreExams = exams.toImmutableList()) }
+ }
+ .onFailure {
+ postSideEffect(SettingSideEffect.ReportError(it))
+ }
+ }
+
+ fun cancelIgnoreExam(examId: Int) = intent {
+ cancelExamIgnoreUseCase(examId = examId)
+ .onSuccess {
+ val ignoreExams = state.ignoreExams.filter { it.id != examId }.toImmutableList()
+ reduce { state.copy(ignoreExams = ignoreExams) }
+ }
+ .onFailure {
+ postSideEffect(SettingSideEffect.ReportError(it))
+ }
+ }
+
+ fun cancelIgnoreUser(userId: Int) = intent {
+ cancelUserIgnoreUseCase(targetId = userId)
+ .onSuccess {
+ val ignoreUsers = state.ignoreUsers.filter { it.id != userId }.toImmutableList()
+ reduce { state.copy(ignoreUsers = ignoreUsers) }
+ }
+ .onFailure {
+ postSideEffect(SettingSideEffect.ReportError(it))
+ }
+ }
+
+ fun getIgnoreUsers() = intent {
+ fetchIgnoreUsers()
+ .onSuccess { users ->
+ reduce { state.copy(ignoreUsers = users) }
+ }
+ .onFailure {
+ postSideEffect(SettingSideEffect.ReportError(it))
+ }
+ }
+
fun updateWithdrawReason(reason: Withdraweason) = intent {
reduce { state.copy(withdrawReasonSelected = reason) }
}
diff --git a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/state/SettingState.kt b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/state/SettingState.kt
index 528820cac..92fc5ecc4 100644
--- a/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/state/SettingState.kt
+++ b/feature/setting/src/main/kotlin/team/duckie/app/android/feature/setting/viewmodel/state/SettingState.kt
@@ -7,10 +7,16 @@
package team.duckie.app.android.feature.setting.viewmodel.state
+import androidx.compose.runtime.Immutable
+import kotlinx.collections.immutable.ImmutableList
+import kotlinx.collections.immutable.persistentListOf
+import team.duckie.app.android.domain.exam.model.IgnoreExam
+import team.duckie.app.android.domain.user.model.IgnoreUser
import team.duckie.app.android.domain.user.model.User
import team.duckie.app.android.feature.setting.constans.SettingType
import team.duckie.app.android.feature.setting.constans.Withdraweason
+@Immutable
internal data class SettingState(
val me: User? = null,
val isStage: Boolean = false,
@@ -24,4 +30,7 @@ internal data class SettingState(
val withdrawReasonSelected: Withdraweason = Withdraweason.INITIAL,
val withdrawUserInputReason: String = "",
val withdrawIsFocused: Boolean = false,
+
+ val ignoreUsers: ImmutableList = persistentListOf(),
+ val ignoreExams: ImmutableList = persistentListOf(),
)
diff --git a/feature/setting/src/main/res/values/strings.xml b/feature/setting/src/main/res/values/strings.xml
index 8af0142f4..0bb093744 100644
--- a/feature/setting/src/main/res/values/strings.xml
+++ b/feature/setting/src/main/res/values/strings.xml
@@ -22,12 +22,17 @@
앗, 정말 로그아웃 하시겠어요?
정말 탈퇴하시겠어요?
안 할래요
+ 차단유저 목록
+ 차단시험 목록
+
+ 사용자 설정
로그인 계정
이메일
문의처
인스타그램
취소
+ 기타
정말 탈퇴하시겠어요?\n%s님과 이별하려니 너무 아쉬워요..
계정을 삭제하면 개인정보 및 출제한 덕력고사, 관심 등 모든 활동 정보가 삭제돼요.
@@ -38,7 +43,6 @@
자주 사용하지 않는 앱이에요.
잦은 오류가 발생해서 쓸 수가 없어요.
새 계정으로 가입하려구요.
- 기타
어떤 점이 불편하셨나요?\n다시 덕키를 방문했을 때, 더 나은 덕키가 될 수 있도록 노력할게요!
조금 더 이용하기
@@ -51,6 +55,9 @@
명예의 전당
공지사항
+ 차단한 유저가 없습니다.
+ 차단한 덕력고사가 없습니다.
+
내가 출제한 덕력고사 및 문제에 대한 알림
내 덕력고사에 다른 유저가 좋아요를 눌렀을 때의 알림
다른 유저가 나를 팔로우했을 때의 알림
diff --git a/feature/solve-problem/build.gradle.kts b/feature/solve-problem/build.gradle.kts
index 6d70e9dbd..f00e1e72d 100644
--- a/feature/solve-problem/build.gradle.kts
+++ b/feature/solve-problem/build.gradle.kts
@@ -26,12 +26,12 @@ dependencies {
projects.common.kotlin,
projects.common.compose,
projects.common.android,
+ libs.kotlin.collections.immutable,
libs.orbit.viewmodel,
libs.orbit.compose,
libs.ktx.lifecycle.runtime,
libs.compose.lifecycle.runtime,
libs.compose.ui.material, // needs for Scaffold
- libs.quack.ui.components,
libs.quack.v2.ui,
libs.firebase.crashlytics,
libs.exoplayer.core,
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/SolveProblemActivity.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/SolveProblemActivity.kt
index 019dbca18..7a06b2a67 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/SolveProblemActivity.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/SolveProblemActivity.kt
@@ -35,6 +35,7 @@ import team.duckie.app.android.common.android.ui.finishWithAnimation
import team.duckie.app.android.common.compose.moveNextPage
import team.duckie.app.android.common.compose.ui.ErrorScreen
import team.duckie.app.android.common.compose.ui.quack.QuackCrossfade
+import team.duckie.app.android.common.compose.util.addFocusCleaner
import team.duckie.app.android.domain.quiz.usecase.SubmitQuizUseCase
import team.duckie.app.android.feature.solve.problem.common.LoadingIndicator
import team.duckie.app.android.feature.solve.problem.screen.QuizScreen
@@ -42,8 +43,8 @@ import team.duckie.app.android.feature.solve.problem.screen.SolveProblemScreen
import team.duckie.app.android.feature.solve.problem.viewmodel.SolveProblemViewModel
import team.duckie.app.android.feature.solve.problem.viewmodel.sideeffect.SolveProblemSideEffect
import team.duckie.app.android.navigator.feature.examresult.ExamResultNavigator
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.theme.QuackTheme
import javax.inject.Inject
@AndroidEntryPoint
@@ -60,7 +61,9 @@ class SolveProblemActivity : BaseActivity() {
QuackTheme {
val state by viewModel.collectAsState()
val progress by viewModel.timerCount.collectAsStateWithLifecycle()
- val pagerState = rememberPagerState()
+ val pagerState = rememberPagerState(
+ pageCount = { state.totalPage },
+ )
LaunchedEffect(viewModel.container.sideEffectFlow) {
viewModel.container.sideEffectFlow.collect { sideEffect ->
@@ -74,7 +77,8 @@ class SolveProblemActivity : BaseActivity() {
QuackCrossfade(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
+ .addFocusCleaner()
.systemBarsPadding()
.navigationBarsPadding()
.imePadding(),
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/AnswerSection.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/AnswerSection.kt
index e7c839837..63d6b3c0e 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/AnswerSection.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/AnswerSection.kt
@@ -18,10 +18,13 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.key
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.SoftwareKeyboardController
+import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import team.duckie.app.android.common.compose.ui.DuckieGridLayout
+import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.feature.solve.problem.answer.choice.ImageAnswerBox
import team.duckie.app.android.feature.solve.problem.answer.choice.TextAnswerBox
@@ -30,6 +33,10 @@ import team.duckie.app.android.feature.solve.problem.viewmodel.state.InputAnswer
private val HorizontalPadding = PaddingValues(horizontal = 16.dp)
+internal object TextFieldMargin {
+ val Top = 16.dp
+}
+
@Composable
internal fun ColumnScope.AnswerSection(
pageIndex: Int,
@@ -38,11 +45,14 @@ internal fun ColumnScope.AnswerSection(
updateInputAnswers: (page: Int, inputAnswer: InputAnswer) -> Unit,
requestFocus: Boolean,
keyboardController: SoftwareKeyboardController?,
+ onShortAnswerSizeChanged: (IntSize) -> Unit,
) {
when (answer) {
is Answer.Choice -> {
Column(
- modifier = Modifier.padding(paddingValues = HorizontalPadding),
+ modifier = Modifier
+ .padding(vertical = 24.dp)
+ .padding(paddingValues = HorizontalPadding),
verticalArrangement = Arrangement.spacedBy(space = 12.dp),
) {
answer.choices.forEachIndexed { index, choice ->
@@ -91,7 +101,9 @@ internal fun ColumnScope.AnswerSection(
}
is Answer.Short -> {
+ Spacer(space = TextFieldMargin.Top)
ShortAnswerForm(
+ modifier = Modifier.onSizeChanged(onShortAnswerSizeChanged),
answer = answer.correctAnswer,
onTextChanged = { inputText ->
updateInputAnswers(pageIndex, InputAnswer(0, inputText))
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/choice/AnswerBox.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/choice/AnswerBox.kt
index ea062c967..640b36aca 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/choice/AnswerBox.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/choice/AnswerBox.kt
@@ -7,7 +7,10 @@
package team.duckie.app.android.feature.solve.problem.answer.choice
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -20,17 +23,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
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 team.duckie.quackquack.ui.animation.QuackAnimatedVisibility
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackSurface
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.Check
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun TextAnswerBox(
@@ -103,7 +109,7 @@ private fun TextAndCheck(
) {
QuackText(
text = text,
- style = QuackTextStyle.Body1.change(
+ typography = QuackTypography.Body1.change(
color = when (selected) {
true -> QuackColor.DuckieOrange
else -> QuackColor.Black
@@ -111,11 +117,11 @@ private fun TextAndCheck(
textAlign = TextAlign.Start,
),
)
- QuackAnimatedVisibility(visible = selected) {
- QuackImage(
- src = QuackIcon.Check,
+ AnimatedVisibility(visible = selected) {
+ QuackIcon(
+ icon = QuackIcon.Outlined.Check,
tint = QuackColor.DuckieOrange,
- size = DpSize(all = 18.dp),
+ size = 18.dp,
)
}
}
@@ -128,12 +134,17 @@ private fun GraySurface(
onClick: () -> Unit,
content: @Composable (BoxScope.() -> Unit),
) {
- QuackSurface(
- modifier = modifier.fillMaxWidth(),
- backgroundColor = QuackColor.Gray4,
- border = setBoxBorder(selected = selected),
- shape = RoundedCornerShape(size = 8.dp),
- onClick = onClick,
+ val shape = RoundedCornerShape(size = 8.dp)
+ Box(
+ modifier = modifier
+ .fillMaxWidth()
+ .clip(shape = shape)
+ .background(color = QuackColor.Gray4.value)
+ .quackClickable(onClick = onClick)
+ .quackBorder(
+ border = setBoxBorder(selected = selected),
+ shape = shape,
+ ),
content = content,
)
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/shortanswer/ShortAnswerForm.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/shortanswer/ShortAnswerForm.kt
index 432e7b893..eeb873195 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/shortanswer/ShortAnswerForm.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/answer/shortanswer/ShortAnswerForm.kt
@@ -43,7 +43,7 @@ import androidx.compose.ui.unit.dp
import team.duckie.quackquack.material.QuackBorder
import team.duckie.quackquack.material.QuackColor
import team.duckie.quackquack.material.quackBorder
-import team.duckie.quackquack.ui.component.QuackBody1
+import team.duckie.quackquack.ui.sugar.QuackBody1
@OptIn(ExperimentalLayoutApi::class)
@Composable
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/BottomBar.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/BottomBar.kt
index 62bf17a89..94288b737 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/BottomBar.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/BottomBar.kt
@@ -7,6 +7,7 @@
package team.duckie.app.android.feature.solve.problem.common
+import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -17,16 +18,15 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
+import team.duckie.app.android.common.compose.ui.QuackMaxWidthDivider
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackSurface
import team.duckie.app.android.feature.solve.problem.R
-import team.duckie.quackquack.ui.animation.QuackAnimatedContent
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackDivider
-import team.duckie.quackquack.ui.component.QuackSurface
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.quackquack.material.QuackBorder
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackBorder
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun ButtonBottomBar(
@@ -38,7 +38,7 @@ internal fun ButtonBottomBar(
Column(
modifier = modifier,
) {
- QuackDivider()
+ QuackMaxWidthDivider()
Row(
modifier = Modifier
.fillMaxWidth()
@@ -49,7 +49,10 @@ internal fun ButtonBottomBar(
horizontalArrangement = Arrangement.SpaceBetween,
) {
Spacer(modifier = Modifier)
- QuackAnimatedContent(targetState = isLastPage) {
+ AnimatedContent(
+ targetState = isLastPage,
+ label = "AnimatedContent",
+ ) {
when (it) {
false -> MediumButton(
text = stringResource(id = R.string.next),
@@ -86,7 +89,7 @@ internal fun DoubleButtonBottomBar(
Column(
modifier = modifier,
) {
- QuackDivider()
+ QuackMaxWidthDivider()
Row(
modifier = Modifier
.fillMaxWidth()
@@ -96,7 +99,10 @@ internal fun DoubleButtonBottomBar(
),
horizontalArrangement = Arrangement.SpaceBetween,
) {
- QuackAnimatedContent(targetState = isFirstPage) {
+ AnimatedContent(
+ targetState = isFirstPage,
+ label = "AnimatedContent",
+ ) {
when (it) {
true -> {
Spacer(modifier = Modifier)
@@ -111,7 +117,10 @@ internal fun DoubleButtonBottomBar(
}
}
- QuackAnimatedContent(targetState = isLastPage) {
+ AnimatedContent(
+ targetState = isLastPage,
+ label = "AnimatedContent",
+ ) {
when (it) {
false -> MediumButton(
text = stringResource(id = R.string.next),
@@ -141,11 +150,13 @@ private fun MediumButton(
textColor: QuackColor = textColorFor(enabled),
) {
QuackSurface(
- modifier = Modifier,
- backgroundColor = backgroundColor,
- border = border,
- shape = RoundedCornerShape(size = 8.dp),
+ modifier = Modifier.quackBorder(
+ shape = RoundedCornerShape(8.dp),
+ border = border,
+ ),
+ shape = RoundedCornerShape(8.dp),
onClick = onClickFor(enabled, onClick),
+ backgroundColor = backgroundColor,
) {
QuackText(
modifier = Modifier.padding(
@@ -153,11 +164,9 @@ private fun MediumButton(
horizontal = 12.dp,
),
text = text,
- style = QuackTextStyle.Body1.change(
+ typography = QuackTypography.Body1.change(
color = textColor,
- textAlign = TextAlign.Center,
),
- singleLine = true,
)
}
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/FlexibleSubjectiveQuestionSection.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/FlexibleSubjectiveQuestionSection.kt
deleted file mode 100644
index a54d43dd4..000000000
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/FlexibleSubjectiveQuestionSection.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-/*
- * Designed and developed by Duckie Team, 2022
- *
- * Licensed under the MIT.
- * Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
- */
-
-@file:OptIn(ExperimentalComposeUiApi::class)
-
-package team.duckie.app.android.feature.solve.problem.common
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.BoxWithConstraints
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.heightIn
-import androidx.compose.foundation.layout.imePadding
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.NonRestartableComposable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.layout.onSizeChanged
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.SoftwareKeyboardController
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import team.duckie.app.android.common.compose.ui.Spacer
-import team.duckie.app.android.common.kotlin.AllowMagicNumber
-import team.duckie.app.android.domain.exam.model.Problem
-import team.duckie.app.android.domain.exam.model.Question
-import team.duckie.app.android.feature.solve.problem.answer.shortanswer.ShortAnswerForm
-import team.duckie.app.android.feature.solve.problem.viewmodel.state.InputAnswer
-import team.duckie.quackquack.material.QuackColor
-import team.duckie.quackquack.ui.QuackImage
-import team.duckie.quackquack.ui.modifier.quackClickable
-import team.duckie.quackquack.ui.sugar.QuackHeadLine2
-
-@AllowMagicNumber("to get flexible image height")
-@NonRestartableComposable
-@Composable
-private fun getFlexibleImageHeight(): Dp {
- val configuration = LocalConfiguration.current
- return ((configuration.screenWidthDp / 4) * 3).dp
-}
-
-private object TextFieldMargin {
- val Top = 24.dp
- val Bottom = 16.dp
- val Vertical get() = Top + Bottom
-}
-
-/**
- * 키보드 상태에 따라서 이미지의 높이가 유동적으로 변하는 주관식 문제를 위한 섹션
- */
-@Composable
-fun FlexibleSubjectiveQuestionSection(
- problem: Problem,
- pageIndex: Int,
- updateInputAnswers: (page: Int, inputAnswer: InputAnswer) -> Unit,
- requestFocus: Boolean,
- keyboardController: SoftwareKeyboardController?,
-) {
- val density = LocalDensity.current
- val question = problem.question as Question.Image
- val flexibleImageHeight = getFlexibleImageHeight()
- var textFieldHeight by remember { mutableStateOf(Dp.Unspecified) }
-
- Column(
- modifier = Modifier
- .fillMaxSize()
- .imePadding()
- .quackClickable(
- rippleEnabled = false,
- ) {
- keyboardController?.hide()
- },
- ) {
- Spacer(space = 16.dp)
- QuackHeadLine2(
- modifier = Modifier.padding(horizontal = 16.dp),
- text = "${pageIndex + 1}. ${question.text}",
- )
- Spacer(space = 12.dp)
- BoxWithConstraints {
- val actualHeight =
- if (maxHeight - textFieldHeight - TextFieldMargin.Vertical >= flexibleImageHeight) {
- flexibleImageHeight
- } else {
- maxHeight - textFieldHeight - TextFieldMargin.Vertical
- }
-
- Box(
- modifier = Modifier
- .fillMaxWidth()
- .heightIn(max = actualHeight)
- .padding(horizontal = 16.dp)
- .background(
- color = QuackColor.Black.value,
- shape = RoundedCornerShape(16.dp),
- ),
- ) {
- QuackImage(
- modifier = Modifier.fillMaxSize(),
- src = question.imageUrl,
- contentScale = ContentScale.Fit,
- )
- }
- }
- Spacer(space = TextFieldMargin.Top)
- ShortAnswerForm(
- modifier = Modifier.onSizeChanged {
- with(density) {
- textFieldHeight = it.height.toDp()
- }
- },
- answer = problem.correctAnswer ?: "",
- onTextChanged = { inputText ->
- updateInputAnswers(pageIndex, InputAnswer(0, inputText))
- },
- requestFocus = requestFocus,
- keyboardController = keyboardController,
- )
- Spacer(space = TextFieldMargin.Bottom)
- }
-}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/LoadingIndicator.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/LoadingIndicator.kt
index 2daad1fae..93b4d4d09 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/LoadingIndicator.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/LoadingIndicator.kt
@@ -13,15 +13,14 @@ import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import team.duckie.quackquack.ui.color.QuackColor
+import team.duckie.quackquack.material.QuackColor
-// TODO(EvergreenTree97): QuackLoadingIndicator로 통합 필요
@Composable
internal fun LoadingIndicator() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
- CircularProgressIndicator(color = QuackColor.DuckieOrange.composeColor)
+ CircularProgressIndicator(color = QuackColor.DuckieOrange.value)
}
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/TopBar.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/TopBar.kt
index 93bc55b80..af134ace4 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/TopBar.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/common/TopBar.kt
@@ -14,20 +14,21 @@ 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.shape.CircleShape
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.unit.dp
-import team.duckie.app.android.common.compose.ui.icon.v1.Clock
import team.duckie.app.android.common.compose.ui.LinearProgressBar
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.component.QuackSubtitle2
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.util.DpSize
+import team.duckie.app.android.common.compose.ui.icon.v2.Clock
+import team.duckie.app.android.common.compose.ui.quack.todo.QuackTopAppBar
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.QuackIcon
+import team.duckie.quackquack.material.icon.quackicon.Outlined
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.ui.QuackIcon
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackSubtitle2
@Composable
internal fun CloseAndPageTopBar(
@@ -38,7 +39,7 @@ internal fun CloseAndPageTopBar(
) {
QuackTopAppBar(
modifier = modifier,
- leadingIcon = QuackIcon.Close,
+ leadingIcon = QuackIcon.Outlined.Close,
onLeadingIconClick = onCloseClick,
trailingContent = {
PageInfo(
@@ -58,25 +59,25 @@ internal fun TimerTopBar(
Column(
modifier = modifier
.fillMaxWidth()
- .padding(horizontal = 16.dp),
+ .padding(bottom = 16.dp),
) {
QuackTopAppBar(
- leadingIcon = QuackIcon.Close,
+ leadingIcon = QuackIcon.Outlined.Close,
onLeadingIconClick = onCloseClick,
)
Box(
- modifier = Modifier.fillMaxWidth(),
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
contentAlignment = Alignment.CenterStart,
) {
LinearProgressBar(
progress = progress(),
)
- QuackImage(
- modifier = Modifier
- .clip(CircleShape)
- .background(QuackColor.White.composeColor),
- src = QuackIcon.Clock,
- size = DpSize(all = 16.dp),
+ QuackIcon(
+ modifier = Modifier.background(QuackColor.White.value),
+ icon = QuackIcon.Clock,
+ size = 16.dp,
)
}
}
@@ -92,13 +93,13 @@ private fun PageInfo(
horizontalArrangement = Arrangement.spacedBy(space = 2.dp),
) {
QuackSubtitle2(text = currentPage.toString())
- QuackSubtitle2(
+ QuackText(
text = " / ",
- color = QuackColor.Gray2,
+ typography = QuackTypography.Subtitle2.change(color = QuackColor.Gray2),
)
- QuackSubtitle2(
+ QuackText(
text = totalPage.toString(),
- color = QuackColor.Gray2,
+ typography = QuackTypography.Subtitle2.change(color = QuackColor.Gray2),
)
}
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/QuestionSection.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/QuestionSection.kt
index e5f423862..2087e6c32 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/QuestionSection.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/QuestionSection.kt
@@ -7,29 +7,38 @@
package team.duckie.app.android.feature.solve.problem.question
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.ColumnScope
-import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import team.duckie.app.android.common.compose.GetHeightRatioW328H240
import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.domain.exam.model.Question
import team.duckie.app.android.feature.solve.problem.question.audio.AudioPlayer
+import team.duckie.app.android.feature.solve.problem.question.image.FlexibleImageBox
import team.duckie.app.android.feature.solve.problem.question.image.ImageBox
import team.duckie.app.android.feature.solve.problem.question.video.VideoPlayer
-import team.duckie.quackquack.ui.component.QuackHeadLine2
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.ui.sugar.QuackHeadLine2
-private val HorizontalPadding = PaddingValues(horizontal = 16.dp)
+private val HorizontalPadding = 16.dp
@Composable
internal fun ColumnScope.QuestionSection(
page: Int,
question: Question,
isImageChoice: Boolean,
+ isRequireFlexibleImage: Boolean,
+ spaceImageToKeyboard: Dp,
+ onImageLoading: (Boolean) -> Unit,
+ onImageSuccess: (Boolean) -> Unit,
+ isImageLoading: Boolean,
) {
val modifier = if (isImageChoice) {
Modifier
@@ -39,22 +48,43 @@ internal fun ColumnScope.QuestionSection(
Modifier.weight(GetHeightRatioW328H240)
}
QuackHeadLine2(
+ modifier = Modifier.padding(horizontal = HorizontalPadding),
text = "${page + 1}. ${question.text}",
- padding = HorizontalPadding,
)
when (question) {
is Question.Text -> {}
is Question.Image -> {
- Spacer(space = 16.dp)
- ImageBox(
- modifier = modifier,
- url = question.imageUrl,
- )
+ Spacer(space = 12.dp)
+ if (isRequireFlexibleImage) {
+ FlexibleImageBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(horizontal = 16.dp),
+ spaceImageToKeyboard = spaceImageToKeyboard,
+ url = question.imageUrl,
+ onImageLoading = onImageLoading,
+ onImageSuccess = onImageSuccess,
+ isImageLoading = isImageLoading,
+ )
+ } else {
+ ImageBox(
+ modifier = modifier
+ .padding(horizontal = 16.dp)
+ .background(
+ color = QuackColor.Gray4.value,
+ shape = RoundedCornerShape(8.dp),
+ ),
+ url = question.imageUrl,
+ onImageLoading = onImageLoading,
+ onImageSuccess = onImageSuccess,
+ isImageLoading = isImageLoading,
+ )
+ }
}
is Question.Audio -> {
AudioPlayer(
- modifier = Modifier.padding(paddingValues = HorizontalPadding),
+ modifier = Modifier.padding(horizontal = HorizontalPadding),
url = question.audioUrl,
)
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/audio/Controller.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/audio/Controller.kt
index 534ed224a..9370d1b57 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/audio/Controller.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/audio/Controller.kt
@@ -5,29 +5,25 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.feature.solve.problem.question.audio
import android.os.Build.VERSION.SDK_INT
import android.os.Build.VERSION_CODES
import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.sizeIn
-import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.AsyncImage
import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import team.duckie.app.android.common.compose.ui.quack.QuackCrossfade
-import team.duckie.quackquack.ui.border.QuackBorder
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackSurface
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.quackquack.ui.QuackButton
+import team.duckie.quackquack.ui.QuackButtonStyle
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
@Composable
internal fun AudioController(
@@ -87,23 +83,10 @@ private fun LargeButton(
text: String,
onClick: () -> Unit,
) {
- QuackSurface(
- modifier = modifier.sizeIn(
- minWidth = 82.dp,
- minHeight = 40.dp,
- ),
- backgroundColor = QuackColor.White,
- border = QuackBorder(
- color = QuackColor.Gray3,
- ),
- shape = RoundedCornerShape(size = 8.dp),
+ QuackButton(
+ modifier = modifier,
+ text = text,
+ style = QuackButtonStyle.SecondaryRoundSmall,
onClick = onClick,
- ) {
- QuackText(
- modifier = Modifier.padding(all = 10.dp),
- text = text,
- style = QuackTextStyle.Subtitle,
- singleLine = true,
- )
- }
+ )
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/image/ImageBox.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/image/ImageBox.kt
index f70859489..50de3510d 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/image/ImageBox.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/image/ImageBox.kt
@@ -7,37 +7,117 @@
package team.duckie.app.android.feature.solve.problem.question.image
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.CircularProgressIndicator
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import coil.compose.AsyncImage
+import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.feature.solve.problem.R
import team.duckie.quackquack.material.QuackColor
-import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+
+@Suppress("MagicNumber")
+@NonRestartableComposable
+@Composable
+private fun getFlexibleImageHeight(): Dp {
+ val configuration = LocalConfiguration.current
+ return ((configuration.screenWidthDp / 4) * 3).dp
+}
+
+@Composable
+internal fun FlexibleImageBox(
+ modifier: Modifier = Modifier,
+ spaceImageToKeyboard: Dp,
+ url: String,
+ onImageLoading: (Boolean) -> Unit,
+ onImageSuccess: (Boolean) -> Unit,
+ isImageLoading: Boolean,
+) {
+ val flexibleImageHeight = getFlexibleImageHeight()
+ BoxWithConstraints(modifier = modifier) {
+ val actualHeight =
+ if (maxHeight - spaceImageToKeyboard >= flexibleImageHeight) {
+ flexibleImageHeight
+ } else {
+ maxHeight - spaceImageToKeyboard
+ }
+
+ ImageBox(
+ modifier = Modifier
+ .fillMaxWidth()
+ .height(height = actualHeight)
+ .background(
+ color = QuackColor.Gray4.value,
+ shape = RoundedCornerShape(8.dp),
+ ),
+ url = url,
+ onImageLoading = onImageLoading,
+ onImageSuccess = onImageSuccess,
+ isImageLoading = isImageLoading,
+ )
+ }
+}
@Composable
internal fun ImageBox(
modifier: Modifier = Modifier,
url: String,
+ onImageLoading: (Boolean) -> Unit,
+ onImageSuccess: (Boolean) -> Unit,
+ isImageLoading: Boolean,
) {
- Box(
- modifier = modifier
- .padding(horizontal = 16.dp)
- .background(
- color = QuackColor.Black.value,
- shape = RoundedCornerShape(8.dp),
- ),
- contentAlignment = Alignment.Center,
- ) {
- QuackImage(
- modifier = Modifier.fillMaxSize(),
- src = url,
- contentScale = ContentScale.Fit,
+ Box(modifier = modifier) {
+ AsyncImage(
+ modifier = Modifier
+ .clip(RoundedCornerShape(8.dp))
+ .fillMaxSize(),
+ onLoading = {
+ onImageLoading(true)
+ },
+ onSuccess = {
+ onImageSuccess(false)
+ },
+ model = url,
+ contentDescription = "",
)
+ AnimatedVisibility(
+ modifier = Modifier
+ .fillMaxSize()
+ .clip(RoundedCornerShape(8.dp))
+ .background(QuackColor.Black.value.copy(alpha = 0.5f)),
+ visible = isImageLoading,
+ ) {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.Center,
+ ) {
+ QuackText(
+ text = stringResource(id = R.string.loading_image),
+ typography = QuackTypography.Body1.change(
+ color = QuackColor.White,
+ ),
+ )
+ Spacer(space = 12.dp)
+ CircularProgressIndicator(color = QuackColor.DuckieOrange.value)
+ }
+ }
}
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Controller.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Controller.kt
index 45bad3b16..df398e859 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Controller.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Controller.kt
@@ -7,13 +7,13 @@
package team.duckie.app.android.feature.solve.problem.question.video
+import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
@@ -31,11 +31,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import team.duckie.app.android.feature.solve.problem.R
-import team.duckie.quackquack.ui.animation.QuackAnimatedVisibility
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody3
-import team.duckie.quackquack.ui.component.QuackImage
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.QuackText
import java.util.Locale
import java.util.concurrent.TimeUnit
@@ -51,7 +51,7 @@ internal fun VideoController(
onTimeChanged: (Float) -> Unit,
) {
var sliderHeight by remember { mutableStateOf(0) }
- QuackAnimatedVisibility(
+ AnimatedVisibility(
modifier = modifier,
visible = isVisible(),
) {
@@ -63,22 +63,23 @@ internal fun VideoController(
)
Column(
modifier = Modifier
- .align(alignment = Alignment.BottomCenter)
- .offset {
- IntOffset(
- x = 0,
- y = sliderHeight / 2,
- )
- },
+ .align(alignment = Alignment.BottomCenter)
+ .offset {
+ IntOffset(
+ x = 0,
+ y = sliderHeight / 2,
+ )
+ },
) {
- QuackBody3(
- modifier = Modifier.padding(start = 12.dp),
+ QuackText(
text = stringResource(
id = R.string.current_between_total,
currentTime().formatMinSec(),
totalTime().formatMinSec(),
),
- color = QuackColor.Gray3,
+ typography = QuackTypography.Body3.change(
+ color = QuackColor.Gray3,
+ ),
)
VideoSlider(
modifier = Modifier
@@ -112,7 +113,7 @@ internal fun InteractionButton(
modifier = modifier
.fillMaxSize()
.alpha(alpha = 0.2f)
- .background(color = QuackColor.Black.composeColor),
+ .background(color = QuackColor.Black.value),
)
QuackImage(
modifier = modifier,
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Slider.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Slider.kt
index 97d17a96e..be9a9a2ac 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Slider.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/question/video/Slider.kt
@@ -14,20 +14,20 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import team.duckie.app.android.common.compose.rememberNoRippleInteractionSource
-import team.duckie.quackquack.ui.color.QuackColor
+import team.duckie.quackquack.material.QuackColor
@Composable
internal fun primarySliderColors() = SliderDefaults.colors(
- thumbColor = QuackColor.DuckieOrange.composeColor,
- activeTrackColor = QuackColor.DuckieOrange.composeColor,
+ thumbColor = QuackColor.DuckieOrange.value,
+ activeTrackColor = QuackColor.DuckieOrange.value,
inactiveTrackColor = Color.Transparent,
)
@Composable
internal fun bufferSliderColors() = SliderDefaults.colors(
disabledThumbColor = Color.Transparent,
- disabledActiveTrackColor = QuackColor.Gray2.composeColor.copy(alpha = 0.5f),
- disabledInactiveTrackColor = QuackColor.Gray3.composeColor,
+ disabledActiveTrackColor = QuackColor.Gray2.value.copy(alpha = 0.5f),
+ disabledInactiveTrackColor = QuackColor.Gray3.value,
)
@Composable
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/QuizScreen.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/QuizScreen.kt
index 1be257b65..ad4281387 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/QuizScreen.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/QuizScreen.kt
@@ -7,8 +7,6 @@
@file:OptIn(
ExperimentalFoundationApi::class,
- ExperimentalComposeUiApi::class,
- ExperimentalFoundationApi::class,
)
package team.duckie.app.android.feature.solve.problem.screen
@@ -29,17 +27,19 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
-import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.launch
import team.duckie.app.android.common.compose.isCurrentPage
+import team.duckie.app.android.common.compose.isTargetPage
import team.duckie.app.android.common.compose.ui.Spacer
import team.duckie.app.android.common.compose.ui.dialog.DuckieDialog
import team.duckie.app.android.common.kotlin.exception.duckieResponseFieldNpe
@@ -47,8 +47,8 @@ import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.domain.exam.model.Problem.Companion.isSubjective
import team.duckie.app.android.feature.solve.problem.R
import team.duckie.app.android.feature.solve.problem.answer.AnswerSection
+import team.duckie.app.android.feature.solve.problem.answer.TextFieldMargin
import team.duckie.app.android.feature.solve.problem.common.ButtonBottomBar
-import team.duckie.app.android.feature.solve.problem.common.FlexibleSubjectiveQuestionSection
import team.duckie.app.android.feature.solve.problem.common.TimerTopBar
import team.duckie.app.android.feature.solve.problem.common.verticalScrollModifierAsCondition
import team.duckie.app.android.feature.solve.problem.question.QuestionSection
@@ -86,10 +86,6 @@ internal fun QuizScreen(
)
}
- LaunchedEffect(pagerState.targetPage) {
- startTimer(state.time.toFloat())
- }
-
LaunchedEffect(timeOver) {
if (timeOver) {
onNextPage(
@@ -132,6 +128,7 @@ internal fun QuizScreen(
updateInputAnswers = { index, answer ->
inputAnswers[index] = answer
},
+ startTimer = startTimer,
)
ButtonBottomBar(
modifier = Modifier
@@ -174,70 +171,89 @@ private fun ContentSection(
state: SolveProblemState,
inputAnswers: ImmutableList,
updateInputAnswers: (page: Int, inputAnswer: InputAnswer) -> Unit,
+ startTimer: (Float) -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current
+ var textFieldHeight by remember { mutableStateOf(Dp.Unspecified) }
+ val density = LocalDensity.current
+ val isImageLoading = remember {
+ mutableStateListOf(
+ elements = Array(
+ size = state.quizProblems.size,
+ init = { true },
+ ),
+ )
+ }
HorizontalPager(
modifier = modifier,
- pageCount = state.totalPage,
state = pagerState,
userScrollEnabled = false,
+ beyondBoundsPageCount = 3,
) { pageIndex ->
val problem = state.quizProblems[pageIndex]
- val requestFocus by remember(key1 = pagerState.currentPage) {
+ val isCurrentPage by remember(pagerState.currentPage) {
derivedStateOf { pagerState.isCurrentPage(pageIndex) }
}
- LaunchedEffect(key1 = requestFocus) {
+ LaunchedEffect(pagerState.targetPage, isImageLoading[pageIndex]) {
+ if (isImageLoading[pageIndex].not() && pagerState.isTargetPage(pageIndex)) {
+ startTimer(state.time.toFloat())
+ }
+ }
+
+ LaunchedEffect(key1 = isCurrentPage) {
if (!problem.isSubjective()) {
keyboardController?.hide()
}
}
val isImageChoice = problem.answer?.isImageChoice == true
+ val isRequireFlexibleImage = problem.isSubjective() && problem.question.isImage()
- when {
- // for keyboard flexible image height
- problem.isSubjective() && problem.question.isImage() -> FlexibleSubjectiveQuestionSection(
- problem = problem,
+ Column(
+ modifier = Modifier
+ .verticalScrollModifierAsCondition(isImageChoice)
+ .fillMaxSize(),
+ ) {
+ QuestionSection(
+ page = pageIndex,
+ question = problem.question,
+ isImageChoice = isImageChoice,
+ isRequireFlexibleImage = isRequireFlexibleImage,
+ spaceImageToKeyboard = textFieldHeight + TextFieldMargin.Top,
+ onImageLoading = {
+ isImageLoading[pageIndex] = it
+ },
+ onImageSuccess = {
+ isImageLoading[pageIndex] = it
+ },
+ isImageLoading = isImageLoading[pageIndex],
+ )
+ val answer = problem.answer
+ AnswerSection(
pageIndex = pageIndex,
+ answer = when (answer) {
+ is Answer.Short -> Answer.Short(
+ problem.correctAnswer
+ ?: duckieResponseFieldNpe("null 이 되면 안됩니다."),
+ )
+
+ is Answer.Choice, is Answer.ImageChoice -> answer
+ else -> duckieResponseFieldNpe("해당 분기로 빠질 수 없는 AnswerType 입니다.")
+ },
+ inputAnswers = inputAnswers,
updateInputAnswers = updateInputAnswers,
- requestFocus = requestFocus,
+ requestFocus = isCurrentPage,
keyboardController = keyboardController,
+ onShortAnswerSizeChanged = {
+ textFieldHeight = with(density) {
+ it.height.toDp()
+ }
+ },
)
-
- else -> Column(
- modifier = Modifier
- .verticalScrollModifierAsCondition(isImageChoice)
- .fillMaxSize(),
- ) {
- Spacer(space = 16.dp)
- QuestionSection(
- page = pageIndex,
- question = problem.question,
- isImageChoice = isImageChoice,
- )
- Spacer(space = 24.dp)
- val answer = problem.answer
- AnswerSection(
- pageIndex = pageIndex,
- answer = when (answer) {
- is Answer.Short -> Answer.Short(
- problem.correctAnswer
- ?: duckieResponseFieldNpe("null 이 되면 안됩니다."),
- )
-
- is Answer.Choice, is Answer.ImageChoice -> answer
- else -> duckieResponseFieldNpe("해당 분기로 빠질 수 없는 AnswerType 입니다.")
- },
- inputAnswers = inputAnswers,
- updateInputAnswers = updateInputAnswers,
- requestFocus = requestFocus,
- keyboardController = keyboardController,
- )
- Spacer(space = 16.dp)
- }
+ Spacer(space = 16.dp)
}
}
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/SolveProblemScreen.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/SolveProblemScreen.kt
index 4ba491ab7..a023989a0 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/SolveProblemScreen.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/screen/SolveProblemScreen.kt
@@ -13,6 +13,7 @@ import androidx.activity.compose.BackHandler
import androidx.compose.foundation.ExperimentalFoundationApi
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.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
@@ -24,13 +25,16 @@ import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId
+import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@@ -46,19 +50,19 @@ import team.duckie.app.android.domain.exam.model.Answer
import team.duckie.app.android.domain.exam.model.Problem.Companion.isSubjective
import team.duckie.app.android.feature.solve.problem.R
import team.duckie.app.android.feature.solve.problem.answer.AnswerSection
+import team.duckie.app.android.feature.solve.problem.answer.TextFieldMargin
import team.duckie.app.android.feature.solve.problem.common.CloseAndPageTopBar
import team.duckie.app.android.feature.solve.problem.common.DoubleButtonBottomBar
-import team.duckie.app.android.feature.solve.problem.common.FlexibleSubjectiveQuestionSection
import team.duckie.app.android.feature.solve.problem.common.verticalScrollModifierAsCondition
import team.duckie.app.android.feature.solve.problem.question.QuestionSection
import team.duckie.app.android.feature.solve.problem.viewmodel.state.InputAnswer
import team.duckie.app.android.feature.solve.problem.viewmodel.state.SolveProblemState
+import team.duckie.quackquack.material.quackClickable
private const val SolveProblemTopAppBarLayoutId = "SolveProblemTopAppBar"
private const val SolveProblemContentLayoutId = "SolveProblemContent"
private const val SolveProblemBottomBarLayoutId = "SolveProblemBottomBar"
-// 6번호
@Composable
internal fun SolveProblemScreen(
state: SolveProblemState,
@@ -66,8 +70,6 @@ internal fun SolveProblemScreen(
finishExam: (List) -> Unit,
pagerState: PagerState,
) {
- val totalPage = remember { state.totalPage }
-
val coroutineScope = rememberCoroutineScope()
var examExitDialogVisible by remember { mutableStateOf(false) }
var examSubmitDialogVisible by remember { mutableStateOf(false) }
@@ -116,13 +118,14 @@ internal fun SolveProblemScreen(
CloseAndPageTopBar(
modifier = Modifier
.layoutId(SolveProblemTopAppBarLayoutId)
+ .fillMaxWidth()
.padding(start = 12.dp)
.padding(end = 16.dp),
onCloseClick = {
examExitDialogVisible = true
},
currentPage = pagerState.currentPage + 1,
- totalPage = totalPage,
+ totalPage = state.totalPage,
)
ContentSection(
modifier = Modifier.layoutId(SolveProblemContentLayoutId),
@@ -136,7 +139,7 @@ internal fun SolveProblemScreen(
DoubleButtonBottomBar(
modifier = Modifier.layoutId(SolveProblemBottomBarLayoutId),
isFirstPage = pagerState.currentPage == 0,
- isLastPage = pagerState.currentPage == totalPage - 1,
+ isLastPage = pagerState.currentPage == state.totalPage - 1,
onLeftButtonClick = {
coroutineScope.launch {
pagerState.movePrevPage()
@@ -144,7 +147,7 @@ internal fun SolveProblemScreen(
},
onRightButtonClick = {
coroutineScope.launch {
- val maximumPage = totalPage - 1
+ val maximumPage = state.totalPage - 1
if (pagerState.currentPage == maximumPage) {
examSubmitDialogVisible = true
} else {
@@ -171,10 +174,11 @@ private fun ContentSection(
updateInputAnswers: (Int, InputAnswer) -> Unit,
) {
val keyboardController = LocalSoftwareKeyboardController.current
+ var textFieldHeight by remember { mutableStateOf(Dp.Unspecified) }
+ val density = LocalDensity.current
HorizontalPager(
modifier = modifier,
- pageCount = state.totalPage,
state = pagerState,
) { pageIndex ->
val problem = state.problems[pageIndex].problem
@@ -185,6 +189,8 @@ private fun ContentSection(
}
}
+ var isImageLoading by rememberSaveable { mutableStateOf(true) }
+
LaunchedEffect(key1 = requestFocus) {
if (!problem.isSubjective()) {
keyboardController?.hide()
@@ -192,48 +198,50 @@ private fun ContentSection(
}
val isImageChoice = problem.answer?.isImageChoice == true
-
- when {
- // for keyboard flexible image height
- problem.isSubjective() && problem.question.isImage() -> FlexibleSubjectiveQuestionSection(
- problem = problem,
+ val isRequireFlexibleImage = problem.isSubjective() && problem.question.isImage()
+ Column(
+ modifier = Modifier
+ .verticalScrollModifierAsCondition(isImageChoice)
+ .fillMaxSize()
+ .quackClickable(
+ rippleEnabled = false,
+ ) {
+ keyboardController?.hide()
+ },
+ ) {
+ Spacer(space = 16.dp)
+ QuestionSection(
+ page = pageIndex,
+ question = problem.question,
+ isRequireFlexibleImage = isRequireFlexibleImage,
+ spaceImageToKeyboard = textFieldHeight + TextFieldMargin.Top,
+ onImageLoading = { isImageLoading = it },
+ onImageSuccess = { isImageLoading = it },
+ isImageLoading = isImageLoading,
+ isImageChoice = isImageChoice,
+ )
+ val answer = problem.answer
+ AnswerSection(
pageIndex = pageIndex,
+ answer = when (answer) {
+ is Answer.Short -> Answer.Short(
+ problem.correctAnswer
+ ?: duckieResponseFieldNpe("null 이 되면 안됩니다."),
+ )
+
+ is Answer.Choice, is Answer.ImageChoice -> answer
+ else -> duckieResponseFieldNpe("해당 분기로 빠질 수 없는 AnswerType 입니다.")
+ },
+ inputAnswers = inputAnswers,
updateInputAnswers = updateInputAnswers,
requestFocus = requestFocus,
keyboardController = keyboardController,
+ onShortAnswerSizeChanged = {
+ textFieldHeight = with(density) {
+ it.height.toDp()
+ }
+ },
)
-
- else -> Column(
- modifier = Modifier
- .verticalScrollModifierAsCondition(isImageChoice)
- .fillMaxSize(),
- ) {
- Spacer(space = 16.dp)
- QuestionSection(
- page = pageIndex,
- question = problem.question,
- isImageChoice = isImageChoice,
- )
- Spacer(space = 24.dp)
- val answer = problem.answer
- AnswerSection(
- pageIndex = pageIndex,
- answer = when (answer) {
- is Answer.Short -> Answer.Short(
- problem.correctAnswer
- ?: duckieResponseFieldNpe("null 이 되면 안됩니다."),
- )
-
- is Answer.Choice, is Answer.ImageChoice -> answer
- else -> duckieResponseFieldNpe("해당 분기로 빠질 수 없는 AnswerType 입니다.")
- },
- inputAnswers = inputAnswers,
- updateInputAnswers = updateInputAnswers,
- requestFocus = requestFocus,
- keyboardController = keyboardController,
- )
- Spacer(space = 16.dp)
- }
}
}
}
diff --git a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/viewmodel/SolveProblemViewModel.kt b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/viewmodel/SolveProblemViewModel.kt
index e26e5ef4f..c20a0d0d6 100644
--- a/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/viewmodel/SolveProblemViewModel.kt
+++ b/feature/solve-problem/src/main/kotlin/team/duckie/app/android/feature/solve/problem/viewmodel/SolveProblemViewModel.kt
@@ -56,7 +56,7 @@ internal class SolveProblemViewModel @Inject constructor(
problemTimer.start(time)
}
- fun stopTimer() {
+ private fun stopTimer() {
problemTimer.stop()
}
@@ -110,7 +110,9 @@ internal class SolveProblemViewModel @Inject constructor(
reduce { state.copy(isProblemsLoading = true, isError = false) }
getQuizUseCase(examId = examId).onSuccess { quizResult ->
- val quizProblems = quizResult.exam.problems
+ val quizProblems = quizResult.exam.problems.also {
+ problemTimer.setTotalTime(quizResult.exam.timer?.toFloat() ?: 0f)
+ }
if (quizProblems == null) {
stopExam()
} else {
@@ -140,7 +142,6 @@ internal class SolveProblemViewModel @Inject constructor(
) = intent {
val correctAnswer = state.quizProblems[pageIndex].correctAnswer
?: throw DuckieClientLogicProblemException(code = CORRECT_ANSWER_IS_NULL)
- state.quizProblems[pageIndex].answer
if (correctAnswer.replace(" ", "").lowercase() != inputAnswer.answer.replace(" ", "")
.lowercase()
) {
diff --git a/feature/solve-problem/src/main/res/values/strings.xml b/feature/solve-problem/src/main/res/values/strings.xml
index 7c5f975cd..9b95b83f7 100644
--- a/feature/solve-problem/src/main/res/values/strings.xml
+++ b/feature/solve-problem/src/main/res/values/strings.xml
@@ -19,4 +19,5 @@
답안을 제출하시겠어요?
아직 풀지 않은 문제가 있는지 확인해주세요.
띄어쓰기 포함 %s자
+ 이미지를 불러오고 있어요 :)
diff --git a/feature/start-exam/build.gradle.kts b/feature/start-exam/build.gradle.kts
index d4690c190..81a6fecd3 100644
--- a/feature/start-exam/build.gradle.kts
+++ b/feature/start-exam/build.gradle.kts
@@ -28,7 +28,7 @@ dependencies {
projects.common.compose,
libs.orbit.viewmodel,
libs.orbit.compose,
- libs.quack.ui.components,
+ libs.quack.v2.ui,
libs.compose.lifecycle.runtime,
libs.compose.ui.material, // needs for CircularProgressIndicator
libs.firebase.crashlytics,
diff --git a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamActivity.kt b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamActivity.kt
index 9ad65647f..812ac7adc 100644
--- a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamActivity.kt
+++ b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamActivity.kt
@@ -21,11 +21,12 @@ import org.orbitmvi.orbit.viewmodel.observe
import team.duckie.app.android.common.android.ui.BaseActivity
import team.duckie.app.android.common.android.ui.const.Extras
import team.duckie.app.android.common.android.ui.finishWithAnimation
+import team.duckie.app.android.common.compose.util.addFocusCleaner
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamSideEffect
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamViewModel
import team.duckie.app.android.navigator.feature.solveproblem.SolveProblemNavigator
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.theme.QuackTheme
import javax.inject.Inject
@AndroidEntryPoint
@@ -43,7 +44,8 @@ class StartExamActivity : BaseActivity() {
StartExamScreen(
Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .addFocusCleaner()
+ .background(color = QuackColor.White.value)
.systemBarsPadding(),
)
}
diff --git a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamScreen.kt b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamScreen.kt
index 6e21a1432..1e23b42c6 100644
--- a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamScreen.kt
+++ b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/StartExamScreen.kt
@@ -15,13 +15,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.feature.start.exam.screen.exam.StartExamInputScreen
import team.duckie.app.android.feature.start.exam.screen.quiz.StartQuizInputScreen
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamState
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamViewModel
-import team.duckie.app.android.common.compose.activityViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackTitle1
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.ui.sugar.QuackTitle1
@Composable
internal fun StartExamScreen(
@@ -57,7 +57,7 @@ private fun StartExamLoadingScreen(modifier: Modifier, viewModel: StartExamViewM
) {
// TODO(riflockle7): 추후 DuckieCircularProgressIndicator.kt 와 합치거나 꽥꽥 컴포넌트로 필요
CircularProgressIndicator(
- color = QuackColor.DuckieOrange.composeColor,
+ color = QuackColor.DuckieOrange.value,
)
}
}
diff --git a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/exam/StartExamInputScreen.kt b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/exam/StartExamInputScreen.kt
index c07118a10..07afd8d11 100644
--- a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/exam/StartExamInputScreen.kt
+++ b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/exam/StartExamInputScreen.kt
@@ -5,6 +5,8 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.feature.start.exam.screen.exam
import androidx.compose.foundation.background
@@ -25,17 +27,16 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import team.duckie.app.android.common.compose.ui.BackPressedTopAppBar
import team.duckie.app.android.common.compose.ui.ImeSpacer
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.feature.start.exam.R
import team.duckie.app.android.feature.start.exam.screen.StartExamScreen
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamState
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackGrayscaleTextField
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.QuackText
+import team.duckie.quackquack.ui.sugar.QuackHeadLine1
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
/**
* 시험 시작 입력 화면
@@ -53,7 +54,7 @@ internal fun StartExamInputScreen(modifier: Modifier, viewModel: StartExamViewMo
state.certifyingStatementInputText
}
- Column(modifier = modifier) {
+ Column(modifier = modifier.fillMaxWidth()) {
// 상단 탭바
BackPressedTopAppBar(onBackPressed = viewModel::finishStartExam)
@@ -83,17 +84,17 @@ internal fun StartExamInputScreen(modifier: Modifier, viewModel: StartExamViewMo
Spacer(modifier = Modifier.weight(1f))
// 시험시작 버튼
- QuackLargeButton(
- modifier = Modifier.padding(
- vertical = 12.dp,
- horizontal = 16.dp,
- ),
- type = QuackLargeButtonType.Fill,
+ TempFlexiblePrimaryLargeButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ vertical = 12.dp,
+ horizontal = 16.dp,
+ ),
text = stringResource(id = R.string.start_exam_start_button),
- enabled = viewModel.startExamValidate(),
onClick = viewModel::startSolveProblem,
+ enabled = viewModel.startExamValidate(),
)
-
ImeSpacer()
}
}
@@ -117,14 +118,14 @@ internal fun StartExamTextField(
BasicTextField(
modifier = modifier
.fillMaxWidth()
- .background(color = QuackColor.Gray4.composeColor)
+ .background(color = QuackColor.Gray4.value)
.padding(
vertical = 17.dp,
horizontal = 20.dp,
),
value = text,
onValueChange = onTextChanged,
- textStyle = QuackTextStyle.Body1.asComposeStyle(),
+ textStyle = QuackTypography.Body1.asComposeStyle(),
keyboardOptions = KeyboardOptions(imeAction = imeAction),
keyboardActions = keyboardActions,
singleLine = true,
@@ -135,7 +136,7 @@ internal fun StartExamTextField(
if (alwaysPlaceholderVisible || text.isEmpty()) {
QuackText(
text = placeholderText,
- style = QuackTextStyle.Body1.change(color = QuackColor.Gray2),
+ typography = QuackTypography.Body1.change(color = QuackColor.Gray2),
)
}
}
diff --git a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/quiz/StartQuizInputScreen.kt b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/quiz/StartQuizInputScreen.kt
index 22be5763c..8d6864261 100644
--- a/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/quiz/StartQuizInputScreen.kt
+++ b/feature/start-exam/src/main/java/team/duckie/app/android/feature/start/exam/screen/quiz/StartQuizInputScreen.kt
@@ -5,6 +5,8 @@
* Please see full license: https://github.com/duckie-team/duckie-android/blob/develop/LICENSE
*/
+@file:OptIn(ExperimentalDesignToken::class, ExperimentalQuackQuackApi::class)
+
package team.duckie.app.android.feature.start.exam.screen.quiz
import androidx.compose.foundation.background
@@ -14,32 +16,37 @@ import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import team.duckie.app.android.common.compose.ui.BackPressedTopAppBar
import team.duckie.app.android.common.compose.ui.ImeSpacer
import team.duckie.app.android.common.compose.ui.Spacer
+import team.duckie.app.android.common.compose.ui.temp.TempFlexiblePrimaryLargeButton
import team.duckie.app.android.feature.start.exam.R
+import team.duckie.app.android.feature.start.exam.screen.exam.StartExamTextField
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamState
import team.duckie.app.android.feature.start.exam.viewmodel.StartExamViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackBody2
-import team.duckie.quackquack.ui.component.QuackGrayscaleTextField
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackLargeButton
-import team.duckie.quackquack.ui.component.QuackLargeButtonType
-import team.duckie.quackquack.ui.component.QuackTitle2
-import team.duckie.quackquack.ui.component.internal.QuackText
-import team.duckie.quackquack.ui.textstyle.QuackTextStyle
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.ui.optin.ExperimentalDesignToken
+import team.duckie.quackquack.ui.sugar.QuackBody2
+import team.duckie.quackquack.ui.sugar.QuackHeadLine1
+import team.duckie.quackquack.ui.sugar.QuackTitle2
+import team.duckie.quackquack.ui.util.ExperimentalQuackQuackApi
@Composable
internal fun StartQuizInputScreen(modifier: Modifier, viewModel: StartExamViewModel) {
@@ -49,8 +56,9 @@ internal fun StartQuizInputScreen(modifier: Modifier, viewModel: StartExamViewMo
val certifyingStatementText: String = remember(state.certifyingStatementInputText) {
state.certifyingStatementInputText
}
+ val keyboard = LocalSoftwareKeyboardController.current
- Column(modifier = modifier) {
+ Column(modifier = modifier.fillMaxWidth()) {
BackPressedTopAppBar(onBackPressed = viewModel::finishStartExam)
Column(
modifier = Modifier
@@ -62,7 +70,7 @@ internal fun StartQuizInputScreen(modifier: Modifier, viewModel: StartExamViewMo
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
- .background(color = QuackColor.Gray4.composeColor)
+ .background(color = QuackColor.Gray4.value)
.padding(all = 12.dp),
limitTime = state.timer,
)
@@ -75,23 +83,31 @@ internal fun StartQuizInputScreen(modifier: Modifier, viewModel: StartExamViewMo
modifier = Modifier.padding(top = 4.dp),
text = state.requirementQuestion, // TODO(EvergreenTree97) 추후 도전 조건 response 생기면 변경
)
- QuackGrayscaleTextField(
- modifier = Modifier.padding(top = 14.dp),
+ StartExamTextField(
+ modifier = Modifier
+ .padding(top = 14.dp)
+ .clip(RoundedCornerShape(8.dp)),
text = certifyingStatementText,
- onTextChanged = viewModel::inputCertifyingStatement,
+ alwaysPlaceholderVisible = false,
placeholderText = "ex) ${state.requirementPlaceholder}",
+ onTextChanged = viewModel::inputCertifyingStatement,
+ keyboardActions = KeyboardActions {
+ keyboard?.hide()
+ viewModel.startSolveProblem()
+ },
)
}
Spacer(modifier = Modifier.weight(1f))
- QuackLargeButton(
- modifier = Modifier.padding(
- vertical = 12.dp,
- horizontal = 16.dp,
- ),
- type = QuackLargeButtonType.Fill,
+ TempFlexiblePrimaryLargeButton(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(
+ vertical = 12.dp,
+ horizontal = 16.dp,
+ ),
text = stringResource(id = R.string.start_exam_quiz_start_button),
- enabled = certifyingStatementText.isNotEmpty(),
onClick = viewModel::startSolveProblem,
+ enabled = certifyingStatementText.isNotEmpty(),
)
ImeSpacer()
}
@@ -109,27 +125,49 @@ private fun InfoBox(
QuackTitle2(text = stringResource(id = R.string.start_exam_information_before_quiz_title))
Spacer(space = 4.dp)
QuackBody2(text = stringResource(id = R.string.start_exam_information_before_quiz_line1))
- QuackText(
- annotatedText = buildAnnotatedString {
+ Text(
+ style = QuackTypography.Body2.asComposeStyle().copy(
+ lineBreak = LineBreak.Simple,
+ ),
+ text = buildAnnotatedString {
append(stringResource(id = R.string.start_exam_information_before_quiz_line2_prefix))
withStyle(
- SpanStyle(
- color = QuackColor.Black.composeColor,
+ style = SpanStyle(
+ color = Color.Black,
fontWeight = FontWeight.Bold,
),
) {
append(
stringResource(
- id = R.string.start_exam_information_before_quiz_line2_infix,
+ id = R.string.start_exam_information_before_quiz_line2_highlight,
limitTime.toString(),
),
)
}
append(
- stringResource(id = R.string.start_exam_information_before_quiz_line2_postfix),
+ stringResource(
+ id = R.string.start_exam_information_before_quiz_line2_infix,
+ limitTime.toString(),
+ ),
)
+ append(stringResource(id = R.string.start_exam_information_before_quiz_line2_postfix))
+ },
+ )
+ Text(
+ style = QuackTypography.Body2.asComposeStyle().copy(
+ lineBreak = LineBreak.Simple,
+ ),
+ text = buildAnnotatedString {
+ append(stringResource(id = R.string.start_exam_information_before_quiz_line3_prefix))
+ withStyle(
+ style = SpanStyle(
+ color = Color.Black,
+ fontWeight = FontWeight.Bold,
+ ),
+ ) {
+ append(stringResource(id = R.string.start_exam_information_before_quiz_line3_highlight))
+ }
},
- style = QuackTextStyle.Body2,
)
}
}
diff --git a/feature/start-exam/src/main/res/values/strings.xml b/feature/start-exam/src/main/res/values/strings.xml
index 1cdf0adc0..7bdc06afc 100644
--- a/feature/start-exam/src/main/res/values/strings.xml
+++ b/feature/start-exam/src/main/res/values/strings.xml
@@ -6,8 +6,11 @@
[ 퀴즈 시작 전 안내사항 ]
※ 문제를 틀릴 경우 바로 도전이 종료됩니다.
※ 문제당
- 약 %s초의 제한시간이 있습니다.
+ 이 있습니다.
+ 약 %s초의 제한시간
문제를 잘 읽고 시간 내에 물음에 답해주세요.
+ ※ 띄어쓰기는 저희가 할게요. 대소문자도 구분할 필요 없어요!
+ 정확한 답만 입력해주세요!
<도전 조건>
덕퀴즈 도전
diff --git a/feature/tag-edit/build.gradle.kts b/feature/tag-edit/build.gradle.kts
index 7bf1bc5ae..22eceb406 100644
--- a/feature/tag-edit/build.gradle.kts
+++ b/feature/tag-edit/build.gradle.kts
@@ -28,7 +28,8 @@ dependencies {
projects.common.compose,
libs.orbit.viewmodel,
libs.orbit.compose,
- libs.quack.ui.components,
+ libs.kotlin.collections.immutable,
+ libs.quack.v2.ui,
libs.compose.lifecycle.runtime,
libs.compose.ui.material, // needs for CircularProgressIndicator
libs.firebase.crashlytics,
diff --git a/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/TagEditActivity.kt b/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/TagEditActivity.kt
index d6bee34e4..7c0279520 100644
--- a/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/TagEditActivity.kt
+++ b/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/TagEditActivity.kt
@@ -16,13 +16,13 @@ import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.ui.Modifier
import dagger.hilt.android.AndroidEntryPoint
import org.orbitmvi.orbit.viewmodel.observe
+import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
+import team.duckie.app.android.common.android.ui.BaseActivity
import team.duckie.app.android.feature.tag.edit.screen.TagEditScreen
import team.duckie.app.android.feature.tag.edit.viewmodel.TagEditSideEffect
import team.duckie.app.android.feature.tag.edit.viewmodel.TagEditViewModel
-import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
-import team.duckie.app.android.common.android.ui.BaseActivity
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.theme.QuackTheme
@AndroidEntryPoint
class TagEditActivity : BaseActivity() {
@@ -36,7 +36,7 @@ class TagEditActivity : BaseActivity() {
TagEditScreen(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
.systemBarsPadding(),
)
}
diff --git a/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/screen/TagEditScreen.kt b/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/screen/TagEditScreen.kt
index 92dd3cd7c..f365d2dd0 100644
--- a/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/screen/TagEditScreen.kt
+++ b/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/screen/TagEditScreen.kt
@@ -10,6 +10,7 @@
package team.duckie.app.android.feature.tag.edit.screen
import android.app.Activity
+import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -26,7 +27,9 @@ import androidx.compose.ui.unit.dp
import kotlinx.collections.immutable.toPersistentList
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.compose.collectAsState
+import team.duckie.app.android.common.compose.HideKeyboardWhenBottomSheetHidden
import team.duckie.app.android.common.compose.activityViewModel
+import team.duckie.app.android.common.compose.ui.BackPressedHeadLine2TopAppBar
import team.duckie.app.android.common.compose.ui.ErrorScreen
import team.duckie.app.android.common.compose.ui.FavoriteTagSection
import team.duckie.app.android.common.compose.ui.LoadingScreen
@@ -35,11 +38,12 @@ import team.duckie.app.android.domain.tag.model.Tag
import team.duckie.app.android.feature.tag.edit.R
import team.duckie.app.android.feature.tag.edit.viewmodel.TagEditState
import team.duckie.app.android.feature.tag.edit.viewmodel.TagEditViewModel
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackSubtitle
-import team.duckie.quackquack.ui.component.QuackTopAppBar
-import team.duckie.quackquack.ui.icon.QuackIcon
-import team.duckie.quackquack.ui.modifier.quackClickable
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.material.QuackTypography
+import team.duckie.quackquack.material.icon.quackicon.OutlinedGroup
+import team.duckie.quackquack.material.icon.quackicon.outlined.Close
+import team.duckie.quackquack.material.quackClickable
+import team.duckie.quackquack.ui.QuackText
@Composable
internal fun TagEditScreen(
@@ -59,7 +63,6 @@ internal fun TagEditScreen(
onTrailingClick = vm::onTrailingClick,
addNewTags = vm::addNewTags,
requestAddTag = vm::requestNewTag,
- onTagClick = vm::onTrailingClick,
)
is TagEditState.Error -> ErrorScreen(
@@ -77,7 +80,6 @@ fun TagEditSuccessScreen(
onTrailingClick: (Int) -> Unit,
addNewTags: (List) -> Unit,
requestAddTag: suspend (String) -> Tag?,
- onTagClick: (Int) -> Unit,
) {
val activity = LocalContext.current as Activity
val coroutineScope = rememberCoroutineScope()
@@ -86,6 +88,16 @@ fun TagEditSuccessScreen(
skipHalfExpanded = true,
)
+ BackHandler {
+ if (sheetState.isVisible) {
+ coroutineScope.launch { sheetState.hide() }
+ } else {
+ activity.finish()
+ }
+ }
+
+ HideKeyboardWhenBottomSheetHidden(sheetState)
+
DuckieTagAddBottomSheet(
sheetState = sheetState,
onDismissRequest = { newAddedTags, clearAction ->
@@ -99,24 +111,17 @@ fun TagEditSuccessScreen(
content = {
Column(modifier = modifier) {
// 상단 탭바
- QuackTopAppBar(
- leadingIcon = QuackIcon.ArrowBack,
- leadingText = stringResource(R.string.title),
- onLeadingIconClick = activity::finish,
+ BackPressedHeadLine2TopAppBar(
+ title = stringResource(R.string.title),
+ onBackPressed = activity::finish,
trailingContent = {
- QuackSubtitle(
+ QuackText(
modifier = Modifier
- .then(Modifier) // prevent Modifier.Companion
- .quackClickable(
- rippleEnabled = false,
- onClick = onEditFinishClick,
- )
- .padding(
- vertical = 4.dp,
- horizontal = 16.dp,
- ),
+ .quackClickable(onClick = onEditFinishClick),
text = stringResource(R.string.edit_finish),
- color = QuackColor.DuckieOrange,
+ typography = QuackTypography.Subtitle.change(
+ color = QuackColor.DuckieOrange,
+ ),
singleLine = true,
)
},
@@ -128,12 +133,11 @@ fun TagEditSuccessScreen(
title = stringResource(id = R.string.my_favorite_tag),
horizontalPadding = PaddingValues(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
- trailingIcon = QuackIcon.Close,
+ trailingIcon = OutlinedGroup.Close,
onTrailingClick = onTrailingClick,
tags = state.myTags.map { it.name }.toPersistentList(),
emptySection = {},
- singleLine = false,
- onTagClick = onTagClick,
+ onTagClick = {},
addButtonTitle = stringResource(id = R.string.tag_edit_add_favorite_tag),
onAddTagClick = {
coroutineScope.launch { sheetState.show() }
diff --git a/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/viewmodel/TagEditViewModel.kt b/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/viewmodel/TagEditViewModel.kt
index 7d3441ff1..6a214a7e2 100644
--- a/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/viewmodel/TagEditViewModel.kt
+++ b/feature/tag-edit/src/main/kotlin/team/duckie/app/android/feature/tag/edit/viewmodel/TagEditViewModel.kt
@@ -12,12 +12,12 @@ import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
-import kotlinx.collections.immutable.toPersistentList
import org.orbitmvi.orbit.ContainerHost
import org.orbitmvi.orbit.syntax.simple.intent
import org.orbitmvi.orbit.syntax.simple.postSideEffect
import org.orbitmvi.orbit.syntax.simple.reduce
import org.orbitmvi.orbit.viewmodel.container
+import team.duckie.app.android.common.kotlin.copy
import team.duckie.app.android.domain.tag.model.Tag
import team.duckie.app.android.domain.tag.usecase.TagCreateUseCase
import team.duckie.app.android.domain.user.model.User
@@ -56,13 +56,14 @@ internal class TagEditViewModel @Inject constructor(
/** 각 태그 항목의 x 버튼 클릭 시 동작 */
fun onTrailingClick(index: Int) = intent {
- val newTags = myTags.toMutableList().apply { removeAt(index) }.toPersistentList()
+ require(state is TagEditState.Success)
+ val newTags = (state as TagEditState.Success).myTags.copy { removeAt(index) }
reduce { TagEditState.Success(myTags = newTags) }
}
/** 바텀 시트에서 오른쪽 방향 화살표를 눌러 태그 추가를 완료한다. */
fun addNewTags(newTag: List) {
- val newTags = myTags.toMutableList().apply { addAll(newTag) }.toPersistentList()
+ val newTags = myTags.copy { addAll(newTag) }
intent { reduce { TagEditState.Success(myTags = newTags) } }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 38242ff3d..88761d828 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -50,7 +50,7 @@ androidx-annotation = "1.6.0"
# compose
compose-core = "1.4.2"
-compose-foundation = "1.4.0-beta01" # use alpha for HorizontalPager
+compose-foundation = "1.6.0-alpha04" # use alpha for HorizontalPager
compose-material = "1.4.2"
compose-runtime = "1.5.0-alpha03" # use alpha for SnapshotStateList#toList
compose-lifecycle = "2.6.1" # use alpha for StateFlow#collectAsStateWithLifecycle
@@ -114,7 +114,8 @@ quack-lint-compose = "1.0.2"
# TODO(sungbin): quack-lint-writing
# quack v2
-quack-v2-ui = "2.0.0-alpha07"
+quack-v2-ui = "2.0.0-alpha11"
+quack-v2-ui-plugin-interceptor-textfield ="2.0.0-alpha01"
# test
test-strikt = "0.34.1"
@@ -258,6 +259,7 @@ quack-lint-quack = { module = "team.duckie.quack:quack-lint-quack", version.ref
quack-lint-compose = { module = "team.duckie.quack:quack-lint-compose", version.ref = "quack-lint-compose" }
quack-v2-ui = { module = "team.duckie.quackquack.ui:ui", version.ref = "quack-v2-ui" }
+quack-v2-ui-plugin-interceptor-textfield = { module = "team.duckie.quackquack.ui:ui-plugin-interceptor-textfield", version.ref = "quack-v2-ui-plugin-interceptor-textfield"}
test-strikt = { module = "io.strikt:strikt-core", version.ref = "test-strikt" }
test-junit-core = { module = "junit:junit", version.ref = "test-junit-core" }
diff --git a/presentation/build.gradle.kts b/presentation/build.gradle.kts
index 02e4989f8..257daa54d 100644
--- a/presentation/build.gradle.kts
+++ b/presentation/build.gradle.kts
@@ -33,7 +33,7 @@ dependencies {
libs.firebase.dynamic.links,
libs.orbit.viewmodel,
libs.androidx.splash,
- libs.quack.ui.components,
+ libs.quack.v2.ui,
libs.orbit.compose,
)
}
diff --git a/presentation/src/main/kotlin/team/duckie/app/android/presentation/IntroActivity.kt b/presentation/src/main/kotlin/team/duckie/app/android/presentation/IntroActivity.kt
index 9dd9f6d01..47755fd73 100644
--- a/presentation/src/main/kotlin/team/duckie/app/android/presentation/IntroActivity.kt
+++ b/presentation/src/main/kotlin/team/duckie/app/android/presentation/IntroActivity.kt
@@ -32,20 +32,20 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import org.orbitmvi.orbit.viewmodel.observe
import team.duckie.app.android.common.android.deeplink.DynamicLinkHelper
-import team.duckie.app.android.domain.user.model.UserStatus
+import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
+import team.duckie.app.android.common.android.ui.BaseActivity
+import team.duckie.app.android.common.android.ui.changeActivityWithAnimation
+import team.duckie.app.android.common.android.ui.const.Extras
+import team.duckie.app.android.common.kotlin.seconds
import team.duckie.app.android.core.datastore.PreferenceKey
import team.duckie.app.android.core.datastore.dataStore
+import team.duckie.app.android.domain.user.model.UserStatus
import team.duckie.app.android.feature.home.screen.MainActivity
import team.duckie.app.android.feature.onboard.OnboardActivity
import team.duckie.app.android.presentation.screen.IntroScreen
import team.duckie.app.android.presentation.viewmodel.IntroViewModel
import team.duckie.app.android.presentation.viewmodel.sideeffect.IntroSideEffect
-import team.duckie.app.android.common.android.exception.handling.reporter.reportToCrashlyticsIfNeeded
-import team.duckie.app.android.common.kotlin.seconds
-import team.duckie.app.android.common.android.ui.BaseActivity
-import team.duckie.app.android.common.android.ui.changeActivityWithAnimation
-import team.duckie.app.android.common.android.ui.const.Extras
-import team.duckie.quackquack.ui.theme.QuackTheme
+import team.duckie.quackquack.material.theme.QuackTheme
private val SplashScreenExitAnimationDurationMillis = 0.2.seconds
private val SplashScreenFinishDurationMillis = 1.5.seconds
diff --git a/presentation/src/main/kotlin/team/duckie/app/android/presentation/screen/IntroScreen.kt b/presentation/src/main/kotlin/team/duckie/app/android/presentation/screen/IntroScreen.kt
index 76da2906c..3f1f8118b 100644
--- a/presentation/src/main/kotlin/team/duckie/app/android/presentation/screen/IntroScreen.kt
+++ b/presentation/src/main/kotlin/team/duckie/app/android/presentation/screen/IntroScreen.kt
@@ -15,6 +15,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -23,16 +24,16 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
-import team.duckie.app.android.presentation.R
-import team.duckie.app.android.presentation.viewmodel.IntroViewModel
+import org.orbitmvi.orbit.compose.collectAsState
import team.duckie.app.android.common.android.intent.goToMarket
import team.duckie.app.android.common.compose.activityViewModel
import team.duckie.app.android.common.compose.systemBarPaddings
-import team.duckie.quackquack.ui.color.QuackColor
-import team.duckie.quackquack.ui.component.QuackHeadLine1
-import team.duckie.quackquack.ui.component.QuackImage
-import org.orbitmvi.orbit.compose.collectAsState
import team.duckie.app.android.common.compose.ui.dialog.DuckieDialog
+import team.duckie.app.android.presentation.R
+import team.duckie.app.android.presentation.viewmodel.IntroViewModel
+import team.duckie.quackquack.material.QuackColor
+import team.duckie.quackquack.ui.QuackImage
+import team.duckie.quackquack.ui.sugar.QuackHeadLine1
@Composable
internal fun IntroScreen(
@@ -43,7 +44,7 @@ internal fun IntroScreen(
Column(
modifier = Modifier
.fillMaxSize()
- .background(color = QuackColor.White.composeColor)
+ .background(color = QuackColor.White.value)
.padding(systemBarPaddings)
.padding(
top = 78.dp,
@@ -60,21 +61,26 @@ internal fun IntroScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
QuackImage(
- src = team.duckie.quackquack.ui.R.drawable.quack_duckie_text_logo,
- size = DpSize(
- width = 110.dp,
- height = 32.dp,
+ modifier = Modifier.size(
+ size = DpSize(
+ width = 110.dp,
+ height = 32.dp,
+ ),
),
+ src = R.drawable.duckie_text_logo,
)
QuackHeadLine1(text = stringResource(R.string.intro_slogan))
}
QuackImage(
- modifier = Modifier.offset(x = 125.dp),
+ modifier = Modifier
+ .size(
+ size = DpSize(
+ width = 276.dp,
+ height = 255.dp,
+ ),
+ )
+ .offset(x = 125.dp),
src = R.drawable.img_duckie_intro,
- size = DpSize(
- width = 276.dp,
- height = 255.dp,
- ),
)
}
diff --git a/presentation/src/main/res/drawable/duckie_text_logo.xml b/presentation/src/main/res/drawable/duckie_text_logo.xml
new file mode 100644
index 000000000..3b570b212
--- /dev/null
+++ b/presentation/src/main/res/drawable/duckie_text_logo.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 48c59ab18..d474bc720 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -51,7 +51,7 @@ include(
":feature:home",
":feature:start-exam",
":feature:solve-problem",
- ":feature:create-problem",
+ ":feature:create-exam",
":feature:detail",
":feature:exam-result",
":feature:profile",