diff --git a/core/data-api/src/main/java/com/droidknights/app/core/data/repository/api/SessionRepository.kt b/core/data-api/src/main/java/com/droidknights/app/core/data/repository/api/SessionRepository.kt index 85c6f156..51e25ed9 100644 --- a/core/data-api/src/main/java/com/droidknights/app/core/data/repository/api/SessionRepository.kt +++ b/core/data-api/src/main/java/com/droidknights/app/core/data/repository/api/SessionRepository.kt @@ -12,4 +12,6 @@ interface SessionRepository { fun getBookmarkedSessionIds(): Flow> suspend fun bookmarkSession(sessionId: String, bookmark: Boolean) + + suspend fun deleteBookmarkedSessions(sessionIds: Set) } diff --git a/core/data/src/main/java/com/droidknights/app/core/data/repository/DefaultSessionRepository.kt b/core/data/src/main/java/com/droidknights/app/core/data/repository/DefaultSessionRepository.kt index 7f9e42cc..d3a21b6a 100644 --- a/core/data/src/main/java/com/droidknights/app/core/data/repository/DefaultSessionRepository.kt +++ b/core/data/src/main/java/com/droidknights/app/core/data/repository/DefaultSessionRepository.kt @@ -49,4 +49,11 @@ internal class DefaultSessionRepository @Inject constructor( } ) } + + override suspend fun deleteBookmarkedSessions(sessionIds: Set) { + val currentBookmarkedSessionIds = bookmarkIds.first() + sessionDataSource.updateBookmarkedSession( + currentBookmarkedSessionIds - sessionIds + ) + } } diff --git a/core/data/src/test/java/com/droidknights/app/core/data/repository/DefaultSessionRepositoryTest.kt b/core/data/src/test/java/com/droidknights/app/core/data/repository/DefaultSessionRepositoryTest.kt index cb5cdaf6..929ffe45 100644 --- a/core/data/src/test/java/com/droidknights/app/core/data/repository/DefaultSessionRepositoryTest.kt +++ b/core/data/src/test/java/com/droidknights/app/core/data/repository/DefaultSessionRepositoryTest.kt @@ -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") + } + } } } diff --git a/core/designsystem/src/main/java/com/droidknights/app/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/droidknights/app/core/designsystem/theme/Theme.kt index 05f438cd..6d5ef77b 100644 --- a/core/designsystem/src/main/java/com/droidknights/app/core/designsystem/theme/Theme.kt +++ b/core/designsystem/src/main/java/com/droidknights/app/core/designsystem/theme/Theme.kt @@ -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, @@ -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, diff --git a/core/domain/src/main/java/com/droidknights/app/core/domain/usecase/DeleteBookmarkedSessionUseCase.kt b/core/domain/src/main/java/com/droidknights/app/core/domain/usecase/DeleteBookmarkedSessionUseCase.kt new file mode 100644 index 00000000..2dead727 --- /dev/null +++ b/core/domain/src/main/java/com/droidknights/app/core/domain/usecase/DeleteBookmarkedSessionUseCase.kt @@ -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) = + sessionRepository.deleteBookmarkedSessions(sessionIds) +} diff --git a/core/domain/src/test/java/com/droidknights/app/core/domain/usecase/FakeSessionRepository.kt b/core/domain/src/test/java/com/droidknights/app/core/domain/usecase/FakeSessionRepository.kt index 6ef73dd7..ced3d1b5 100644 --- a/core/domain/src/test/java/com/droidknights/app/core/domain/usecase/FakeSessionRepository.kt +++ b/core/domain/src/test/java/com/droidknights/app/core/domain/usecase/FakeSessionRepository.kt @@ -25,4 +25,8 @@ internal class FakeSessionRepository( override suspend fun bookmarkSession(sessionId: String, bookmark: Boolean) { return } + + override suspend fun deleteBookmarkedSessions(sessionIds: Set) { + TODO("Not yet implemented") + } } diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkScreen.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkScreen.kt index fb933af3..2a32dbf0 100644 --- a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkScreen.kt +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkScreen.kt @@ -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 @@ -11,10 +16,13 @@ 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 @@ -22,21 +30,27 @@ 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 @@ -59,7 +73,9 @@ internal fun BookmarkRoute( ) { BookmarkContent( uiState = bookmarkUiState, - onClickEditButton = { viewModel.clickEditButton() } + toggleEditMode = viewModel::toggleEditMode, + onSelectedItem = viewModel::selectSession, + onDeletedSessions = viewModel::deleteSessions ) } } @@ -67,14 +83,19 @@ internal fun BookmarkRoute( @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, ) } } @@ -90,52 +111,102 @@ private fun BookmarkLoading() { private fun BookmarkScreen( isEditMode: Boolean, bookmarkItems: ImmutableList, - onClickEditButton: () -> Unit, + toggleEditMode: () -> Unit, + selectedSessionIds: ImmutableSet, + 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, + ) } } } @@ -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 ) } } @@ -162,7 +233,7 @@ private fun BookmarkTopAppBar( targetValue = if (isEditMode) { Purple01 } else { - DuskGray + Gray } ) @@ -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 { @@ -193,3 +264,39 @@ private fun BookmarkTopAppBar( ) } } + +@Composable +private fun EditModeLeadingItem( + itemState: BookmarkItemUiState, + selectedSessionIds: ImmutableSet, + 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 + ) + ) + } +} diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkViewModel.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkViewModel.kt index 3c1e0398..009bc78b 100644 --- a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkViewModel.kt +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/BookmarkViewModel.kt @@ -2,23 +2,32 @@ package com.droidknights.app.feature.bookmark import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.droidknights.app.core.domain.usecase.DeleteBookmarkedSessionUseCase import com.droidknights.app.core.domain.usecase.GetBookmarkedSessionsUseCase +import com.droidknights.app.core.model.Session import com.droidknights.app.feature.bookmark.model.BookmarkItemUiState import com.droidknights.app.feature.bookmark.model.BookmarkUiState import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.persistentSetOf import kotlinx.collections.immutable.toPersistentList +import kotlinx.collections.immutable.toPersistentSet import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class BookmarkViewModel @Inject constructor( private val getBookmarkedSessionsUseCase: GetBookmarkedSessionsUseCase, + private val deleteBookmarkedSessionUseCase: DeleteBookmarkedSessionUseCase, ) : ViewModel() { private val _errorFlow = MutableSharedFlow() @@ -36,13 +45,12 @@ class BookmarkViewModel @Inject constructor( when (bookmarkUiState) { is BookmarkUiState.Loading -> { BookmarkUiState.Success( - isEditButtonSelected = false, + isEditMode = false, bookmarks = bookmarkSessions .mapIndexed { index, session -> BookmarkItemUiState( index = index, session = session, - isEditMode = false ) } .toPersistentList() @@ -56,7 +64,6 @@ class BookmarkViewModel @Inject constructor( BookmarkItemUiState( index = index, session = session, - isEditMode = bookmarkUiState.isEditButtonSelected ) } .toPersistentList() @@ -69,19 +76,51 @@ class BookmarkViewModel @Inject constructor( } } - fun clickEditButton() { + fun toggleEditMode() { val state = _bookmarkUiState.value if (state !is BookmarkUiState.Success) { return } _bookmarkUiState.value = state.copy( - isEditButtonSelected = state.isEditButtonSelected.not(), - bookmarks = state.bookmarks - .map { - it.copy(isEditMode = !it.isEditMode.not()) - } - .toPersistentList() + isEditMode = state.isEditMode.not(), + bookmarks = state.bookmarks, + selectedSessionIds = persistentSetOf() ) } + + fun selectSession(session: Session) { + val state = _bookmarkUiState.value + if (state !is BookmarkUiState.Success) { + return + } + + val isAlreadySelected = state.selectedSessionIds.contains(session.id) + val newSelectedIds = if (isAlreadySelected) { + state.selectedSessionIds - session.id + } else { + state.selectedSessionIds + session.id + } + + _bookmarkUiState.value = state.copy( + selectedSessionIds = newSelectedIds.toPersistentSet() + ) + } + + fun deleteSessions() { + val state = _bookmarkUiState.value + if (state !is BookmarkUiState.Success) { + return + } + + flow { + emit(deleteBookmarkedSessionUseCase(state.selectedSessionIds)) + }.onEach { + _bookmarkUiState.update { + state.copy(selectedSessionIds = persistentSetOf()) + } + }.catch { throwable -> + _errorFlow.emit(throwable) + }.launchIn(viewModelScope) + } } diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkCard.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkCard.kt index 735df7d2..497c6d52 100644 --- a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkCard.kt +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkCard.kt @@ -10,17 +10,15 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.droidknights.app.core.designsystem.theme.DarkGray -import com.droidknights.app.core.designsystem.theme.Green04 import com.droidknights.app.core.designsystem.theme.KnightsTheme import com.droidknights.app.core.designsystem.theme.Purple01 -import com.droidknights.app.core.designsystem.theme.White import com.droidknights.app.core.model.Room import com.droidknights.app.core.ui.RoomText @@ -35,7 +33,10 @@ internal fun BookmarkCard( Column( modifier = modifier .fillMaxWidth() - .background(color = White, shape = RoundedCornerShape(8.dp)) + .background( + color = MaterialTheme.colorScheme.primaryContainer, + shape = RoundedCornerShape(8.dp) + ) .padding(start = 16.dp, end = 18.dp, top = 16.dp, bottom = 16.dp), verticalArrangement = Arrangement.spacedBy(space = 8.dp) ) { @@ -53,25 +54,25 @@ internal fun BookmarkCard( modifier = Modifier.weight(1F), text = tagLabel, style = KnightsTheme.typography.labelSmallM, - color = DarkGray + color = MaterialTheme.colorScheme.onSurfaceVariant ) RoomText( room, style = KnightsTheme.typography.labelSmallM, - color = DarkGray + color = MaterialTheme.colorScheme.onSurfaceVariant ) } Text( text = title, style = KnightsTheme.typography.titleSmallB, - color = Green04 + color = MaterialTheme.colorScheme.onSecondaryContainer ) Text( text = speaker, style = KnightsTheme.typography.labelSmallM, - color = Green04 + color = MaterialTheme.colorScheme.onSecondaryContainer ) } } diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkItem.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkItem.kt index 84820a6c..b6b22f26 100644 --- a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkItem.kt +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/BookmarkItem.kt @@ -4,24 +4,31 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally -import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.droidknights.app.core.designsystem.theme.KnightsTheme -import com.droidknights.app.core.designsystem.theme.LightGray import com.droidknights.app.core.model.Room +import com.droidknights.app.feature.bookmark.R import java.time.LocalTime /** @@ -36,16 +43,21 @@ internal fun BookmarkItem( leadingContent: @Composable () -> Unit, midContent: @Composable () -> Unit, trailingContent: @Composable () -> Unit, - isShowTrailingContent: Boolean, - leadingContentWidth: Dp = 44.dp, + isEditMode: Boolean, + modifier: Modifier = Modifier, + leadingContentWidth: Dp = 60.dp, leadingContentHeight: Dp = 58.dp, ) { + val lineColor = MaterialTheme.colorScheme.outline + Box( - modifier = Modifier + modifier = modifier .fillMaxWidth() .drawBehind { + if (isEditMode) return@drawBehind + drawLine( - color = LightGray, + color = lineColor, start = Offset(x = leadingContentWidth.toPx() / 2, y = 0F), end = Offset( x = leadingContentWidth.toPx() / 2, @@ -55,7 +67,7 @@ internal fun BookmarkItem( ) drawLine( - color = LightGray, + color = lineColor, start = Offset( x = leadingContentWidth.toPx() / 2, y = (this.size.height / 2) + (leadingContentHeight.toPx() / 2) @@ -64,13 +76,12 @@ internal fun BookmarkItem( strokeWidth = 1.dp.toPx() ) } - .padding(end = 8.dp, top = 8.dp, bottom = 8.dp) + .padding(vertical = 8.dp) ) { Row( modifier = Modifier .fillMaxWidth() .animateContentSize(), - horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically ) { leadingContent() @@ -84,7 +95,7 @@ internal fun BookmarkItem( } AnimatedVisibility( - visible = isShowTrailingContent, + visible = isEditMode, enter = slideInHorizontally { it }, exit = slideOutHorizontally { it } ) { @@ -94,48 +105,72 @@ internal fun BookmarkItem( } } +/** + * Preview 출력용으로만 사용하세요. + */ +@Composable +private fun BookMarkItemForPreview(isEditMode: Boolean) { + BookmarkItem( + modifier = Modifier.padding(end = 16.dp), + leadingContent = @Composable { + if (isEditMode) { + Box( + modifier = Modifier + .padding(horizontal = 18.dp) + .size(24.dp) + .border( + width = 1.dp, + color = MaterialTheme.colorScheme.onSurfaceVariant, + shape = CircleShape + ) + ) + } else { + BookmarkTimelineItem( + modifier = Modifier.padding(horizontal = 8.dp), + sequence = 2, + time = LocalTime.of(1, 20) + ) + } + }, + midContent = @Composable { + BookmarkCard( + tagLabel = "효율적인 코드 베이스", + room = Room.TRACK2, + title = "Jetpack Compose에 있는 것, 없는것", + speaker = "홍길동" + ) + }, + isEditMode = isEditMode, + trailingContent = { + 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) + ) + } + ) +} + @Preview(showBackground = true, backgroundColor = 0xFFF9F9F9) @Composable private fun BookmarkItemPreview() { KnightsTheme { Column { - BookmarkItem( - leadingContent = @Composable { - BookmarkTimelineItem( - sequence = 1, - time = LocalTime.of(1, 20) - ) - }, - midContent = @Composable { - BookmarkCard( - tagLabel = "효율적인 코드 베이스", - room = Room.TRACK2, - title = "Jetpack Compose에 있는 것, 없는것", - speaker = "홍길동" - ) - }, - isShowTrailingContent = false, - trailingContent = {} - ) + BookMarkItemForPreview(isEditMode = false) + BookMarkItemForPreview(isEditMode = false) + } + } +} - BookmarkItem( - leadingContent = @Composable { - BookmarkTimelineItem( - sequence = 2, - time = LocalTime.of(1, 20) - ) - }, - midContent = @Composable { - BookmarkCard( - tagLabel = "효율적인 코드 베이스", - room = Room.TRACK2, - title = "Jetpack Compose에 있는 것, 없는것", - speaker = "홍길동" - ) - }, - isShowTrailingContent = false, - trailingContent = {} - ) +@Preview(showBackground = true, backgroundColor = 0xFFF9F9F9) +@Composable +private fun BookmarkItemEditModePreview() { + KnightsTheme { + Column { + BookMarkItemForPreview(isEditMode = true) + BookMarkItemForPreview(isEditMode = true) } } } diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/RemoveBookmarkSnackBar.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/RemoveBookmarkSnackBar.kt new file mode 100644 index 00000000..b0d94cbf --- /dev/null +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/component/RemoveBookmarkSnackBar.kt @@ -0,0 +1,61 @@ +package com.droidknights.app.feature.bookmark.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.droidknights.app.core.designsystem.theme.Graphite +import com.droidknights.app.core.designsystem.theme.KnightsTheme +import com.droidknights.app.core.designsystem.theme.Purple01 +import com.droidknights.app.feature.bookmark.R + +@Composable +internal fun RemoveBookmarkSnackBar( + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Row( + modifier = modifier + .height(50.dp) + .fillMaxWidth() + .clip(shape = RoundedCornerShape(4.dp)) + .clickable(onClick = onClick) + .background(Graphite), + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier.padding(end = 8.dp).size(24.dp), + imageVector = ImageVector.vectorResource(id = R.drawable.ic_trash), + contentDescription = null, + tint = Purple01 + ) + Text( + text = stringResource(id = R.string.remove_from_bookmark), + style = KnightsTheme.typography.bodyMediumR, + color = Purple01 + ) + } +} + +@Preview(backgroundColor = 0xFFFFFF) +@Composable +private fun BookmarkStatePopupPreview() { + RemoveBookmarkSnackBar(onClick = {}) +} diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkItemUiState.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkItemUiState.kt index 3bf737b4..523d35eb 100644 --- a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkItemUiState.kt +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkItemUiState.kt @@ -9,7 +9,6 @@ import kotlinx.datetime.toJavaLocalDateTime data class BookmarkItemUiState( val index: Int, val session: Session, - val isEditMode: Boolean, ) { val sequence: Int diff --git a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkUiState.kt b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkUiState.kt index ee533d50..433ce564 100644 --- a/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkUiState.kt +++ b/feature/bookmark/src/main/java/com/droidknights/app/feature/bookmark/model/BookmarkUiState.kt @@ -3,7 +3,9 @@ package com.droidknights.app.feature.bookmark.model import androidx.compose.runtime.Immutable import androidx.compose.runtime.Stable import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableSet import kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentSetOf @Stable sealed interface BookmarkUiState { @@ -13,7 +15,8 @@ sealed interface BookmarkUiState { @Immutable data class Success( - val isEditButtonSelected: Boolean = false, + val isEditMode: Boolean = false, val bookmarks: ImmutableList = persistentListOf(), + val selectedSessionIds: ImmutableSet = persistentSetOf(), ) : BookmarkUiState } diff --git a/feature/bookmark/src/main/res/drawable/ic_check.xml b/feature/bookmark/src/main/res/drawable/ic_check.xml new file mode 100644 index 00000000..d3abf9e7 --- /dev/null +++ b/feature/bookmark/src/main/res/drawable/ic_check.xml @@ -0,0 +1,11 @@ + + + diff --git a/feature/bookmark/src/main/res/drawable/ic_menu.xml b/feature/bookmark/src/main/res/drawable/ic_menu.xml new file mode 100644 index 00000000..bc2090eb --- /dev/null +++ b/feature/bookmark/src/main/res/drawable/ic_menu.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/feature/bookmark/src/main/res/drawable/ic_trash.xml b/feature/bookmark/src/main/res/drawable/ic_trash.xml new file mode 100644 index 00000000..ddde66f3 --- /dev/null +++ b/feature/bookmark/src/main/res/drawable/ic_trash.xml @@ -0,0 +1,9 @@ + + + diff --git a/feature/bookmark/src/main/res/values/string.xml b/feature/bookmark/src/main/res/values/string.xml index 56ab453e..3aff9fe8 100644 --- a/feature/bookmark/src/main/res/values/string.xml +++ b/feature/bookmark/src/main/res/values/string.xml @@ -4,6 +4,8 @@ 편집 완료 북마크된 아이템이 없습니다 + 드래그 앤 드롭 + 북마크에서 삭제하기 HH:mm