diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 498836d1..7974739d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + + diff --git a/core/designsystem/src/main/java/com/record/designsystem/component/badge/RecordyLocationBadge.kt b/core/designsystem/src/main/java/com/record/designsystem/component/badge/RecordyLocationBadge.kt index ae223334..7cb6c6a1 100644 --- a/core/designsystem/src/main/java/com/record/designsystem/component/badge/RecordyLocationBadge.kt +++ b/core/designsystem/src/main/java/com/record/designsystem/component/badge/RecordyLocationBadge.kt @@ -54,7 +54,7 @@ fun RecordyLocationBadge( maxLines = 1, overflow = TextOverflow.Ellipsis, style = RecordyTheme.typography.caption1, - color = RecordyTheme.colors.gray01, + color = RecordyTheme.colors.white, textAlign = TextAlign.Center, ) } diff --git a/core/model/src/main/java/com/record/model/Cursor.kt b/core/model/src/main/java/com/record/model/Cursor.kt index ba3e543d..02c1dfb4 100644 --- a/core/model/src/main/java/com/record/model/Cursor.kt +++ b/core/model/src/main/java/com/record/model/Cursor.kt @@ -2,6 +2,6 @@ package com.record.model data class Cursor( val hasNext: Boolean, - val nextCursor: Int, + val nextCursor: Int?, val data: List, ) diff --git a/core/ui/src/main/java/com/record/ui/scroll/OnBottomReached.kt b/core/ui/src/main/java/com/record/ui/scroll/OnBottomReached.kt index 100f5065..77318f58 100644 --- a/core/ui/src/main/java/com/record/ui/scroll/OnBottomReached.kt +++ b/core/ui/src/main/java/com/record/ui/scroll/OnBottomReached.kt @@ -2,6 +2,7 @@ package com.record.ui.scroll import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -42,6 +43,32 @@ fun LazyListState.OnBottomReached( fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 +@Composable +fun LazyGridState.OnBottomReached( + buffer: Int = 0, + onLoadMore: () -> Unit, +) { + require(buffer >= 0) { "buffer cannot be negative, but was $buffer" } + + val shouldLoadMore = remember { + derivedStateOf { + val lastVisibleItem = layoutInfo.visibleItemsInfo.lastOrNull() ?: return@derivedStateOf false + + lastVisibleItem.index >= layoutInfo.totalItemsCount - 1 - buffer + } + } + LaunchedEffect(shouldLoadMore) { + snapshotFlow { shouldLoadMore.value } + .collectLatest { + if (it) { + onLoadMore() + } + } + } +} + +fun LazyGridState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 + @OptIn(ExperimentalFoundationApi::class) @Composable fun PagerState.onBottomReached( diff --git a/data/video/src/main/java/com/record/video/model/remote/response/ResponseGetSliceVideoDto.kt b/data/video/src/main/java/com/record/video/model/remote/response/ResponseGetSliceVideoDto.kt index 27958055..1e380ae9 100644 --- a/data/video/src/main/java/com/record/video/model/remote/response/ResponseGetSliceVideoDto.kt +++ b/data/video/src/main/java/com/record/video/model/remote/response/ResponseGetSliceVideoDto.kt @@ -11,7 +11,7 @@ data class ResponseGetSliceVideoDto( @SerialName("hasNext") val hasNext: Boolean, @SerialName("nextCursor") - val nextCursor: Int, + val nextCursor: Int?, ) fun ResponseGetSliceVideoDto.toCore() = Cursor( diff --git a/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt b/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt index 329914e3..fb17619f 100644 --- a/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt +++ b/data/video/src/main/java/com/record/video/repository/VideoRepositoryImpl.kt @@ -7,12 +7,14 @@ import com.record.model.exception.ApiError import com.record.video.model.VideoData import com.record.video.model.remote.response.toCore import com.record.video.model.remote.response.toDomain +import com.record.video.source.local.LocalUserInfoDataSource import com.record.video.source.remote.RemoteVideoDataSource import retrofit2.HttpException import javax.inject.Inject class VideoRepositoryImpl @Inject constructor( private val remoteVideoDataSource: RemoteVideoDataSource, + private val localUserInfoDataSource: LocalUserInfoDataSource, ) : VideoRepository { override suspend fun getAllVideos(cursorId: Long, pageSize: Int): Result> = runCatching { remoteVideoDataSource.getAllVideos(cursorId, pageSize) @@ -30,10 +32,10 @@ class VideoRepositoryImpl @Inject constructor( } } - override suspend fun getRecentVideos(keywords: List?, pageNumber: Int, pageSize: Int): Result> = runCatching { + override suspend fun getRecentVideos(keywords: List?, cursor: Long, pageSize: Int): Result> = runCatching { val encodedKeywords = keywords?.map { it.replace(" ", "_") }?.map { toUTF8(it) } - remoteVideoDataSource.getRecentVideos(encodedKeywords, pageNumber, pageSize) + remoteVideoDataSource.getRecentVideos(encodedKeywords, cursor, pageSize) }.mapCatching { it.toCore() }.recoverCatching { exception -> @@ -81,6 +83,26 @@ class VideoRepositoryImpl @Inject constructor( } } + override suspend fun getMyVideos(cursorId: Long, size: Int): Result> = runCatching { + remoteVideoDataSource.getUserVideos( + localUserInfoDataSource.getMyId(), + cursorId, + size, + ) + }.mapCatching { + it.toCore() + }.recoverCatching { exception -> + when (exception) { + is HttpException -> { + throw ApiError(exception.message()) + } + + else -> { + throw exception + } + } + } + override suspend fun getFollowingVideos(userId: Long, cursorId: Long, size: Int): Result> = runCatching { remoteVideoDataSource.getFollowingVideos(userId, cursorId, size) }.mapCatching { diff --git a/data/video/src/main/java/com/record/video/source/local/LocalUserInfoDataSource.kt b/data/video/src/main/java/com/record/video/source/local/LocalUserInfoDataSource.kt new file mode 100644 index 00000000..c4ef1d16 --- /dev/null +++ b/data/video/src/main/java/com/record/video/source/local/LocalUserInfoDataSource.kt @@ -0,0 +1,5 @@ +package com.record.video.source.local + +interface LocalUserInfoDataSource { + suspend fun getMyId(): Long +} diff --git a/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt b/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt index a47725fc..d6b602fe 100644 --- a/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt +++ b/data/video/src/main/java/com/record/video/source/remote/RemoteVideoDataSource.kt @@ -6,7 +6,7 @@ import com.record.video.model.remote.response.ResponseGetVideoDto interface RemoteVideoDataSource { suspend fun getAllVideos(cursorId: Long, size: Int): List - suspend fun getRecentVideos(keywords: List?, pageNumber: Int, pageSize: Int): ResponseGetSliceVideoDto + suspend fun getRecentVideos(keywords: List?, cursor: Long, pageSize: Int): ResponseGetSliceVideoDto suspend fun getPopularVideos(keywords: List?, pageNumber: Int, pageSize: Int): ResponseGetPagingVideoDto suspend fun getUserVideos(otherUserId: Long, cursorId: Long, size: Int): ResponseGetSliceVideoDto suspend fun getFollowingVideos(userId: Long, cursorId: Long, size: Int): ResponseGetSliceVideoDto diff --git a/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt b/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt index 48cd8c28..bfe0bbd2 100644 --- a/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt +++ b/domain/video/src/main/java/com/record/video/repository/VideoRepository.kt @@ -6,9 +6,10 @@ import com.record.video.model.VideoData interface VideoRepository { suspend fun getAllVideos(cursorId: Long, pageSize: Int): Result> - suspend fun getRecentVideos(keywords: List?, pageNumber: Int, pageSize: Int): Result> + suspend fun getRecentVideos(keywords: List?, cursor: Long, pageSize: Int): Result> suspend fun getPopularVideos(keywords: List?, pageNumber: Int, pageSize: Int): Result> suspend fun getUserVideos(otherUserId: Long, cursorId: Long, size: Int): Result> + suspend fun getMyVideos(cursorId: Long, size: Int): Result> suspend fun getFollowingVideos(userId: Long, cursorId: Long, size: Int): Result> suspend fun getBookmarkVideos(cursorId: Long, size: Int): Result> suspend fun bookmark(videoId: Long): Result diff --git a/feature/home/src/main/java/com/record/home/HomeContract.kt b/feature/home/src/main/java/com/record/home/HomeContract.kt index efb2973f..8e700286 100644 --- a/feature/home/src/main/java/com/record/home/HomeContract.kt +++ b/feature/home/src/main/java/com/record/home/HomeContract.kt @@ -4,17 +4,18 @@ import com.record.model.VideoType import com.record.ui.base.SideEffect import com.record.ui.base.UiState import com.record.video.model.VideoData +import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.toImmutableList data class HomeState( - val chipList: List = emptyList().toImmutableList(), - val popularList: List = emptyList().toImmutableList(), - val recentList: List = emptyList().toImmutableList(), + val chipList: ImmutableList = emptyList().toImmutableList(), + val popularList: ImmutableList = emptyList().toImmutableList(), + val recentList: ImmutableList = emptyList().toImmutableList(), val selectedChipIndex: Int? = null, val isLoading: Boolean = false, ) : UiState sealed interface HomeSideEffect : SideEffect { data object navigateToUpload : HomeSideEffect - data class navigateToVideo(val index: Long, val type: VideoType, val keyword: String?) : HomeSideEffect + data class navigateToVideo(val id: Long, val type: VideoType, val keyword: String?) : HomeSideEffect } diff --git a/feature/home/src/main/java/com/record/home/HomeScreen.kt b/feature/home/src/main/java/com/record/home/HomeScreen.kt index db4b2f2f..96daa60a 100644 --- a/feature/home/src/main/java/com/record/home/HomeScreen.kt +++ b/feature/home/src/main/java/com/record/home/HomeScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -52,9 +53,11 @@ import com.record.ui.extension.customClickable import com.record.ui.lifecycle.LaunchedEffectWithLifecycle import com.record.video.model.VideoData import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import me.onebone.toolbar.CollapsingToolbarScaffold import me.onebone.toolbar.CollapsingToolbarScaffoldState import me.onebone.toolbar.CollapsingToolbarScope +import me.onebone.toolbar.ExperimentalToolbarApi import me.onebone.toolbar.ScrollStrategy import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState @@ -68,13 +71,14 @@ fun HomeRoute( val state by viewModel.uiState.collectAsStateWithLifecycle() LaunchedEffectWithLifecycle { + viewModel.getVideos() viewModel.sideEffect.collectLatest { sideEffect -> when (sideEffect) { HomeSideEffect.navigateToUpload -> { } is HomeSideEffect.navigateToVideo -> { - navigateToVideoDetail(sideEffect.type, sideEffect.index, sideEffect.keyword, 0) + navigateToVideoDetail(sideEffect.type, sideEffect.id, sideEffect.keyword, 0) } } } @@ -191,7 +195,7 @@ fun CollapsingToolbar( ToolbarContent(toolbarState) }, ) { - ChipRow(state.chipList, state.selectedChipIndex, onChipButtonClick) + ChipRow(toolbarState, state.chipList, state.selectedChipIndex, onChipButtonClick) Content( state = state, onVideoClick = onVideoClick, @@ -246,12 +250,15 @@ fun CollapsingToolbarScope.ToolbarContent(toolbarState: CollapsingToolbarScaffol } } +@OptIn(ExperimentalToolbarApi::class) @Composable fun ChipRow( + state: CollapsingToolbarScaffoldState, chipList: List, selectedChip: Int?, onChipButtonClick: (Int) -> Unit, ) { + val coroutineScope = rememberCoroutineScope() LazyRow( modifier = Modifier .padding(bottom = 12.dp), @@ -262,7 +269,12 @@ fun ChipRow( RecordyChipButton( text = item, isActive = selectedChip == i, - onClick = { onChipButtonClick(i) }, + onClick = { + onChipButtonClick(i) + coroutineScope.launch { + state.toolbarState.collapse(200) + } + }, ) } item { Spacer(modifier = Modifier.width(8.dp)) } diff --git a/feature/home/src/main/java/com/record/home/HomeViewModel.kt b/feature/home/src/main/java/com/record/home/HomeViewModel.kt index daad116b..267be908 100644 --- a/feature/home/src/main/java/com/record/home/HomeViewModel.kt +++ b/feature/home/src/main/java/com/record/home/HomeViewModel.kt @@ -8,8 +8,8 @@ import com.record.model.exception.ApiError import com.record.ui.base.BaseViewModel import com.record.video.repository.VideoRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch -import okhttp3.internal.toImmutableList import javax.inject.Inject @HiltViewModel @@ -18,12 +18,6 @@ class HomeViewModel @Inject constructor( private val keywordRepository: KeywordRepository, ) : BaseViewModel(HomeState()) { - init { - getPreferenceKeywords() - getPopularVideos() - getRecentVideos() - } - fun navigateToUpload() { postSideEffect(HomeSideEffect.navigateToUpload) } @@ -36,11 +30,17 @@ class HomeViewModel @Inject constructor( getRecentVideos() } + fun getVideos() { + getPreferenceKeywords() + getPopularVideos() + getRecentVideos() + } + private fun getPreferenceKeywords() { viewModelScope.launch { keywordRepository.getKeywords().onSuccess { intent { - copy(chipList = it.keywords) + copy(chipList = it.keywords.toImmutableList()) } }.onFailure { when (it) { @@ -58,7 +58,7 @@ class HomeViewModel @Inject constructor( val keyword = if (keyIndex != null) listOf(uiState.value.chipList[keyIndex]) else null videoRepository.getRecentVideos( keywords = keyword, - pageNumber = 0, + cursor = 0, pageSize = 10, ).onSuccess { intent { @@ -124,8 +124,8 @@ class HomeViewModel @Inject constructor( Log.e("반환값", updatedPopularList.toString()) copy( - recentList = updatedRecentList, - popularList = updatedPopularList, + recentList = updatedRecentList.toImmutableList(), + popularList = updatedPopularList.toImmutableList(), ) } viewModelScope.launch { @@ -151,8 +151,8 @@ class HomeViewModel @Inject constructor( Log.e("반환값", updatedPopularList1.toString()) intent { copy( - recentList = updatedRecentList1, - popularList = updatedPopularList1, + recentList = updatedRecentList1.toImmutableList(), + popularList = updatedPopularList1.toImmutableList(), ) } }.onFailure { diff --git a/feature/mypage/src/main/java/com/record/mypage/MypageContract.kt b/feature/mypage/src/main/java/com/record/mypage/MypageContract.kt index c432f0b4..c9a20aef 100644 --- a/feature/mypage/src/main/java/com/record/mypage/MypageContract.kt +++ b/feature/mypage/src/main/java/com/record/mypage/MypageContract.kt @@ -12,6 +12,10 @@ data class MypageState( val nickname: String = "", val followerNum: Int = 0, val followingNum: Int = 0, + val bookmarkCursor: Long = 0, + val bookmarkIsEnd: Boolean = false, + val recordCursor: Long = 0, + val recordIsEnd: Boolean = false, val mypageTab: MypageTab = MypageTab.TASTE, val preferences: ImmutableList> = emptyList>().toImmutableList(), val myRecordList: ImmutableList = emptyList().toImmutableList(), diff --git a/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt b/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt index 17825b74..21bed78d 100644 --- a/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt +++ b/feature/mypage/src/main/java/com/record/mypage/MypageScreen.kt @@ -68,12 +68,11 @@ fun MypageRoute( navigateToVideo: (VideoType, Long) -> Unit, ) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() - viewModel.fetchUserProfile() - viewModel.fetchUserPreferences() - viewModel.fetchUserVideos() - viewModel.fetchBookmarkVideos() - LaunchedEffectWithLifecycle() { + LaunchedEffectWithLifecycle { + viewModel.fetchUserPreferences() + viewModel.fetchUserProfile() + viewModel.initialData() viewModel.sideEffect.collectLatest { sideEffect -> when (sideEffect) { MypageSideEffect.NavigateToFollower -> { @@ -107,6 +106,9 @@ fun MypageRoute( onFollowerClick = viewModel::navigateToFollower, navigateToSetting = viewModel::navigateToSetting, navigateToVideo = viewModel::navigateToVideoDetail, + onLoadMoreBookmarks = viewModel::loadMoreBookmarkVideos, + onLoadMoreRecords = viewModel::loadMoreUserVideos, + onBookmarkClick = viewModel::bookmark, ) } } @@ -120,6 +122,9 @@ fun MypageScreen( onFollowerClick: () -> Unit, onFollowingClick: () -> Unit, navigateToVideo: (VideoType, Long) -> Unit, + onLoadMoreRecords: () -> Unit, + onLoadMoreBookmarks: () -> Unit, + onBookmarkClick: (Long) -> Unit, ) { val pagerState = rememberPagerState( initialPage = state.mypageTab.ordinal, @@ -217,6 +222,8 @@ fun MypageScreen( videoItems = state.myRecordList, recordCount = state.recordVideoCount, onItemClick = navigateToVideo, + onLoadMore = onLoadMoreRecords, + onBookmarkClick = onBookmarkClick, ) } @@ -225,6 +232,8 @@ fun MypageScreen( videoItems = state.myBookmarkList, recordCount = state.bookmarkVideoCount, onItemClick = navigateToVideo, + onLoadMore = onLoadMoreBookmarks, + onBookmarkClick = onBookmarkClick, ) } } diff --git a/feature/mypage/src/main/java/com/record/mypage/MypageViewModel.kt b/feature/mypage/src/main/java/com/record/mypage/MypageViewModel.kt index b1a15cd6..0d0c62f5 100644 --- a/feature/mypage/src/main/java/com/record/mypage/MypageViewModel.kt +++ b/feature/mypage/src/main/java/com/record/mypage/MypageViewModel.kt @@ -9,6 +9,8 @@ import com.record.user.repository.UserRepository import com.record.video.repository.VideoRepository import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.collections.immutable.toImmutableList +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import javax.inject.Inject @@ -84,49 +86,128 @@ class MypageViewModel @Inject constructor( } } - fun fetchUserVideos() { - viewModelScope.launch { - userRepository.getUserId().onSuccess { userId -> - videoRepository.getUserVideos( - userId, - cursorId = 0, - size = 40, - ).onSuccess { cursor -> - intent { - copy( - myRecordList = cursor.data.toImmutableList(), - recordVideoCount = myRecordList.size, - ) - } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("error", it.message) - } - } + fun initialData() = viewModelScope.launch { + val myVideosResult = async { + videoRepository.getMyVideos(0, 10) + } + val bookmarkVideosResult = async { + videoRepository.getBookmarkVideos(0, 10) + } + + val results = awaitAll(myVideosResult, bookmarkVideosResult) + val myVideoResult = results[0] + val bookmarkVideoResult = results[1] + if (myVideoResult.isSuccess && bookmarkVideoResult.isSuccess) { + val myVideo = myVideoResult.getOrThrow() + val bookmarkVideo = bookmarkVideoResult.getOrThrow() + intent { + copy( + myRecordList = myVideo.data.toImmutableList(), + myBookmarkList = bookmarkVideo.data.toImmutableList(), + recordCursor = myVideo.nextCursor?.plus(1)?.toLong() ?: 0, + bookmarkCursor = bookmarkVideo.nextCursor?.plus(1)?.toLong() ?: 0, + recordVideoCount = myVideo.data.size, + bookmarkVideoCount = bookmarkVideo.data.size, + recordIsEnd = false, + bookmarkIsEnd = false, + ) + } + } + } + + fun loadMoreUserVideos() = viewModelScope.launch { + Log.e("로드", "로드") + val list = uiState.value.myRecordList.toList() + if (uiState.value.recordIsEnd) return@launch + videoRepository.getMyVideos(uiState.value.recordCursor, 10).onSuccess { + intent { + copy( + recordCursor = it.nextCursor?.plus(1)?.toLong() ?: 0, + myRecordList = (list + it.data).toImmutableList(), + recordVideoCount = (list + it.data).size, + ) + } + if (!it.hasNext) { + intent { + copy(recordIsEnd = true) + } + } + } + } + + fun loadMoreBookmarkVideos() = viewModelScope.launch { + Log.e("로드", "로드") + val list = uiState.value.myBookmarkList.toList() + if (uiState.value.bookmarkIsEnd) return@launch + videoRepository.getBookmarkVideos(uiState.value.bookmarkCursor, 10).onSuccess { + intent { + copy( + bookmarkCursor = it.nextCursor?.plus(1)?.toLong() ?: 0, + myBookmarkList = (list + it.data).toImmutableList(), + bookmarkVideoCount = (list + it.data).size, + ) + } + if (!it.hasNext) { + intent { + copy(bookmarkIsEnd = true) } } } } + fun bookmark(id: Long) { + intent { + val updatedMyRecordList = uiState.value.myRecordList.map { video -> + if (video.id == id) { + Log.e("태그", "변경") + video.copy(isBookmark = !video.isBookmark) + } else { + video + } + } + + val updatedMyBookmarkList = uiState.value.myBookmarkList.map { video -> + if (video.id == id) { + Log.e("태그", "변경") + video.copy(isBookmark = !video.isBookmark) + } else { + video + } + } - fun fetchBookmarkVideos() { + Log.e("반환값", updatedMyBookmarkList.toString()) + copy( + myRecordList = updatedMyRecordList.toImmutableList(), + myBookmarkList = updatedMyBookmarkList.toImmutableList(), + ) + } viewModelScope.launch { - videoRepository.getBookmarkVideos( - cursorId = 0, - size = 20, - ).onSuccess { cursor -> + videoRepository.bookmark(id).onSuccess { + val updatedMyRecordList = uiState.value.myRecordList.map { video -> + if (video.id == id) { + Log.e("태그", "변경") + video.copy(isBookmark = it) + } else { + video + } + } + + val updatedMyBookmarkList = uiState.value.myBookmarkList.map { video -> + if (video.id == id) { + Log.e("태그", "변경") + video.copy(isBookmark = it) + } else { + video + } + } + + Log.e("반환값", updatedMyBookmarkList.toString()) intent { copy( - myBookmarkList = cursor.data.toImmutableList(), - bookmarkVideoCount = myBookmarkList.size, + myRecordList = updatedMyRecordList.toImmutableList(), + myBookmarkList = updatedMyBookmarkList.toImmutableList(), ) } }.onFailure { - when (it) { - is ApiError -> { - Log.e("error", it.message) - } - } } } } diff --git a/feature/mypage/src/main/java/com/record/mypage/screen/BookmarkScreen.kt b/feature/mypage/src/main/java/com/record/mypage/screen/BookmarkScreen.kt index 9ccb9cfd..ca6310dc 100644 --- a/feature/mypage/src/main/java/com/record/mypage/screen/BookmarkScreen.kt +++ b/feature/mypage/src/main/java/com/record/mypage/screen/BookmarkScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -19,6 +20,7 @@ import androidx.compose.ui.unit.dp import com.record.designsystem.component.RecordyVideoThumbnail import com.record.designsystem.theme.RecordyTheme import com.record.model.VideoType +import com.record.ui.scroll.OnBottomReached import com.record.video.model.VideoData import kotlinx.collections.immutable.ImmutableList @@ -27,9 +29,14 @@ fun BookmarkScreen( videoItems: ImmutableList, recordCount: Int, onItemClick: (VideoType, Long) -> Unit, + onLoadMore: () -> Unit, + onBookmarkClick: (Long) -> Unit, ) { val videos = remember { mutableStateOf(videoItems) } - + val lazyGridState = rememberLazyGridState() + lazyGridState.OnBottomReached { + onLoadMore() + } if (videos.value.isEmpty()) { EmptyDataScreen( imageRes = com.record.designsystem.R.drawable.img_bookmark, @@ -38,6 +45,7 @@ fun BookmarkScreen( ) } else { LazyVerticalGrid( + state = lazyGridState, modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), @@ -65,7 +73,7 @@ fun BookmarkScreen( isBookmarkable = true, isBookmark = item.isBookmark, onBookmarkClick = { - // + onBookmarkClick(item.id) }, location = item.location, onClick = { diff --git a/feature/mypage/src/main/java/com/record/mypage/screen/RecordScreen.kt b/feature/mypage/src/main/java/com/record/mypage/screen/RecordScreen.kt index 2d8ba8b2..fce2bcb6 100644 --- a/feature/mypage/src/main/java/com/record/mypage/screen/RecordScreen.kt +++ b/feature/mypage/src/main/java/com/record/mypage/screen/RecordScreen.kt @@ -9,6 +9,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf @@ -23,6 +24,7 @@ import androidx.compose.ui.unit.dp import com.record.designsystem.component.RecordyVideoThumbnail import com.record.designsystem.theme.RecordyTheme import com.record.model.VideoType +import com.record.ui.scroll.OnBottomReached import com.record.video.model.VideoData import kotlinx.collections.immutable.ImmutableList @@ -31,9 +33,14 @@ fun RecordScreen( videoItems: ImmutableList, recordCount: Int, onItemClick: (VideoType, Long) -> Unit, + onLoadMore: () -> Unit, + onBookmarkClick: (Long) -> Unit, ) { val videos = remember { mutableStateOf(videoItems) } - + val lazyGridState = rememberLazyGridState() + lazyGridState.OnBottomReached { + onLoadMore() + } if (videos.value.isEmpty()) { EmptyDataScreen( imageRes = com.record.designsystem.R.drawable.img_camera, @@ -45,6 +52,7 @@ fun RecordScreen( ) } else { LazyVerticalGrid( + state = lazyGridState, modifier = Modifier .fillMaxSize() .padding(horizontal = 16.dp), @@ -72,7 +80,7 @@ fun RecordScreen( isBookmarkable = true, isBookmark = item.isBookmark, onBookmarkClick = { - // + onBookmarkClick(item.id) }, location = item.location, onClick = { diff --git a/feature/navigator/src/main/java/com/record/navigator/InMainNavTab.kt b/feature/navigator/src/main/java/com/record/navigator/InMainNavTab.kt new file mode 100644 index 00000000..2da7f211 --- /dev/null +++ b/feature/navigator/src/main/java/com/record/navigator/InMainNavTab.kt @@ -0,0 +1,24 @@ +package com.record.navigator + +import com.record.mypage.navigation.MypageRoute +import com.record.profile.navigation.ProfileRoute +import com.record.video.navigation.VideoRoute + +enum class InMainNavTab( + val route: String, +) { + VIDEO_DETAIL(VideoRoute.detailRoute), + FOLLOWING(MypageRoute.followingRoute), + FOLLOWER(MypageRoute.followerRoute), + PROFILE(ProfileRoute.route), + ; + companion object { + operator fun contains(route: String): Boolean { + return InMainNavTab.entries.map { it.route }.contains(route) + } + + fun find(route: String): InMainNavTab? { + return InMainNavTab.entries.find { it.route == route } + } + } +} diff --git a/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt b/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt index c3086d95..c47aba05 100644 --- a/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt +++ b/feature/navigator/src/main/java/com/record/navigator/MainNavigator.kt @@ -15,6 +15,7 @@ import com.record.model.VideoType import com.record.mypage.navigation.navigateMypage import com.record.mypage.navigation.navigateToFollower import com.record.mypage.navigation.navigateToFollowing +import com.record.profile.navigation.ProfileRoute import com.record.profile.navigation.navigateProfile import com.record.setting.navigate.navigateSetting import com.record.video.navigation.navigateVideo @@ -28,10 +29,17 @@ internal class MainNavigator( @Composable get() = navController .currentBackStackEntryAsState().value?.destination + private var _currentTab: MainNavTab? = null + val currentTab: MainNavTab? - @Composable get() = currentDestination - ?.route - ?.let(MainNavTab::find) + @Composable get() { + val currentRoute = currentDestination?.route + val mainTab = currentRoute?.let(MainNavTab::find) + if (mainTab != null) { + _currentTab = mainTab + } + return _currentTab + } fun navigate(tab: MainNavTab) { val navOptions = navOptions { @@ -39,9 +47,15 @@ internal class MainNavigator( saveState = true } launchSingleTop = true - restoreState = true + restoreState = when (tab) { + MainNavTab.HOME -> false + MainNavTab.VIDEO -> true + MainNavTab.MYPAGE -> true + } } + _currentTab = tab + when (tab) { MainNavTab.HOME -> navController.navigateHome(navOptions) MainNavTab.VIDEO -> navController.navigateVideo(navOptions) @@ -59,7 +73,7 @@ internal class MainNavigator( fun navigateLogin() { navController.navigate(LoginRoute.route) { - popUpTo(navController.graph.id) { + popUpTo(navController.graph.startDestinationId) { inclusive = true } } @@ -67,7 +81,7 @@ internal class MainNavigator( fun navigateSignUp() { navController.navigate(SignupRoute.route) { - popUpTo(navController.graph.id) { + popUpTo(navController.graph.startDestinationId) { inclusive = true } } @@ -103,8 +117,11 @@ internal class MainNavigator( } fun popBackStackIfNotHome() { - if (!isSameCurrentDestination(HomeRoute.route)) { + val homeRoute = HomeRoute.route + if (navController.currentDestination?.route == homeRoute) { navController.popBackStack() + } else { + navController.popBackStack(homeRoute, inclusive = false) } } @@ -114,7 +131,9 @@ internal class MainNavigator( @Composable fun shouldShowBottomBar(): Boolean { val currentRoute = currentDestination?.route ?: return false - return currentRoute in MainNavTab + return currentRoute in MainNavTab || currentRoute in InMainNavTab || currentRoute.contains("detail") || currentRoute.contains( + ProfileRoute.route, + ) } } diff --git a/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt b/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt index 6680c3b9..30b7ac34 100644 --- a/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt +++ b/feature/navigator/src/main/java/com/record/navigator/MainScreen.kt @@ -4,6 +4,8 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.EnterTransition import androidx.compose.animation.ExitTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -82,6 +84,13 @@ internal fun MainScreen( profileNavGraph( padding = innerPadding, + navigateToVideoDetail = { type, videoId, userId -> + navigator.navigateVideoDetail( + videoType = type, + videoId = videoId, + userId = userId, + ) + }, ) uploadNavGraph( @@ -92,7 +101,8 @@ internal fun MainScreen( padding = innerPadding, onShowSnackBar = viewModel::onShowSnackbar, navigateToMypage = { navigator.navigateMypage() }, - navigateToProfile = { navigator::navigateProfile }, + popBackStack = navigator::popBackStackIfNotHome, + navigateToProfile = navigator::navigateProfile, ) mypageNavGraph( @@ -136,6 +146,8 @@ private fun MainBottomNavigationBar( ) { AnimatedVisibility( visible = visible, + enter = fadeIn(), + exit = fadeOut(), ) { Column { HorizontalDivider( diff --git a/feature/profile/src/main/java/com/record/profile/ProfileContract.kt b/feature/profile/src/main/java/com/record/profile/ProfileContract.kt new file mode 100644 index 00000000..98c0593c --- /dev/null +++ b/feature/profile/src/main/java/com/record/profile/ProfileContract.kt @@ -0,0 +1,25 @@ +package com.record.profile + +import com.record.model.VideoType +import com.record.ui.base.SideEffect +import com.record.ui.base.UiState +import com.record.video.model.VideoData +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.toImmutableList + +data class ProfileState( + val id: Long = 0, + val followerCount: Int = 0, + val followingCount: Int = 0, + val isFollowing: Boolean = false, + val nickname: String = "", + val profileImageUrl: String = "", + val recordCount: Int = 0, + val cursorId: Long = 0, + val userVideos: ImmutableList = emptyList().toImmutableList(), + val isEnd: Boolean = false, +) : UiState + +sealed class ProfileSideEffect : SideEffect { + data class navigateToVideoDetail(val type: VideoType, val id: Long, val userId: Long) : ProfileSideEffect() +} diff --git a/feature/profile/src/main/java/com/record/profile/ProfileScreen.kt b/feature/profile/src/main/java/com/record/profile/ProfileScreen.kt index b3db38b5..d9ca59ef 100644 --- a/feature/profile/src/main/java/com/record/profile/ProfileScreen.kt +++ b/feature/profile/src/main/java/com/record/profile/ProfileScreen.kt @@ -18,6 +18,7 @@ import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridItemSpan import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -38,27 +39,55 @@ import com.record.designsystem.component.RecordyVideoThumbnail import com.record.designsystem.component.button.FollowButton import com.record.designsystem.component.navbar.TopNavigationBar import com.record.designsystem.theme.RecordyTheme -import com.record.model.SampleData +import com.record.model.VideoType +import com.record.ui.lifecycle.LaunchedEffectWithLifecycle +import com.record.ui.scroll.OnBottomReached +import kotlinx.coroutines.flow.collectLatest @Composable fun ProfileRoute( padding: PaddingValues, modifier: Modifier = Modifier, + viewModel: ProfileViewModel = hiltViewModel(), + navigateToVideoDetail: (VideoType, Long, Long) -> Unit, ) { + val uiState by viewModel.uiState.collectAsStateWithLifecycle() + + LaunchedEffectWithLifecycle { + viewModel.sideEffect.collectLatest { sideEffect -> + when (sideEffect) { + is ProfileSideEffect.navigateToVideoDetail -> { + navigateToVideoDetail(sideEffect.type, sideEffect.id, sideEffect.userId) + } + } + } + } + ProfileScreen( + state = uiState, padding = padding, modifier = modifier, + onLoadMore = viewModel::getVideos, + onFollowClick = viewModel::toggleFollow, + onBookmarkClick = viewModel::bookmark, + onVideoClick = viewModel::navigateVideo, ) } @Composable fun ProfileScreen( + state: ProfileState, padding: PaddingValues, modifier: Modifier = Modifier, - viewModel: ProfileViewModel = hiltViewModel(), + onLoadMore: () -> Unit, + onFollowClick: () -> Unit, + onBookmarkClick: (Long) -> Unit, + onVideoClick: (VideoType, Long) -> Unit, ) { - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - + val lazyGridState = rememberLazyGridState() + lazyGridState.OnBottomReached { + onLoadMore() + } Column( modifier = Modifier .fillMaxSize() @@ -77,7 +106,7 @@ fun ProfileScreen( .padding(bottom = 24.dp), ) { AsyncImage( - model = uiState.user.profileImageUrl, + model = state.profileImageUrl, contentDescription = "profile", contentScale = ContentScale.Crop, modifier = Modifier @@ -89,29 +118,30 @@ fun ProfileScreen( Column { Text( - text = uiState.user.nickname, + text = state.nickname, style = RecordyTheme.typography.subtitle, color = RecordyTheme.colors.white, ) Spacer(modifier = Modifier.height(4.dp)) BuildFollowerFollowingRow( - followerNum = uiState.user.followerCount, + followerNum = state.followerCount, ) } Spacer(modifier = Modifier.weight(1f)) - if (uiState.user.nickname != "유영") { + if (state.nickname != "유영") { FollowButton( - isFollowing = uiState.user.isFollowing, - onClick = { viewModel.toggleFollow(user = uiState.user) }, + isFollowing = state.isFollowing, + onClick = { onFollowClick() }, ) Spacer(modifier = Modifier.width(16.dp)) } } LazyVerticalGrid( + state = lazyGridState, modifier = Modifier .fillMaxHeight() .padding(horizontal = 16.dp) @@ -126,7 +156,7 @@ fun ProfileScreen( contentAlignment = Alignment.CenterEnd, ) { Text( - text = buildRecordCountText(uiState.user.recordCount), + text = buildRecordCountText(state.recordCount), style = RecordyTheme.typography.body2M, color = RecordyTheme.colors.gray01, modifier = Modifier @@ -135,11 +165,18 @@ fun ProfileScreen( } } - items(SampleData.sampleVideos) { item -> + items(state.userVideos) { item -> RecordyVideoThumbnail( - imageUri = item.previewUri, + imageUri = item.previewUrl, isBookmarkable = true, - isBookmark = false, + isBookmark = item.isBookmark, + location = item.location, + onClick = { + onVideoClick(VideoType.PROFILE, item.id) + }, + onBookmarkClick = { + onBookmarkClick(item.id) + }, ) } } diff --git a/feature/profile/src/main/java/com/record/profile/ProfileState.kt b/feature/profile/src/main/java/com/record/profile/ProfileState.kt deleted file mode 100644 index f138cce4..00000000 --- a/feature/profile/src/main/java/com/record/profile/ProfileState.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.record.profile - -import com.record.ui.base.SideEffect -import com.record.ui.base.UiState -import com.record.user.model.Profile - -data class ProfileState( - val user: Profile = Profile( - id = 0, - followerCount = 0, - followingCount = 0, - isFollowing = false, - nickname = "", - profileImageUrl = "", - recordCount = 0, - ), -) : UiState - -sealed class ProfileSideEffect : SideEffect diff --git a/feature/profile/src/main/java/com/record/profile/ProfileViewModel.kt b/feature/profile/src/main/java/com/record/profile/ProfileViewModel.kt index 0a29d454..e148c095 100644 --- a/feature/profile/src/main/java/com/record/profile/ProfileViewModel.kt +++ b/feature/profile/src/main/java/com/record/profile/ProfileViewModel.kt @@ -3,56 +3,161 @@ package com.record.profile import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.record.model.VideoType import com.record.model.exception.ApiError import com.record.profile.navigation.ProfileRoute import com.record.ui.base.BaseViewModel -import com.record.user.model.Profile import com.record.user.repository.UserRepository +import com.record.video.repository.VideoRepository import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.collections.immutable.toImmutableList import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class ProfileViewModel @Inject constructor( private val userRepository: UserRepository, + private val videoRepository: VideoRepository, savedStateHandle: SavedStateHandle, ) : BaseViewModel(ProfileState()) { private val userId = savedStateHandle.get(ProfileRoute.PROFILE_ID_ARG_NAME) init { - userId?.let { getProfile(it.toInt()) } + intent { + copy(id = userId?.toLong() ?: 0) + } + getProfile() + initialData() } - fun getProfile(userId: Int) { - viewModelScope.launch { - userRepository.getUserProfile(userId.toLong()).onSuccess { response -> + fun getProfile() = viewModelScope.launch { + userRepository.getUserProfile(uiState.value.id).onSuccess { response -> + intent { + copy( + id = response.id.toLong(), + followerCount = response.followerCount, + followingCount = response.followingCount, + isFollowing = response.isFollowing, + nickname = response.nickname, + profileImageUrl = response.profileImageUrl, + recordCount = response.recordCount, + ) + } + }.onFailure { + when (it) { + is ApiError -> { + Log.e("ProfileViewModel", it.message) + } + } + } + } + + fun initialData() = viewModelScope.launch { + videoRepository.getUserVideos(uiState.value.id, 0, 10).onSuccess { + intent { + copy( + userVideos = it.data.toImmutableList(), + cursorId = it.nextCursor?.plus(1)?.toLong() ?: 0, + isEnd = !it.hasNext, + ) + } + } + } + + fun getVideos() = viewModelScope.launch { + if (uiState.value.isEnd) return@launch + val list = uiState.value.userVideos.toList() + videoRepository.getUserVideos(uiState.value.id, uiState.value.cursorId, 10).onSuccess { + intent { + copy(userVideos = (list + it.data).toImmutableList(), cursorId = (it.nextCursor?.plus(1))?.toLong() ?: 0) + } + if (!it.hasNext) { intent { - copy(user = response) + copy(isEnd = true) } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("ProfileViewModel", it.message) - } + } + }.onFailure { + when (it) { + is ApiError -> { + Log.e("ProfileViewModel", it.message) } } } } - fun toggleFollow(user: Profile) { + fun toggleFollow() { + val originalIsFollowing = uiState.value.isFollowing + val originalFollowerCount = uiState.value.followerCount viewModelScope.launch { - val updatedUser = user.copy(isFollowing = !user.isFollowing) - userRepository.postFollow(user.id.toLong()).onSuccess { + intent { + copy( + isFollowing = !originalIsFollowing, + followerCount = if (originalIsFollowing) { + originalFollowerCount - 1 + } else { + originalFollowerCount + 1 + }, + ) + } + userRepository.postFollow(uiState.value.id).onSuccess { intent { - copy(user = updatedUser) + copy( + isFollowing = !originalIsFollowing, + followerCount = if (originalIsFollowing) { + originalFollowerCount - 1 + } else { + originalFollowerCount + 1 + }, + ) } }.onFailure { - when (it) { - is ApiError -> { - Log.e("ProfileViewModel", it.message) + intent { + copy( + isFollowing = originalIsFollowing, + followerCount = originalFollowerCount, + ) + } + } + } + } + + fun bookmark(id: Long) { + intent { + val updatedMyRecordList = uiState.value.userVideos.map { video -> + if (video.id == id) { + Log.e("태그", "변경") + video.copy(isBookmark = !video.isBookmark) + } else { + video + } + } + + copy( + userVideos = updatedMyRecordList.toImmutableList(), + ) + } + viewModelScope.launch { + videoRepository.bookmark(id).onSuccess { + val updatedMyRecordList = uiState.value.userVideos.map { video -> + if (video.id == id) { + Log.e("태그", "변경") + video.copy(isBookmark = it) + } else { + video } } + + intent { + copy( + userVideos = updatedMyRecordList.toImmutableList(), + ) + } + }.onFailure { } } } + + fun navigateVideo(videoType: VideoType, id: Long) { + postSideEffect(ProfileSideEffect.navigateToVideoDetail(videoType, id, uiState.value.id)) + } } diff --git a/feature/profile/src/main/java/com/record/profile/navigation/ProfileNavigation.kt b/feature/profile/src/main/java/com/record/profile/navigation/ProfileNavigation.kt index e36e5894..00dce4f1 100644 --- a/feature/profile/src/main/java/com/record/profile/navigation/ProfileNavigation.kt +++ b/feature/profile/src/main/java/com/record/profile/navigation/ProfileNavigation.kt @@ -5,6 +5,7 @@ import androidx.compose.ui.Modifier import androidx.navigation.NavController import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable +import com.record.model.VideoType import com.record.profile.ProfileRoute fun NavController.navigateProfile(id: Long) { @@ -14,24 +15,19 @@ fun NavController.navigateProfile(id: Long) { fun NavGraphBuilder.profileNavGraph( padding: PaddingValues, modifier: Modifier = Modifier, + navigateToVideoDetail: (VideoType, Long, Long) -> Unit, ) { - composable(route = ProfileRoute.route) { - ProfileRoute( - padding = padding, - modifier = modifier, - ) - } - composable(route = ProfileRoute.profileRoute("{${ProfileRoute.PROFILE_ID_ARG_NAME}}")) { ProfileRoute( padding = padding, modifier = modifier, + navigateToVideoDetail = navigateToVideoDetail, ) } } object ProfileRoute { - const val route = "Profile" + const val route = "profile" const val PROFILE_ID_ARG_NAME = "profile-id" - fun profileRoute(userId: String) = "profile/$userId" + fun profileRoute(userId: String) = "$route/$userId" } diff --git a/feature/video/src/main/java/com/record/video/VideoContract.kt b/feature/video/src/main/java/com/record/video/VideoContract.kt index a3939d49..e54b50d9 100644 --- a/feature/video/src/main/java/com/record/video/VideoContract.kt +++ b/feature/video/src/main/java/com/record/video/VideoContract.kt @@ -11,7 +11,7 @@ data class VideoState( val isAll: Boolean = true, val isPlaying: Boolean = false, val showDeleteDialog: Boolean = false, - val deleteVideoId: Int = 0, + val deleteVideoId: Long = 0, val allCursor: Long = 0, val followingCursor: Long = 0, ) : UiState @@ -19,6 +19,6 @@ data class VideoState( sealed interface VideoSideEffect : SideEffect { data class ShowNetworkErrorSnackbar(val msg: String) : VideoSideEffect data object NavigateToMypage : VideoSideEffect - data class NavigateToUserProfile(val id: Int) : VideoSideEffect + data class NavigateToUserProfile(val id: Long) : VideoSideEffect data class MovePage(val index: Int) : VideoSideEffect } diff --git a/feature/video/src/main/java/com/record/video/VideoScreen.kt b/feature/video/src/main/java/com/record/video/VideoScreen.kt index e875b4b1..3a0a10f4 100644 --- a/feature/video/src/main/java/com/record/video/VideoScreen.kt +++ b/feature/video/src/main/java/com/record/video/VideoScreen.kt @@ -34,7 +34,7 @@ fun VideoRoute( viewModel: VideoViewModel = hiltViewModel(), onShowSnackbar: (String, SnackBarType) -> Unit, navigateToMypage: () -> Unit, - navigateToProfile: (Int) -> Unit, + navigateToProfile: (Long) -> Unit, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val pagerState = rememberPagerState( @@ -47,10 +47,14 @@ fun VideoRoute( is VideoSideEffect.NavigateToUserProfile -> { navigateToProfile(sideEffect.id) } + VideoSideEffect.NavigateToMypage -> { navigateToMypage() } - is VideoSideEffect.ShowNetworkErrorSnackbar -> { onShowSnackbar(sideEffect.msg, SnackBarType.WARNING) } + + is VideoSideEffect.ShowNetworkErrorSnackbar -> { + onShowSnackbar(sideEffect.msg, SnackBarType.WARNING) + } is VideoSideEffect.MovePage -> { pagerState.scrollToPage(pagerState.currentPage - sideEffect.index) @@ -72,6 +76,7 @@ fun VideoRoute( onPlayVideo = viewModel::watchVideo, onNicknameClick = viewModel::navigateToProfile, loadMoreVideos = viewModel::loadMoreVideos, + onDialogDeleteButtonClick = viewModel::deleteVideo, ) } @@ -82,13 +87,14 @@ fun VideoScreen( modifier: Modifier = Modifier, state: VideoState, onToggleClick: () -> Unit, - onDeleteClick: (Int) -> Unit, - onBookmarkClick: (Int) -> Unit, - onNicknameClick: (Int) -> Unit, + onDeleteClick: (Long) -> Unit, + onBookmarkClick: (Long) -> Unit, + onNicknameClick: (Long) -> Unit, onDeleteDialogDismissRequest: () -> Unit, onError: (String) -> Unit, onPlayVideo: (Long) -> Unit, loadMoreVideos: () -> Unit, + onDialogDeleteButtonClick: (Long) -> Unit, ) { pagerState.onBottomReached( buffer = 3, @@ -99,7 +105,7 @@ fun VideoScreen( ) { VerticalPager( state = pagerState, - beyondBoundsPageCount = 1, + beyondBoundsPageCount = 0, modifier = Modifier.fillMaxSize(), ) { page -> Box { @@ -119,11 +125,11 @@ fun VideoScreen( nickname = nickname, content = content, isBookmark = isBookmark, - bookmarkCount = 123, + bookmarkCount = bookmarkCount, isMyVideo = isMine, - onBookmarkClick = { onBookmarkClick(id.toInt()) }, - onDeleteClick = { onDeleteClick(id.toInt()) }, - onNicknameClick = { onNicknameClick(id.toInt()) }, + onBookmarkClick = { onBookmarkClick(id) }, + onDeleteClick = { onDeleteClick(id) }, + onNicknameClick = { onNicknameClick(uploaderId) }, ) } } @@ -144,7 +150,7 @@ fun VideoScreen( negativeButtonLabel = "취소", positiveButtonLabel = "삭제", onDismissRequest = { onDeleteDialogDismissRequest() }, - onPositiveButtonClick = {}, + onPositiveButtonClick = { onDialogDeleteButtonClick(state.deleteVideoId) }, ) } } diff --git a/feature/video/src/main/java/com/record/video/VideoViewModel.kt b/feature/video/src/main/java/com/record/video/VideoViewModel.kt index 6ca7be79..158f7c54 100644 --- a/feature/video/src/main/java/com/record/video/VideoViewModel.kt +++ b/feature/video/src/main/java/com/record/video/VideoViewModel.kt @@ -36,11 +36,7 @@ class VideoViewModel @Inject constructor( copy(videos = (list + it).toImmutableList()) } }.onFailure { - when (it) { - is ApiError -> { - Log.e("에러", it.message) - } - } + handleError(it) } } @@ -51,19 +47,66 @@ class VideoViewModel @Inject constructor( } } - fun bookmark(id: Int) { + fun bookmark(id: Long) { + var originalBookmarkCount = 0 + var originalIsBookmark = false + val videos = uiState.value.videos.toList().map { video -> + if (video.id == id) { + originalBookmarkCount = video.bookmarkCount + originalIsBookmark = video.isBookmark + video.copy( + isBookmark = !video.isBookmark, + bookmarkCount = if (originalIsBookmark) originalBookmarkCount - 1 else originalBookmarkCount + 1, + ) + } else { + video + } + } intent { - val videos = uiState.value.videos.toList() - videos[videos.indexOfFirst { it.id.toInt() == id }].run { - copy(isBookmark = !isBookmark) + copy(videos = videos.toImmutableList()) + } + viewModelScope.launch { + videoRepository.bookmark(id).onSuccess { response -> + updateBookmarkStatus(id, response, originalBookmarkCount) + }.onFailure { + revertBookmarkStatus(id, originalBookmarkCount, originalIsBookmark) + } + } + } + + private fun updateBookmarkStatus(id: Long, response: Boolean, originalBookmarkCount: Int) { + val videos = uiState.value.videos.toList().map { video -> + if (video.id == id) { + video.copy( + isBookmark = response, + bookmarkCount = if (response) originalBookmarkCount + 1 else originalBookmarkCount - 1, + ) + } else { + video } - copy( - videos = videos.toImmutableList(), - ) + } + intent { + copy(videos = videos.toImmutableList()) } } - fun showDeleteDialog(id: Int) { + private fun revertBookmarkStatus(id: Long, originalBookmarkCount: Int, originalIsBookmark: Boolean) { + val videos = uiState.value.videos.toList().map { video -> + if (video.id == id) { + video.copy( + isBookmark = originalIsBookmark, + bookmarkCount = originalBookmarkCount, + ) + } else { + video + } + } + intent { + copy(videos = videos.toImmutableList()) + } + } + + fun showDeleteDialog(id: Long) { intent { copy(showDeleteDialog = true, deleteVideoId = id) } @@ -76,22 +119,17 @@ class VideoViewModel @Inject constructor( } fun deleteVideo(id: Long) = viewModelScope.launch { + dismissDeleteDialog() videoCoreRepository.deleteVideo(id).onSuccess { - val list = uiState.value.videos.toList() - val firstSize = list.size - val newlist = list.filter { it.id != id } - val secondSize = newlist.size - val removeItemCount = firstSize - secondSize - postSideEffect(VideoSideEffect.MovePage(removeItemCount)) - intent { - copy(videos = newlist.toImmutableList()) - } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("실패", it.message) - } - } + val videos = uiState.value.videos.filter { it.id != id }.toImmutableList() + postSideEffect(VideoSideEffect.MovePage(uiState.value.videos.size - videos.size)) + intent { copy(videos = videos) } + }.onFailure { handleError(it) } + } + + private fun handleError(throwable: Throwable) { + if (throwable is ApiError) { + Log.e("에러", throwable.message) } } @@ -103,7 +141,7 @@ class VideoViewModel @Inject constructor( videoCoreRepository.watchVideo(id) } - fun navigateToProfile(id: Int) { + fun navigateToProfile(id: Long) { postSideEffect(VideoSideEffect.NavigateToUserProfile(id)) } } diff --git a/feature/video/src/main/java/com/record/video/navigation/VideoNavigation.kt b/feature/video/src/main/java/com/record/video/navigation/VideoNavigation.kt index 2f6e6c49..f9ebcf64 100644 --- a/feature/video/src/main/java/com/record/video/navigation/VideoNavigation.kt +++ b/feature/video/src/main/java/com/record/video/navigation/VideoNavigation.kt @@ -24,7 +24,8 @@ fun NavGraphBuilder.videoNavGraph( modifier: Modifier = Modifier, onShowSnackBar: (String, SnackBarType) -> Unit, navigateToMypage: () -> Unit, - navigateToProfile: (Int) -> Unit, + navigateToProfile: (Long) -> Unit, + popBackStack: () -> Unit, ) { composable(route = VideoRoute.route) { VideoRoute( @@ -49,6 +50,7 @@ fun NavGraphBuilder.videoNavGraph( onShowSnackbar = onShowSnackBar, navigateToMypage = navigateToMypage, navigateToUserProfile = navigateToProfile, + popBackStack = popBackStack, ) } } @@ -59,5 +61,6 @@ object VideoRoute { const val VIDEO_INDEX = "video-index" const val VIDEO_KEYWORD = "video-keyword" const val VIDEO_USER_ID = "video-user-id" - fun detailRoute(type: String, id: String, keyword: String?, userId: String) = "detail/$type/$id/${keyword ?: "all"}/$userId" + const val detailRoute = "detail" + fun detailRoute(type: String, id: String, keyword: String?, userId: String) = "$detailRoute/$type/$id/${keyword ?: "all"}/$userId" } diff --git a/feature/video/src/main/java/com/record/video/videodetail/VideoDetailContract.kt b/feature/video/src/main/java/com/record/video/videodetail/VideoDetailContract.kt index 44482a1c..f48d0e00 100644 --- a/feature/video/src/main/java/com/record/video/videodetail/VideoDetailContract.kt +++ b/feature/video/src/main/java/com/record/video/videodetail/VideoDetailContract.kt @@ -20,13 +20,14 @@ data class VideoDetailState( val cursor: Long = 0, val userId: Long = 0, val isEnd: Boolean = false, - val init: Boolean = false, + val isInit: Boolean = false, ) : UiState sealed interface VideoDetailSideEffect : SideEffect { data class ShowNetworkErrorSnackbar(val msg: String) : VideoDetailSideEffect data object NavigateToMypage : VideoDetailSideEffect - data class NavigateToUserProfile(val id: Int) : VideoDetailSideEffect + data class NavigateToUserProfile(val id: Long) : VideoDetailSideEffect data class InitialPagerState(val index: Int) : VideoDetailSideEffect data class MovePage(val index: Int) : VideoDetailSideEffect + data object NavigateToBack : VideoDetailSideEffect } diff --git a/feature/video/src/main/java/com/record/video/videodetail/VideoDetailScreen.kt b/feature/video/src/main/java/com/record/video/videodetail/VideoDetailScreen.kt index 156feca6..7b9d9b5b 100644 --- a/feature/video/src/main/java/com/record/video/videodetail/VideoDetailScreen.kt +++ b/feature/video/src/main/java/com/record/video/videodetail/VideoDetailScreen.kt @@ -8,10 +8,12 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.pager.PagerState import androidx.compose.foundation.pager.VerticalPager import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.Icon import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -21,6 +23,8 @@ import com.record.designsystem.component.dialog.RecordyDialog import com.record.designsystem.component.snackbar.SnackBarType import com.record.designsystem.component.videoplayer.RecordyVideoText import com.record.designsystem.component.videoplayer.VideoPlayer +import com.record.designsystem.theme.RecordyTheme +import com.record.ui.extension.customClickable import com.record.ui.lifecycle.LaunchedEffectWithLifecycle import com.record.ui.scroll.onBottomReached import kotlinx.coroutines.flow.collectLatest @@ -32,8 +36,9 @@ fun VideoDetailRoute( modifier: Modifier, viewModel: VideoDetailViewModel = hiltViewModel(), onShowSnackbar: (String, SnackBarType) -> Unit, - navigateToUserProfile: (Int) -> Unit, + navigateToUserProfile: (Long) -> Unit, navigateToMypage: () -> Unit, + popBackStack: () -> Unit, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() val pagerState = rememberPagerState( @@ -62,6 +67,10 @@ fun VideoDetailRoute( is VideoDetailSideEffect.MovePage -> { pagerState.scrollToPage(pagerState.currentPage - sideEffect.index) } + + VideoDetailSideEffect.NavigateToBack -> { + popBackStack() + } } } } @@ -78,6 +87,8 @@ fun VideoDetailRoute( onPlayVideo = viewModel::watchVideo, onNickNameClick = viewModel::navigateToProfile, loadMoreVideos = viewModel::getVideos, + onBackButtonClick = viewModel::navigateToBack, + onDialogDeleteButtonClick = viewModel::deleteVideo, ) } @@ -88,12 +99,14 @@ fun VideoDetailScreen( modifier: Modifier = Modifier, state: VideoDetailState, onDeleteClick: (Long) -> Unit, - onBookmarkClick: (Int) -> Unit, + onBookmarkClick: (Long) -> Unit, onDeleteDialogDismissRequest: () -> Unit, - onNickNameClick: (Int) -> Unit, + onNickNameClick: (Long) -> Unit, onError: (String) -> Unit, onPlayVideo: (Long) -> Unit, loadMoreVideos: () -> Unit, + onBackButtonClick: () -> Unit, + onDialogDeleteButtonClick: () -> Unit, ) { pagerState.onBottomReached( buffer = 3, @@ -104,7 +117,7 @@ fun VideoDetailScreen( ) { VerticalPager( state = pagerState, - beyondBoundsPageCount = 1, + beyondBoundsPageCount = 0, modifier = Modifier.fillMaxSize(), ) { page -> Box { @@ -124,16 +137,25 @@ fun VideoDetailScreen( nickname = nickname, content = content, isBookmark = isBookmark, - bookmarkCount = 123, + bookmarkCount = bookmarkCount, isMyVideo = isMine, - onBookmarkClick = { onBookmarkClick(id.toInt()) }, + onBookmarkClick = { onBookmarkClick(id) }, onDeleteClick = { onDeleteClick(id) }, - onNicknameClick = { onNickNameClick(id.toInt()) }, + onNicknameClick = { onNickNameClick(uploaderId) }, ) } } } } + Icon( + modifier = Modifier + .align(Alignment.TopStart) + .padding(start = 16.dp, top = 46.dp, end = 16.dp, bottom = 16.dp) + .customClickable(rippleEnabled = false) { onBackButtonClick() }, + painter = painterResource(id = R.drawable.ic_angle_left_24), + contentDescription = "back", + tint = RecordyTheme.colors.white, + ) if (state.showDeleteDialog) { RecordyDialog( graphicAsset = R.drawable.img_trashcan, @@ -142,7 +164,7 @@ fun VideoDetailScreen( negativeButtonLabel = "취소", positiveButtonLabel = "삭제", onDismissRequest = { onDeleteDialogDismissRequest() }, - onPositiveButtonClick = {}, + onPositiveButtonClick = { onDialogDeleteButtonClick() }, ) } } diff --git a/feature/video/src/main/java/com/record/video/videodetail/VideoDetailViewModel.kt b/feature/video/src/main/java/com/record/video/videodetail/VideoDetailViewModel.kt index 4d8e54df..7a8d533a 100644 --- a/feature/video/src/main/java/com/record/video/videodetail/VideoDetailViewModel.kt +++ b/feature/video/src/main/java/com/record/video/videodetail/VideoDetailViewModel.kt @@ -3,9 +3,12 @@ package com.record.video.videodetail import android.util.Log import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope +import com.record.model.Cursor +import com.record.model.Page import com.record.model.VideoType import com.record.model.exception.ApiError import com.record.ui.base.BaseViewModel +import com.record.video.model.VideoData import com.record.video.navigation.VideoRoute import com.record.video.repository.VideoCoreRepository import com.record.video.repository.VideoRepository @@ -21,162 +24,194 @@ class VideoDetailViewModel @Inject constructor( savedStateHandle: SavedStateHandle, ) : BaseViewModel(VideoDetailState()) { private val videoTypeString = savedStateHandle.get(VideoRoute.VIDEO_TYPE_ARG_NAME) - private val videoIndexString = savedStateHandle.get(VideoRoute.VIDEO_INDEX) + private val videoIdString = savedStateHandle.get(VideoRoute.VIDEO_INDEX) private val userIdString = savedStateHandle.get(VideoRoute.VIDEO_USER_ID) private val keyword = savedStateHandle.get(VideoRoute.VIDEO_KEYWORD) private val videoTypeEnum = videoTypeString?.let { VideoType.valueOf(videoTypeString) } - private val videoIndex = videoIndexString?.toInt() - private val userId = userIdString?.toLong() + private val videoId = videoIdString?.toLong() + private val otherUserId = userIdString?.toLong() init { - val page = videoIndex?.div(10) ?: 0 - val index = videoIndex?.rem(10) ?: 0 val realKeyword = if (keyword == "all" || keyword == null) "" else keyword - Log.e("순서대로 페이지 인덱스 키워드", "$page $index $realKeyword") intent { copy( videoType = videoTypeEnum ?: VideoType.MY, - observingIndex = index, - page = page, + observingId = videoId ?: 0, + page = 0, + cursor = videoId ?: 0, keyword = realKeyword, - userId = userId, + userId = otherUserId ?: 0, ) } - runCatching { getVideos() }.onSuccess { - } + getVideos() } fun getVideos() { if (uiState.value.isEnd) return when (uiState.value.videoType) { - VideoType.PROFILE -> { - } + VideoType.PROFILE -> fetchVideos { getUserVideos() } + VideoType.BOOKMARK -> fetchVideos { getBookmarkVideos() } + VideoType.MY -> fetchVideos { getMyVideos() } + VideoType.POPULAR -> fetchVideos { getPopularVideos() } + VideoType.RECENT -> fetchVideos { getRecentVideos() } + } + } - VideoType.BOOKMARK -> {} - VideoType.MY -> { - } + private inline fun fetchVideos(crossinline fetch: suspend () -> Unit) = viewModelScope.launch { + runCatching { fetch() }.onFailure { handleError(it) } + } - VideoType.POPULAR -> { - getPopularVideo() - } + private suspend fun getPopularVideos() { + val videos = uiState.value.videos.toList() + val keyword = uiState.value.keyword.takeIf { it.isNotBlank() }?.let { listOf(it) } + videoRepository.getPopularVideos(keyword, uiState.value.page, 10).handlePageResponse(videos) + } - VideoType.RECENT -> { - getRecentVideo() - } - } + private suspend fun getRecentVideos() { + val videos = uiState.value.videos.toList() + val keyword = uiState.value.keyword.takeIf { it.isNotBlank() }?.let { listOf(it) } + videoRepository.getRecentVideos(keyword, uiState.value.cursor + 1, 10).handleCursorResponse(videos) + } + + private suspend fun getMyVideos() { + val videos = uiState.value.videos.toList() + videoRepository.getMyVideos(uiState.value.cursor + 1, 10).handleCursorResponse(videos) + } + + private suspend fun getUserVideos() { + val videos = uiState.value.videos.toList() + videoRepository.getUserVideos(uiState.value.userId, uiState.value.cursor + 1, 10).handleCursorResponse(videos) } - fun getPopularVideo() = viewModelScope.launch { - val keyword = if (uiState.value.keyword.isBlank()) null else listOf(uiState.value.keyword) + private suspend fun getBookmarkVideos() { val videos = uiState.value.videos.toList() - videoRepository.getPopularVideos(keyword, uiState.value.page, 10).onSuccess { + videoRepository.getBookmarkVideos(uiState.value.cursor + 1, 10).handleCursorResponse(videos) + } + + private fun Result>.handleCursorResponse(existingVideos: List) { + onSuccess { response -> + val newVideos = (existingVideos + response.data).toImmutableList() intent { - copy(videos = (videos + it.data).toImmutableList(), page = it.page + 1) - } - if (!it.hasNext) { - intent { - copy(isEnd = true) - } - } - if (!uiState.value.init) { - postSideEffect(VideoDetailSideEffect.InitialPagerState((uiState.value.page - 1) * 10 + uiState.value.observingIndex)) - intent { - copy(init = true) - } + copy(videos = newVideos, cursor = response.nextCursor?.toLong() ?: 0) } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("에러", it.message) - } + intent { + copy(isEnd = !response.hasNext) } } } - fun getRecentVideo() = viewModelScope.launch { - val keyword = if (uiState.value.keyword.isBlank()) null else listOf(uiState.value.keyword) - val videos = uiState.value.videos.toList() - videoRepository.getRecentVideos(keyword, uiState.value.page, 10).onSuccess { + private fun Result>.handlePageResponse(existingVideos: List) { + onSuccess { response -> + val newVideos = (existingVideos + response.data).toImmutableList() + val observingIndex = newVideos.indexOfFirst { it.id == uiState.value.observingId } intent { - copy(videos = (videos + it.data).toImmutableList(), page = uiState.value.page + 1) - } - if (!it.hasNext) { - intent { - copy(isEnd = true) - } - } - if (!uiState.value.init) { - postSideEffect(VideoDetailSideEffect.InitialPagerState((uiState.value.page - 1) * 10 + uiState.value.observingIndex)) - intent { - copy(init = true) - } + copy(videos = newVideos) } intent { - copy(page = it.nextCursor) + copy(isEnd = !response.hasNext) } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("에러", it.message) + if (!uiState.value.isInit) { + postSideEffect(VideoDetailSideEffect.InitialPagerState(observingIndex)) + intent { + copy(isInit = true) } } } } - fun bookmark(id: Int) { + private fun handleError(throwable: Throwable) { + if (throwable is ApiError) { + Log.e("에러", throwable.message) + } + } + + fun bookmark(id: Long) { + var originalBookmarkCount = 0 + var originalIsBookmark = false + val videos = uiState.value.videos.toList().map { video -> + if (video.id == id) { + originalBookmarkCount = video.bookmarkCount + originalIsBookmark = video.isBookmark + video.copy( + isBookmark = !video.isBookmark, + bookmarkCount = if (originalIsBookmark) originalBookmarkCount - 1 else originalBookmarkCount + 1, + ) + } else { + video + } + } intent { - val videos = uiState.value.videos.toList() - videos[videos.indexOfFirst { it.id.toInt() == id }].run { - copy(isBookmark = !isBookmark) + copy(videos = videos.toImmutableList()) + } + viewModelScope.launch { + videoRepository.bookmark(id).onSuccess { response -> + updateBookmarkStatus(id, response, originalBookmarkCount) + }.onFailure { + revertBookmarkStatus(id, originalBookmarkCount, originalIsBookmark) } - copy( - videos = videos.toImmutableList(), - ) } } - fun deleteVideo(id: Long) = viewModelScope.launch { - videoCoreRepository.deleteVideo(id).onSuccess { - val list = uiState.value.videos.toList() - val firstSize = list.size - val newlist = list.filter { it.id != id } - val secondSize = newlist.size - val removeItemCount = firstSize - secondSize - postSideEffect(VideoDetailSideEffect.MovePage(removeItemCount)) - intent { - copy(videos = newlist.toImmutableList()) + private fun updateBookmarkStatus(id: Long, response: Boolean, originalBookmarkCount: Int) { + val videos = uiState.value.videos.toList().map { video -> + if (video.id == id) { + video.copy( + isBookmark = response, + bookmarkCount = if (response) originalBookmarkCount + 1 else originalBookmarkCount - 1, + ) + } else { + video } - }.onFailure { - when (it) { - is ApiError -> { - Log.e("실패", it.message) - } + } + intent { + copy(videos = videos.toImmutableList()) + } + } + + private fun revertBookmarkStatus(id: Long, originalBookmarkCount: Int, originalIsBookmark: Boolean) { + val videos = uiState.value.videos.toList().map { video -> + if (video.id == id) { + video.copy( + isBookmark = originalIsBookmark, + bookmarkCount = originalBookmarkCount, + ) + } else { + video } } + intent { + copy(videos = videos.toImmutableList()) + } } - fun watchVideo(id: Long) = viewModelScope.launch { - videoCoreRepository.watchVideo(id) + + fun deleteVideo() = viewModelScope.launch { + val id = uiState.value.deleteVideoId + videoCoreRepository.deleteVideo(id).onSuccess { + val videos = uiState.value.videos.filter { it.id != id }.toImmutableList() + postSideEffect(VideoDetailSideEffect.MovePage(uiState.value.videos.size - videos.size)) + intent { copy(videos = videos) } + }.onFailure { handleError(it) } } + + fun watchVideo(id: Long) = viewModelScope.launch { videoCoreRepository.watchVideo(id) } + fun showDeleteDialog(id: Long) { - intent { - copy(showDeleteDialog = true, deleteVideoId = id) - } + intent { copy(showDeleteDialog = true, deleteVideoId = id) } } fun dismissDeleteDialog() { - intent { - copy(showDeleteDialog = false, deleteVideoId = 0) - } + intent { copy(showDeleteDialog = false, deleteVideoId = 0) } } fun showNetworkErrorSnackbar(msg: String) { postSideEffect(VideoDetailSideEffect.ShowNetworkErrorSnackbar(msg)) } - fun watchVideo(id: Int) { + fun navigateToProfile(id: Long) { + postSideEffect(VideoDetailSideEffect.NavigateToUserProfile(id)) } - fun navigateToProfile(id: Int) { - postSideEffect(VideoDetailSideEffect.NavigateToUserProfile(id)) + fun navigateToBack() { + postSideEffect(VideoDetailSideEffect.NavigateToBack) } } diff --git a/local/user/build.gradle.kts b/local/user/build.gradle.kts index f6868bce..224de53a 100644 --- a/local/user/build.gradle.kts +++ b/local/user/build.gradle.kts @@ -10,6 +10,7 @@ dependencies { implementation(projects.core.model) implementation(projects.core.datastore) implementation(projects.data.user) + implementation(projects.data.video) implementation(libs.kotlinx.serialization.json) implementation(libs.bundles.datastore) diff --git a/local/user/src/main/java/com/record/user/datasource/UserLocalDataSourceImpl.kt b/local/user/src/main/java/com/record/user/datasource/UserLocalDataSourceImpl.kt index f7fe210e..a2f940c2 100644 --- a/local/user/src/main/java/com/record/user/datasource/UserLocalDataSourceImpl.kt +++ b/local/user/src/main/java/com/record/user/datasource/UserLocalDataSourceImpl.kt @@ -3,14 +3,18 @@ package com.record.user.datasource import com.record.datastore.token.UserDataStore import com.record.datastore.user.UserData import com.record.user.source.local.UserLocalDataSource +import com.record.video.source.local.LocalUserInfoDataSource import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.first import javax.inject.Inject class UserLocalDataSourceImpl @Inject constructor( private val userLocalDataSource: UserDataStore, -) : UserLocalDataSource { +) : UserLocalDataSource, LocalUserInfoDataSource { override val userLocalData: Flow = userLocalDataSource.user override suspend fun setUserLocalData(userData: UserData) { userLocalDataSource.setUserId(userData) } + + override suspend fun getMyId(): Long = userLocalData.first().userid } diff --git a/local/user/src/main/java/com/record/user/di/UserLocalDataModule.kt b/local/user/src/main/java/com/record/user/di/UserLocalDataModule.kt index 6c1f2900..74cdc71e 100644 --- a/local/user/src/main/java/com/record/user/di/UserLocalDataModule.kt +++ b/local/user/src/main/java/com/record/user/di/UserLocalDataModule.kt @@ -2,6 +2,7 @@ package com.record.user.di import com.record.user.datasource.UserLocalDataSourceImpl import com.record.user.source.local.UserLocalDataSource +import com.record.video.source.local.LocalUserInfoDataSource import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -14,4 +15,8 @@ abstract class UserLocalDataModule { @Binds @Singleton abstract fun bindsUserLocalDataModule(userLocalDataSource: UserLocalDataSourceImpl): UserLocalDataSource + + @Binds + @Singleton + abstract fun bindsLocalUserInfoDataSource(userLocalDataSourceImpl: UserLocalDataSourceImpl): LocalUserInfoDataSource } diff --git a/project.dot.png b/project.dot.png index 0ba53b07..2c0d7030 100644 Binary files a/project.dot.png and b/project.dot.png differ diff --git a/remote/video/src/main/java/com/record/video/api/VideoApi.kt b/remote/video/src/main/java/com/record/video/api/VideoApi.kt index 180c53c0..cd5e93c3 100644 --- a/remote/video/src/main/java/com/record/video/api/VideoApi.kt +++ b/remote/video/src/main/java/com/record/video/api/VideoApi.kt @@ -19,8 +19,8 @@ interface VideoApi { @GET("/api/v1/records/recent") suspend fun getRecentVideos( @Query("keywords") keywords: List?, - @Query("pageNumber") pageNumber: Int, - @Query("pageSize") pageSize: Int, + @Query("cursorId") cursor: Long, + @Query("size") pageSize: Int, ): ResponseGetSliceVideoDto @GET("/api/v1/records/famous") diff --git a/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt b/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt index b47c9eda..8e9c3c50 100644 --- a/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt +++ b/remote/video/src/main/java/com/record/video/datasource/RemoteVideoDataSourceImpl.kt @@ -12,10 +12,10 @@ class RemoteVideoDataSourceImpl @Inject constructor( ) : RemoteVideoDataSource { override suspend fun getAllVideos(cursorId: Long, size: Int): List = videoApi.getAllVideos(cursorId, size) - override suspend fun getRecentVideos(keywords: List?, pageNumber: Int, pageSize: Int): ResponseGetSliceVideoDto = + override suspend fun getRecentVideos(keywords: List?, cursor: Long, pageSize: Int): ResponseGetSliceVideoDto = videoApi.getRecentVideos( keywords, - pageNumber, + cursor, pageSize, ) diff --git a/settings.gradle.kts b/settings.gradle.kts index c4e84140..d49f2328 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -57,3 +57,4 @@ include(":domain:keyword") include(":data:keyword") include(":local:user") +include(":core:cache")