Skip to content
Merged
12 changes: 12 additions & 0 deletions core/designsystem/src/main/res/drawable/icon_memo_filled.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M22.496,11.125L15.875,17.747C15.795,17.826 15.731,17.918 15.683,18.019L14.768,19.967C14.569,20.392 15.012,20.833 15.436,20.63L17.373,19.705C17.472,19.658 17.563,19.594 17.64,19.517L24.264,12.892C24.642,12.515 24.642,11.903 24.264,11.526L23.863,11.125C23.486,10.747 22.874,10.747 22.496,11.125Z"
android:fillColor="#4F525F"/>
<path
android:pathData="M19.051,3.033C20.385,3.033 21.467,4.115 21.467,5.449V9.512C21.358,9.593 21.251,9.681 21.152,9.78L14.53,16.402C14.296,16.637 14.105,16.911 13.964,17.211L13.048,19.158C12.09,21.199 14.22,23.315 16.255,22.344L18.192,21.419C18.486,21.279 18.754,21.089 18.984,20.859L21.467,18.376V22.551C21.467,23.885 20.385,24.967 19.051,24.967H4.949C3.615,24.967 2.533,23.885 2.533,22.551V9.967H7.051C8.385,9.967 9.467,8.885 9.467,7.551V3.033H19.051ZM21.467,15.689L17.641,19.516C17.563,19.593 17.472,19.657 17.373,19.704L15.436,20.629L15.356,20.66C14.959,20.777 14.582,20.364 14.769,19.966L15.684,18.019C15.707,17.968 15.736,17.919 15.768,17.873L15.875,17.746L21.467,12.153V15.689ZM6.969,16.649C6.436,16.649 6.004,17.081 6.004,17.614C6.004,18.147 6.436,18.579 6.969,18.579H11.3C11.833,18.579 12.265,18.147 12.265,17.614C12.265,17.081 11.833,16.649 11.3,16.649H6.969ZM6.969,12.649C6.436,12.649 6.004,13.081 6.004,13.614C6.004,14.147 6.436,14.579 6.969,14.579H14.766C15.298,14.579 15.731,14.147 15.731,13.614C15.731,13.082 15.298,12.65 14.766,12.649H6.969ZM7.533,7.551C7.533,7.817 7.317,8.033 7.051,8.033H3.113C3.154,7.985 3.196,7.937 3.241,7.892L7.392,3.741C7.437,3.696 7.485,3.654 7.533,3.612V7.551Z"
android:fillColor="#4F525F"/>
</vector>
29 changes: 29 additions & 0 deletions core/designsystem/src/main/res/drawable/icon_memo_stroked.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M22.996,11.125L16.375,17.747C16.295,17.826 16.231,17.918 16.183,18.019L15.268,19.967C15.069,20.392 15.512,20.833 15.936,20.63L17.873,19.705C17.972,19.658 18.063,19.594 18.14,19.517L24.764,12.892C25.142,12.515 25.142,11.903 24.764,11.526L24.363,11.125C23.986,10.747 23.374,10.747 22.996,11.125Z"
android:fillColor="#4F525F"/>
<path
android:pathData="M19.551,3.033C20.885,3.033 21.967,4.115 21.967,5.449V9.512C21.858,9.593 21.751,9.681 21.652,9.78L20.033,11.399V5.449C20.033,5.183 19.817,4.967 19.551,4.967H9.601C9.472,4.967 9.349,5.018 9.259,5.108L5.108,9.259C5.018,9.349 4.967,9.472 4.967,9.601V22.551C4.967,22.817 5.183,23.033 5.449,23.033H19.551C19.817,23.033 20.033,22.817 20.033,22.551V20.31L21.967,18.376V22.551C21.967,23.885 20.885,24.967 19.551,24.967H5.449C4.115,24.967 3.033,23.885 3.033,22.551V9.601C3.033,8.96 3.288,8.345 3.741,7.892L7.892,3.741C8.345,3.288 8.96,3.033 9.601,3.033H19.551ZM21.967,15.689L20.033,17.622V14.087L21.967,12.153V15.689Z"
android:fillColor="#4F525F"/>
<path
android:pathData="M4,9H7.55C8.351,9 9,8.351 9,7.55V4"
android:strokeWidth="1.93285"
android:fillColor="#00000000"
android:strokeColor="#4F525F"/>
<path
android:pathData="M7.469,13.614H15.265"
android:strokeWidth="1.93"
android:fillColor="#00000000"
android:strokeColor="#4F525F"
android:strokeLineCap="round"/>
<path
android:pathData="M7.469,17.614H11.8"
android:strokeWidth="1.93"
android:fillColor="#00000000"
android:strokeColor="#4F525F"
android:strokeLineCap="round"/>
</vector>
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.neki.android.feature.archive.impl.photo_detail

