diff --git a/app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt b/app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt deleted file mode 100644 index 3a65d791..00000000 --- a/app/src/main/java/com/flint/core/designsystem/component/progressbar/UnderImageProgressBar.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.flint.core.designsystem.component.progressbar - -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.tooling.preview.PreviewParameter -import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.dp -import com.flint.core.designsystem.theme.FlintTheme - -@Composable -fun UnderImageProgressBar( - progress: Float, - modifier: Modifier = Modifier, -) { - Box( - modifier = - modifier - .height(5.dp) - .background(FlintTheme.colors.gray700), - ) { - Box( - modifier = - Modifier - .fillMaxWidth(progress) - .fillMaxHeight() - .clip(CircleShape) - .background(FlintTheme.colors.secondary500), - ) - } -} - -@Preview -@Composable -private fun UnderImageProgressBarPreview( - @PreviewParameter(UnderImageProgressBarPreviewParameterProvider::class) progress: Float, -) { - FlintTheme { - UnderImageProgressBar( - progress = progress, - modifier = Modifier.fillMaxWidth(), - ) - } -} - -private class UnderImageProgressBarPreviewParameterProvider : PreviewParameterProvider { - override val values: Sequence = sequenceOf(0f, 0.25f, 0.5f, 0.75f, 1f) -} diff --git a/app/src/main/java/com/flint/core/navigation/Route.kt b/app/src/main/java/com/flint/core/navigation/Route.kt index 66dd2a26..927214cf 100644 --- a/app/src/main/java/com/flint/core/navigation/Route.kt +++ b/app/src/main/java/com/flint/core/navigation/Route.kt @@ -40,13 +40,21 @@ interface Route { data class CollectionDetail( val collectionId: String, val targetImageUrl: String? = null, + val showEditSuccessToast: Boolean = false, + ) : Route + + @Serializable + data class CollectionReport( + val collectionId: String, ) : Route @Serializable data object CollectionCreate : Route @Serializable - data object CollectionCreateGraph : Route + data class CollectionCreateGraph( + val collectionId: String? = null, + ) : Route @Serializable data object SavedContentList : Route diff --git a/app/src/main/java/com/flint/data/api/CollectionApi.kt b/app/src/main/java/com/flint/data/api/CollectionApi.kt index 47bb9bba..ce09beea 100644 --- a/app/src/main/java/com/flint/data/api/CollectionApi.kt +++ b/app/src/main/java/com/flint/data/api/CollectionApi.kt @@ -1,5 +1,6 @@ package com.flint.data.api +import com.flint.data.dto.base.BaseEmptyResponse import com.flint.data.dto.base.BaseResponse import com.flint.data.dto.collection.request.CollectionCreateRequestDto import com.flint.data.dto.collection.response.CollectionCreateResponseDto @@ -7,8 +8,10 @@ import com.flint.data.dto.collection.response.CollectionDetailResponseDto import com.flint.data.dto.collection.response.CollectionsResponseDto import com.flint.data.dto.collection.response.RecentCollectionListResponseDto import retrofit2.http.Body +import retrofit2.http.DELETE import retrofit2.http.GET import retrofit2.http.POST +import retrofit2.http.PUT import retrofit2.http.Path import retrofit2.http.Query @@ -32,6 +35,19 @@ interface CollectionApi { @Path("collectionId") collectionId: String, ): BaseResponse + // 컬렉션 수정 + @PUT("/api/v1/collections/{collectionId}") + suspend fun updateCollection( + @Path("collectionId") collectionId: String, + @Body requestDto: CollectionCreateRequestDto, + ): BaseEmptyResponse + + // 컬렉션 삭제 + @DELETE("/api/v1/collections/{collectionId}") + suspend fun deleteCollection( + @Path("collectionId") collectionId: String, + ): BaseEmptyResponse + // 최근 본 컬렉션 목록 조회 @GET("/api/v1/collections/recent") suspend fun getRecentCollectionList(): BaseResponse diff --git a/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt b/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt index a0791eec..2f04acee 100644 --- a/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt +++ b/app/src/main/java/com/flint/data/dto/collection/request/CollectionCreateRequestDto.kt @@ -24,7 +24,7 @@ data class CollectionCreateRequestDto( val isSpoiler: Boolean, @SerialName("reason") val reason: String, - @SerialName("imageUrls") + @SerialName("customImages") val imageUrls: List = emptyList(), ) } diff --git a/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt b/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt index c1ba8c3d..a92c54a8 100644 --- a/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt +++ b/app/src/main/java/com/flint/data/dto/collection/response/CollectionDetailResponseDto.kt @@ -12,6 +12,7 @@ data class CollectionDetailResponseDto( @SerialName("id") val id: String, @SerialName("thumbnailUrl") val thumbnailUrl: String, @SerialName("isBookmarked") val isBookmarked: Boolean, + @SerialName("isPublic") val isPublic: Boolean, @SerialName("title") val title: String, ) { @Serializable @@ -31,6 +32,7 @@ data class CollectionDetailResponseDto( @SerialName("isSpoiler") val isSpoiler: Boolean, @SerialName("reason") val reason: String, @SerialName("imageUrl") val imageUrl: String, + @SerialName("customImageUrls") val customImageUrls: List = emptyList(), @SerialName("title") val title: String, @SerialName("year") val year: Int, ) diff --git a/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt b/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt index f274d793..1d56bac5 100644 --- a/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt +++ b/app/src/main/java/com/flint/domain/mapper/collection/CollectionDetailMapper.kt @@ -17,6 +17,7 @@ fun CollectionDetailResponseDto.toModel(): CollectionDetailModelNew { id = id, thumbnailUrl = thumbnailUrl, isBookmarked = isBookmarked, + isPublic = isPublic, title = title, ) } @@ -39,6 +40,7 @@ private fun CollectionDetailResponseDto.Content.toModel(): ContentModelNew { isSpoiler = isSpoiler, reason = reason, imageUrl = imageUrl, + customImageUrls = customImageUrls, title = title, year = year, ) diff --git a/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt b/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt index 3810500b..4477bc43 100644 --- a/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt +++ b/app/src/main/java/com/flint/domain/model/collection/CollectionDetailModel.kt @@ -13,5 +13,6 @@ data class CollectionDetailModelNew( val id: String, val thumbnailUrl: String, val isBookmarked: Boolean, + val isPublic: Boolean, val title: String, ) diff --git a/app/src/main/java/com/flint/domain/model/content/ContentModel.kt b/app/src/main/java/com/flint/domain/model/content/ContentModel.kt index 61048659..9951dac0 100644 --- a/app/src/main/java/com/flint/domain/model/content/ContentModel.kt +++ b/app/src/main/java/com/flint/domain/model/content/ContentModel.kt @@ -11,6 +11,7 @@ data class ContentModelNew( val isSpoiler: Boolean, val reason: String, val imageUrl: String, + val customImageUrls: List = emptyList(), val title: String, val year: Int, ) diff --git a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt index 46c0ce2c..2a0e315c 100644 --- a/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt +++ b/app/src/main/java/com/flint/domain/repository/CollectionRepository.kt @@ -41,6 +41,23 @@ class CollectionRepository @Inject constructor( response.toModel() } + // 컬렉션 수정 + suspend fun updateCollection( + collectionId: String, + requestDto: CollectionCreateRequestDto, + ): Result = + suspendRunCatching { + apiService.updateCollection(collectionId, requestDto) + Unit + } + + // 컬렉션 삭제 + suspend fun deleteCollection(collectionId: String): Result = + suspendRunCatching { + apiService.deleteCollection(collectionId) + Unit + } + // 최근 본 컬렉션 목록 조회 suspend fun getRecentCollectionList(): Result = suspendRunCatching { apiService.getRecentCollectionList().data.toModel() } diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt index 5055cd2f..3369a129 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateScreen.kt @@ -120,6 +120,7 @@ fun CollectionCreateRoute( contentImageLauncher.launch("image/*") } }, + onRemoveExistingContentImage = viewModel::removeExistingContentImageUrl, onRemoveContentImage = viewModel::removeContentImageUri, modifier = Modifier.padding(paddingValues), ) @@ -141,6 +142,7 @@ fun CollectionCreateScreen( onGalleryClick: () -> Unit = {}, onThumbnailDelete: () -> Unit = {}, onSelectContentImage: (contentId: String) -> Unit = {}, + onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, onRemoveContentImage: (contentId: String, index: Int) -> Unit = { _, _ -> }, modifier: Modifier = Modifier, ) { @@ -163,7 +165,7 @@ fun CollectionCreateScreen( // 썸네일 item { CollectionCreateThumbnail( - imageUrl = uiState.thumbnailImageUri, + imageUrl = uiState.thumbnailImageUri ?: uiState.existingThumbnailUrl, onClick = { isThumbnailBottomSheetVisible = true }, ) @@ -212,6 +214,7 @@ fun CollectionCreateScreen( onReasonChanged = onReasonChanged, onAddContentClick = onAddContentClick, onSelectContentImage = onSelectContentImage, + onRemoveExistingContentImage = onRemoveExistingContentImage, onRemoveContentImage = onRemoveContentImage, modifier = Modifier.padding(horizontal = (16).dp), ) @@ -408,6 +411,7 @@ private fun CollectionAddContentSection( onReasonChanged: (String, String) -> Unit, onAddContentClick: () -> Unit, onSelectContentImage: (contentId: String) -> Unit, + onRemoveExistingContentImage: (contentId: String, index: Int) -> Unit, onRemoveContentImage: (contentId: String, index: Int) -> Unit, modifier: Modifier = Modifier, ) { @@ -460,9 +464,11 @@ private fun CollectionAddContentSection( Spacer(Modifier.height(16.dp)) - if (detail.contentImageUris.isNotEmpty()) { + if (detail.existingImageUrls.isNotEmpty() || detail.contentImageUris.isNotEmpty()) { CollectionCreateContentImage( + existingImageUrls = detail.existingImageUrls, imageUris = detail.contentImageUris, + onDeleteExistingClick = { index -> onRemoveExistingContentImage(content.id, index) }, onDeleteClick = { index -> onRemoveContentImage(content.id, index) }, ) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt index df4a9c5b..8f0be3e2 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/CollectionCreateViewModel.kt @@ -2,6 +2,7 @@ package com.flint.presentation.collectioncreate import android.content.Context import android.net.Uri +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.flint.core.common.util.UiState @@ -35,11 +36,15 @@ import javax.inject.Inject @HiltViewModel class CollectionCreateViewModel @Inject constructor( + savedStateHandle: SavedStateHandle, @ApplicationContext private val context: Context, private val collectionRepository: CollectionRepository, private val searchRepository: SearchRepository, private val storageRepository: StorageRepository, ) : ViewModel() { + + private val editingCollectionId: String? = savedStateHandle["collectionId"] + val isEditMode: Boolean = editingCollectionId != null private val _uiState = MutableStateFlow(CollectionCreateUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -52,11 +57,18 @@ class CollectionCreateViewModel @Inject constructor( init { observeSearchQuery() loadInitialContents() + if (editingCollectionId != null) { + loadCollectionForEdit(editingCollectionId) + } } fun onClickFinish() { if (_uiState.value.isLoading) return - postCollectionCreate() + if (editingCollectionId != null) { + putCollectionUpdate(editingCollectionId) + } else { + postCollectionCreate() + } } private fun postCollectionCreate() { @@ -102,6 +114,106 @@ class CollectionCreateViewModel @Inject constructor( } } + private fun loadCollectionForEdit(collectionId: String) { + viewModelScope.launch { + collectionRepository.getCollectionDetail(collectionId) + .onSuccess { detail -> + val selectedContents = detail.contents.map { content -> + SearchContentItemModel( + id = content.id, + title = content.title, + author = content.director, + posterUrl = content.imageUrl, + year = content.year, + ) + }.toImmutableList() + + val contentDetailsMap = detail.contents.associate { content -> + content.id to ContentDetail( + isSpoiler = content.isSpoiler, + reason = content.reason, + existingImageUrls = content.customImageUrls, + ) + } + + val thumbnailUrl = detail.thumbnailUrl.ifBlank { null } + val originalDetails = detail.contents.associate { content -> + content.id to Pair(content.isSpoiler, content.reason) + } + val originalImageUrls = detail.contents.associate { content -> + content.id to content.customImageUrls + } + + _uiState.update { + it.copy( + existingThumbnailUrl = thumbnailUrl, + title = detail.title, + description = detail.description, + isPublic = detail.isPublic, + selectedContents = selectedContents, + contentDetailsMap = contentDetailsMap, + originalTitle = detail.title, + originalDescription = detail.description, + originalIsPublic = detail.isPublic, + originalThumbnailUrl = thumbnailUrl, + originalContentIds = detail.contents.map { it.id }.toSet(), + originalContentDetails = originalDetails, + originalContentImageUrls = originalImageUrls, + ) + } + } + .onFailure { e -> Timber.e(e, "컬렉션 편집 로드 실패") } + } + } + + private fun putCollectionUpdate(collectionId: String) { + viewModelScope.launch { + _uiState.update { it.copy(isLoading = true) } + try { + val thumbnailKey = if (_uiState.value.thumbnailImageUri != null) { + uploadImageIfNeeded(_uiState.value.thumbnailImageUri, StoragePathType.COLLECTION_THUMBNAIL) + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } + } else { + null + } + + val contentImageKeysMap = uploadContentImagesIfNeeded() + .getOrElse { + _createSuccess.emit(UiState.Failure) + return@launch + } + + val requestModel = CollectionCreateRequestModel( + imageUrl = thumbnailKey ?: _uiState.value.existingThumbnailUrl ?: "", + title = _uiState.value.title, + description = _uiState.value.description.ifBlank { "" }, + isPublic = _uiState.value.isPublic ?: true, + contentList = _uiState.value.selectedContents.map { content -> + val detail = _uiState.value.contentDetailsMap[content.id] ?: ContentDetail() + CollectionCreateContentModel( + contentId = content.id, + isSpoiler = detail.isSpoiler, + reason = detail.reason.ifBlank { "" }, + imageUrls = detail.existingImageUrls + (contentImageKeysMap[content.id] ?: emptyList()), + ) + }, + ) + + collectionRepository + .updateCollection(collectionId, requestModel.toDto()) + .onSuccess { + _createSuccess.emit(UiState.Success(collectionId)) + } + .onFailure { e -> Timber.e(e, "컬렉션 수정 실패") } + } finally { + _uiState.update { it.copy(isLoading = false) } + } + } + } + fun resetCreateSuccess() = viewModelScope.launch { _createSuccess.emit(UiState.Empty) } @@ -236,6 +348,17 @@ class CollectionCreateViewModel @Inject constructor( } } + fun removeExistingContentImageUrl(contentId: String, index: Int) { + _uiState.update { state -> + val current = state.contentDetailsMap[contentId] ?: return@update state + if (index !in current.existingImageUrls.indices) return@update state + val updated = current.copy( + existingImageUrls = current.existingImageUrls.toMutableList().also { it.removeAt(index) } + ) + state.copy(contentDetailsMap = state.contentDetailsMap + (contentId to updated)) + } + } + fun removeContentImageUri(contentId: String, index: Int) { _uiState.update { state -> val current = state.contentDetailsMap[contentId] ?: return@update state diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt index 127b0647..e1d797ca 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/component/CollectionCreateContentImage.kt @@ -37,17 +37,20 @@ import com.flint.core.designsystem.theme.FlintTheme @Composable fun CollectionCreateContentImage( + existingImageUrls: List, imageUris: List, + onDeleteExistingClick: (index: Int) -> Unit, onDeleteClick: (index: Int) -> Unit, modifier: Modifier = Modifier, ) { - if (imageUris.isEmpty()) return + val images: List = existingImageUrls + imageUris + if (images.isEmpty()) return val pageCount = Int.MAX_VALUE val pagerState = rememberPagerState( - initialPage = pageCount / 2 - (pageCount / 2) % imageUris.size, + initialPage = pageCount / 2 - (pageCount / 2) % images.size, ) { pageCount } - val currentIndex = pagerState.currentPage % imageUris.size + val currentIndex = pagerState.currentPage % images.size var prevSize by remember { mutableIntStateOf(-1) } var deletedIndex by remember { mutableIntStateOf(-1) } @@ -81,13 +84,13 @@ fun CollectionCreateContentImage( Column(modifier = modifier) { HorizontalPager( state = pagerState, - userScrollEnabled = imageUris.size > 1, + userScrollEnabled = images.size > 1, modifier = Modifier.fillMaxWidth(), ) { page -> - val index = page % imageUris.size + val index = page % images.size Box { NetworkImage( - imageUrl = imageUris[index], + imageUrl = images[index], contentScale = ContentScale.FillBounds, modifier = Modifier .fillMaxWidth() @@ -100,23 +103,27 @@ fun CollectionCreateContentImage( modifier = Modifier .align(Alignment.TopEnd) .clickable { - deletedIndex = index - onDeleteClick(index) - } + deletedIndex = index + if (index < existingImageUrls.size) { + onDeleteExistingClick(index) + } else { + onDeleteClick(index - existingImageUrls.size) + } + } .padding(all = 16.dp) .size(24.dp), ) } } - if (imageUris.size > 1) { + if (images.size > 1) { Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), ) { - repeat(imageUris.size) { index -> + repeat(images.size) { index -> Box( modifier = Modifier .size(8.dp) @@ -137,11 +144,12 @@ fun CollectionCreateContentImage( private fun CollectionCreateContentImagePreview() { FlintTheme { CollectionCreateContentImage( + existingImageUrls = listOf("https://example.com/1"), imageUris = listOf( - Uri.parse("https://example.com/1"), Uri.parse("https://example.com/2"), Uri.parse("https://example.com/3"), ), + onDeleteExistingClick = {}, onDeleteClick = {}, ) } diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt b/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt index 27e4f19c..17ecfa87 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/navigation/CollectionCreateNavigation.kt @@ -21,6 +21,13 @@ fun NavController.navigateToCollectionCreate( navigate(Route.CollectionCreate, navOptions) } +fun NavController.navigateToCollectionEdit( + collectionId: String, + navOptions: NavOptions? = null, +) { + navigate(Route.CollectionCreateGraph(collectionId = collectionId), navOptions) +} + fun NavController.navigateToAddContent( navOptions: NavOptions? = null ) { @@ -41,13 +48,19 @@ fun NavGraphBuilder.collectionCreateNavGraph( paddingValues = paddingValues, navigateToAddContent = navController::navigateToAddContent, navigateUp = navController::navigateUp, - navigateToCollectionDetail = { - navController.navigateToCollectionDetail(it, - navOptions = navOptions { - popUpTo { - inclusive = true // 현재 화면을 백스택에서 제거 + navigateToCollectionDetail = { collectionId -> + if (viewModel.isEditMode) { + navController.navigateToCollectionDetail( + collectionId = collectionId, + showEditSuccessToast = true, + navOptions = navOptions { + popUpTo { inclusive = true } } - }) + ) + } else { + navController.popBackStack(inclusive = true) + navController.navigateToCollectionDetail(collectionId = collectionId) + } }, viewModel = viewModel ) diff --git a/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt b/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt index de80c34b..9939addf 100644 --- a/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt +++ b/app/src/main/java/com/flint/presentation/collectioncreate/uistate/CollectionCreateUiState.kt @@ -9,6 +9,7 @@ import kotlinx.collections.immutable.persistentListOf @Immutable data class CollectionCreateUiState( val thumbnailImageUri: Uri? = null, + val existingThumbnailUrl: String? = null, val title: String = "", val description: String = "", val isPublic: Boolean? = null, @@ -17,12 +18,39 @@ data class CollectionCreateUiState( val contents: ImmutableList = persistentListOf(), val searchText: String = "", val isLoading: Boolean = false, + // 수정 모드 원본값 (null이면 생성 모드) + val originalTitle: String? = null, + val originalDescription: String = "", + val originalIsPublic: Boolean? = null, + val originalThumbnailUrl: String? = null, + val originalContentIds: Set = emptySet(), + val originalContentDetails: Map> = emptyMap(), + val originalContentImageUrls: Map> = emptyMap(), ) { - val isFinishButtonEnabled: Boolean = + private val isEditMode: Boolean get() = originalTitle != null + + private val hasChanges: Boolean get() = isEditMode && ( + title != originalTitle || + description != originalDescription || + isPublic != originalIsPublic || + thumbnailImageUri != null || + existingThumbnailUrl != originalThumbnailUrl || + selectedContents.map { it.id }.toSet() != originalContentIds || + contentDetailsMap.any { (id, detail) -> + val original = originalContentDetails[id] + detail.isSpoiler != original?.first || + detail.reason != original?.second || + detail.contentImageUris.isNotEmpty() || + detail.existingImageUrls != (originalContentImageUrls[id] ?: emptyList()) + } + ) + + val isFinishButtonEnabled: Boolean get() = !isLoading && title.isNotBlank() && isPublic != null && - selectedContents.size >= 2 + selectedContents.size >= 2 && + (!isEditMode || hasChanges) val isCancelModalVisible: Boolean = contentDetailsMap.values.any { it.reason.isNotBlank() } @@ -32,5 +60,6 @@ data class CollectionCreateUiState( data class ContentDetail( val isSpoiler: Boolean = false, val reason: String = "", + val existingImageUrls: List = emptyList(), val contentImageUris: List = emptyList(), ) \ No newline at end of file diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt index a4eb4e72..94e59db4 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailScreen.kt @@ -1,78 +1,51 @@ package com.flint.presentation.collectiondetail -import androidx.compose.foundation.Image import androidx.compose.foundation.LocalOverscrollFactory -import androidx.compose.foundation.ScrollState 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 import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.verticalScroll +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.LayoutCoordinates -import androidx.compose.ui.layout.onGloballyPositioned -import androidx.compose.ui.layout.positionInParent -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.vectorResource -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.flint.R -import com.flint.core.common.extension.noRippleClickable import com.flint.core.common.util.UiState -import com.flint.core.designsystem.component.button.FlintSaveDoneButton -import com.flint.core.designsystem.component.button.FlintSaveNoneButton import com.flint.core.designsystem.component.collection.PeopleBottomSheet -import com.flint.core.designsystem.component.collection.Spoiler -import com.flint.core.designsystem.component.image.NetworkImage -import com.flint.core.designsystem.component.image.ProfileImage import com.flint.core.designsystem.component.indicator.FlintLoadingIndicator -import com.flint.core.designsystem.component.progressbar.UnderImageProgressBar import com.flint.core.designsystem.component.toast.ShowSaveToast import com.flint.core.designsystem.component.toast.ShowToast -import com.flint.core.designsystem.component.topappbar.FlintBackTopAppbar import com.flint.core.designsystem.theme.FlintTheme import com.flint.core.navigation.model.CollectionListRouteType import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel import com.flint.domain.model.collection.CollectionDetailModelNew import com.flint.domain.model.content.ContentModelNew import com.flint.domain.type.UserRoleType +import com.flint.presentation.collectiondetail.component.CollectionCopyrightFooter +import com.flint.presentation.collectiondetail.component.CollectionDetailDeleteModal +import com.flint.presentation.collectiondetail.component.CollectionDetailDescription +import com.flint.presentation.collectiondetail.component.CollectionDetailThumbnail +import com.flint.presentation.collectiondetail.component.CollectionDetailContent +import com.flint.presentation.collectiondetail.component.PeopleWhoSavedThisCollection import com.flint.presentation.collectiondetail.sideeffect.CollectionDetailSideEffect import com.flint.presentation.collectiondetail.uistate.CollectionDetailUiState import kotlinx.collections.immutable.ImmutableList @@ -80,6 +53,7 @@ import kotlinx.collections.immutable.persistentListOf import java.time.format.DateTimeFormatter private const val DATE_FORMAT_TO_SHOW = "yyyy. MM. dd." +private const val CONTENT_LIST_HEADER_ITEM_COUNT = 1 @Composable fun CollectionDetailRoute( @@ -87,7 +61,10 @@ fun CollectionDetailRoute( navigateToCollectionList: (CollectionListRouteType) -> Unit, navigateToProfile: (authorId: String) -> Unit, navigateUp: () -> Unit, + navigateUpWithDeleteSuccess: () -> Unit, + navigateToCollectionEdit: (collectionId: String) -> Unit, targetImageUrl: String? = null, + showEditSuccessToast: Boolean = false, viewModel: CollectionDetailViewModel = hiltViewModel(), ) { val uiState: UiState by viewModel.uiState.collectAsStateWithLifecycle() @@ -95,6 +72,8 @@ fun CollectionDetailRoute( var showCollectionSaveToast: Boolean by remember { mutableStateOf(false) } var showContentSaveToast: Boolean by remember { mutableStateOf(false) } var showContentCancelToast: Boolean by remember { mutableStateOf(false) } + var showDeleteModal: Boolean by remember { mutableStateOf(false) } + var showEditSuccessToastState: Boolean by remember { mutableStateOf(showEditSuccessToast) } when (val uiState = uiState) { UiState.Loading -> { @@ -109,6 +88,7 @@ fun CollectionDetailRoute( CollectionDetailScreen( paddingValues = paddingValues, targetImageUrl = targetImageUrl, + thumbnailUrl = collectionDetail.thumbnailUrl, title = collectionDetail.title, isBookmarked = collectionDetail.isBookmarked, authorNickname = collectionDetail.author.nickname, @@ -126,12 +106,42 @@ fun CollectionDetailRoute( onSpoilClick = viewModel::spoil, onAuthorNicknameClick = { navigateToProfile(collectionDetail.author.id) }, onAuthorClick = navigateToProfile, + isMine = uiState.data.isMine, + onEditClick = { + navigateToCollectionEdit(collectionDetail.id) + }, + onDeleteClick = { + showDeleteModal = true + }, + onReportClick = { + // TODO: 신고(Route.CollectionReport) 화면 복구 후 navigateToCollectionReport(collectionDetail.id) 연결 + }, ) } else -> {} } + if (showDeleteModal) { + CollectionDetailDeleteModal( + onConfirm = { + showDeleteModal = false + viewModel.deleteCollection() + }, + onDismiss = { showDeleteModal = false }, + ) + } + + if (showEditSuccessToastState) { + ShowToast( + text = "컬렉션이 수정되었어요", + imageVector = null, + paddingValues = paddingValues, + yOffset = 12.dp, + hide = { showEditSuccessToastState = false }, + ) + } + if (showCollectionCancelToast) { ShowToast( text = "컬렉션 저장이 취소되었어요", @@ -206,6 +216,12 @@ fun CollectionDetailRoute( showContentSaveToast = false } } + + CollectionDetailSideEffect.DeleteCollectionSuccess -> navigateUpWithDeleteSuccess() + + CollectionDetailSideEffect.DeleteCollectionFailure -> { + // TODO: 삭제 실패 다이얼로그 + } } } } @@ -215,6 +231,7 @@ fun CollectionDetailRoute( @Composable fun CollectionDetailScreen( paddingValues: PaddingValues, + thumbnailUrl: String, title: String, isBookmarked: Boolean, authorNickname: String, @@ -230,30 +247,24 @@ fun CollectionDetailScreen( onSpoilClick: (String) -> Unit, onAuthorNicknameClick: () -> Unit, onAuthorClick: (authorId: String) -> Unit, + isMine: Boolean, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onReportClick: () -> Unit, targetImageUrl: String? = null, ) { CompositionLocalProvider( LocalOverscrollFactory provides null, ) { var showPeopleBottomSheet: Boolean by remember { mutableStateOf(false) } - val scrollState: ScrollState = rememberScrollState() - var thumbnailHeight: Int by remember { mutableIntStateOf(0) } - val contentPositions: MutableMap = remember { mutableMapOf() } - - val scrollProgress: Float = - if (scrollState.maxValue > 0) { - scrollState.value.toFloat() / scrollState.maxValue - } else { - 0f - } - - val isProgressBarSticky: Boolean = scrollState.value >= thumbnailHeight + val lazyListState: LazyListState = rememberLazyListState() LaunchedEffect(Unit) { if (targetImageUrl == null) return@LaunchedEffect - val targetPosition: Int = contentPositions[targetImageUrl] ?: return@LaunchedEffect + val targetIndex: Int = contents.indexOfFirst { it.imageUrl == targetImageUrl } + if (targetIndex == -1) return@LaunchedEffect - scrollState.animateScrollTo(targetPosition) + lazyListState.animateScrollToItem(CONTENT_LIST_HEADER_ITEM_COUNT + targetIndex) } if (showPeopleBottomSheet) { @@ -268,661 +279,89 @@ fun CollectionDetailScreen( } Column( - modifier = - Modifier - .fillMaxSize() - .padding(paddingValues) - .background(color = FlintTheme.colors.background), + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .background(color = FlintTheme.colors.background), ) { - FlintBackTopAppbar( - onClick = navigateUp, - backgroundColor = Color.Transparent, + // 썸네일 영역은 스크롤해도 화면 상단에 고정 + CollectionDetailThumbnail( + thumbnailImage = thumbnailUrl, + title = title, + isBookmarked = isBookmarked, + isMine = isMine, + onBackClick = navigateUp, + onSaveDoneButtonClick = onSaveDoneButtonClick, + onSaveNoneButtonClick = onSaveNoneButtonClick, + onEditClick = onEditClick, + onDeleteClick = onDeleteClick, + onReportClick = onReportClick, ) - Box { - Column( - modifier = - Modifier - .fillMaxSize() - .verticalScroll(scrollState) - .padding(bottom = 24.dp), - ) { - Thumbnail( - title = title, - isBookmarked = isBookmarked, - onSaveDoneButtonClick = onSaveDoneButtonClick, - onSaveNoneButtonClick = onSaveNoneButtonClick, - modifier = Modifier.onGloballyPositioned { coordinates: LayoutCoordinates -> - thumbnailHeight = coordinates.size.height - }, - ) - - if (!isProgressBarSticky) { - UnderImageProgressBar(progress = scrollProgress) - } else { - // sticky 상태일 때 공간 유지 - Spacer(Modifier.height(5.dp)) - } - - Spacer(Modifier.height(24.dp)) - - CollectionDetailDescription( - authorNickname = authorNickname, - authorUserRoleType = authorUserRoleType, - createdAt = createdAt, - collectionContent = description, - onAuthorNicknameClick = onAuthorNicknameClick, - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) - - Spacer(Modifier.height(48.dp)) - - contents.forEach { content: ContentModelNew -> - Content( - content = content, - onBookmarkIconClick = onBookmarkIconClick, - onSpoilClick = onSpoilClick, - modifier = Modifier.onGloballyPositioned { coordinates -> - contentPositions[content.imageUrl] = - coordinates.positionInParent().y.toInt() - }, - ) - } - - if (people.isNotEmpty()) { - PeopleWhoSavedThisCollection( - people = people, - onMoreClick = { showPeopleBottomSheet = true }, + LazyColumn( + state = lazyListState, + modifier = Modifier + .weight(1f) + .fillMaxWidth(), + ) { + item { + Column { + Spacer(Modifier.height(24.dp)) + + CollectionDetailDescription( + authorNickname = authorNickname, + authorUserRoleType = authorUserRoleType, + createdAt = createdAt, + collectionContent = description, + onAuthorNicknameClick = onAuthorNicknameClick, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), ) } } - // Sticky ProgressBar - if (isProgressBarSticky) { - UnderImageProgressBar( - progress = scrollProgress, - modifier = Modifier.fillMaxWidth(), - ) - } - } - } - } -} - -@Composable -private fun PeopleWhoSavedThisCollection( - people: ImmutableList, - onMoreClick: () -> Unit, -) { - Column( - modifier = Modifier.padding(vertical = 10.dp), - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Text( - text = "이 컬렉션을 저장한 사람들", - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - modifier = - Modifier - .weight(1f) - .padding(horizontal = 16.dp), - ) - - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_more), - contentDescription = null, - modifier = - Modifier - .size(48.dp) - .clickable(onClick = onMoreClick) - .padding(12.dp), - tint = FlintTheme.colors.white, - ) - } - - Spacer(Modifier.height(24.dp)) - - Row( - modifier = Modifier.padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - horizontalArrangement = Arrangement.spacedBy((-12).dp), - ) { - people.take(5).forEach { author: CollectionBookmarkUsersModel.User -> - ProfileImage( - imageUrl = author.profileImageUrl, - modifier = - Modifier - .size(56.dp) - .border(3.dp, FlintTheme.colors.background, CircleShape), - contentDescription = author.nickName, + items( + items = contents, + key = { content: ContentModelNew -> content.id }, + ) { content: ContentModelNew -> + CollectionDetailContent( + content = content, + onBookmarkIconClick = onBookmarkIconClick, + onSpoilClick = onSpoilClick, ) } - } - - Spacer(Modifier.width(8.dp)) - if (people.size >= 6) { - Row { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_plus), - contentDescription = "그 외", - tint = FlintTheme.colors.white, - ) - - Text( - text = (people.size - 5).toString(), - color = FlintTheme.colors.gray50, - style = FlintTheme.typography.head2M20, - ) - } - } - } - } -} - -private class PeoplePreviewProvider : - PreviewParameterProvider> { - override val values: Sequence> = - sequenceOf( - persistentListOf( - CollectionBookmarkUsersModel.User( - userId = "1", - nickName = "유저1", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - ), - persistentListOf( - CollectionBookmarkUsersModel.User( - userId = "1", - nickName = "유저1", - profileImageUrl = "", - userRole = UserRoleType.ADMIN, - ), - CollectionBookmarkUsersModel.User( - userId = "2", - nickName = "유저2", - profileImageUrl = "", - userRole = UserRoleType.FLINER, - ), - CollectionBookmarkUsersModel.User( - userId = "3", - nickName = "유저3", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "4", - nickName = "유저4", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "5", - nickName = "유저5", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - ), - persistentListOf( - CollectionBookmarkUsersModel.User( - userId = "1", - nickName = "유저1", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "2", - nickName = "유저2", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "3", - nickName = "유저3", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "4", - nickName = "유저4", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "5", - nickName = "유저5", - profileImageUrl = "", - userRole = UserRoleType.FLING, - ), - CollectionBookmarkUsersModel.User( - userId = "6", - nickName = "유저6", - profileImageUrl = "", - userRole = UserRoleType.FLINER, - ), - CollectionBookmarkUsersModel.User( - userId = "7", - nickName = "유저7", - profileImageUrl = "", - userRole = UserRoleType.ADMIN, - ), - ), - ) -} - -@Preview -@Composable -private fun PeopleWhoSavedThisCollectionPreview( - @PreviewParameter(PeoplePreviewProvider::class) people: ImmutableList, -) { - FlintTheme { - PeopleWhoSavedThisCollection( - people = people, - onMoreClick = {}, - ) - } -} - -@Composable -private fun Thumbnail( - title: String, - isBookmarked: Boolean, - onSaveDoneButtonClick: () -> Unit, - onSaveNoneButtonClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Box( - modifier = - modifier - .fillMaxWidth() - .aspectRatio(360f / 270f), - ) { - Image( - painter = painterResource(R.drawable.img_collection_bg2), - contentDescription = null, - modifier = Modifier.fillMaxSize(), - contentScale = ContentScale.FillBounds, - ) - - Column( - modifier = - Modifier - .fillMaxSize() - .padding(horizontal = 16.dp) - .padding(top = 55.dp, bottom = 19.dp), - horizontalAlignment = Alignment.End, - verticalArrangement = Arrangement.SpaceBetween, - ) { - Text( - text = title, - color = FlintTheme.colors.white, - style = FlintTheme.typography.display2M28, - modifier = Modifier.fillMaxWidth(), - maxLines = 2, - overflow = TextOverflow.Ellipsis - ) - if (isBookmarked) { - FlintSaveDoneButton( - onClick = { - onSaveDoneButtonClick() - }, - ) - } else { - FlintSaveNoneButton( - onClick = { - onSaveNoneButtonClick() - }, - ) - } - } - } -} - -@Composable -private fun CollectionDetailDescription( - authorNickname: String, - authorUserRoleType: UserRoleType, - createdAt: String, - collectionContent: String, - onAuthorNicknameClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(24.dp), - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = authorNickname, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - modifier = Modifier.noRippleClickable( - onClick = { onAuthorNicknameClick() } - ) - ) - - if (authorUserRoleType == UserRoleType.FLINER) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_qualified), - contentDescription = "플리너", - tint = Color.Unspecified, - ) - } else { - Text( - "|", - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.head3M18, - ) - } - - Text( - text = createdAt, - color = FlintTheme.colors.gray200, - style = FlintTheme.typography.body2M14, - ) - } - - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .clip(CircleShape) - .background(color = FlintTheme.colors.gray300), - ) - - Text( - text = collectionContent, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - ) - } -} - -@Composable -private fun Content( - content: ContentModelNew, - onBookmarkIconClick: (contentId: String) -> Unit, - onSpoilClick: (contentId: String) -> Unit, - modifier: Modifier = Modifier, -) { - Column(modifier = modifier) { - NetworkImage( - imageUrl = content.imageUrl, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(360f / 480f), - contentScale = ContentScale.Crop, - ) - - Spacer(Modifier.height(32.dp)) - - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column { - Text( - content.title, - color = FlintTheme.colors.white, - style = FlintTheme.typography.head2Sb20, - ) - - Spacer(Modifier.height(12.dp)) - - Text( - content.year.toString(), - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body1R16, - ) - - Text( - content.director, - color = FlintTheme.colors.gray300, - style = FlintTheme.typography.body1R16, - ) + item { + Spacer(Modifier.height(24.dp)) } - Row( - modifier = - Modifier.noRippleClickable(onClick = { onBookmarkIconClick(content.id) }), - ) { - Column( - modifier = - Modifier - .padding(start = 24.dp) - .padding(vertical = 3.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { - if (content.isBookmarked) { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_fill), - contentDescription = "저장됨", - tint = Color.Unspecified, - ) - } else { - Icon( - imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_empty), - contentDescription = "저장되지 않음", - tint = Color.White, - ) - } - - Text( - text = content.bookmarkCount.toString(), - color = FlintTheme.colors.white, - style = FlintTheme.typography.caption1M12, + if (people.isNotEmpty()) { + item { + PeopleWhoSavedThisCollection( + people = people, + onMoreClick = { showPeopleBottomSheet = true }, ) } } - } - Spacer(Modifier.height(24.dp)) - - Box( - modifier = - Modifier - .fillMaxWidth() - .height(1.dp) - .background(FlintTheme.colors.gray500), - ) - - Spacer(Modifier.height(24.dp)) - - if (content.isSpoiler) { - Spoiler( - onSpoilClick = { onSpoilClick(content.id) }, - modifier = Modifier.fillMaxWidth(), - ) { - Text( - text = content.reason, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - modifier = - Modifier - .defaultMinSize(minHeight = 183.dp) - .fillMaxWidth(), - ) + item { + CollectionCopyrightFooter() } - } else { - Text( - text = content.reason, - color = FlintTheme.colors.gray100, - style = FlintTheme.typography.body1R16, - modifier = - Modifier - .defaultMinSize(minHeight = 183.dp) - .fillMaxWidth(), - ) } - - Spacer(Modifier.height(64.dp)) } } } -private data class HeaderPreviewData( - val title: String, - val isMine: Boolean, - val isBookmarked: Boolean, -) - -private class HeaderPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - HeaderPreviewData( - title = "한번 보면 못 빠져나오는 여운남는 사랑이야기".repeat(2), - isMine = false, - isBookmarked = true, - ), - HeaderPreviewData( - title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", - isMine = false, - isBookmarked = false, - ), - ) -} - -@Preview -@Composable -private fun ThumbnailPreview( - @PreviewParameter(HeaderPreviewProvider::class) data: HeaderPreviewData, -) { - FlintTheme { - Thumbnail( - title = data.title, - isBookmarked = data.isBookmarked, - onSaveDoneButtonClick = {}, - onSaveNoneButtonClick = {} - ) - } -} - -private data class DescriptionPreviewData( - val authorNickname: String, - val authorUserRoleType: UserRoleType, - val createdAt: String, - val collectionContent: String, -) - -private class DescriptionPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - DescriptionPreviewData( - authorNickname = "키카", - authorUserRoleType = UserRoleType.FLINER, - createdAt = "2026. 01. 07.", - collectionContent = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", - ), - DescriptionPreviewData( - authorNickname = "일반유저", - authorUserRoleType = UserRoleType.FLING, - createdAt = "2026. 01. 15.", - collectionContent = "한글자 두글자 세글자 네글자 다섯글자 ".repeat(10), - ), - DescriptionPreviewData( - authorNickname = "관리자", - authorUserRoleType = UserRoleType.ADMIN, - createdAt = "2026. 01. 01.", - collectionContent = "공식 추천 컬렉션입니다", - ), - ) -} - -@Preview -@Composable -private fun CollectionDetailDescriptionPreview( - @PreviewParameter(DescriptionPreviewProvider::class) data: DescriptionPreviewData, -) { - FlintTheme { - CollectionDetailDescription( - authorNickname = data.authorNickname, - authorUserRoleType = data.authorUserRoleType, - createdAt = data.createdAt, - collectionContent = data.collectionContent, - onAuthorNicknameClick = {} - ) - } -} - -private class ContentPreviewProvider : PreviewParameterProvider { - override val values: Sequence = - sequenceOf( - ContentModelNew( - id = "0", - title = "드라마 제목", - year = 2000, - imageUrl = "", - director = "가스 제닝스", - reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", - isSpoiler = false, - isBookmarked = false, - bookmarkCount = 42, - ), - ContentModelNew( - id = "0", - title = "스포일러 있는 영화", - year = 2024, - imageUrl = "", - director = "감독 이름", - reason = "이 내용은 스포일러가 포함되어 있습니다.", - isSpoiler = true, - isBookmarked = false, - bookmarkCount = 42, - ), - ContentModelNew( - id = "0", - title = "저장된 영화", - year = 2023, - imageUrl = "", - director = "다른 감독", - reason = "내가 저장한 영화입니다.", - isSpoiler = false, - isBookmarked = true, - bookmarkCount = 42, - ), - ) -} - - -@Preview -@Composable -private fun ContentPreview( - @PreviewParameter(ContentPreviewProvider::class) content: ContentModelNew, -) { - FlintTheme { - Content( - content = content, - onBookmarkIconClick = {}, - onSpoilClick = {} - ) - } -} - private data class ScreenPreviewData( + val thumbnailUrl: String, val title: String, val isBookmarked: Boolean, val authorNickname: String, val authorUserRoleType: UserRoleType, val contents: ImmutableList, val people: ImmutableList, + val isMine: Boolean, ) private class ScreenPreviewProvider : PreviewParameterProvider { @@ -964,24 +403,29 @@ private class ScreenPreviewProvider : PreviewParameterProvider = sequenceOf( ScreenPreviewData( + thumbnailUrl = "", title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", isBookmarked = true, authorNickname = "키카", authorUserRoleType = UserRoleType.FLINER, contents = persistentListOf(sampleContent, sampleContent.copy(isSpoiler = true)), people = samplePeople, + isMine = true, ), ScreenPreviewData( + thumbnailUrl = "https://buly.kr/DEaVFRZ", title = "새로운 컬렉션", isBookmarked = false, authorNickname = "일반유저", authorUserRoleType = UserRoleType.FLING, contents = persistentListOf(sampleContent, sampleContent.copy(isSpoiler = true)), people = persistentListOf(), + isMine = false, ), ) } +@OptIn(ExperimentalMaterial3Api::class) @Preview @Composable private fun CollectionDetailScreenPreview( @@ -991,6 +435,7 @@ private fun CollectionDetailScreenPreview( Scaffold { paddingValues: PaddingValues -> CollectionDetailScreen( paddingValues = paddingValues, + thumbnailUrl = data.thumbnailUrl, title = data.title, isBookmarked = data.isBookmarked, authorNickname = data.authorNickname, @@ -1004,8 +449,12 @@ private fun CollectionDetailScreenPreview( navigateUp = {}, onBookmarkIconClick = {}, onSpoilClick = {}, + onAuthorNicknameClick = {}, onAuthorClick = {}, - onAuthorNicknameClick = {} + isMine = data.isMine, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, ) } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt index 2f115f4d..f7bed4f2 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/CollectionDetailViewModel.kt @@ -4,8 +4,10 @@ import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.toRoute +import com.flint.core.common.util.DataStoreKey.USER_ID import com.flint.core.common.util.UiState import com.flint.core.navigation.Route +import com.flint.data.local.PreferencesManager import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel import com.flint.domain.model.collection.CollectionDetailModelNew import com.flint.domain.model.content.ContentModelNew @@ -14,6 +16,7 @@ import com.flint.domain.repository.CollectionRepository import com.flint.presentation.collectiondetail.sideeffect.CollectionDetailSideEffect import com.flint.presentation.collectiondetail.uistate.CollectionDetailUiState import dagger.hilt.android.lifecycle.HiltViewModel +import timber.log.Timber import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.Deferred import kotlinx.coroutines.Job @@ -25,6 +28,7 @@ import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import javax.inject.Inject @@ -34,6 +38,7 @@ class CollectionDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, private val bookmarkRepository: BookmarkRepository, private val collectionRepository: CollectionRepository, + private val preferencesManager: PreferencesManager, ) : ViewModel() { init { val collectionId: String = savedStateHandle.toRoute().collectionId @@ -155,6 +160,21 @@ class CollectionDetailViewModel @Inject constructor( } } + fun deleteCollection() { + val collectionId = (_uiState.value as? UiState.Success)?.data?.collectionDetail?.id ?: return + viewModelScope.launch { + collectionRepository.deleteCollection(collectionId) + .onSuccess { + Timber.d("DELETE SUCCESS collectionId=$collectionId") + _sideEffect.emit(CollectionDetailSideEffect.DeleteCollectionSuccess) + } + .onFailure { + Timber.e(it, "DELETE FAILURE collectionId=$collectionId") + _sideEffect.emit(CollectionDetailSideEffect.DeleteCollectionFailure) + } + } + } + fun spoil(contentId: String) { _uiState.update { uiState: UiState -> if (uiState !is UiState.Success) return@update uiState @@ -203,11 +223,20 @@ class CollectionDetailViewModel @Inject constructor( async { collectionRepository.getCollectionDetail(collectionId) } val collectionBookmarkUsers: Deferred> = async { bookmarkRepository.getCollectionBookmarkUsers(collectionId) } + val myUserId: String = + runCatching { preferencesManager.getString(USER_ID).first() } + .getOrElse { + Timber.w(it, "USER_ID 조회 실패. isMine=false로 처리") + "" + } + + val collectionDetailResult: CollectionDetailModelNew = collectionDetail.await().getOrThrow() UiState.Success( CollectionDetailUiState( - collectionDetail = collectionDetail.await().getOrThrow(), - collectionBookmarkUsers = collectionBookmarkUsers.await().getOrThrow() + collectionDetail = collectionDetailResult, + collectionBookmarkUsers = collectionBookmarkUsers.await().getOrThrow(), + isMine = myUserId.isNotBlank() && collectionDetailResult.author.id == myUserId, ) ) }.onSuccess { newUiState: UiState.Success -> diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt new file mode 100644 index 00000000..0758a767 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionCopyrightFooter.kt @@ -0,0 +1,35 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionCopyrightFooter( + modifier: Modifier = Modifier, +) { + Text( + text = "Flint에서 제공하는 영화 · 드라마를 포함한 모든 콘텐츠의 저작권은 각 권리자에게 있으며, 관련 법령에 따라 보호됩니다.\n " + + "컬렉션 이용 시 저작권을 준수해 주세요.", + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body2R14, + modifier = modifier + .fillMaxWidth() + .background(color = FlintTheme.colors.gray800) + .padding(horizontal = 16.dp, vertical = 32.dp), + ) +} + +@Preview +@Composable +private fun CollectionCopyrightFooterPreview() { + FlintTheme { + CollectionCopyrightFooter() + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt new file mode 100644 index 00000000..a50172c6 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailContent.kt @@ -0,0 +1,269 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.HorizontalDivider +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.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.component.collection.Spoiler +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.content.ContentModelNew + +@Composable +fun CollectionDetailContent( + content: ContentModelNew, + onBookmarkIconClick: (contentId: String) -> Unit, + onSpoilClick: (contentId: String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Spacer(Modifier.height(48.dp)) + + if (content.customImageUrls.isNotEmpty()) { + CollectionDetailContentCarousel(content = content) + + Spacer(Modifier.height(32.dp)) + } + + CollectionDetailContentInfo( + content = content, + onBookmarkIconClick = onBookmarkIconClick, + onSpoilClick = onSpoilClick, + ) + } +} + +@Composable +private fun CollectionDetailContentCarousel(content: ContentModelNew) { + val images = content.customImageUrls + val pageCount = Int.MAX_VALUE + val pagerState = rememberPagerState( + initialPage = pageCount / 2 - (pageCount / 2) % images.size, + ) { pageCount } + val currentIndex = pagerState.currentPage % images.size + + HorizontalPager( + state = pagerState, + userScrollEnabled = images.size > 1, + modifier = Modifier.fillMaxWidth(), + ) { page -> + NetworkImage( + imageUrl = images[page % images.size], + modifier = Modifier + .fillMaxWidth() + .aspectRatio(360f / 270f), + contentScale = ContentScale.Crop, + ) + } + + if (images.size > 1) { + Spacer(Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), + ) { + repeat(images.size) { index -> + Box( + modifier = Modifier + .size(8.dp) + .clip(CircleShape) + .background( + if (index == currentIndex) { + FlintTheme.colors.secondary400 + } else { + FlintTheme.colors.gray500 + }, + ), + ) + } + } + } +} + +@Composable +private fun CollectionDetailContentInfo( + content: ContentModelNew, + onBookmarkIconClick: (contentId: String) -> Unit, + onSpoilClick: (contentId: String) -> Unit, +) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Row { + NetworkImage( + imageUrl = content.imageUrl, + modifier = Modifier.size(width = 60.dp, height = 90.dp), + contentScale = ContentScale.Crop, + ) + + Spacer(modifier = Modifier.width(14.dp)) + + Column { + Text( + text = content.title, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + ) + + Spacer(modifier = Modifier.height(12.dp)) + + Text( + text = content.year.toString(), + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1R16, + ) + + Text( + text = content.director, + color = FlintTheme.colors.gray300, + style = FlintTheme.typography.body1R16, + ) + } + } + + Column( + modifier = Modifier + .noRippleClickable(onClick = { onBookmarkIconClick(content.id) }) + .padding(top = 6.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + if (content.isBookmarked) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_fill), + contentDescription = "저장됨", + tint = Color.Unspecified, + ) + } else { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_bookmark_empty), + contentDescription = "저장되지 않음", + tint = Color.White, + ) + } + + Text( + text = content.bookmarkCount.toString(), + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.caption1M12, + ) + } + } + + Spacer(Modifier.height(24.dp)) + + HorizontalDivider(color = FlintTheme.colors.gray500, thickness = 1.dp) + + Spacer(Modifier.height(24.dp)) + + if (content.isSpoiler) { + Spoiler( + onSpoilClick = { onSpoilClick(content.id) }, + modifier = Modifier.fillMaxWidth(), + ) { + Text( + text = content.reason, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + modifier = Modifier.fillMaxWidth(), + ) + } + } else { + Text( + text = content.reason, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + modifier = Modifier.fillMaxWidth(), + ) + } + + Spacer(modifier = Modifier.height(20.dp)) + } +} + +private class CollectionDetailContentPreviewProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + ContentModelNew( + id = "0", + title = "드라마 제목", + year = 2000, + imageUrl = "", + director = "가스 제닝스", + reason = "달라진 온도\n-\n같은 구도에 채도를 달리해 변해버린 사랑을 시각적으로 담아낸 장면들", + isSpoiler = false, + isBookmarked = false, + bookmarkCount = 42, + ), + ContentModelNew( + id = "0", + title = "스포일러 있는 영화", + year = 2024, + imageUrl = "", + director = "감독 이름", + reason = "이 내용은 스포일러가 포함되어 있습니다.", + isSpoiler = true, + isBookmarked = false, + bookmarkCount = 42, + ), + ContentModelNew( + id = "0", + title = "저장된 영화", + year = 2023, + imageUrl = "", + director = "다른 감독", + reason = "내가 저장한 영화입니다.", + isSpoiler = false, + isBookmarked = true, + bookmarkCount = 42, + ), + ) +} + +@Preview +@Composable +private fun CollectionDetailContentPreview( + @PreviewParameter(CollectionDetailContentPreviewProvider::class) content: ContentModelNew, +) { + FlintTheme { + CollectionDetailContent( + content = content, + onBookmarkIconClick = {}, + onSpoilClick = {} + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt new file mode 100644 index 00000000..50df3af5 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDeleteModal.kt @@ -0,0 +1,35 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview +import com.flint.R +import com.flint.core.designsystem.component.modal.TwoButtonModal +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionDetailDeleteModal( + onConfirm: () -> Unit, + onDismiss: () -> Unit, +) { + TwoButtonModal( + title = "컬렉션을 삭제할까요?", + message = "내가 작성한 컬렉션이 삭제돼요.", + cancelText = "취소", + confirmText = "삭제", + onConfirm = onConfirm, + onDismiss = onDismiss, + icon = R.drawable.ic_gradient_trash, + isDestructive = true, + ) +} + +@Preview(showBackground = true) +@Composable +private fun CollectionDetailDeleteModalPreview() { + FlintTheme { + CollectionDetailDeleteModal( + onConfirm = {}, + onDismiss = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt new file mode 100644 index 00000000..716c1609 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDescription.kt @@ -0,0 +1,138 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.HorizontalDivider +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.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.type.UserRoleType + +@Composable +fun CollectionDetailDescription( + authorNickname: String, + authorUserRoleType: UserRoleType, + createdAt: String, + collectionContent: String, + onAuthorNicknameClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(24.dp), + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(10.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = authorNickname, + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + modifier = Modifier.noRippleClickable( + onClick = { onAuthorNicknameClick() } + ) + ) + + if (authorUserRoleType == UserRoleType.FLINER) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_qualified), + contentDescription = "플리너", + tint = Color.Unspecified, + ) + } else { + Text( + "|", + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.head3M18, + ) + } + + Text( + text = createdAt, + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.body2M14, + ) + } + + HorizontalDivider( + modifier = Modifier.fillMaxWidth(), + color = FlintTheme.colors.gray300, + ) + + if (collectionContent.isNotBlank()) { + Text( + text = collectionContent, + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1R16, + ) + } + } +} + +private data class DescriptionPreviewData( + val authorNickname: String, + val authorUserRoleType: UserRoleType, + val createdAt: String, + val collectionContent: String, +) + +private class DescriptionPreviewProvider : PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + DescriptionPreviewData( + authorNickname = "키카", + authorUserRoleType = UserRoleType.FLINER, + createdAt = "2026. 01. 07.", + collectionContent = "시간이 흘러도 빛이 바래지 않는,\n사랑의 미묘한 온도를 담은 제 최애 영화 모음집입니다", + ), + DescriptionPreviewData( + authorNickname = "일반유저", + authorUserRoleType = UserRoleType.FLING, + createdAt = "2026. 01. 15.", + collectionContent = "한글자 두글자 세글자 네글자 다섯글자 ".repeat(10), + ), + DescriptionPreviewData( + authorNickname = "관리자", + authorUserRoleType = UserRoleType.ADMIN, + createdAt = "2026. 01. 01.", + collectionContent = "공식 추천 컬렉션입니다", + ), + DescriptionPreviewData( + authorNickname = "내용없는유저", + authorUserRoleType = UserRoleType.FLING, + createdAt = "2026. 06. 16.", + collectionContent = "", + ), + ) +} + +@Preview +@Composable +private fun CollectionDetailDescriptionPreview( + @PreviewParameter(DescriptionPreviewProvider::class) data: DescriptionPreviewData, +) { + FlintTheme { + CollectionDetailDescription( + authorNickname = data.authorNickname, + authorUserRoleType = data.authorUserRoleType, + createdAt = data.createdAt, + collectionContent = data.collectionContent, + onAuthorNicknameClick = {} + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt new file mode 100644 index 00000000..3e970386 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailDropdownMenuItem.kt @@ -0,0 +1,76 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme + +private val DropdownMenuItemShape = RoundedCornerShape(8.dp) +private val DropdownMenuItemPadding = Modifier.padding(horizontal = 24.dp, vertical = 6.dp) + +@Composable +fun CollectionReportDropdownMenuItem(onClick: () -> Unit) { + Box( + modifier = Modifier + .background(color = FlintTheme.colors.gray700, shape = DropdownMenuItemShape) + .noRippleClickable(onClick = onClick) + .then(DropdownMenuItemPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "신고", + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1M16, + ) + } +} + +@Composable +fun CollectionEditDeleteDropdownMenuItem( + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, +) { + Column( + modifier = Modifier.background(color = FlintTheme.colors.gray700, shape = DropdownMenuItemShape), + ) { + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable(onClick = onEditClick) + .then(DropdownMenuItemPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "수정", + color = FlintTheme.colors.gray100, + style = FlintTheme.typography.body1M16, + ) + } + + HorizontalDivider(color = FlintTheme.colors.gray400, thickness = 1.dp) + + Box( + modifier = Modifier + .fillMaxWidth() + .noRippleClickable(onClick = onDeleteClick) + .then(DropdownMenuItemPadding), + contentAlignment = Alignment.Center, + ) { + Text( + text = "삭제", + color = FlintTheme.colors.error500, + style = FlintTheme.typography.body1M16, + ) + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt new file mode 100644 index 00000000..6548a3dd --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailThumbnail.kt @@ -0,0 +1,128 @@ +package com.flint.presentation.collectiondetail.component + +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.aspectRatio +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.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.flint.core.designsystem.component.button.FlintSaveDoneButton +import com.flint.core.designsystem.component.button.FlintSaveNoneButton +import com.flint.core.designsystem.component.image.NetworkImage +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionDetailThumbnail( + thumbnailImage: String, + title: String, + isBookmarked: Boolean, + isMine: Boolean, + onBackClick: () -> Unit, + onSaveDoneButtonClick: () -> Unit, + onSaveNoneButtonClick: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onReportClick: () -> Unit, + modifier: Modifier = Modifier, +) { + Box( + modifier = modifier + .fillMaxWidth() + .aspectRatio(360f / 270f), + ) { + NetworkImage( + imageUrl = thumbnailImage, + contentDescription = null, + modifier = Modifier.fillMaxSize(), + contentScale = ContentScale.FillBounds, + ) + + CollectionDetailTopAppBar( + isMine = isMine, + onBackClick = onBackClick, + onEditClick = onEditClick, + onDeleteClick = onDeleteClick, + onReportClick = onReportClick, + backgroundColor = Color.Transparent, + ) + + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp) + .padding(top = 57.dp, bottom = 22.dp), + horizontalAlignment = Alignment.End, + verticalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = title, + color = FlintTheme.colors.white, + style = FlintTheme.typography.display2M28, + modifier = Modifier.fillMaxWidth(), + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + if (isBookmarked) { + FlintSaveDoneButton( + onClick = { + onSaveDoneButtonClick() + }, + ) + } else { + FlintSaveNoneButton( + onClick = { + onSaveNoneButtonClick() + }, + ) + } + } + } +} + +@Preview +@Composable +private fun ThumbnailPreview() { + FlintTheme { + Column { + CollectionDetailThumbnail( + thumbnailImage = "", + title = "한번 보면 못 빠져나오는 여운남는 사랑이야기".repeat(2), + isBookmarked = true, + isMine = true, + onBackClick = {}, + onSaveDoneButtonClick = {}, + onSaveNoneButtonClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + + Spacer(Modifier.height(20.dp)) + + CollectionDetailThumbnail( + thumbnailImage = "https://buly.kr/DEaVFRZ", + title = "한번 보면 못 빠져나오는 여운남는 사랑이야기", + isBookmarked = false, + isMine = false, + onBackClick = {}, + onSaveDoneButtonClick = {}, + onSaveNoneButtonClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt new file mode 100644 index 00000000..58289736 --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/CollectionDetailTopAppBar.kt @@ -0,0 +1,126 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.RectangleShape +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.flint.R +import com.flint.core.common.extension.noRippleClickable +import com.flint.core.designsystem.theme.FlintTheme + +@Composable +fun CollectionDetailTopAppBar( + isMine: Boolean, + onBackClick: () -> Unit, + onEditClick: () -> Unit, + onDeleteClick: () -> Unit, + onReportClick: () -> Unit, + modifier: Modifier = Modifier, + backgroundColor: Color = FlintTheme.colors.background, +) { + var showSettingsMenu: Boolean by remember { mutableStateOf(false) } + + Row( + modifier = modifier + .fillMaxWidth() + .background(backgroundColor) + .padding(horizontal = 12.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + modifier = Modifier + .size(48.dp) + .noRippleClickable(onClick = onBackClick) + .padding(12.dp), + imageVector = ImageVector.vectorResource(R.drawable.ic_back), + contentDescription = stringResource(R.string.cd_back), + tint = FlintTheme.colors.white, + ) + + Box( + modifier = Modifier.size(48.dp), + ) { + Icon( + modifier = Modifier + .size(48.dp) + .noRippleClickable(onClick = { showSettingsMenu = true }), + imageVector = ImageVector.vectorResource(R.drawable.ic_kebab), + contentDescription = stringResource(R.string.cd_more_options), + tint = FlintTheme.colors.white, + ) + + DropdownMenu( + expanded = showSettingsMenu, + onDismissRequest = { showSettingsMenu = false }, + containerColor = Color.Transparent, + shape = RectangleShape, + tonalElevation = 0.dp, + shadowElevation = 0.dp, + ) { + if (isMine) { + CollectionEditDeleteDropdownMenuItem( + onEditClick = { + showSettingsMenu = false + onEditClick() + }, + onDeleteClick = { + showSettingsMenu = false + onDeleteClick() + }, + ) + } else { + CollectionReportDropdownMenuItem( + onClick = { + showSettingsMenu = false + onReportClick() + }, + ) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun CollectionDetailTopAppBarPreview() { + FlintTheme { + Column { + CollectionDetailTopAppBar( + isMine = true, + onBackClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + CollectionDetailTopAppBar( + isMine = false, + onBackClick = {}, + onEditClick = {}, + onDeleteClick = {}, + onReportClick = {}, + ) + } + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt new file mode 100644 index 00000000..ef6850cd --- /dev/null +++ b/app/src/main/java/com/flint/presentation/collectiondetail/component/PeopleWhoSavedThisCollection.kt @@ -0,0 +1,213 @@ +package com.flint.presentation.collectiondetail.component + +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +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.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import com.flint.R +import com.flint.core.designsystem.component.image.ProfileImage +import com.flint.core.designsystem.theme.FlintTheme +import com.flint.domain.model.bookmark.CollectionBookmarkUsersModel +import com.flint.domain.type.UserRoleType +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf + +@Composable +fun PeopleWhoSavedThisCollection( + people: ImmutableList, + onMoreClick: () -> Unit, +) { + Column( + modifier = Modifier + .padding(horizontal = 16.dp) + .padding(top = 24.dp, bottom = 32.dp), + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = "이 컬렉션을 저장한 사람들", + color = FlintTheme.colors.white, + style = FlintTheme.typography.head2Sb20, + modifier = Modifier, + ) + + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_more), + contentDescription = null, + modifier = Modifier + .size(24.dp) + .clickable(onClick = onMoreClick) + .padding(vertical = 1.dp) + .padding(end = 3.dp), + tint = FlintTheme.colors.white, + ) + } + + Spacer(Modifier.height(32.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + horizontalArrangement = Arrangement.spacedBy((-12).dp), + ) { + people.take(6).forEach { author: CollectionBookmarkUsersModel.User -> + ProfileImage( + imageUrl = author.profileImageUrl, + modifier = Modifier + .size(56.dp) + .border(3.dp, FlintTheme.colors.background, CircleShape), + contentDescription = author.nickName, + ) + } + } + + Spacer(Modifier.width(6.dp)) + + if (people.size >= 7) { + Row ( + verticalAlignment = Alignment.CenterVertically + ){ + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_plus), + contentDescription = "그 외", + tint = FlintTheme.colors.gray200, + ) + + Text( + text = (people.size - 6).toString(), + color = FlintTheme.colors.gray200, + style = FlintTheme.typography.head3M18, + ) + } + } + } + } +} + +private class PeoplePreviewProvider : + PreviewParameterProvider> { + override val values: Sequence> = + sequenceOf( + persistentListOf( + CollectionBookmarkUsersModel.User( + userId = "1", + nickName = "유저1", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + ), + persistentListOf( + CollectionBookmarkUsersModel.User( + userId = "1", + nickName = "유저1", + profileImageUrl = "", + userRole = UserRoleType.ADMIN, + ), + CollectionBookmarkUsersModel.User( + userId = "2", + nickName = "유저2", + profileImageUrl = "", + userRole = UserRoleType.FLINER, + ), + CollectionBookmarkUsersModel.User( + userId = "3", + nickName = "유저3", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "4", + nickName = "유저4", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "5", + nickName = "유저5", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + ), + persistentListOf( + CollectionBookmarkUsersModel.User( + userId = "1", + nickName = "유저1", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "2", + nickName = "유저2", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "3", + nickName = "유저3", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "4", + nickName = "유저4", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "5", + nickName = "유저5", + profileImageUrl = "", + userRole = UserRoleType.FLING, + ), + CollectionBookmarkUsersModel.User( + userId = "6", + nickName = "유저6", + profileImageUrl = "", + userRole = UserRoleType.FLINER, + ), + CollectionBookmarkUsersModel.User( + userId = "7", + nickName = "유저7", + profileImageUrl = "", + userRole = UserRoleType.ADMIN, + ), + ), + ) +} + +@Preview +@Composable +private fun PeopleWhoSavedThisCollectionPreview( + @PreviewParameter(PeoplePreviewProvider::class) people: ImmutableList, +) { + FlintTheme { + PeopleWhoSavedThisCollection( + people = people, + onMoreClick = {}, + ) + } +} diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt index 218b50e8..ab328e11 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/navigation/CollectionDetailNavigation.kt @@ -8,36 +8,52 @@ import androidx.navigation.compose.composable import androidx.navigation.toRoute import com.flint.core.navigation.Route import com.flint.core.navigation.model.CollectionListRouteType +import com.flint.presentation.collectioncreate.navigation.navigateToCollectionEdit import com.flint.presentation.collectiondetail.CollectionDetailRoute fun NavController.navigateToCollectionDetail( collectionId: String, targetImageUrl: String? = null, + showEditSuccessToast: Boolean = false, navOptions: NavOptions? = null, ) { navigate( Route.CollectionDetail( collectionId = collectionId, targetImageUrl = targetImageUrl, + showEditSuccessToast = showEditSuccessToast, ), navOptions, ) } +const val KEY_SHOW_DELETE_SUCCESS_TOAST = "showDeleteSuccessToast" + fun NavGraphBuilder.collectionDetailNavGraph( paddingValues: PaddingValues, navigateToCollectionList: (CollectionListRouteType) -> Unit, navigateUp: () -> Unit, navigateToProfile: (userId: String) -> Unit, + navController: NavController, ) { composable { backStackEntry -> val route = backStackEntry.toRoute() CollectionDetailRoute( paddingValues = paddingValues, targetImageUrl = route.targetImageUrl, + showEditSuccessToast = route.showEditSuccessToast, navigateToCollectionList = navigateToCollectionList, navigateUp = navigateUp, + navigateUpWithDeleteSuccess = { + navController.previousBackStackEntry + ?.savedStateHandle + ?.set(KEY_SHOW_DELETE_SUCCESS_TOAST, true) + navController.navigateUp() + }, navigateToProfile = navigateToProfile, + navigateToCollectionEdit = { collectionId -> + navController.navigateToCollectionEdit(collectionId) + }, ) } } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt b/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt index c8d4f65c..0bfedc1e 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/sideeffect/CollectionDetailSideEffect.kt @@ -6,4 +6,8 @@ sealed interface CollectionDetailSideEffect { object ToggleCollectionBookmarkFailure : CollectionDetailSideEffect class ToggleContentBookmarkSuccess(val isBookmarked: Boolean) : CollectionDetailSideEffect + + object DeleteCollectionSuccess : CollectionDetailSideEffect + + object DeleteCollectionFailure : CollectionDetailSideEffect } diff --git a/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt b/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt index f750228c..c515ce57 100644 --- a/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt +++ b/app/src/main/java/com/flint/presentation/collectiondetail/uistate/CollectionDetailUiState.kt @@ -6,4 +6,5 @@ import com.flint.domain.model.collection.CollectionDetailModelNew data class CollectionDetailUiState( val collectionDetail: CollectionDetailModelNew, val collectionBookmarkUsers: CollectionBookmarkUsersModel, + val isMine: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt index a4d5a26b..8c86c6ce 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavHost.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavHost.kt @@ -5,10 +5,15 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation.compose.NavHost +import androidx.navigation.compose.currentBackStackEntryAsState +import com.flint.core.designsystem.component.toast.ShowToast import com.flint.core.designsystem.theme.FlintTheme import com.flint.presentation.collectioncreate.navigation.collectionCreateNavGraph +import com.flint.presentation.collectiondetail.navigation.KEY_SHOW_DELETE_SUCCESS_TOAST import com.flint.presentation.collectiondetail.navigation.collectionDetailNavGraph import com.flint.presentation.collectionlist.navigation.collectionListNavGraph import com.flint.presentation.explore.navigation.exploreNavGraph @@ -30,6 +35,11 @@ fun MainNavHost( paddingValues: PaddingValues, modifier: Modifier = Modifier, ) { + val currentBackStackEntry by navigator.navController.currentBackStackEntryAsState() + val showDeleteSuccessToast = currentBackStackEntry + ?.savedStateHandle + ?.get(KEY_SHOW_DELETE_SUCCESS_TOAST) ?: false + Box( modifier = modifier @@ -77,7 +87,8 @@ fun MainNavHost( paddingValues = paddingValues, navigateToCollectionList = navigator::navigateToCollectionList, navigateUp = navigator::navigateUp, - navigateToProfile = navigator::navigateToProfile + navigateToProfile = navigator::navigateToProfile, + navController = navigator.navController, ) collectionCreateNavGraph( @@ -131,5 +142,17 @@ fun MainNavHost( navigateToLogin = navigator::navigateToLogin, ) } + + if (showDeleteSuccessToast) { + ShowToast( + text = "컬렉션을 삭제했어요", + imageVector = null, + paddingValues = paddingValues, + yOffset = 12.dp, + hide = { + currentBackStackEntry?.savedStateHandle?.set(KEY_SHOW_DELETE_SUCCESS_TOAST, false) + }, + ) + } } } diff --git a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt index a53f9d39..0ca0d58c 100644 --- a/app/src/main/java/com/flint/presentation/main/MainNavigator.kt +++ b/app/src/main/java/com/flint/presentation/main/MainNavigator.kt @@ -13,6 +13,7 @@ import androidx.navigation.navOptions import com.flint.core.navigation.Route import com.flint.core.navigation.model.CollectionListRouteType import com.flint.presentation.collectioncreate.navigation.navigateToCollectionCreate +import com.flint.presentation.collectioncreate.navigation.navigateToCollectionEdit import com.flint.presentation.collectiondetail.navigation.navigateToCollectionDetail import com.flint.presentation.collectionlist.navigation.navigateToCollectionList import com.flint.presentation.explore.navigation.navigateToExplore @@ -151,6 +152,10 @@ class MainNavigator( navController.navigateToCollectionCreate(navOptions) } + fun navigateToCollectionEdit(collectionId: String, navOptions: NavOptions? = null) { + navController.navigateToCollectionEdit(collectionId, navOptions) + } + fun navigateToSavedContent(navOptions: NavOptions? = null) { navController.navigateToSavedContentList(navOptions) } diff --git a/app/src/main/res/drawable/ic_kebab.xml b/app/src/main/res/drawable/ic_kebab.xml index 86ab0961..b1cdf068 100644 --- a/app/src/main/res/drawable/ic_kebab.xml +++ b/app/src/main/res/drawable/ic_kebab.xml @@ -1,15 +1,30 @@ + + android:width="48dp" + android:height="48dp" + android:viewportWidth="48" + android:viewportHeight="48"> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f72dc375..54676e38 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ Flint + 뒤로 가기 + 더 보기 \ No newline at end of file