Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[북마크] 일괄 삭제 기능 추가 #311

Merged
merged 11 commits into from
Jun 3, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,6 @@ interface SessionRepository {
fun getBookmarkedSessionIds(): Flow<Set<String>>

suspend fun bookmarkSession(sessionId: String, bookmark: Boolean)

suspend fun deleteBookmarkedSessions(sessionIds: Set<String>)
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,11 @@ internal class DefaultSessionRepository @Inject constructor(
}
)
}

override suspend fun deleteBookmarkedSessions(sessionIds: Set<String>) {
val currentBookmarkedSessionIds = bookmarkIds.first()
sessionDataSource.updateBookmarkedSession(
currentBookmarkedSessionIds - sessionIds
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,5 +64,25 @@ internal class DefaultSessionRepositoryTest : StringSpec() {
awaitItem() shouldBe setOf("1")
}
}

"북마크 일괄 제거 테스트" {
// given : [1, 2, 3, 4]
val bookmarkedSessionIds = listOf("1", "2", "3", "4")
bookmarkedSessionIds.forEach {
repository.bookmarkSession(it, true)
}

repository.getBookmarkedSessionIds().test {
awaitItem() shouldBe setOf("1", "2", "3", "4")

// [1, 2, 3, 4] -> [1, 3, 4]
repository.deleteBookmarkedSessions(setOf("2"))
awaitItem() shouldBe setOf("1", "3", "4")

// [1, 3, 4] -> [1]
repository.deleteBookmarkedSessions(setOf("3", "4"))
awaitItem() shouldBe setOf("1")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ private val DarkColorScheme = darkColorScheme(
onErrorContainer = Red01,
surface = Graphite,
onSurface = White,
onSurfaceVariant = White,
surfaceDim = Black,
surfaceContainerHigh = DuskGray,
inverseSurface = Neon05,
inverseOnSurface = Black,
outline = DarkGray,
Expand Down Expand Up @@ -64,7 +66,9 @@ private val LightColorScheme = lightColorScheme(
onErrorContainer = Red06,
surface = PaperGray,
onSurface = DuskGray,
onSurfaceVariant = DarkGray,
surfaceDim = PaleGray,
surfaceContainerHigh = LightGray,
inverseSurface = Yellow05,
inverseOnSurface = White,
outline = LightGray,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.droidknights.app.core.domain.usecase

import com.droidknights.app.core.data.repository.api.SessionRepository
import javax.inject.Inject

class DeleteBookmarkedSessionUseCase @Inject constructor(
private val sessionRepository: SessionRepository,
) {
suspend operator fun invoke(sessionIds: Set<String>) =
sessionRepository.deleteBookmarkedSessions(sessionIds)
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,8 @@ internal class FakeSessionRepository(
override suspend fun bookmarkSession(sessionId: String, bookmark: Boolean) {
return
}

override suspend fun deleteBookmarkedSessions(sessionIds: Set<String>) {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package com.droidknights.app.feature.bookmark

import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
Expand All @@ -11,32 +16,41 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.droidknights.app.core.designsystem.theme.DuskGray
import com.droidknights.app.core.designsystem.theme.Gray
import com.droidknights.app.core.designsystem.theme.KnightsTheme
import com.droidknights.app.core.designsystem.theme.PaleGray
import com.droidknights.app.core.designsystem.theme.Purple01
import com.droidknights.app.core.designsystem.theme.White
import com.droidknights.app.core.model.Session
import com.droidknights.app.feature.bookmark.component.BookmarkCard
import com.droidknights.app.feature.bookmark.component.BookmarkItem
import com.droidknights.app.feature.bookmark.component.BookmarkTimelineItem
import com.droidknights.app.feature.bookmark.component.RemoveBookmarkSnackBar
import com.droidknights.app.feature.bookmark.model.BookmarkItemUiState
import com.droidknights.app.feature.bookmark.model.BookmarkUiState
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.ImmutableSet
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.collectLatest

Expand All @@ -59,22 +73,29 @@ internal fun BookmarkRoute(
) {
BookmarkContent(
uiState = bookmarkUiState,
onClickEditButton = { viewModel.clickEditButton() }
toggleEditMode = viewModel::toggleEditMode,
onSelectedItem = viewModel::selectSession,
onDeletedSessions = viewModel::deleteSessions
)
}
}

@Composable
private fun BookmarkContent(
uiState: BookmarkUiState,
onClickEditButton: () -> Unit,
toggleEditMode: () -> Unit,
onSelectedItem: (Session) -> Unit,
onDeletedSessions: () -> Unit,
) {
when (uiState) {
BookmarkUiState.Loading -> BookmarkLoading()
is BookmarkUiState.Success -> BookmarkScreen(
isEditMode = uiState.isEditButtonSelected,
isEditMode = uiState.isEditMode,
bookmarkItems = uiState.bookmarks.toImmutableList(),
onClickEditButton = onClickEditButton
toggleEditMode = toggleEditMode,
selectedSessionIds = uiState.selectedSessionIds,
onSelectedItem = onSelectedItem,
onDeletedSessions = onDeletedSessions,
)
}
}
Expand All @@ -90,52 +111,102 @@ private fun BookmarkLoading() {
private fun BookmarkScreen(
isEditMode: Boolean,
bookmarkItems: ImmutableList<BookmarkItemUiState>,
onClickEditButton: () -> Unit,
toggleEditMode: () -> Unit,
selectedSessionIds: ImmutableSet<String>,
onSelectedItem: (Session) -> Unit,
onDeletedSessions: () -> Unit,
listContentBottomPadding: Dp = 72.dp,
) {
Column(
Modifier
.fillMaxSize()
.background(color = PaleGray)
.padding(horizontal = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
BackHandler(isEditMode) {
toggleEditMode()
}

Box(
contentAlignment = Alignment.BottomCenter
) {
BookmarkTopAppBar(isEditMode = isEditMode, onClickEditButton = onClickEditButton)
Column(
Modifier
.fillMaxSize()
.background(color = MaterialTheme.colorScheme.surfaceDim),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
BookmarkTopAppBar(isEditMode = isEditMode, onClickEditButton = toggleEditMode)

if (bookmarkItems.isEmpty()) {
BookmarkEmptyScreen()
}

if (bookmarkItems.isEmpty()) {
BookmarkEmptyScreen()
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = listContentBottomPadding)
) {
items(
items = bookmarkItems,
key = { item -> item.session.id }
) { itemState ->
val isSelected = selectedSessionIds.contains(itemState.session.id)
BookmarkItem(
modifier = Modifier
.background(
color = if (isSelected) {
MaterialTheme.colorScheme.surfaceContainerHigh
} else {
MaterialTheme.colorScheme.surfaceDim
}
)
.padding(
end = if (isEditMode) 0.dp else 16.dp
),
leadingContent = @Composable {
if (isEditMode) {
EditModeLeadingItem(
itemState = itemState,
selectedSessionIds = selectedSessionIds,
onSelectedItem = onSelectedItem
)
} else {
BookmarkTimelineItem(
modifier = Modifier.padding(horizontal = 8.dp),
sequence = itemState.sequence,
time = itemState.time
)
}
},
midContent = @Composable {
BookmarkCard(
tagLabel = itemState.tagLabel,
room = itemState.session.room,
title = itemState.session.title,
speaker = itemState.speakerLabel
)
},
isEditMode = isEditMode,
trailingContent = @Composable {
Icon(
modifier = Modifier
.padding(horizontal = 18.dp)
.size(24.dp),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_menu),
contentDescription = stringResource(id = R.string.drag_and_drop),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
)
}
}
}

LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(bottom = listContentBottomPadding)
AnimatedVisibility(
visible = selectedSessionIds.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut(),
) {
items(
items = bookmarkItems,
key = { item -> item.session.id }
) { itemState ->
/** 편집모드 나타내는 Trailing 컨텐츠를 이곳에 구현하세요 */
BookmarkItem(
leadingContent = @Composable {
BookmarkTimelineItem(
sequence = itemState.sequence,
time = itemState.time
)
},
midContent = @Composable {
BookmarkCard(
tagLabel = itemState.tagLabel,
room = itemState.session.room,
title = itemState.session.title,
speaker = itemState.speakerLabel
)
},
isShowTrailingContent = itemState.isEditMode,
trailingContent = @Composable {
/** 편집모드 나타내는 Trailing 컨텐츠를 이곳에 구현하세요 */
}
)
}
RemoveBookmarkSnackBar(
modifier = Modifier
.padding(bottom = 92.dp)
.padding(horizontal = 8.dp),
onClick = onDeletedSessions,
)
}
}
}
Expand All @@ -147,7 +218,7 @@ private fun BookmarkEmptyScreen() {
modifier = Modifier.align(Alignment.Center),
text = stringResource(id = R.string.empty_bookmark_item_description),
style = KnightsTheme.typography.titleSmallM,
color = DuskGray
color = MaterialTheme.colorScheme.onSurface
)
}
}
Expand All @@ -162,7 +233,7 @@ private fun BookmarkTopAppBar(
targetValue = if (isEditMode) {
Purple01
} else {
DuskGray
Gray
}
)

Expand All @@ -175,14 +246,14 @@ private fun BookmarkTopAppBar(
modifier = Modifier.align(Alignment.Center),
text = stringResource(id = R.string.book_mark_top_bar_title),
style = KnightsTheme.typography.titleSmallM,
color = DuskGray
color = MaterialTheme.colorScheme.onSurface
)

Text(
modifier = Modifier
.align(Alignment.CenterEnd)
.clickable(onClick = onClickEditButton)
.padding(6.dp),
.padding(horizontal = 12.dp),
text = if (isEditMode) {
stringResource(id = R.string.edit_button_confirm_label)
} else {
Expand All @@ -193,3 +264,39 @@ private fun BookmarkTopAppBar(
)
}
}

@Composable
private fun EditModeLeadingItem(
itemState: BookmarkItemUiState,
selectedSessionIds: ImmutableSet<String>,
onSelectedItem: (Session) -> Unit,
) {
val isSelectedItem = selectedSessionIds.contains(itemState.session.id)
val baseModifier = Modifier
.padding(horizontal = 18.dp)
.size(24.dp)
.clip(CircleShape)
.clickable { onSelectedItem(itemState.session) }
if (isSelectedItem) {
Box(
modifier = baseModifier.background(Purple01),
contentAlignment = Alignment.Center
) {
Icon(
modifier = Modifier.size(16.dp),
imageVector = ImageVector.vectorResource(id = R.drawable.ic_check),
contentDescription = null,
tint = White
)
}
} else {
Box(
modifier = baseModifier
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.surfaceContainerHigh,
shape = CircleShape
)
)
}
}
Loading
Loading