import com.neki.android.core.model.Photo
import kotlinx.collections.immutable.ImmutableMap
import kotlinx.collections.immutable.persistentMapOf

enum class MemoMode {
Closed,
Expand All @@ -18,12 +16,10 @@ data class PhotoDetailState(
val isShowDeleteDialog: Boolean = false,
val isShowOptionPopup: Boolean = false,
val memo: String = "",
val memoModes: ImmutableMap<Long, MemoMode> = persistentMapOf(),
val memoMode: MemoMode = MemoMode.Closed,
) {
val currentIndex get() = if (photos.isEmpty()) 0 else currentPage.coerceIn(0, photos.lastIndex)
val photo: Photo get() = photos.getOrElse(currentIndex) { Photo() }
val currentMemoMode: MemoMode get() = memoModes[photo.id] ?: MemoMode.Closed
fun memoModeOf(photoId: Long): MemoMode = memoModes[photoId] ?: MemoMode.Closed
}

sealed interface PhotoDetailIntent {
Expand All @@ -37,6 +33,7 @@ sealed interface PhotoDetailIntent {
data object ClickLeftPhoto : PhotoDetailIntent
data object ClickRightPhoto : PhotoDetailIntent
data class PageChanged(val page: Int) : PhotoDetailIntent
data object PageScrollStarted : PhotoDetailIntent

// ActionBar Intent
data object ClickDownloadIcon : PhotoDetailIntent
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
package com.neki.android.feature.archive.impl.photo_detail

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.spring
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.exclude
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
Expand All @@ -16,17 +29,19 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.foundation.layout.width
import androidx.compose.ui.Alignment
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.neki.android.core.designsystem.DevicePreview
import com.neki.android.core.designsystem.modifier.noRippleClickableSingle
import com.neki.android.core.designsystem.topbar.BackTitleOptionTopBar
import com.neki.android.core.designsystem.ui.theme.NekiTheme
import com.neki.android.core.model.Photo
Expand All @@ -37,10 +52,12 @@ import com.neki.android.core.ui.compose.collectWithLifecycle
import com.neki.android.core.ui.toast.NekiToast
import com.neki.android.feature.archive.api.PhotoDetailResult
import com.neki.android.feature.archive.impl.component.DeletePhotoDialog
import com.neki.android.feature.archive.impl.photo_detail.component.MemoTextField
import com.neki.android.feature.archive.impl.photo_detail.component.PhotoDetailActionBar
import com.neki.android.feature.archive.impl.photo_detail.component.PhotoDetailImageItem
import com.neki.android.feature.archive.impl.util.ImageDownloader
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.launch
import timber.log.Timber

Expand All @@ -66,6 +83,14 @@ internal fun PhotoDetailRoute(
}
}

LaunchedEffect(pagerState) {
snapshotFlow { pagerState.isScrollInProgress }
.filter { it }
.collect {
viewModel.store.onIntent(PhotoDetailIntent.PageScrollStarted)
}
}

viewModel.store.sideEffects.collectWithLifecycle { sideEffect ->
when (sideEffect) {
PhotoDetailSideEffect.NavigateBack -> navigateBack()
Expand Down Expand Up @@ -111,8 +136,6 @@ internal fun PhotoDetailScreen(
onIntent: (PhotoDetailIntent) -> Unit = {},
pagerState: PagerState = rememberPagerState { uiState.photos.size.coerceAtLeast(1) },
) {
val isMemoActive = uiState.currentMemoMode == MemoMode.Expanded ||
uiState.currentMemoMode == MemoMode.Editing
val density = LocalDensity.current
var actionBarHeightDp by remember { mutableStateOf(0.dp) }

Expand All @@ -127,46 +150,24 @@ internal fun PhotoDetailScreen(
onClickIcon = { onIntent(PhotoDetailIntent.ClickOptionIcon) },
)

HorizontalPager(
PhotoDetailPagerArea(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.clipToBounds(),
state = pagerState,
beyondViewportPageCount = 1,
userScrollEnabled = !isMemoActive,
) { page ->
val index = if (uiState.photos.isEmpty()) 0 else page.coerceIn(0, uiState.photos.lastIndex)

val photo = uiState.photos.getOrNull(index)
val pageMemoMode = uiState.memoModeOf(photo?.id ?: 0L)
val pageMemo = if (index == uiState.currentIndex) uiState.memo
else photo?.memo.orEmpty()

PhotoDetailImageItem(
imageUrl = photo?.imageUrl,
memo = pageMemo,
memoMode = pageMemoMode,
actionBarHeight = actionBarHeightDp,
isScrollInProgress = pagerState.isScrollInProgress,
isTapEnabled = pageMemoMode != MemoMode.Expanded && pageMemoMode != MemoMode.Editing,
onClickLeft = { onIntent(PhotoDetailIntent.ClickLeftPhoto) },
onClickRight = { onIntent(PhotoDetailIntent.ClickRightPhoto) },
onClickMemoMore = { onIntent(PhotoDetailIntent.ClickMemoMore) },
onClickMemoText = { onIntent(PhotoDetailIntent.ClickMemoText) },
onClickMemoFold = { onIntent(PhotoDetailIntent.ClickMemoFold) },
onClickMemoCancel = { onIntent(PhotoDetailIntent.ClickMemoCancel) },
onClickMemoDone = { onIntent(PhotoDetailIntent.ClickMemoDone(it)) },
onMemoTextChanged = { onIntent(PhotoDetailIntent.MemoTextChanged(it)) },
)
}
uiState = uiState,
actionBarHeightDp = actionBarHeightDp,
pagerState = pagerState,
onIntent = onIntent,
)

if (uiState.currentMemoMode != MemoMode.Editing) {
if (uiState.memoMode != MemoMode.Editing) {
PhotoDetailActionBar(
modifier = Modifier.onSizeChanged { size ->
actionBarHeightDp = with(density) { size.height.toDp() }
},
isFavorite = uiState.photo.isFavorite,
hasMemo = uiState.photo.memo.isNotEmpty(),
onClickDownload = { onIntent(PhotoDetailIntent.ClickDownloadIcon) },
onClickFavorite = { onIntent(PhotoDetailIntent.ClickFavoriteIcon) },
onClickMemo = { onIntent(PhotoDetailIntent.ClickMemoIcon) },
Expand Down Expand Up @@ -204,6 +205,78 @@ internal fun PhotoDetailScreen(
}
}

@Composable
private fun PhotoDetailPagerArea(
modifier: Modifier,
uiState: PhotoDetailState,
actionBarHeightDp: Dp,
pagerState: PagerState,
onIntent: (PhotoDetailIntent) -> Unit,
) {
val isMemoActive = uiState.memoMode == MemoMode.Expanded ||
uiState.memoMode == MemoMode.Editing

Box(modifier = modifier) {
HorizontalPager(
modifier = Modifier
.fillMaxSize()
.padding(bottom = if (uiState.memoMode == MemoMode.Editing) actionBarHeightDp else 0.dp),
state = pagerState,
beyondViewportPageCount = 1,
userScrollEnabled = !isMemoActive,
Comment thread
ikseong00 marked this conversation as resolved.
) { page ->
val index = if (uiState.photos.isEmpty()) 0 else page.coerceIn(0, uiState.photos.lastIndex)
val photo = uiState.photos.getOrNull(index)

PhotoDetailImageItem(
imageUrl = photo?.imageUrl,
isScrollInProgress = pagerState.isScrollInProgress,
isTapEnabled = !isMemoActive,
onClickLeft = { onIntent(PhotoDetailIntent.ClickLeftPhoto) },
onClickRight = { onIntent(PhotoDetailIntent.ClickRightPhoto) },
)
}

AnimatedVisibility(
visible = isMemoActive,
enter = fadeIn(),
exit = fadeOut(),
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0x80202227))
.noRippleClickableSingle { onIntent(PhotoDetailIntent.ClickMemoFold) },
)
}

AnimatedVisibility(
modifier = Modifier
.align(Alignment.BottomCenter)
.then(
if (uiState.memoMode == MemoMode.Editing) Modifier.imePadding()
else Modifier.windowInsetsPadding(
WindowInsets.ime.exclude(WindowInsets(bottom = actionBarHeightDp)),
),
),
visible = uiState.memoMode != MemoMode.Closed,
enter = expandVertically(expandFrom = Alignment.Top),
exit = shrinkVertically(shrinkTowards = Alignment.Top),
) {
MemoTextField(
memo = uiState.memo,
memoMode = uiState.memoMode,
onClickMemoMore = { onIntent(PhotoDetailIntent.ClickMemoMore) },
onClickMemoText = { onIntent(PhotoDetailIntent.ClickMemoText) },
onClickMemoFold = { onIntent(PhotoDetailIntent.ClickMemoFold) },
onClickMemoCancel = { onIntent(PhotoDetailIntent.ClickMemoCancel) },
onClickMemoDone = { onIntent(PhotoDetailIntent.ClickMemoDone(it)) },
onMemoTextChanged = { onIntent(PhotoDetailIntent.MemoTextChanged(it)) },
)
}
}
}

@DevicePreview
@Composable
private fun PhotoDetailScreenPreview() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.collections.immutable.toImmutableMap
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.MutableSharedFlow
Expand Down Expand Up @@ -111,6 +110,8 @@ class PhotoDetailViewModel @AssistedInject constructor(
preloadIfNeeded(reduce)
}

PhotoDetailIntent.PageScrollStarted -> reduce { copy(memoMode = MemoMode.Closed) }

// ActionBar Intent
PhotoDetailIntent.ClickDownloadIcon -> postSideEffect(PhotoDetailSideEffect.DownloadImage(state.photo.imageUrl))
PhotoDetailIntent.ClickFavoriteIcon -> handleFavoriteToggle(state, reduce)
Expand All @@ -132,40 +133,36 @@ class PhotoDetailViewModel @AssistedInject constructor(
// Memo Intent
is PhotoDetailIntent.MemoTextChanged -> reduce { copy(memo = intent.text) }
PhotoDetailIntent.ClickMemoIcon -> reduce {
val photoId = photo.id
val current = memoModeOf(photoId)
val newMode = if (current == MemoMode.Closed) MemoMode.Preview else MemoMode.Closed
copy(memoModes = (memoModes + (photoId to newMode)).toImmutableMap())
copy(memoMode = if (memoMode == MemoMode.Closed) MemoMode.Preview else MemoMode.Closed)
}

PhotoDetailIntent.ClickMemoMore -> reduce {
copy(memoModes = (memoModes + (photo.id to MemoMode.Expanded)).toImmutableMap())
copy(memoMode = MemoMode.Expanded)
}

PhotoDetailIntent.ClickMemoText -> reduce {
copy(memoModes = (memoModes + (photo.id to MemoMode.Editing)).toImmutableMap())
copy(memoMode = MemoMode.Editing)
}

PhotoDetailIntent.ClickMemoFold -> reduce {
copy(
memo = photo.memo,
memoModes = (memoModes + (photo.id to MemoMode.Preview)).toImmutableMap(),
memoMode = MemoMode.Preview,
)
}

PhotoDetailIntent.ClickMemoCancel -> reduce {
copy(
memo = photo.memo,
memoModes = (memoModes + (photo.id to MemoMode.Preview)).toImmutableMap(),
memoMode = MemoMode.Preview,
)
}

is PhotoDetailIntent.ClickMemoDone -> {
val photoId = state.photo.id
reduce {
copy(
memo = intent.memo,
memoModes = (memoModes + (photoId to MemoMode.Preview)).toImmutableMap(),
memoMode = MemoMode.Preview,
)
}
saveMemo(state.copy(memo = intent.memo), reduce, postSideEffect)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import com.neki.android.core.designsystem.ui.theme.NekiTheme
@Composable
internal fun PhotoDetailActionBar(
isFavorite: Boolean,
hasMemo: Boolean,
modifier: Modifier = Modifier,
onClickDownload: () -> Unit = {},
onClickFavorite: () -> Unit = {},
Expand Down Expand Up @@ -61,7 +62,10 @@ internal fun PhotoDetailActionBar(
) {
Icon(
modifier = Modifier.size(28.dp),
imageVector = ImageVector.vectorResource(R.drawable.icon_memo),
imageVector = ImageVector.vectorResource(
if (hasMemo) R.drawable.icon_memo_filled
else R.drawable.icon_memo_stroked,
),
contentDescription = null,
tint = NekiTheme.colorScheme.gray700,
)
Expand All @@ -88,6 +92,7 @@ private fun PhotoDetailActionBarClosedPreview() {
NekiTheme {
PhotoDetailActionBar(
isFavorite = false,
hasMemo = false,
)
}
}
Expand All @@ -98,6 +103,7 @@ private fun PhotoDetailActionBarMemoActivePreview() {
NekiTheme {
PhotoDetailActionBar(
isFavorite = true,
hasMemo = true,
)
}
}
Loading
Loading