diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/flow/CollectAsEffect.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/flow/CollectAsEffect.kt new file mode 100644 index 00000000..bb004398 --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/flow/CollectAsEffect.kt @@ -0,0 +1,22 @@ +package pokitmons.pokit.core.feature.flow + +import android.annotation.SuppressLint +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +@SuppressLint("ComposableNaming") +@Composable +fun Flow.collectAsEffect( + context: CoroutineContext = EmptyCoroutineContext, + block: (T) -> Unit, +) { + LaunchedEffect(Unit) { + onEach(block).flowOn(context).launchIn(this) + } +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/flow/EventFlow.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/flow/EventFlow.kt new file mode 100644 index 00000000..9c938339 --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/flow/EventFlow.kt @@ -0,0 +1,47 @@ +package pokitmons.pokit.core.feature.flow + +import kotlinx.coroutines.InternalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.FlowCollector +import kotlinx.coroutines.flow.MutableSharedFlow +import java.util.concurrent.atomic.AtomicBoolean + +interface EventFlow : Flow { + companion object { + const val DEFAULT_REPLAY: Int = 1 + } +} + +interface MutableEventFlow : EventFlow, FlowCollector + +fun MutableEventFlow( + replay: Int = EventFlow.DEFAULT_REPLAY, +): MutableEventFlow = EventFlowImpl(replay) + +fun MutableEventFlow.asEventFlow(): EventFlow = ReadOnlyEventFlow(this) + +private class ReadOnlyEventFlow(flow: EventFlow) : EventFlow by flow + +private class EventFlowImpl( + replay: Int, +) : MutableEventFlow { + + private val flow: MutableSharedFlow> = MutableSharedFlow(replay = replay) + + @InternalCoroutinesApi + override suspend fun collect(collector: FlowCollector) = flow + .collect { slot -> + if (!slot.markConsumed()) { + collector.emit(slot.value) + } + } + + override suspend fun emit(value: T) { + flow.emit(EventFlowSlot(value)) + } +} + +private class EventFlowSlot(val value: T) { + private val consumed: AtomicBoolean = AtomicBoolean(false) + fun markConsumed(): Boolean = consumed.getAndSet(true) +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/model/NetworkState.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/NetworkState.kt new file mode 100644 index 00000000..4732be10 --- /dev/null +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/model/NetworkState.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.core.feature.model + +enum class NetworkState { + IDLE, LOADING, ERROR +} diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkArg.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkArg.kt index 12e8e623..eff7a88d 100644 --- a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkArg.kt +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkArg.kt @@ -10,4 +10,5 @@ data class LinkArg( val thumbnail: String, val createdAt: String, val domain: String, + val pokitId: Int, ) : Parcelable diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkUpdateEvent.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkUpdateEvent.kt index acbee359..2877d356 100644 --- a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkUpdateEvent.kt +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/LinkUpdateEvent.kt @@ -13,6 +13,9 @@ object LinkUpdateEvent { private val _removedLink = MutableSharedFlow() val removedLink = _removedLink.asSharedFlow() + private val _addedLink = MutableSharedFlow() + val addedLink = _addedLink.asSharedFlow() + fun modifySuccess(link: LinkArg) { CoroutineScope(Dispatchers.Default).launch { _updatedLink.emit(link) @@ -24,4 +27,10 @@ object LinkUpdateEvent { _removedLink.emit(linkId) } } + + fun createSuccess(link: LinkArg) { + CoroutineScope(Dispatchers.Default).launch { + _addedLink.emit(link) + } + } } diff --git a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt index 37a28625..a506e3ec 100644 --- a/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt +++ b/core/feature/src/main/java/pokitmons/pokit/core/feature/navigation/args/PokitUpdateEvent.kt @@ -13,6 +13,9 @@ object PokitUpdateEvent { private val _removedPokitId = MutableSharedFlow() val removedPokitId = _removedPokitId.asSharedFlow() + private val _addedPokit = MutableSharedFlow() + val addedPokit = _addedPokit.asSharedFlow() + fun updatePokit(pokitArg: PokitArg) { CoroutineScope(Dispatchers.Default).launch { _updatedPokit.emit(pokitArg) @@ -24,4 +27,10 @@ object PokitUpdateEvent { _removedPokitId.emit(pokitId) } } + + fun createPokit(pokitArg: PokitArg) { + CoroutineScope(Dispatchers.Default).launch { + _addedPokit.emit(pokitArg) + } + } } diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/loading/LoadingProgress.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/loading/LoadingProgress.kt new file mode 100644 index 00000000..1e832cb3 --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/atom/loading/LoadingProgress.kt @@ -0,0 +1,24 @@ +package pokitmons.pokit.core.ui.components.atom.loading + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.width +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun LoadingProgress(modifier: Modifier = Modifier) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator( + modifier = Modifier.width(64.dp), + color = PokitTheme.colors.brand, + trackColor = PokitTheme.colors.borderTertiary + ) + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt index 1f7dea8e..99d7c89f 100644 --- a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/linkcard/LinkCard.kt @@ -40,7 +40,7 @@ fun LinkCard( sub: String, painter: Painter, notRead: Boolean, - badgeText: String, + badgeText: String?, onClickKebab: (T) -> Unit, onClickItem: (T) -> Unit, modifier: Modifier = Modifier, @@ -119,16 +119,18 @@ fun LinkCard( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(6.dp) ) { - Text( - text = badgeText, - modifier = Modifier - .background( - color = PokitTheme.colors.backgroundPrimary, - shape = RoundedCornerShape(4.dp) - ) - .padding(horizontal = 8.dp, vertical = 4.dp), - style = PokitTheme.typography.label4.copy(color = PokitTheme.colors.textTertiary) - ) + badgeText?.let { badge -> + Text( + text = badge, + modifier = Modifier + .background( + color = PokitTheme.colors.backgroundPrimary, + shape = RoundedCornerShape(4.dp) + ) + .padding(horizontal = 8.dp, vertical = 4.dp), + style = PokitTheme.typography.label4.copy(color = PokitTheme.colors.textTertiary) + ) + } if (notRead) { Text( diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokittoast/PokitToast.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokittoast/PokitToast.kt new file mode 100644 index 00000000..494115cf --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokittoast/PokitToast.kt @@ -0,0 +1,60 @@ +package pokitmons.pokit.core.ui.components.block.pokittoast + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +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.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun PokitToast( + modifier: Modifier = Modifier, + text: String, + onClick: (() -> Unit)? = null, + onClickClose: () -> Unit = {}, +) { + Row( + modifier = modifier + .clip(RoundedCornerShape(9999.dp)) + .background(PokitTheme.colors.backgroundTertiary) + .clickable( + enabled = onClick != null, + onClick = onClick ?: {} + ) + .padding(start = 20.dp, end = 14.dp, top = 12.dp, bottom = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = text, + style = PokitTheme.typography.body3Medium.copy(color = PokitTheme.colors.inverseWh), + modifier = Modifier.weight(1f) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + IconButton( + onClick = onClickClose, + modifier = Modifier.size(36.dp) + ) { + Icon( + painter = painterResource(id = R.drawable.icon_24_x), + contentDescription = null, + tint = PokitTheme.colors.inverseWh + ) + } + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokittoast/Preview.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokittoast/Preview.kt new file mode 100644 index 00000000..663d3435 --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/block/pokittoast/Preview.kt @@ -0,0 +1,28 @@ +package pokitmons.pokit.core.ui.components.block.pokittoast + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + PokitToast( + modifier = Modifier.padding(20.dp), + text = "최대 30개의 포킷을 생성할 수 있습니다.\n포킷을 삭제한 뒤에 추가해주세요.", + onClick = {} + ) + } + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkiempty/EmptyPokki.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkiempty/EmptyPokki.kt new file mode 100644 index 00000000..48ab6577 --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkiempty/EmptyPokki.kt @@ -0,0 +1,48 @@ +package pokitmons.pokit.core.ui.components.template.pokkiempty + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun EmptyPokki( + modifier: Modifier = Modifier, + title: String, + sub: String, +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier + .height(180.dp) + .width(180.dp), + painter = painterResource(id = R.drawable.empty_pokki), + contentDescription = "empty" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text(text = title, style = PokitTheme.typography.title2.copy(color = PokitTheme.colors.textPrimary)) + + Spacer(modifier = Modifier.height(8.dp)) + + Text(text = sub, style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary)) + } + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkiempty/Preview.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkiempty/Preview.kt new file mode 100644 index 00000000..ee20afed --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkiempty/Preview.kt @@ -0,0 +1,18 @@ +package pokitmons.pokit.core.ui.components.template.pokkiempty + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Surface(modifier = Modifier.fillMaxSize()) { + EmptyPokki(title = "저장된 포킷이 없어요!", sub = "포킷을 생성해 링크를 저장해보세요") + } + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkierror/ErrorPokki.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkierror/ErrorPokki.kt new file mode 100644 index 00000000..666fdcb1 --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkierror/ErrorPokki.kt @@ -0,0 +1,69 @@ +package pokitmons.pokit.core.ui.components.template.pokkierror + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.components.atom.button.PokitButton +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonStyle +import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonType +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Composable +fun ErrorPokki( + modifier: Modifier = Modifier, + pokkiSize: Dp = 180.dp, + title: String, + sub: String, + onClickRetry: (() -> Unit)? = null, +) { + Box( + modifier = modifier, + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + modifier = Modifier + .height(pokkiSize) + .width(pokkiSize), + painter = painterResource(id = R.drawable.cry_pokki), + contentDescription = "empty" + ) + + Spacer(modifier = Modifier.height(16.dp)) + + Text(text = title, style = PokitTheme.typography.title2.copy(color = PokitTheme.colors.textPrimary)) + + Spacer(modifier = Modifier.height(8.dp)) + + Text(text = sub, style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary)) + + onClickRetry?.let { onClick -> + Spacer(modifier = Modifier.height(16.dp)) + + PokitButton( + type = PokitButtonType.SECONDARY, + size = PokitButtonSize.SMALL, + style = PokitButtonStyle.DEFAULT, + text = stringResource(id = R.string.retry), + icon = null, + onClick = onClick + ) + } + } + } +} diff --git a/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkierror/Preview.kt b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkierror/Preview.kt new file mode 100644 index 00000000..e603d87c --- /dev/null +++ b/core/ui/src/main/java/pokitmons/pokit/core/ui/components/template/pokkierror/Preview.kt @@ -0,0 +1,19 @@ +package pokitmons.pokit.core.ui.components.template.pokkierror + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Surface +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Surface(modifier = Modifier.fillMaxSize()) { + ErrorPokki(title = "오류가 발생했어요", sub = "조금 뒤 다시 접속해주세요", onClickRetry = null) + // ErrorPokki(title = "오류가 발생했어요", sub = "조금 뒤 다시 접속해주세요", onClickRetry = {}) + } + } +} diff --git a/core/ui/src/main/res/drawable-hdpi/big_pokki.png b/core/ui/src/main/res/drawable-hdpi/big_pokki.png new file mode 100644 index 00000000..a1dc2b36 Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/big_pokki.png differ diff --git a/core/ui/src/main/res/drawable-hdpi/cry_pokki.png b/core/ui/src/main/res/drawable-hdpi/cry_pokki.png new file mode 100644 index 00000000..7dbe473c Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/cry_pokki.png differ diff --git a/core/ui/src/main/res/drawable-hdpi/empty_pokki.png b/core/ui/src/main/res/drawable-hdpi/empty_pokki.png new file mode 100644 index 00000000..f76c39ce Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/empty_pokki.png differ diff --git a/core/ui/src/main/res/drawable-hdpi/party_popper.png b/core/ui/src/main/res/drawable-hdpi/party_popper.png new file mode 100644 index 00000000..395d288f Binary files /dev/null and b/core/ui/src/main/res/drawable-hdpi/party_popper.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/big_pokki.png b/core/ui/src/main/res/drawable-mdpi/big_pokki.png new file mode 100644 index 00000000..ab47481e Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/big_pokki.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/cry_pokki.png b/core/ui/src/main/res/drawable-mdpi/cry_pokki.png new file mode 100644 index 00000000..72b0b0d5 Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/cry_pokki.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/empty_pokki.png b/core/ui/src/main/res/drawable-mdpi/empty_pokki.png new file mode 100644 index 00000000..cc886afc Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/empty_pokki.png differ diff --git a/core/ui/src/main/res/drawable-mdpi/party_popper.png b/core/ui/src/main/res/drawable-mdpi/party_popper.png new file mode 100644 index 00000000..68649dfe Binary files /dev/null and b/core/ui/src/main/res/drawable-mdpi/party_popper.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/big_pokki.png b/core/ui/src/main/res/drawable-xhdpi/big_pokki.png new file mode 100644 index 00000000..c129ea2f Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/big_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/cry_pokki.png b/core/ui/src/main/res/drawable-xhdpi/cry_pokki.png new file mode 100644 index 00000000..14bcf0df Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/cry_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/empty_pokki.png b/core/ui/src/main/res/drawable-xhdpi/empty_pokki.png new file mode 100644 index 00000000..357179b8 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/empty_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xhdpi/party_popper.png b/core/ui/src/main/res/drawable-xhdpi/party_popper.png new file mode 100644 index 00000000..47ab9426 Binary files /dev/null and b/core/ui/src/main/res/drawable-xhdpi/party_popper.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/big_pokki.png b/core/ui/src/main/res/drawable-xxhdpi/big_pokki.png new file mode 100644 index 00000000..f89d6f9a Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/big_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/cry_pokki.png b/core/ui/src/main/res/drawable-xxhdpi/cry_pokki.png new file mode 100644 index 00000000..c2ec17c9 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/cry_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/empty_pokki.png b/core/ui/src/main/res/drawable-xxhdpi/empty_pokki.png new file mode 100644 index 00000000..752d8ce4 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/empty_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xxhdpi/party_popper.png b/core/ui/src/main/res/drawable-xxhdpi/party_popper.png new file mode 100644 index 00000000..38e75d86 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxhdpi/party_popper.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/big_pokki.png b/core/ui/src/main/res/drawable-xxxhdpi/big_pokki.png new file mode 100644 index 00000000..29377d0d Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/big_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/cry_pokki.png b/core/ui/src/main/res/drawable-xxxhdpi/cry_pokki.png new file mode 100644 index 00000000..830fd495 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/cry_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/empty_pokki.png b/core/ui/src/main/res/drawable-xxxhdpi/empty_pokki.png new file mode 100644 index 00000000..da4c5ba3 Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/empty_pokki.png differ diff --git a/core/ui/src/main/res/drawable-xxxhdpi/party_popper.png b/core/ui/src/main/res/drawable-xxxhdpi/party_popper.png new file mode 100644 index 00000000..be739b2a Binary files /dev/null and b/core/ui/src/main/res/drawable-xxxhdpi/party_popper.png differ diff --git a/core/ui/src/main/res/values/string.xml b/core/ui/src/main/res/values/string.xml index 200ad902..218ebd8d 100644 --- a/core/ui/src/main/res/values/string.xml +++ b/core/ui/src/main/res/values/string.xml @@ -11,4 +11,24 @@ 삭제하기 확인 + + 다시 시도하기 + + 오류가 발생했어요 + 조금 뒤 다시 접속해주세요 + + 저장된 포킷이 없어요! + 포킷을 생성해 링크를 저장해보세요 + + 저장된 링크가 없어요! + 다양한 링크를 한 곳에 저장해보세요 + + 링크가 부족해요! + 링크를 5개 이상 저장하고 추천을 받아보세요 + + 즐겨찾기 링크가 없어요! + 링크를 즐겨찾기로 관리해보세요 + + 검색된 링크가 없어요 + 검색어를 다시 확인해주세요 \ No newline at end of file diff --git a/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt b/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt index 11c92782..f7a45050 100644 --- a/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt +++ b/data/src/main/java/pokitmons/pokit/data/api/AlertApi.kt @@ -2,6 +2,7 @@ package pokitmons.pokit.data.api import pokitmons.pokit.data.model.alert.GetAlertsResponse import pokitmons.pokit.domain.model.link.LinksSort +import retrofit2.Response import retrofit2.http.GET import retrofit2.http.PUT import retrofit2.http.Path @@ -18,5 +19,5 @@ interface AlertApi { @PUT("alert/{alertId}") suspend fun deleteAlert( @Path("alertId") alertId: Int, - ) + ): Response } diff --git a/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt b/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt index 812ee227..090a48dc 100644 --- a/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt +++ b/data/src/main/java/pokitmons/pokit/data/api/LinkApi.kt @@ -6,6 +6,7 @@ import pokitmons.pokit.data.model.link.response.GetLinkResponse import pokitmons.pokit.data.model.link.response.GetLinksResponse import pokitmons.pokit.data.model.link.response.ModifyLinkResponse import pokitmons.pokit.domain.model.link.LinksSort +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PATCH @@ -33,8 +34,8 @@ interface LinkApi { @Query("page") page: Int = 0, @Query("size") size: Int = 10, @Query("sort") sort: List = listOf(LinksSort.RECENT.value), - @Query("isRead") isRead: Boolean = false, - @Query("favorites") favorites: Boolean = false, + @Query("isRead") isRead: Boolean? = null, + @Query("favorites") favorites: Boolean? = null, @Query("startDate") startDate: String? = null, @Query("endDate") endDate: String? = null, @Query("categoryIds") categoryIds: List? = null, @@ -44,7 +45,7 @@ interface LinkApi { @PUT("content/{contentId}") suspend fun deleteLink( @Path("contentId") contentId: Int = 0, - ) + ): Response @POST("content/{contentId}") suspend fun getLink( diff --git a/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt b/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt index c271b0fb..b3540e3a 100644 --- a/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt +++ b/data/src/main/java/pokitmons/pokit/data/api/PokitApi.kt @@ -9,6 +9,7 @@ import pokitmons.pokit.data.model.pokit.response.GetPokitResponse import pokitmons.pokit.data.model.pokit.response.GetPokitsResponse import pokitmons.pokit.data.model.pokit.response.ModifyPokitResponse import pokitmons.pokit.domain.model.pokit.PokitsSort +import retrofit2.Response import retrofit2.http.Body import retrofit2.http.GET import retrofit2.http.PATCH @@ -48,7 +49,7 @@ interface PokitApi { @PUT("category/{categoryId}") suspend fun deletePokit( @Path("categoryId") categoryId: Int, - ) + ): Response @GET("category/count") suspend fun getPokitCount(): GetPokitCountResponse diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt index d9f3cf8f..d5125ef5 100644 --- a/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/alert/RemoteAlertDataSource.kt @@ -12,6 +12,6 @@ class RemoteAlertDataSource @Inject constructor( } override suspend fun deleteAlert(alertId: Int) { - return api.deleteAlert(alertId) + api.deleteAlert(alertId) } } diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt index 412d70f3..fa8ca060 100644 --- a/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/LinkDataSource.kt @@ -24,8 +24,8 @@ interface LinkDataSource { page: Int = 0, size: Int = 10, sort: List = listOf(LinksSort.RECENT.value), - isRead: Boolean = false, - favorites: Boolean = false, + isRead: Boolean? = null, + favorites: Boolean? = null, startDate: String? = null, endDate: String? = null, categoryIds: List? = null, diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt index 4d746ffa..c060f074 100644 --- a/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/link/RemoteLinkDataSource.kt @@ -40,8 +40,8 @@ class RemoteLinkDataSource @Inject constructor( page: Int, size: Int, sort: List, - isRead: Boolean, - favorites: Boolean, + isRead: Boolean?, + favorites: Boolean?, startDate: String?, endDate: String?, categoryIds: List?, @@ -61,7 +61,7 @@ class RemoteLinkDataSource @Inject constructor( } override suspend fun deleteLink(contentId: Int) { - return linkApi.deleteLink(contentId = contentId) + linkApi.deleteLink(contentId = contentId) } override suspend fun getLink(contentId: Int): GetLinkResponse { diff --git a/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt b/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt index 1d1614c8..32c1a829 100644 --- a/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt +++ b/data/src/main/java/pokitmons/pokit/data/datasource/remote/pokit/RemotePokitDataSource.kt @@ -41,7 +41,7 @@ class RemotePokitDataSource @Inject constructor( } override suspend fun deletePokit(pokitId: Int) { - return pokitApi.deletePokit(pokitId) + pokitApi.deletePokit(pokitId) } override suspend fun getPokitCount(): GetPokitCountResponse { diff --git a/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt b/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt index e877a52e..3cd860f3 100644 --- a/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt +++ b/data/src/main/java/pokitmons/pokit/data/repository/link/LinkRepositoryImpl.kt @@ -48,8 +48,8 @@ class LinkRepositoryImpl @Inject constructor( page: Int, size: Int, sort: List, - isRead: Boolean, - favorites: Boolean, + isRead: Boolean?, + favorites: Boolean?, startDate: String?, endDate: String?, categoryIds: List?, diff --git a/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Cosnt.kt b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Cosnt.kt new file mode 100644 index 00000000..55e90547 --- /dev/null +++ b/domain/src/main/java/pokitmons/pokit/domain/model/pokit/Cosnt.kt @@ -0,0 +1,3 @@ +package pokitmons.pokit.domain.model.pokit + +const val MAX_POKIT_COUNT = 30 diff --git a/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt b/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt index 65a3883a..c2336195 100644 --- a/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt +++ b/domain/src/main/java/pokitmons/pokit/domain/repository/link/LinkRepository.kt @@ -22,8 +22,8 @@ interface LinkRepository { page: Int, size: Int, sort: List, - isRead: Boolean, - favorites: Boolean, + isRead: Boolean?, + favorites: Boolean?, startDate: String?, endDate: String?, categoryIds: List?, diff --git a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt index 3e213ebb..8deaa87a 100644 --- a/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt +++ b/domain/src/main/java/pokitmons/pokit/domain/usecase/link/SearchLinksUseCase.kt @@ -12,8 +12,8 @@ class SearchLinksUseCase @Inject constructor( page: Int, size: Int, sort: List, - isRead: Boolean, - favorites: Boolean, + isRead: Boolean?, + favorites: Boolean?, startDate: String?, endDate: String?, categoryIds: List?, diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt index 71dfd3b9..b23cb3a0 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkScreen.kt @@ -1,6 +1,5 @@ package com.strayalpaca.addlink -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.LocalOverscrollConfiguration import androidx.compose.foundation.background @@ -28,7 +27,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp @@ -37,6 +35,7 @@ import com.strayalpaca.addlink.components.block.Toolbar import com.strayalpaca.addlink.model.AddLinkScreenSideEffect import com.strayalpaca.addlink.model.AddLinkScreenState import com.strayalpaca.addlink.model.ScreenStep +import com.strayalpaca.addlink.model.ToastMessageEvent import com.strayalpaca.addlink.paging.SimplePagingState import com.strayalpaca.addlink.utils.BackPressHandler import org.orbitmvi.orbit.compose.collectAsState @@ -49,6 +48,7 @@ import pokitmons.pokit.core.ui.components.atom.inputarea.PokitInputArea import pokitmons.pokit.core.ui.components.block.labeledinput.LabeledInput import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList import pokitmons.pokit.core.ui.components.block.pokitlist.attributes.PokitListState +import pokitmons.pokit.core.ui.components.block.pokittoast.PokitToast import pokitmons.pokit.core.ui.components.block.select.PokitSelect import pokitmons.pokit.core.ui.components.block.switchradio.PokitSwitchRadio import pokitmons.pokit.core.ui.components.block.switchradio.attributes.PokitSwitchRadioStyle @@ -62,7 +62,6 @@ fun AddLinkScreenContainer( onNavigateToAddPokit: () -> Unit, ) { val state by viewModel.collectAsState() - val context = LocalContext.current BackPressHandler(onBackPressed = viewModel::onBackPressed) @@ -76,8 +75,8 @@ fun AddLinkScreenContainer( onBackPressed() } - is AddLinkScreenSideEffect.ToastMessage -> { - Toast.makeText(context, context.getString(sideEffect.toastMessageEvent.stringResourceId), Toast.LENGTH_SHORT).show() + AddLinkScreenSideEffect.OnNavigateToAddPokit -> { + onNavigateToAddPokit() } } } @@ -137,11 +136,12 @@ fun AddLinkScreenContainer( inputUrl = viewModel::inputLinkUrl, inputTitle = viewModel::inputTitle, inputMemo = viewModel::inputMemo, - onClickAddPokit = onNavigateToAddPokit, + onClickAddPokit = viewModel::checkPokitCount, onClickSelectPokit = viewModel::showSelectPokitBottomSheet, toggleRemindRadio = viewModel::setRemind, onBackPressed = viewModel::onBackPressed, - onClickSaveButton = viewModel::saveLink + onClickSaveButton = viewModel::saveLink, + closeToast = viewModel::closeToastMessage ) } @@ -161,6 +161,7 @@ fun AddLinkScreen( toggleRemindRadio: (Boolean) -> Unit, onBackPressed: () -> Unit, onClickSaveButton: () -> Unit, + closeToast: () -> Unit, ) { val scrollState = rememberScrollState() val enable = remember(state.step) { @@ -184,137 +185,154 @@ fun AddLinkScreen( title = if (isModifyLink) stringResource(id = R.string.modify_link) else stringResource(id = R.string.add_link) ) - CompositionLocalProvider( - LocalOverscrollConfiguration provides null + Box( + modifier = Modifier + .fillMaxWidth() + .weight(1f) ) { - Column( - modifier = Modifier - .padding(horizontal = 20.dp) - .weight(1f) - .verticalScroll( - state = scrollState, - flingBehavior = null - ) + CompositionLocalProvider( + LocalOverscrollConfiguration provides null ) { - Spacer(modifier = Modifier.height(16.dp)) - - if (state.link != null) { - Link(state.link) + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp) + .verticalScroll( + state = scrollState, + flingBehavior = null + ) + ) { Spacer(modifier = Modifier.height(16.dp)) - } - LabeledInput( - label = stringResource(id = R.string.link), - sub = "", - maxLength = null, - inputText = url, - hintText = stringResource(id = R.string.placeholder_link), - onChangeText = inputUrl, - enable = enable - ) - - Spacer(modifier = Modifier.height(24.dp)) - - LabeledInput( - label = stringResource(id = R.string.title), - sub = "", - maxLength = 20, - inputText = title, - hintText = stringResource(id = R.string.placeholder_title), - onChangeText = inputTitle, - enable = enable - ) + if (state.link != null) { + Link(state.link) + Spacer(modifier = Modifier.height(16.dp)) + } + + LabeledInput( + label = stringResource(id = R.string.link), + sub = "", + maxLength = null, + inputText = url, + hintText = stringResource(id = R.string.placeholder_link), + onChangeText = inputUrl, + enable = enable + ) - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(24.dp)) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.Bottom - ) { - PokitSelect( - text = if (state.currentPokit == null) stringResource(id = R.string.uncategorized) else state.currentPokit.title, - hintText = stringResource(id = R.string.uncategorized), - label = stringResource(id = R.string.pokit), - modifier = Modifier.weight(1f), - onClick = onClickSelectPokit, + LabeledInput( + label = stringResource(id = R.string.title), + sub = "", + maxLength = 20, + inputText = title, + hintText = stringResource(id = R.string.placeholder_title), + onChangeText = inputTitle, enable = enable ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.height(24.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.Bottom + ) { + PokitSelect( + text = if (state.currentPokit == null) stringResource(id = R.string.uncategorized) else state.currentPokit.title, + hintText = stringResource(id = R.string.uncategorized), + label = stringResource(id = R.string.pokit), + modifier = Modifier.weight(1f), + onClick = onClickSelectPokit, + enable = enable + ) + + Spacer(modifier = Modifier.width(8.dp)) + + PokitButton( + text = null, + icon = PokitButtonIcon( + resourceId = pokitmons.pokit.core.ui.R.drawable.icon_24_plus, + position = PokitButtonIconPosition.LEFT + ), + size = PokitButtonSize.LARGE, + onClick = onClickAddPokit, + enable = enable + ) + } + + Spacer(modifier = Modifier.height(24.dp)) + + Text( + text = stringResource(id = R.string.memo), + style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary) + ) - PokitButton( - text = null, - icon = PokitButtonIcon( - resourceId = pokitmons.pokit.core.ui.R.drawable.icon_24_plus, - position = PokitButtonIconPosition.LEFT - ), - size = PokitButtonSize.LARGE, - onClick = onClickAddPokit, + Spacer(modifier = Modifier.height(8.dp)) + + PokitInputArea( + text = memo, + hintText = stringResource(id = R.string.placeholder_memo), + onChangeText = inputMemo, enable = enable ) - } - Spacer(modifier = Modifier.height(24.dp)) + Spacer(modifier = Modifier.height(4.dp)) - Text( - text = stringResource(id = R.string.memo), - style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary) - ) - - Spacer(modifier = Modifier.height(8.dp)) + Text( + modifier = Modifier.fillMaxWidth(), + text = "${memo.length}/100", + style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary), + textAlign = TextAlign.End + ) - PokitInputArea( - text = memo, - hintText = stringResource(id = R.string.placeholder_memo), - onChangeText = inputMemo, - enable = enable - ) + Spacer(modifier = Modifier.height(24.dp)) - Spacer(modifier = Modifier.height(4.dp)) + Text( + text = stringResource(id = R.string.title_remind), + style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary) + ) - Text( - modifier = Modifier.fillMaxWidth(), - text = "${memo.length}/100", - style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary), - textAlign = TextAlign.End - ) + Spacer(modifier = Modifier.height(12.dp)) - Spacer(modifier = Modifier.height(24.dp)) + PokitSwitchRadio( + modifier = Modifier.fillMaxWidth(), + itemList = listOf( + Pair(stringResource(id = R.string.reject_remind), false), + Pair(stringResource(id = R.string.accept_remind), true) + ), + style = PokitSwitchRadioStyle.STROKE, + selectedItem = if (state.useRemind) { + Pair(stringResource(id = R.string.accept_remind), true) + } else { + Pair(stringResource(id = R.string.reject_remind), false) + }, + onClickItem = { + toggleRemindRadio(it.second) + }, + getTitleFromItem = { it.first }, + enabled = enable + ) - Text( - text = stringResource(id = R.string.title_remind), - style = PokitTheme.typography.body2Medium.copy(color = PokitTheme.colors.textSecondary) - ) + Spacer(modifier = Modifier.height(8.dp)) - Spacer(modifier = Modifier.height(12.dp)) - - PokitSwitchRadio( - modifier = Modifier.fillMaxWidth(), - itemList = listOf( - Pair(stringResource(id = R.string.reject_remind), false), - Pair(stringResource(id = R.string.accept_remind), true) - ), - style = PokitSwitchRadioStyle.STROKE, - selectedItem = if (state.useRemind) { - Pair(stringResource(id = R.string.accept_remind), true) - } else { - Pair(stringResource(id = R.string.reject_remind), false) - }, - onClickItem = { - toggleRemindRadio(it.second) - }, - getTitleFromItem = { it.first }, - enabled = enable - ) + Text( + text = stringResource(id = R.string.sub_remind), + style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary) + ) - Spacer(modifier = Modifier.height(8.dp)) + Spacer(modifier = Modifier.height(32.dp)) + } + } - Text( - text = stringResource(id = R.string.sub_remind), - style = PokitTheme.typography.detail1.copy(color = PokitTheme.colors.textTertiary) + state.toastMessage?.let { toastMessageEvent: ToastMessageEvent -> + PokitToast( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.BottomCenter) + .padding(start = 12.dp, end = 12.dp, bottom = 16.dp), + text = stringResource(id = toastMessageEvent.stringResourceId), + onClickClose = closeToast ) - - Spacer(modifier = Modifier.height(32.dp)) } } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt index 71f40ec8..dd1bbf0b 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/AddLinkViewModel.kt @@ -29,10 +29,12 @@ import org.orbitmvi.orbit.viewmodel.container import pokitmons.pokit.core.feature.navigation.args.LinkArg import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.model.pokit.MAX_POKIT_COUNT import pokitmons.pokit.domain.usecase.link.CreateLinkUseCase import pokitmons.pokit.domain.usecase.link.GetLinkCardUseCase import pokitmons.pokit.domain.usecase.link.GetLinkUseCase import pokitmons.pokit.domain.usecase.link.ModifyLinkUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitCountUseCase import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase import javax.inject.Inject @@ -42,6 +44,7 @@ class AddLinkViewModel @Inject constructor( private val getLinkCardUseCase: GetLinkCardUseCase, private val createLinkUseCase: CreateLinkUseCase, private val modifyLinkUseCase: ModifyLinkUseCase, + private val getPokitCountUseCase: GetPokitCountUseCase, getPokitsUseCase: GetPokitsUseCase, savedStateHandle: SavedStateHandle, ) : ContainerHost, ViewModel() { @@ -193,13 +196,20 @@ class AddLinkViewModel @Inject constructor( title = responseLink.title, thumbnail = responseLink.thumbnail, domain = responseLink.domain, - createdAt = responseLink.createdAt + createdAt = responseLink.createdAt, + pokitId = responseLink.categoryId ) - LinkUpdateEvent.modifySuccess(linkArg) + + val isCreate = (currentLinkId == null) + if (isCreate) { + LinkUpdateEvent.createSuccess(linkArg) + } else { + LinkUpdateEvent.modifySuccess(linkArg) + } + postSideEffect(AddLinkScreenSideEffect.AddLinkSuccess) } else { - reduce { state.copy(step = ScreenStep.IDLE) } - postSideEffect(AddLinkScreenSideEffect.ToastMessage(ToastMessageEvent.NETWORK_ERROR)) + reduce { state.copy(step = ScreenStep.IDLE, toastMessage = ToastMessageEvent.NETWORK_ERROR) } } } } @@ -223,4 +233,23 @@ class AddLinkViewModel @Inject constructor( pokitPaging.refresh() } } + + fun checkPokitCount() = intent { + viewModelScope.launch { + val response = getPokitCountUseCase.getPokitCount() + if (response is PokitResult.Success) { + if (response.result >= MAX_POKIT_COUNT) { + reduce { state.copy(toastMessage = ToastMessageEvent.CANNOT_CREATE_POKIT_MORE) } + } else { + postSideEffect(AddLinkScreenSideEffect.OnNavigateToAddPokit) + } + } else { + reduce { state.copy(toastMessage = ToastMessageEvent.NETWORK_ERROR) } + } + } + } + + fun closeToastMessage() = intent { + reduce { state.copy(toastMessage = null) } + } } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt index d8aff252..f7bab065 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/Preview.kt @@ -29,7 +29,8 @@ fun AddLinkScreenPreview() { onClickSelectPokit = {}, toggleRemindRadio = {}, onBackPressed = {}, - onClickSaveButton = {} + onClickSaveButton = {}, + closeToast = {} ) } } diff --git a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt index d6f7cf77..33b673e5 100644 --- a/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt +++ b/feature/addlink/src/main/java/com/strayalpaca/addlink/model/AddLinkScreenState.kt @@ -7,6 +7,7 @@ data class AddLinkScreenState( val currentPokit: Pokit? = null, val useRemind: Boolean = false, val step: ScreenStep = ScreenStep.IDLE, + val toastMessage: ToastMessageEvent? = null, ) sealed class ScreenStep { @@ -19,12 +20,13 @@ sealed class ScreenStep { data object SAVE_LOADING : ScreenStep() } -sealed class AddLinkScreenSideEffect() { +sealed class AddLinkScreenSideEffect { data object AddLinkSuccess : AddLinkScreenSideEffect() - data class ToastMessage(val toastMessageEvent: ToastMessageEvent) : AddLinkScreenSideEffect() data object OnNavigationBack : AddLinkScreenSideEffect() + data object OnNavigateToAddPokit : AddLinkScreenSideEffect() } enum class ToastMessageEvent(val stringResourceId: Int) { NETWORK_ERROR(R.string.network_error), + CANNOT_CREATE_POKIT_MORE(R.string.toast_cannot_create_pokit), } diff --git a/feature/addlink/src/main/res/values/string.xml b/feature/addlink/src/main/res/values/string.xml index cf43735d..a87848ca 100644 --- a/feature/addlink/src/main/res/values/string.xml +++ b/feature/addlink/src/main/res/values/string.xml @@ -21,4 +21,5 @@ 링크 %d개 네트워크 에러가 발생했습니다. 네트워크 환경을 확인해주세요. + 최대 30개의 포킷을 생성할 수 있습니다.\n포킷을 삭제한 뒤에 추가해주세요. \ No newline at end of file diff --git a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt index 29cc0f33..0410116a 100644 --- a/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt +++ b/feature/addpokit/src/main/java/com/strayalpaca/addpokit/AddPokitViewModel.kt @@ -144,7 +144,17 @@ class AddPokitViewModel @Inject constructor( if (response is PokitResult.Success) { reduce { state.copy(step = AddPokitScreenStep.IDLE) } - PokitUpdateEvent.updatePokit(PokitArg(id = pokitId ?: 0, title = currentPokitName, imageUrl = state.pokitImage?.url ?: "", imageId = pokitImageId)) + val isCreate = (pokitId == null) + if (isCreate) { + PokitUpdateEvent.createPokit( + PokitArg(id = response.result, title = currentPokitName, imageUrl = state.pokitImage?.url ?: "", imageId = pokitImageId) + ) + } else { + PokitUpdateEvent.updatePokit( + PokitArg(id = pokitId ?: 0, title = currentPokitName, imageUrl = state.pokitImage?.url ?: "", imageId = pokitImageId) + ) + } + postSideEffect(AddPokitSideEffect.OnNavigationBack) } else { response as PokitResult.Error diff --git a/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt index 34420e83..bb606c2e 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/HomeScreen.kt @@ -20,6 +20,7 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -29,11 +30,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch +import pokitmons.pokit.core.feature.flow.collectAsEffect import pokitmons.pokit.core.ui.R +import pokitmons.pokit.core.ui.components.block.pokittoast.PokitToast import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.core.ui.utils.noRippleClickable +import pokitmons.pokit.home.model.HomeSideEffect import pokitmons.pokit.home.pokit.PokitScreen import pokitmons.pokit.home.pokit.PokitViewModel import pokitmons.pokit.home.pokit.ScreenType @@ -55,6 +60,16 @@ fun HomeScreen( val scope = rememberCoroutineScope() var showBottomSheet by remember { mutableStateOf(false) } + val toastMessage by viewModel.toastMessage.collectAsState() + + viewModel.sideEffect.collectAsEffect { homeSideEffect: HomeSideEffect -> + when (homeSideEffect) { + HomeSideEffect.NavigateToAddPokit -> { + onNavigateAddPokit() + } + } + } + Box( modifier = Modifier .background(color = Color.White) @@ -117,7 +132,7 @@ fun HomeScreen( scope.launch { sheetState.hide() showBottomSheet = false - onNavigateAddPokit() + viewModel.checkPokitCount() } }, verticalArrangement = Arrangement.Center, @@ -154,19 +169,32 @@ fun HomeScreen( Scaffold( bottomBar = { BottomNavigationBar() } ) { padding -> - when (viewModel.screenType.value) { - is ScreenType.Pokit -> { - PokitScreen( - viewModel = viewModel, - modifier = Modifier.padding(padding), - onNavigateToPokitDetail = onNavigateToPokitDetail - ) + Box { + when (viewModel.screenType.value) { + is ScreenType.Pokit -> { + PokitScreen( + viewModel = viewModel, + modifier = Modifier.padding(padding), + onNavigateToPokitDetail = onNavigateToPokitDetail + ) + } + + is ScreenType.Remind -> { + RemindScreen( + modifier = Modifier.padding(padding), + onNavigateToLinkModify = onNavigateToLinkModify + ) + } } - is ScreenType.Remind -> { - RemindScreen( - modifier = Modifier.padding(padding), - onNavigateToLinkModify = onNavigateToLinkModify + toastMessage?.let { toastMessageEvent -> + PokitToast( + modifier = Modifier + .align(Alignment.BottomCenter) + .padding(padding) + .padding(start = 12.dp, end = 12.dp, bottom = 48.dp), + text = stringResource(id = toastMessageEvent.resourceId), + onClickClose = viewModel::closeToastMessage ) } } diff --git a/feature/home/src/main/java/pokitmons/pokit/home/model/HomeSideEffect.kt b/feature/home/src/main/java/pokitmons/pokit/home/model/HomeSideEffect.kt new file mode 100644 index 00000000..3b0f6344 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/model/HomeSideEffect.kt @@ -0,0 +1,5 @@ +package pokitmons.pokit.home.model + +sealed class HomeSideEffect { + data object NavigateToAddPokit : HomeSideEffect() +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/model/HomeToastMessage.kt b/feature/home/src/main/java/pokitmons/pokit/home/model/HomeToastMessage.kt new file mode 100644 index 00000000..030598f9 --- /dev/null +++ b/feature/home/src/main/java/pokitmons/pokit/home/model/HomeToastMessage.kt @@ -0,0 +1,7 @@ +package pokitmons.pokit.home.model + +import pokitmons.pokit.home.R + +enum class HomeToastMessage(val resourceId: Int) { + CANNOT_CREATE_POKIT_MORE(R.string.toast_cannot_create_pokit), +} diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt index 3377eba5..1668b0e8 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitScreen.kt @@ -12,16 +12,23 @@ import androidx.compose.foundation.lazy.grid.items import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import coil.compose.rememberAsyncImagePainter +import com.strayalpaca.pokitdetail.R +import com.strayalpaca.pokitdetail.model.BottomSheetType +import com.strayalpaca.pokitdetail.paging.SimplePagingState +import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.block.pokitcard.PokitCard import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent +import pokitmons.pokit.core.ui.components.template.pokkiempty.EmptyPokki +import pokitmons.pokit.core.ui.components.template.pokkierror.ErrorPokki +import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent +import pokitmons.pokit.core.ui.R.string as coreString @Composable fun PokitScreen( @@ -30,8 +37,48 @@ fun PokitScreen( onNavigateToPokitDetail: (String) -> Unit, ) { viewModel.loadPokits() - var showBottomSheet by remember { mutableStateOf(false) } val pokits = viewModel.pokits.collectAsState() + val pokitsState by viewModel.pokitsState.collectAsState() + val selectedCategory by viewModel.selectedCategory + val unCategoryLinks = viewModel.unCategoryLinks.collectAsState() + val unCategoryLinksState by viewModel.linksState.collectAsState() + + val pokitOptionBottomSheetType by viewModel.pokitOptionBottomSheetType.collectAsState() + val currentDetailSelectedCategory by viewModel.currentDetailSelectedCategory.collectAsState() + + PokitBottomSheet( + onHideBottomSheet = viewModel::hidePokitDetailRemoveBottomSheet, + show = pokitOptionBottomSheetType != null + ) { + when (pokitOptionBottomSheetType) { + BottomSheetType.MODIFY -> { + ModifyBottomSheetContent( + onClickShare = {}, + onClickModify = remember { + { + viewModel.hidePokitDetailRemoveBottomSheet() + onNavigateToPokitDetail(currentDetailSelectedCategory!!.id) + } + }, + onClickRemove = viewModel::showPokitDetailRemoveBottomSheet + ) + } + BottomSheetType.REMOVE -> { + TwoButtonBottomSheetContent( + title = stringResource(id = R.string.title_remove_pokit), + subText = stringResource(id = R.string.sub_remove_pokit), + onClickLeftButton = viewModel::hidePokitDetailRemoveBottomSheet, + onClickRightButton = remember { + { + viewModel.removeCurrentDetailSelectedCategory() + viewModel.hidePokitDetailRemoveBottomSheet() + } + } + ) + } + else -> {} + } + } Column( modifier = modifier @@ -41,44 +88,75 @@ fun PokitScreen( ) { HomeMid() - when (viewModel.selectedCategory.value) { + when (selectedCategory) { is Category.Pokit -> { - LazyVerticalGrid( - modifier = Modifier.fillMaxSize(), - columns = GridCells.Fixed(2), - contentPadding = PaddingValues(bottom = 100.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - items(pokits.value) { pokitDetail -> - PokitCard( - text = pokitDetail.title, - linkCount = pokitDetail.count, - painter = rememberAsyncImagePainter(model = pokitDetail.image.url), - onClick = { onNavigateToPokitDetail(pokitDetail.id) }, - onClickKebab = { - showBottomSheet = true - } + when { + (pokitsState == SimplePagingState.LOADING_INIT) -> { + LoadingProgress(modifier = Modifier.fillMaxSize()) + } + (pokitsState == SimplePagingState.FAILURE_INIT) -> { + ErrorPokki( + modifier = Modifier.fillMaxSize(), + title = stringResource(id = coreString.title_error), + sub = stringResource(id = coreString.sub_error), + onClickRetry = viewModel::loadPokits + ) + } + (pokits.value.isEmpty()) -> { + EmptyPokki( + modifier = Modifier.fillMaxSize(), + title = stringResource(id = coreString.title_empty_pokits), + sub = stringResource(id = coreString.sub_empty_pokits) ) } + else -> { + LazyVerticalGrid( + modifier = Modifier.fillMaxSize(), + columns = GridCells.Fixed(2), + contentPadding = PaddingValues(bottom = 100.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(pokits.value) { pokitDetail -> + PokitCard( + text = pokitDetail.title, + linkCount = pokitDetail.count, + painter = rememberAsyncImagePainter(model = pokitDetail.image.url), + onClick = { onNavigateToPokitDetail(pokitDetail.id) }, + onClickKebab = { + viewModel.showPokitDetailOptionBottomSheet(pokitDetail) + } + ) + } + } + } } } is Category.Unclassified -> { - UnclassifiedScreen() - } - } - - if (showBottomSheet) { - PokitBottomSheet( - onHideBottomSheet = { showBottomSheet = false }, - show = showBottomSheet - ) { - ModifyBottomSheetContent( - onClickShare = { }, - onClickRemove = { }, - onClickModify = { } - ) + when { + (unCategoryLinksState == SimplePagingState.LOADING_INIT) -> { + LoadingProgress(modifier = Modifier.fillMaxSize()) + } + (unCategoryLinksState == SimplePagingState.FAILURE_INIT) -> { + ErrorPokki( + modifier = Modifier.fillMaxSize(), + title = stringResource(id = coreString.title_error), + sub = stringResource(id = coreString.sub_error), + onClickRetry = viewModel::loadUnCategoryLinks + ) + } + (unCategoryLinks.value.isEmpty()) -> { + EmptyPokki( + modifier = Modifier.fillMaxSize(), + title = stringResource(id = coreString.title_empty_links), + sub = stringResource(id = coreString.sub_empty_links) + ) + } + else -> { + UnclassifiedScreen() + } + } } } } diff --git a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt index 83a043e6..e20b629a 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/pokit/PokitViewModel.kt @@ -3,6 +3,7 @@ package pokitmons.pokit.home.pokit import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.strayalpaca.pokitdetail.model.BottomSheetType import com.strayalpaca.pokitdetail.model.Pokit import com.strayalpaca.pokitdetail.paging.LinkPaging import com.strayalpaca.pokitdetail.paging.PokitPaging @@ -13,13 +14,20 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import pokitmons.pokit.core.feature.flow.MutableEventFlow +import pokitmons.pokit.core.feature.flow.asEventFlow import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent import pokitmons.pokit.domain.commom.PokitResult import pokitmons.pokit.domain.model.link.Link import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.model.pokit.MAX_POKIT_COUNT import pokitmons.pokit.domain.usecase.link.GetLinksUseCase +import pokitmons.pokit.domain.usecase.pokit.DeletePokitUseCase +import pokitmons.pokit.domain.usecase.pokit.GetPokitCountUseCase import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import pokitmons.pokit.home.model.HomeSideEffect +import pokitmons.pokit.home.model.HomeToastMessage import javax.inject.Inject import com.strayalpaca.pokitdetail.model.Link as DetailLink import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit @@ -28,13 +36,15 @@ import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit class PokitViewModel @Inject constructor( private val getPokitsUseCase: GetPokitsUseCase, private val getLinksUseCase: GetLinksUseCase, + private val deletePokitUseCase: DeletePokitUseCase, + private val getPokitCountUseCase: GetPokitCountUseCase, ) : ViewModel() { - init { - initLinkUpdateEventDetector() - initPokitUpdateEventDetector() - initPokitRemoveEventDetector() - } + private val _sideEffect = MutableEventFlow() + val sideEffect = _sideEffect.asEventFlow() + + private val _toastMessage = MutableStateFlow(null) + val toastMessage = _toastMessage.asStateFlow() private fun initLinkUpdateEventDetector() { viewModelScope.launch { @@ -51,6 +61,16 @@ class PokitViewModel @Inject constructor( } } + private fun initLinkAddEventDetector() { + viewModelScope.launch { + LinkUpdateEvent.addedLink.collectLatest { addedLink -> + val linkAddedPokit = pokitPaging.pagingData.value.find { it.id == addedLink.pokitId.toString() } ?: return@collectLatest + val modifiedPokit = linkAddedPokit.copy(count = (linkAddedPokit.count + 1)) + pokitPaging.modifyItem(modifiedPokit) + } + } + } + private fun initPokitUpdateEventDetector() { viewModelScope.launch { PokitUpdateEvent.updatedPokit.collectLatest { updatedPokit -> @@ -71,6 +91,14 @@ class PokitViewModel @Inject constructor( } } + private fun initPokitAddEventDetector() { + viewModelScope.launch { + PokitUpdateEvent.addedPokit.collectLatest { + pokitPaging.refresh() + } + } + } + var selectedCategory = mutableStateOf(Category.Pokit) private set @@ -99,10 +127,28 @@ class PokitViewModel @Inject constructor( val pokits: StateFlow> get() = _pokits.asStateFlow() + val pokitsState = pokitPaging.pagingState + private var _unCategoryLinks: MutableStateFlow> = linkPaging._pagingData val unCategoryLinks: StateFlow> get() = _unCategoryLinks.asStateFlow() + val linksState = linkPaging.pagingState + + private val _currentDetailSelectedCategory = MutableStateFlow(null) + val currentDetailSelectedCategory = _currentDetailSelectedCategory.asStateFlow() + + private val _pokitOptionBottomSheetType = MutableStateFlow(null) + val pokitOptionBottomSheetType = _pokitOptionBottomSheetType.asStateFlow() + + init { + initLinkUpdateEventDetector() + initPokitUpdateEventDetector() + initPokitRemoveEventDetector() + initLinkAddEventDetector() + initPokitAddEventDetector() + } + fun updateCategory(category: Category) { selectedCategory.value = category } @@ -154,6 +200,51 @@ class PokitViewModel @Inject constructor( linkPaging.load() } } + + fun showPokitDetailOptionBottomSheet(pokit: Pokit) { + _currentDetailSelectedCategory.update { pokit } + _pokitOptionBottomSheetType.update { BottomSheetType.MODIFY } + } + + fun showPokitDetailRemoveBottomSheet() { + _pokitOptionBottomSheetType.update { + BottomSheetType.REMOVE + } + } + + fun hidePokitDetailRemoveBottomSheet() { + _currentDetailSelectedCategory.update { null } + _pokitOptionBottomSheetType.update { null } + } + + fun removeCurrentDetailSelectedCategory() { + viewModelScope.launch { + val currentDetailSelectedPokit = currentDetailSelectedCategory.value ?: return@launch + val pokitId = currentDetailSelectedPokit.id.toInt() + val response = deletePokitUseCase.deletePokit(pokitId) + if (response is PokitResult.Success) { + PokitUpdateEvent.removePokit(pokitId) + } + } + } + + fun checkPokitCount() { + viewModelScope.launch { + _toastMessage.update { null } + val response = getPokitCountUseCase.getPokitCount() + if (response is PokitResult.Success) { + if (response.result >= MAX_POKIT_COUNT) { + _toastMessage.update { HomeToastMessage.CANNOT_CREATE_POKIT_MORE } + } else { + _sideEffect.emit(HomeSideEffect.NavigateToAddPokit) + } + } + } + } + + fun closeToastMessage() { + _toastMessage.update { null } + } } sealed class Category { diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt index 73ecfb11..d4e1cea8 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindScreen.kt @@ -6,12 +6,14 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @@ -23,10 +25,15 @@ import com.strayalpaca.pokitdetail.R import com.strayalpaca.pokitdetail.components.template.linkdetailbottomsheet.LinkDetailBottomSheet import com.strayalpaca.pokitdetail.model.BottomSheetType import com.strayalpaca.pokitdetail.model.Link +import pokitmons.pokit.core.feature.model.NetworkState +import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent +import pokitmons.pokit.core.ui.components.template.pokkiempty.EmptyPokki +import pokitmons.pokit.core.ui.components.template.pokkierror.ErrorPokki import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent +import pokitmons.pokit.core.ui.R.string as coreString @Composable fun RemindScreen( @@ -35,14 +42,28 @@ fun RemindScreen( onNavigateToLinkModify: (String) -> Unit, ) { val unreadContents = viewModel.unReadContents.collectAsState() + val unreadContentsState by viewModel.unreadContentNetworkState.collectAsState() + val todayContents = viewModel.todayContents.collectAsState() + val todayContentsState by viewModel.todayContentsNetworkState.collectAsState() + val bookmarkContents = viewModel.bookmarkContents.collectAsState() + val bookmarkContentState by viewModel.bookmarkContentsNetworkState.collectAsState() val currentDetailShowLink by viewModel.currentShowingLink.collectAsState() val pokitOptionBottomSheetType by viewModel.pokitOptionBottomSheetType.collectAsState() val currentSelectedLink by viewModel.currentSelectedLink.collectAsState() + val showTotalEmpty by remember { + derivedStateOf { + todayContentsState == NetworkState.IDLE && + todayContents.value.size < 5 && + unreadContentsState == NetworkState.IDLE && + unreadContents.value.isEmpty() + } + } + PokitBottomSheet( onHideBottomSheet = viewModel::hideLinkOptionBottomSheet, show = pokitOptionBottomSheetType != null @@ -62,8 +83,8 @@ fun RemindScreen( } BottomSheetType.REMOVE -> { TwoButtonBottomSheetContent( - title = stringResource(id = R.string.title_remove_pokit), - subText = stringResource(id = R.string.sub_remove_pokit), + title = stringResource(id = R.string.title_remove_link), + subText = stringResource(id = R.string.sub_remove_link), onClickLeftButton = viewModel::hideLinkOptionBottomSheet, onClickRightButton = remember { { @@ -83,89 +104,168 @@ fun RemindScreen( onHideBottomSheet = viewModel::hideDetailLinkBottomSheet ) - Column( - modifier = modifier - .padding(20.dp) - .fillMaxHeight() - .verticalScroll(rememberScrollState()) - ) { - Spacer(modifier = Modifier.height(4.dp)) - - RemindSection(title = "오늘 이 링크는 어때요?") { - Spacer(modifier = Modifier.height(12.dp)) - Row( - modifier = Modifier.horizontalScroll(rememberScrollState()), - horizontalArrangement = Arrangement.spacedBy(12.dp) - ) { - todayContents.value.forEach { todayContent -> - ToadyLinkCard( - title = todayContent.title, - sub = todayContent.createdAt, - painter = rememberAsyncImagePainter(todayContent.thumbNail), - badgeText = todayContent.data, - domain = todayContent.domain, - onClick = { - viewModel.showDetailLinkBottomSheet(remindResult = todayContent) - }, - onClickKebab = { - viewModel.showLinkOptionBottomSheet(remindResult = todayContent) + if (showTotalEmpty) { + ErrorPokki( + modifier = Modifier + .fillMaxWidth() + .fillMaxHeight() + .padding(20.dp), + title = stringResource(id = coreString.title_lack_of_links), + sub = stringResource(id = coreString.sub_lack_of_links) + ) + } else { + Column( + modifier = modifier + .padding(20.dp) + .fillMaxHeight() + .verticalScroll(rememberScrollState()) + ) { + Spacer(modifier = Modifier.height(4.dp)) + + RemindSection(title = "오늘 이 링크는 어때요?") { + Spacer(modifier = Modifier.height(12.dp)) + + when (todayContentsState) { + NetworkState.IDLE -> { + if (todayContents.value.isEmpty()) { + ErrorPokki( + modifier = Modifier + .fillMaxWidth() + .height(208.dp), + pokkiSize = 140.dp, + title = stringResource(id = coreString.title_lack_of_links), + sub = stringResource(id = coreString.sub_lack_of_links) + ) + } else { + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()), + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + todayContents.value.forEach { todayContent -> + ToadyLinkCard( + title = todayContent.title, + sub = todayContent.createdAt, + painter = rememberAsyncImagePainter(todayContent.thumbNail), + badgeText = todayContent.data, + domain = todayContent.domain, + onClick = { + viewModel.showDetailLinkBottomSheet(remindResult = todayContent) + }, + onClickKebab = { + viewModel.showLinkOptionBottomSheet(remindResult = todayContent) + } + ) + } + } } - ) + } + NetworkState.LOADING -> { + LoadingProgress( + modifier = Modifier + .fillMaxWidth() + .height(208.dp) + ) + } + NetworkState.ERROR -> { + ErrorPokki( + modifier = Modifier + .fillMaxWidth() + .height(208.dp), + pokkiSize = 140.dp, + title = stringResource(id = coreString.title_error), + sub = stringResource(id = coreString.sub_error) + ) + } } } - } - Spacer(modifier = Modifier.height(32.dp)) - - RemindSection(title = "한번도 읽지 않았어요") { - Spacer(modifier = Modifier.height(16.dp)) - Column( - modifier = Modifier, - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - unreadContents.value.forEach { unReadContent -> - LinkCard( - item = unReadContent.title, - title = unReadContent.title, - sub = "${unReadContent.createdAt} • ${unReadContent.domain}", - painter = rememberAsyncImagePainter(unReadContent.thumbNail), - notRead = unReadContent.isRead, - badgeText = unReadContent.data, - onClickKebab = { - viewModel.showLinkOptionBottomSheet(remindResult = unReadContent) - }, - onClickItem = { - viewModel.showDetailLinkBottomSheet(remindResult = unReadContent) + Spacer(modifier = Modifier.height(32.dp)) + + if ((unreadContentsState == NetworkState.IDLE && unreadContents.value.isNotEmpty())) { + RemindSection(title = "한번도 읽지 않았어요") { + Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + unreadContents.value.forEach { unReadContent -> + LinkCard( + item = unReadContent.title, + title = unReadContent.title, + sub = "${unReadContent.createdAt} • ${unReadContent.domain}", + painter = rememberAsyncImagePainter(unReadContent.thumbNail), + notRead = !unReadContent.isRead, + badgeText = null, + onClickKebab = { + viewModel.showLinkOptionBottomSheet(remindResult = unReadContent) + }, + onClickItem = { + viewModel.showDetailLinkBottomSheet(remindResult = unReadContent) + } + ) } - ) + } } + Spacer(modifier = Modifier.height(32.dp)) } - } - Spacer(modifier = Modifier.height(32.dp)) - - RemindSection(title = "즐겨찾기 링크만 모았어요") { - Spacer(modifier = Modifier.height(12.dp)) - Column( - modifier = Modifier, - verticalArrangement = Arrangement.spacedBy(20.dp) - ) { - bookmarkContents.value.forEach { favoriteContent -> - LinkCard( - item = favoriteContent.title, - title = favoriteContent.title, - sub = "${favoriteContent.createdAt} • ${favoriteContent.domain}", - painter = rememberAsyncImagePainter(favoriteContent.thumbNail), - notRead = favoriteContent.isRead, - badgeText = favoriteContent.data, - onClickKebab = { - viewModel.showLinkOptionBottomSheet(remindResult = favoriteContent) - }, - onClickItem = { - viewModel.showDetailLinkBottomSheet(remindResult = favoriteContent) + RemindSection(title = "즐겨찾기 링크만 모았어요") { + Spacer(modifier = Modifier.height(12.dp)) + + when (bookmarkContentState) { + NetworkState.IDLE -> { + if (bookmarkContents.value.isEmpty()) { + EmptyPokki( + modifier = Modifier + .fillMaxWidth() + .height(252.dp), + title = stringResource(id = coreString.title_empty_favorite), + sub = stringResource(id = coreString.sub_empty_favorite) + ) + } else { + Column( + modifier = Modifier, + verticalArrangement = Arrangement.spacedBy(20.dp) + ) { + bookmarkContents.value.forEach { favoriteContent -> + LinkCard( + item = favoriteContent.title, + title = favoriteContent.title, + sub = "${favoriteContent.createdAt} • ${favoriteContent.domain}", + painter = rememberAsyncImagePainter(favoriteContent.thumbNail), + notRead = favoriteContent.isRead, + badgeText = favoriteContent.data, + onClickKebab = { + viewModel.showLinkOptionBottomSheet(remindResult = favoriteContent) + }, + onClickItem = { + viewModel.showDetailLinkBottomSheet(remindResult = favoriteContent) + } + ) + } + } } - ) + } + NetworkState.LOADING -> { + LoadingProgress( + modifier = Modifier + .fillMaxWidth() + .height(252.dp) + ) + } + NetworkState.ERROR -> { + ErrorPokki( + modifier = Modifier + .fillMaxWidth() + .height(252.dp), + pokkiSize = 140.dp, + title = stringResource(id = coreString.title_empty_favorite), + sub = stringResource(id = coreString.sub_empty_favorite) + ) + } } + + Spacer(modifier = Modifier.height(32.dp)) } } } diff --git a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt index a8bf75aa..12d2388f 100644 --- a/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt +++ b/feature/home/src/main/java/pokitmons/pokit/home/remind/RemindViewModel.kt @@ -11,7 +11,9 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import pokitmons.pokit.core.feature.model.NetworkState import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent +import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent import pokitmons.pokit.domain.commom.PokitResult import pokitmons.pokit.domain.model.home.remind.RemindResult import pokitmons.pokit.domain.usecase.home.remind.BookMarkContentsUseCase @@ -28,24 +30,27 @@ class RemindViewModel @Inject constructor( private val deleteLinkUseCase: DeleteLinkUseCase, ) : ViewModel() { - init { - loadContents() - initLinkUpdateEventDetector() - initLinkRemoveEventDetector() - } - private var _unReadContents: MutableStateFlow> = MutableStateFlow(emptyList()) val unReadContents: StateFlow> get() = _unReadContents.asStateFlow() + private val _unReadContentNetworkState: MutableStateFlow = MutableStateFlow(NetworkState.IDLE) + val unreadContentNetworkState = _unReadContentNetworkState.asStateFlow() + private var _todayContents: MutableStateFlow> = MutableStateFlow(emptyList()) val todayContents: StateFlow> get() = _todayContents.asStateFlow() + private val _todayContentsNetworkState: MutableStateFlow = MutableStateFlow(NetworkState.IDLE) + val todayContentsNetworkState = _todayContentsNetworkState.asStateFlow() + private var _bookmarkContents: MutableStateFlow> = MutableStateFlow(emptyList()) val bookmarkContents: StateFlow> get() = _bookmarkContents.asStateFlow() + private val _bookmarkContentsNetworkState: MutableStateFlow = MutableStateFlow(NetworkState.IDLE) + val bookmarkContentsNetworkState = _bookmarkContentsNetworkState.asStateFlow() + private val _currentSelectedLink = MutableStateFlow(null) val currentSelectedLink = _currentSelectedLink.asStateFlow() @@ -55,6 +60,14 @@ class RemindViewModel @Inject constructor( private val _currentShowingLink = MutableStateFlow(null) val currentShowingLink = _currentShowingLink.asStateFlow() + init { + initLinkUpdateEventDetector() + initLinkRemoveEventDetector() + initLinkAddEventDetector() + initPokitRemoveEventDetector() + loadContents() + } + private fun initLinkUpdateEventDetector() { viewModelScope.launch { LinkUpdateEvent.updatedLink.collectLatest { updatedLink -> @@ -136,24 +149,69 @@ class RemindViewModel @Inject constructor( } } + private fun initLinkAddEventDetector() { + viewModelScope.launch { + LinkUpdateEvent.addedLink.collectLatest { + loadContents() + } + } + } + + private fun initPokitRemoveEventDetector() { + viewModelScope.launch { + PokitUpdateEvent.removedPokitId.collectLatest { + loadContents() + } + } + } + fun loadContents() { + loadUnReadContents() + loadTodayContents() + loadMarkContents() + } + + private fun loadUnReadContents() { viewModelScope.launch { + _unReadContentNetworkState.update { NetworkState.LOADING } when (val response = unReadContentsUseCase.getUnreadContents()) { - is PokitResult.Success -> _unReadContents.value = response.result.take(3) - is PokitResult.Error -> {} + is PokitResult.Success -> { + _unReadContentNetworkState.update { NetworkState.IDLE } + _unReadContents.value = response.result.take(3) + } + is PokitResult.Error -> { + _unReadContentNetworkState.update { NetworkState.ERROR } + } } + } + } + private fun loadTodayContents() { + viewModelScope.launch { + _todayContentsNetworkState.update { NetworkState.LOADING } when (val response = todayContentsUseCase.getTodayContents()) { is PokitResult.Success -> { - _todayContents.value = response.result + _todayContentsNetworkState.update { NetworkState.IDLE } + _todayContents.value = response.result.take(3) } is PokitResult.Error -> { + _todayContentsNetworkState.update { NetworkState.ERROR } } } + } + } + private fun loadMarkContents() { + viewModelScope.launch { + _bookmarkContentsNetworkState.update { NetworkState.LOADING } when (val response = bookMarkContentsUseCase.getBookmarkContents()) { - is PokitResult.Success -> _bookmarkContents.value = response.result.take(3) - is PokitResult.Error -> {} + is PokitResult.Success -> { + _bookmarkContentsNetworkState.update { NetworkState.IDLE } + _bookmarkContents.value = response.result.take(3) + } + is PokitResult.Error -> { + _bookmarkContentsNetworkState.update { NetworkState.ERROR } + } } } } diff --git a/feature/home/src/main/res/values/strings.xml b/feature/home/src/main/res/values/strings.xml index 19bf5d4e..b98ddae5 100644 --- a/feature/home/src/main/res/values/strings.xml +++ b/feature/home/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ home + + 최대 30개의 포킷을 생성할 수 있습니다.\n포킷을 삭제한 뒤에 추가해주세요. \ No newline at end of file diff --git a/feature/login/src/main/java/pokitmons/pokit/success/Preview.kt b/feature/login/src/main/java/pokitmons/pokit/success/Preview.kt new file mode 100644 index 00000000..9f4f29b2 --- /dev/null +++ b/feature/login/src/main/java/pokitmons/pokit/success/Preview.kt @@ -0,0 +1,21 @@ +package pokitmons.pokit.success + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import pokitmons.pokit.core.ui.theme.PokitTheme + +@Preview(showBackground = true) +@Composable +private fun Preview() { + PokitTheme { + Column( + modifier = Modifier.fillMaxSize() + ) { + SignUpSuccessScreen { + } + } + } +} diff --git a/feature/login/src/main/java/pokitmons/pokit/success/SignUpSuccessScreen.kt b/feature/login/src/main/java/pokitmons/pokit/success/SignUpSuccessScreen.kt index 812e4ecd..8fbd5d79 100644 --- a/feature/login/src/main/java/pokitmons/pokit/success/SignUpSuccessScreen.kt +++ b/feature/login/src/main/java/pokitmons/pokit/success/SignUpSuccessScreen.kt @@ -3,10 +3,8 @@ package pokitmons.pokit.success import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.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 @@ -14,7 +12,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -26,25 +23,27 @@ import pokitmons.pokit.core.ui.components.atom.button.PokitButton import pokitmons.pokit.core.ui.components.atom.button.attributes.PokitButtonSize import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.login.R +import pokitmons.pokit.core.ui.R.drawable as coreDrawable @Composable fun SignUpSuccessScreen( onNavigateToMainScreen: () -> Unit, ) { - Box( + Column( modifier = Modifier .background(color = Color.White) .padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 28.dp) ) { Icon( modifier = Modifier.padding(start = 4.dp), - painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_arrow_left), + painter = painterResource(id = coreDrawable.icon_24_arrow_left), contentDescription = null ) Column( modifier = Modifier - .fillMaxSize(), + .fillMaxWidth() + .weight(1f), verticalArrangement = Arrangement.Top, horizontalAlignment = Alignment.CenterHorizontally ) { @@ -52,7 +51,7 @@ fun SignUpSuccessScreen( Image( modifier = Modifier.size(90.dp), - painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.sign_up_icon), + painter = painterResource(id = coreDrawable.party_popper), contentDescription = null ) @@ -71,12 +70,21 @@ fun SignUpSuccessScreen( style = PokitTheme.typography.body1Bold, text = stringResource(id = R.string.manage_links) ) + + Spacer(modifier = Modifier.weight(1f)) + + Image( + modifier = Modifier.height(308.dp), + painter = painterResource(id = coreDrawable.big_pokki), + contentDescription = "big_pokki" + ) + + Spacer(modifier = Modifier.height(16.dp)) } PokitButton( modifier = Modifier - .fillMaxWidth() - .align(Alignment.BottomCenter), + .fillMaxWidth(), text = stringResource(id = R.string.start), icon = null, size = PokitButtonSize.LARGE, diff --git a/feature/pokitdetail/build.gradle.kts b/feature/pokitdetail/build.gradle.kts index b350afe6..965a79f8 100644 --- a/feature/pokitdetail/build.gradle.kts +++ b/feature/pokitdetail/build.gradle.kts @@ -78,6 +78,9 @@ dependencies { implementation(libs.hilt) kapt(libs.hilt.compiler) + // coil + implementation(libs.coil.compose) + implementation(project(":core:ui")) implementation(project(":core:feature")) implementation(project(":domain")) diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt index d8788948..ff69eeea 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailScreen.kt @@ -17,9 +17,9 @@ import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter import com.strayalpaca.pokitdetail.components.block.TitleArea import com.strayalpaca.pokitdetail.components.block.Toolbar import com.strayalpaca.pokitdetail.components.template.filterselectbottomsheet.FilterSelectBottomSheet @@ -30,14 +30,18 @@ import com.strayalpaca.pokitdetail.model.Link import com.strayalpaca.pokitdetail.model.Pokit import com.strayalpaca.pokitdetail.model.PokitDetailScreenState import com.strayalpaca.pokitdetail.paging.SimplePagingState +import pokitmons.pokit.core.feature.flow.collectAsEffect +import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.components.block.pokitlist.PokitList import pokitmons.pokit.core.ui.components.block.pokitlist.attributes.PokitListState import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent +import pokitmons.pokit.core.ui.components.template.pokkiempty.EmptyPokki +import pokitmons.pokit.core.ui.components.template.pokkierror.ErrorPokki import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent import pokitmons.pokit.core.ui.theme.PokitTheme -import pokitmons.pokit.core.ui.R.drawable as coreDrawable +import pokitmons.pokit.core.ui.R.string as coreString @Composable fun PokitDetailScreenContainer( @@ -52,6 +56,10 @@ fun PokitDetailScreenContainer( val pokitList by viewModel.pokitList.collectAsState() val pokitListState by viewModel.pokitListState.collectAsState() + viewModel.moveToBackEvent.collectAsEffect { + onBackPressed() + } + PokitDetailScreen( onBackPressed = onBackPressed, onClickFilter = viewModel::showFilterChangeBottomSheet, @@ -76,6 +84,7 @@ fun PokitDetailScreenContainer( onClickPokitModify = onNavigateToPokitModify, onClickPokitRemove = viewModel::deletePokit, onClickLinkModify = onNavigateToLinkModify, + onClickLinkRemove = viewModel::deleteLink, loadNextPokits = viewModel::loadNextPokits, refreshPokits = viewModel::refreshPokits, loadNextLinks = viewModel::loadNextLinks @@ -107,6 +116,7 @@ fun PokitDetailScreen( onClickPokitModify: (String) -> Unit = {}, onClickPokitRemove: () -> Unit = {}, onClickLinkModify: (String) -> Unit = {}, + onClickLinkRemove: () -> Unit = {}, loadNextPokits: () -> Unit = {}, refreshPokits: () -> Unit = {}, loadNextLinks: () -> Unit = {}, @@ -143,31 +153,63 @@ fun PokitDetailScreen( } } - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - state = linkLazyColumnListState - ) { - items(linkList) { link -> - LinkCard( - item = link, - title = link.title, - sub = "${link.dateString} · ${link.domainUrl}", - painter = painterResource(id = coreDrawable.icon_24_google), - notRead = link.isRead, - badgeText = stringResource(id = link.linkType.textResourceId), - onClickKebab = showLinkModifyBottomSheet, - onClickItem = onClickLink, - modifier = Modifier.padding(20.dp) + when { + (linkListState == SimplePagingState.LOADING_INIT) -> { + LoadingProgress( + modifier = Modifier + .fillMaxWidth() + .weight(1f) ) - - HorizontalDivider( - modifier = Modifier.padding(horizontal = 20.dp), - thickness = 1.dp, - color = PokitTheme.colors.borderTertiary + } + (linkListState == SimplePagingState.FAILURE_INIT) -> { + ErrorPokki( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + title = stringResource(id = coreString.title_error), + sub = stringResource(id = coreString.sub_error) + ) + } + (linkList.isEmpty()) -> { + EmptyPokki( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + title = stringResource(id = coreString.title_empty_links), + sub = stringResource(id = coreString.sub_empty_links) ) } + else -> { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + state = linkLazyColumnListState + ) { + items( + items = linkList, + key = { it.id } + ) { link -> + LinkCard( + item = link, + title = link.title, + sub = "${link.dateString} · ${link.domainUrl}", + painter = rememberAsyncImagePainter(link.imageUrl), + notRead = link.isRead, + badgeText = stringResource(id = link.linkType.textResourceId), + onClickKebab = showLinkModifyBottomSheet, + onClickItem = onClickLink, + modifier = Modifier.padding(20.dp) + ) + + HorizontalDivider( + modifier = Modifier.padding(horizontal = 20.dp), + thickness = 1.dp, + color = PokitTheme.colors.borderTertiary + ) + } + } + } } LinkDetailBottomSheet( @@ -248,7 +290,10 @@ fun PokitDetailScreen( title = stringResource(id = R.string.title_remove_link), subText = stringResource(id = R.string.sub_remove_link), onClickLeftButton = hideLinkModifyBottomSheet, - onClickRightButton = {} + onClickRightButton = { + onClickLinkRemove() + hideLinkModifyBottomSheet() + } ) } @@ -281,8 +326,8 @@ fun PokitDetailScreen( onClickLeftButton = hidePokitModifyBottomSheet, onClickRightButton = remember { { - hidePokitModifyBottomSheet() onClickPokitRemove() + hidePokitModifyBottomSheet() } } ) diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt index aedb0445..682a4789 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/PokitDetailViewModel.kt @@ -18,10 +18,13 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import pokitmons.pokit.core.feature.flow.MutableEventFlow +import pokitmons.pokit.core.feature.flow.asEventFlow import pokitmons.pokit.core.feature.navigation.args.LinkUpdateEvent import pokitmons.pokit.core.feature.navigation.args.PokitUpdateEvent import pokitmons.pokit.domain.commom.PokitResult import pokitmons.pokit.domain.model.link.LinksSort +import pokitmons.pokit.domain.usecase.link.DeleteLinkUseCase import pokitmons.pokit.domain.usecase.link.GetLinksUseCase import pokitmons.pokit.domain.usecase.pokit.DeletePokitUseCase import pokitmons.pokit.domain.usecase.pokit.GetPokitUseCase @@ -35,6 +38,7 @@ class PokitDetailViewModel @Inject constructor( private val getLinksUseCase: GetLinksUseCase, private val getPokitUseCase: GetPokitUseCase, private val deletePokitUseCase: DeletePokitUseCase, + private val deleteLinkUseCase: DeleteLinkUseCase, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val pokitPaging = PokitPaging( @@ -61,6 +65,9 @@ class PokitDetailViewModel @Inject constructor( val linkList: StateFlow> = linkPaging.pagingData val linkListState: StateFlow = linkPaging.pagingState + private val _moveToBackEvent = MutableEventFlow() + val moveToBackEvent = _moveToBackEvent.asEventFlow() + init { savedStateHandle.get("pokit_id")?.toIntOrNull()?.let { pokitId -> linkPaging.changeOptions(categoryId = pokitId, sort = LinksSort.RECENT) @@ -218,7 +225,18 @@ class PokitDetailViewModel @Inject constructor( val response = deletePokitUseCase.deletePokit(pokitId) if (response is PokitResult.Success) { PokitUpdateEvent.removePokit(pokitId) - // 뒤로가기? + _moveToBackEvent.emit(true) + } + } + } + + fun deleteLink() { + val currentLink = state.value.currentLink ?: return + val linkId = currentLink.id.toInt() + viewModelScope.launch { + val response = deleteLinkUseCase.deleteLink(linkId) + if (response is PokitResult.Success) { + _moveToBackEvent.emit(true) } } } diff --git a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/block/Link.kt b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/block/Link.kt index a04f8883..fe948454 100644 --- a/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/block/Link.kt +++ b/feature/pokitdetail/src/main/java/com/strayalpaca/pokitdetail/components/block/Link.kt @@ -16,8 +16,8 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter import com.strayalpaca.pokitdetail.model.Link import pokitmons.pokit.core.ui.theme.PokitTheme @@ -38,7 +38,9 @@ internal fun Link( ) ) { Image( - painter = painterResource(id = pokitmons.pokit.core.ui.R.drawable.icon_24_google), + painter = rememberAsyncImagePainter( + model = link.imageUrl + ), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.width(124.dp) diff --git a/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt b/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt new file mode 100644 index 00000000..29a7c2cf --- /dev/null +++ b/feature/pokitdetail/src/test/java/com/strayalpaca/pokitdetail/PokitPagingTest.kt @@ -0,0 +1,72 @@ +package com.strayalpaca.pokitdetail + +import com.strayalpaca.pokitdetail.paging.PokitPaging +import com.strayalpaca.pokitdetail.paging.SimplePagingState +import io.kotest.common.ExperimentalKotest +import io.kotest.core.spec.style.DescribeSpec +import io.kotest.core.test.testCoroutineScheduler +import io.kotest.matchers.shouldBe +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.first +import pokitmons.pokit.domain.commom.PokitResult +import pokitmons.pokit.domain.usecase.pokit.GetPokitsUseCase +import pokitmons.pokit.domain.model.pokit.Pokit as DomainPokit + +const val PER_PAGE_SAMPLE = 3 +const val FIRST_REQUEST_PAGE_SAMPLE = 3 + +@OptIn(ExperimentalKotest::class, ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class) +class PokitPagingTest : DescribeSpec({ + val sampleGetPokitsUseCase: GetPokitsUseCase = mockk() + + describe("PokitPaging").config(coroutineTestScope = true) { + val coroutineScope = this + val pokitPaging = PokitPaging( + getPokits = sampleGetPokitsUseCase, + coroutineScope = coroutineScope, + firstRequestPage = FIRST_REQUEST_PAGE_SAMPLE, + perPage = PER_PAGE_SAMPLE + ) + coEvery { sampleGetPokitsUseCase.getPokits(size = PER_PAGE_SAMPLE * FIRST_REQUEST_PAGE_SAMPLE, page = 0) } coAnswers { + delay(1000L) + PokitResult.Success(result = listOf(DomainPokit(1, 1, "", DomainPokit.Image(1, ""), 1, ""))) + } + coEvery { sampleGetPokitsUseCase.getPokits(size = PER_PAGE_SAMPLE, page = 0) } coAnswers { + delay(1000L) + PokitResult.Success(result = listOf(DomainPokit(1, 1, "", DomainPokit.Image(1, ""), 1, ""))) + } + + context("새로고침을 하는 경우") { + it("새로고침 로딩 상태가 되어야 한다.") { + pokitPaging.refresh() + pokitPaging.pagingState.value shouldBe SimplePagingState.LOADING_INIT + } + } + + context("기존 페이지를 로드하던 중 다른 페이지 요청이 들어온 경우") { + it("해당 요청을 무시하고 기존 상태를 유지한다.") { + pokitPaging.refresh() + pokitPaging.load() + + coroutineScope.testCoroutineScheduler.advanceTimeBy(5000L) + + val state = pokitPaging.pagingState.first() + state shouldBe SimplePagingState.LAST + + // testCoroutineScheduler.advanceUntilIdle() + // it 내의 this(coroutineScope)와 전체 describe의 coroutineScope가 서로 다르다! + } + } + + context("기존 페이지를 로드하던 중 새로고침 요청이 들어온 경우") { + it("기존 작업을 무시하고 새로고침을 수행한다.") { + pokitPaging.load() + pokitPaging.refresh() + pokitPaging.pagingState.value shouldBe SimplePagingState.LOADING_INIT + } + } + } +}) diff --git a/feature/search/build.gradle.kts b/feature/search/build.gradle.kts index 04c4de52..e49b88f9 100644 --- a/feature/search/build.gradle.kts +++ b/feature/search/build.gradle.kts @@ -66,6 +66,9 @@ dependencies { implementation(libs.hilt) kapt(libs.hilt.compiler) + // coil + implementation(libs.coil.compose) + implementation(project(":core:ui")) implementation(project(":core:feature")) implementation(project(":domain")) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt index b3a10829..17d4325f 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/Preview.kt @@ -8,6 +8,8 @@ import androidx.compose.ui.tooling.preview.Preview import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.model.Link import pokitmons.pokit.search.model.LinkType +import pokitmons.pokit.search.model.SearchScreenState +import pokitmons.pokit.search.model.SearchScreenStep @Preview(showBackground = true) @Composable @@ -17,6 +19,7 @@ private fun Preview() { modifier = Modifier.fillMaxSize() ) { SearchScreen( + state = SearchScreenState(step = SearchScreenStep.RESULT), linkList = sampleLinks ) } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt index 042b733a..32ca49cf 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/SearchScreen.kt @@ -12,8 +12,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import pokitmons.pokit.core.ui.components.atom.loading.LoadingProgress import pokitmons.pokit.core.ui.components.template.bottomsheet.PokitBottomSheet import pokitmons.pokit.core.ui.components.template.modifybottomsheet.ModifyBottomSheetContent +import pokitmons.pokit.core.ui.components.template.pokkiempty.EmptyPokki +import pokitmons.pokit.core.ui.components.template.pokkierror.ErrorPokki import pokitmons.pokit.core.ui.components.template.removeItemBottomSheet.TwoButtonBottomSheetContent import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.components.filter.FilterArea @@ -29,6 +32,7 @@ import pokitmons.pokit.search.model.Link import pokitmons.pokit.search.model.SearchScreenState import pokitmons.pokit.search.model.SearchScreenStep import pokitmons.pokit.search.paging.SimplePagingState +import pokitmons.pokit.core.ui.R.string as coreString @Composable fun SearchScreenContainer( @@ -188,18 +192,44 @@ fun SearchScreen( ) if (state.step == SearchScreenStep.RESULT) { - SearchItemList( - modifier = Modifier - .fillMaxWidth() - .weight(1f), - onToggleSort = toggleSortOrder, - useRecentOrder = state.sortRecent, - onClickLinkKebab = showLinkModifyBottomSheet, - onClickLink = showLinkDetailBottomSheet, - links = linkList, - linkPagingState = linkPagingState, - loadNextLinks = loadNextLinks - ) + when { + (linkPagingState == SimplePagingState.LOADING_INIT) -> { + LoadingProgress( + modifier = Modifier + .fillMaxWidth() + .weight(1f) + ) + } + (linkPagingState == SimplePagingState.FAILURE_INIT) -> { + ErrorPokki( + modifier = Modifier.fillMaxWidth().weight(1f), + title = stringResource(id = coreString.title_error), + sub = stringResource(id = coreString.sub_error), + onClickRetry = onClickSearch + ) + } + (linkList.isEmpty()) -> { + EmptyPokki( + modifier = Modifier.fillMaxWidth().weight(1f), + title = stringResource(id = coreString.title_empty_search), + sub = stringResource(id = coreString.sub_empty_search) + ) + } + else -> { + SearchItemList( + modifier = Modifier + .fillMaxWidth() + .weight(1f), + onToggleSort = toggleSortOrder, + useRecentOrder = state.sortRecent, + onClickLinkKebab = showLinkModifyBottomSheet, + onClickLink = showLinkDetailBottomSheet, + links = linkList, + linkPagingState = linkPagingState, + loadNextLinks = loadNextLinks + ) + } + } } } } diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt index 7535141e..39567561 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/linkdetailbottomsheet/Link.kt @@ -16,11 +16,10 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.model.Link -import pokitmons.pokit.core.ui.R.drawable as coreDrawable @Composable internal fun Link( @@ -39,7 +38,7 @@ internal fun Link( ) ) { Image( - painter = painterResource(id = coreDrawable.icon_24_google), + painter = rememberAsyncImagePainter(link.imageUrl), contentDescription = null, contentScale = ContentScale.Crop, modifier = Modifier.width(124.dp) diff --git a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt index ec696e7f..d9a7ea9c 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/components/searchitemlist/SearchItemList.kt @@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import coil.compose.rememberAsyncImagePainter import pokitmons.pokit.core.ui.components.block.linkcard.LinkCard import pokitmons.pokit.core.ui.theme.PokitTheme import pokitmons.pokit.search.model.Link @@ -89,8 +90,8 @@ internal fun SearchItemList( item = link, title = link.title, sub = "${link.dateString} · ${link.domainUrl}", - painter = painterResource(id = coreDrawable.icon_24_google), - notRead = link.isRead, + painter = rememberAsyncImagePainter(link.imageUrl), + notRead = !link.isRead, badgeText = stringResource(id = link.linkType.textResourceId), onClickKebab = onClickLinkKebab, onClickItem = onClickLink, diff --git a/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt b/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt index 7646fa85..54e31ec0 100644 --- a/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt +++ b/feature/search/src/main/java/pokitmons/pokit/search/paging/LinkPaging.kt @@ -58,8 +58,8 @@ class LinkPaging( page = currentPageIndex, size = perPage * firstRequestPage, sort = listOf(), - isRead = !filter.notRead, - favorites = filter.bookmark, + isRead = if (filter.notRead) false else null, + favorites = if (filter.bookmark) true else null, startDate = filter.startDate?.toDateString(), endDate = filter.endDate?.toDateString(), categoryIds = filter.selectedPokits.mapNotNull { it.id.toIntOrNull() }, @@ -96,8 +96,8 @@ class LinkPaging( page = currentPageIndex, size = perPage, sort = listOf(), - isRead = !filter.notRead, - favorites = filter.bookmark, + isRead = if (filter.notRead) false else null, + favorites = if (filter.bookmark) true else null, startDate = filter.startDate?.toDateString(), endDate = filter.endDate?.toDateString(), categoryIds = filter.selectedPokits.mapNotNull { it.id.toIntOrNull() },