From 2af7897f5b5a4f894c4dc6db2230be9b6d6e410c Mon Sep 17 00:00:00 2001 From: WeiHe Date: Tue, 5 Dec 2023 23:43:12 +0800 Subject: [PATCH] feat: Implement functionality of the VideoLibraryScreen --- .../com/wei/picquest/ui/PqAppStateTest.kt | 6 +- .../main/java/com/wei/picquest/ui/PqApp.kt | 3 - .../wei/picquest/core/data/di/DataModule.kt | 7 + ...ry.kt => DefaultSearchVideosRepository.kt} | 4 +- ...epository.kt => SearchVideosRepository.kt} | 2 +- .../network/retrofit/RetrofitPqNetwork.kt | 2 +- .../manager/RecentSearchManager.kt | 25 ---- .../video/videolibrary/VideoLibraryScreen.kt | 127 +++++++++++++++--- .../videolibrary/VideoLibraryViewModel.kt | 44 ++++++ .../videolibrary/VideoLibraryViewState.kt | 8 ++ 10 files changed, 174 insertions(+), 54 deletions(-) rename core/data/src/main/java/com/wei/picquest/core/data/repository/{DefaultSearchVideoRepository.kt => DefaultSearchVideosRepository.kt} (92%) rename core/data/src/main/java/com/wei/picquest/core/data/repository/{SearchVideoRepository.kt => SearchVideosRepository.kt} (87%) delete mode 100644 feature/photo/src/main/java/com/wei/picquest/feature/photo/photosearch/manager/RecentSearchManager.kt create mode 100644 feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewModel.kt create mode 100644 feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewState.kt diff --git a/app/src/androidTest/java/com/wei/picquest/ui/PqAppStateTest.kt b/app/src/androidTest/java/com/wei/picquest/ui/PqAppStateTest.kt index f604ea2..ab4a92a 100644 --- a/app/src/androidTest/java/com/wei/picquest/ui/PqAppStateTest.kt +++ b/app/src/androidTest/java/com/wei/picquest/ui/PqAppStateTest.kt @@ -88,10 +88,11 @@ class PqAppStateTest { ) } - assertThat(state.topLevelDestinations).hasSize(3) + assertThat(state.topLevelDestinations).hasSize(4) assertThat(state.topLevelDestinations[0].name).ignoringCase().contains("home") assertThat(state.topLevelDestinations[1].name).ignoringCase().contains("photo") - assertThat(state.topLevelDestinations[2].name).ignoringCase().contains("contact_me") + assertThat(state.topLevelDestinations[2].name).ignoringCase().contains("video") + assertThat(state.topLevelDestinations[3].name).ignoringCase().contains("contact_me") } @Test @@ -280,6 +281,7 @@ private fun rememberTestNavController(): TestNavHostController { composable("a") { } composable("b") { } composable("c") { } + composable("d") { } } } } diff --git a/app/src/main/java/com/wei/picquest/ui/PqApp.kt b/app/src/main/java/com/wei/picquest/ui/PqApp.kt index 088bb9a..23bd43f 100644 --- a/app/src/main/java/com/wei/picquest/ui/PqApp.kt +++ b/app/src/main/java/com/wei/picquest/ui/PqApp.kt @@ -58,7 +58,6 @@ import com.wei.picquest.core.manager.SnackbarState import com.wei.picquest.core.utils.UiText import com.wei.picquest.navigation.PqNavHost import com.wei.picquest.navigation.TopLevelDestination -import timber.log.Timber @OptIn( ExperimentalMaterial3Api::class, @@ -295,8 +294,6 @@ private fun PqBottomBar( private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) = this?.hierarchy?.any { - Timber.e("PQ isTopLevelDestinationInHierarchy " + it.route.toString()) - Timber.e("PQ destination " + destination.name.toString()) it.route?.contains(destination.name, true) ?: false } ?: false diff --git a/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt b/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt index b0fbfa0..7613291 100644 --- a/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt +++ b/core/data/src/main/java/com/wei/picquest/core/data/di/DataModule.kt @@ -1,8 +1,10 @@ package com.wei.picquest.core.data.di import com.wei.picquest.core.data.repository.DefaultSearchImagesRepository +import com.wei.picquest.core.data.repository.DefaultSearchVideosRepository import com.wei.picquest.core.data.repository.DefaultUserDataRepository import com.wei.picquest.core.data.repository.SearchImagesRepository +import com.wei.picquest.core.data.repository.SearchVideosRepository import com.wei.picquest.core.data.repository.UserDataRepository import com.wei.picquest.core.data.utils.ConnectivityManagerNetworkMonitor import com.wei.picquest.core.data.utils.NetworkMonitor @@ -20,6 +22,11 @@ interface DataModule { searchImagesRepository: DefaultSearchImagesRepository, ): SearchImagesRepository + @Binds + fun bindsSearchVideosRepository( + searchVideosRepository: DefaultSearchVideosRepository, + ): SearchVideosRepository + @Binds fun bindsNetworkMonitor( networkMonitor: ConnectivityManagerNetworkMonitor, diff --git a/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchVideoRepository.kt b/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchVideosRepository.kt similarity index 92% rename from core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchVideoRepository.kt rename to core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchVideosRepository.kt index 8f75180..4c11b2a 100644 --- a/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchVideoRepository.kt +++ b/core/data/src/main/java/com/wei/picquest/core/data/repository/DefaultSearchVideosRepository.kt @@ -12,9 +12,9 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Inject -class DefaultSearchVideoRepository @Inject constructor( +class DefaultSearchVideosRepository @Inject constructor( private val pqNetworkDataSource: PqNetworkDataSource, -) : SearchVideoRepository { +) : SearchVideosRepository { override suspend fun getSearchVideo(query: String): Flow> { return Pager( diff --git a/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchVideoRepository.kt b/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchVideosRepository.kt similarity index 87% rename from core/data/src/main/java/com/wei/picquest/core/data/repository/SearchVideoRepository.kt rename to core/data/src/main/java/com/wei/picquest/core/data/repository/SearchVideosRepository.kt index cda6f63..9255245 100644 --- a/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchVideoRepository.kt +++ b/core/data/src/main/java/com/wei/picquest/core/data/repository/SearchVideosRepository.kt @@ -4,7 +4,7 @@ import androidx.paging.PagingData import com.wei.picquest.core.data.model.VideoDetail import kotlinx.coroutines.flow.Flow -interface SearchVideoRepository { +interface SearchVideosRepository { suspend fun getSearchVideo(query: String): Flow> } diff --git a/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt b/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt index d0148d2..4b7d771 100644 --- a/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt +++ b/core/network/src/main/java/com/wei/picquest/core/network/retrofit/RetrofitPqNetwork.kt @@ -37,7 +37,7 @@ interface RetrofitPixabayApi { /** * https://pixabay.com/api/videos/?key=${api key}&q=yellow+flowers */ - @GET("/videos/") + @GET("./videos/") suspend fun searchVideos( @Query("key") apiKey: String = API_KEY, @Query("q") query: String, diff --git a/feature/photo/src/main/java/com/wei/picquest/feature/photo/photosearch/manager/RecentSearchManager.kt b/feature/photo/src/main/java/com/wei/picquest/feature/photo/photosearch/manager/RecentSearchManager.kt deleted file mode 100644 index 24d77ee..0000000 --- a/feature/photo/src/main/java/com/wei/picquest/feature/photo/photosearch/manager/RecentSearchManager.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.wei.picquest.feature.photo.photosearch.manager - -class RecentSearchManager(private val maxCapacity: Int = 10) { - private val recentSearches = linkedSetOf() - - fun addSearchQuery(query: String) { - if (query in recentSearches) { - recentSearches.remove(query) - } - - recentSearches.add(query) - - if (recentSearches.size > maxCapacity) { - val oldestQuery = recentSearches.iterator().next() - recentSearches.remove(oldestQuery) - } - } - - fun clearSearches() { - recentSearches.clear() - } - - val recentSearchQueries: List - get() = recentSearches.toList().asReversed() -} diff --git a/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryScreen.kt b/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryScreen.kt index ac331d7..52836cc 100644 --- a/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryScreen.kt +++ b/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryScreen.kt @@ -1,26 +1,48 @@ package com.wei.picquest.feature.video.videolibrary +import android.net.Uri +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.safeDrawing +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.windowInsetsBottomHeight import androidx.compose.foundation.layout.windowInsetsTopHeight +import androidx.compose.foundation.pager.VerticalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.net.toUri +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.media3.common.C +import androidx.media3.common.MediaItem +import androidx.media3.common.Player +import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.exoplayer.ExoPlayer +import androidx.media3.exoplayer.source.ProgressiveMediaSource +import androidx.media3.ui.AspectRatioFrameLayout +import androidx.media3.ui.PlayerView import androidx.navigation.NavController +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import com.wei.picquest.core.data.model.VideoDetail import com.wei.picquest.core.designsystem.component.FunctionalityNotAvailablePopup -import com.wei.picquest.core.designsystem.component.ThemePreviews -import com.wei.picquest.core.designsystem.theme.PqTheme /** * @@ -54,12 +76,17 @@ import com.wei.picquest.core.designsystem.theme.PqTheme @Composable internal fun VideoLibraryRoute( navController: NavController, + viewModel: VideoLibraryViewModel = hiltViewModel(), ) { - VideoLibraryScreen() + val lazyPagingItems = viewModel.videosState.collectAsLazyPagingItems() + + VideoLibraryScreen(lazyPagingItems) } +@OptIn(ExperimentalFoundationApi::class) @Composable internal fun VideoLibraryScreen( + lazyPagingItems: LazyPagingItems, withTopSpacer: Boolean = true, withBottomSpacer: Boolean = true, ) { @@ -84,16 +111,20 @@ internal fun VideoLibraryScreen( Spacer(Modifier.windowInsetsTopHeight(WindowInsets.safeDrawing)) } - Column { - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "Screen not available \uD83D\uDE48", - color = MaterialTheme.colorScheme.error, - style = MaterialTheme.typography.headlineMedium, - modifier = Modifier - .semantics { contentDescription = "" }, - ) - Spacer(modifier = Modifier.weight(1f)) + val pagerState = rememberPagerState( + initialPage = 0, + initialPageOffsetFraction = 0f, + pageCount = { lazyPagingItems.itemCount }, + ) + + VerticalPager( + state = pagerState, + modifier = Modifier.fillMaxSize(), + ) { page -> + val videoDetail = lazyPagingItems[page] + videoDetail?.let { + VideoPlayer(uri = it.videos.tiny.url.toUri()) + } } if (withBottomSpacer) { @@ -103,10 +134,66 @@ internal fun VideoLibraryScreen( } } -@ThemePreviews +@androidx.annotation.OptIn(UnstableApi::class) @Composable -fun VideoLibraryScreenPreview() { - PqTheme { - VideoLibraryScreen() +fun VideoPlayer(uri: Uri) { + val context = LocalContext.current + val isPlayerReady = remember { mutableStateOf(false) } + + val exoPlayer = remember(uri) { + ExoPlayer.Builder(context).build().apply { + playWhenReady = true + videoScalingMode = C.VIDEO_SCALING_MODE_SCALE_TO_FIT + repeatMode = Player.REPEAT_MODE_ONE + + addListener(object : Player.Listener { + override fun onPlaybackStateChanged(state: Int) { + isPlayerReady.value = (state == Player.STATE_READY) + } + }) + + val dataSourceFactory: DataSource.Factory = DefaultDataSource.Factory(context) + val mediaSource = ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(MediaItem.fromUri(uri)) + + setMediaSource(mediaSource) + prepare() + } + } + + DisposableEffect(uri) { + onDispose { + exoPlayer.release() + } + } + + Box { + AndroidView( + factory = { + PlayerView(it).apply { + useController = false + resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT + player = exoPlayer + alpha = if (isPlayerReady.value) 0f else 1f + } + }, + modifier = Modifier.fillMaxSize(), + ) + + if (!isPlayerReady.value) { + LoadingView() + } + } +} + +@Composable +private fun LoadingView() { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + ) { + CircularProgressIndicator(modifier = Modifier.size(30.dp)) } } diff --git a/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewModel.kt b/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewModel.kt new file mode 100644 index 0000000..36cbb7c --- /dev/null +++ b/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewModel.kt @@ -0,0 +1,44 @@ +package com.wei.picquest.feature.video.videolibrary + +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn +import com.wei.picquest.core.base.BaseViewModel +import com.wei.picquest.core.data.model.VideoDetail +import com.wei.picquest.core.data.repository.SearchVideosRepository +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class VideoLibraryViewModel @Inject constructor( + private val searchVideoRepository: SearchVideosRepository, +) : BaseViewModel< + VideoLibraryViewAction, + VideoLibraryViewState, + >(VideoLibraryViewState) { + + private val _videosState: MutableStateFlow> = + MutableStateFlow(value = PagingData.empty()) + val videosState: MutableStateFlow> get() = _videosState + + init { + searchVideos("cat") + } + + private fun searchVideos(query: String) { + viewModelScope.launch { + searchVideoRepository.getSearchVideo(query) + .distinctUntilChanged() + .cachedIn(viewModelScope) + .collect { pagingData -> + _videosState.value = pagingData + } + } + } + + override fun dispatch(action: VideoLibraryViewAction) { + } +} diff --git a/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewState.kt b/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewState.kt new file mode 100644 index 0000000..0c8b296 --- /dev/null +++ b/feature/video/src/main/java/com/wei/picquest/feature/video/videolibrary/VideoLibraryViewState.kt @@ -0,0 +1,8 @@ +package com.wei.picquest.feature.video.videolibrary + +import com.wei.picquest.core.base.Action +import com.wei.picquest.core.base.State + +sealed class VideoLibraryViewAction : Action + +object VideoLibraryViewState : State