From 647c0327b7e5f81cfef46c662f4f02db746d5451 Mon Sep 17 00:00:00 2001 From: brahmkshatriya <69040506+brahmkshatriya@users.noreply.github.com> Date: Mon, 5 Feb 2024 23:18:17 +0530 Subject: [PATCH] - Add HomeFeed in OfflineExtension - Create ContainerLoadingAdapter - Switch to clients instead of OfflineExtension - Fix some issues with player view --- .../echo/data/extensions/OfflineExtension.kt | 99 ++++++++------ .../echo/data/models/MediaItem.kt | 3 + .../echo/data/models/MediaItemsContainer.kt | 16 ++- .../brahmkshatriya/echo/di/PluginModule.kt | 18 ++- .../ui/adapters/ContainerLoadingAdapter.kt | 52 ++++++++ .../ui/adapters/MediaItemsContainerAdapter.kt | 123 +++++++++++++++--- .../echo/ui/adapters/ShimmerAdapter.kt | 22 ---- .../echo/ui/home/HomeFragment.kt | 23 +++- .../echo/ui/home/HomeViewModel.kt | 29 ++++- .../echo/ui/library/LibraryFragment.kt | 4 +- .../echo/ui/player/PlayerListener.kt | 40 +++--- .../echo/ui/player/PlayerView.kt | 14 +- .../echo/ui/player/PlayerViewModel.kt | 6 +- .../echo/ui/search/SearchViewModel.kt | 6 +- app/src/main/res/color/bottom_item_icon.xml | 2 +- app/src/main/res/color/button_play_pause.xml | 2 +- app/src/main/res/drawable/ic_refresh.xml | 9 ++ app/src/main/res/layout/bottom_player.xml | 38 +++--- ...m_media_recycler.xml => item_category.xml} | 16 ++- app/src/main/res/layout/item_error.xml | 26 ++++ app/src/main/res/layout/item_track.xml | 61 +++++++++ .../layout/skeleton_item_album_recycler.xml | 41 ------ .../res/layout/skeleton_item_category.xml | 29 +++++ .../res/layout/skeleton_item_container.xml | 30 +++++ ...item_album.xml => skeleton_item_media.xml} | 0 .../layout/skeleton_item_media_recycler.xml | 15 +++ .../main/res/layout/skeleton_item_track.xml | 37 ++++++ app/src/main/res/values/strings.xml | 2 + 28 files changed, 565 insertions(+), 198 deletions(-) create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ContainerLoadingAdapter.kt delete mode 100644 app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ShimmerAdapter.kt create mode 100644 app/src/main/res/drawable/ic_refresh.xml rename app/src/main/res/layout/{item_media_recycler.xml => item_category.xml} (89%) create mode 100644 app/src/main/res/layout/item_error.xml create mode 100644 app/src/main/res/layout/item_track.xml delete mode 100644 app/src/main/res/layout/skeleton_item_album_recycler.xml create mode 100644 app/src/main/res/layout/skeleton_item_category.xml create mode 100644 app/src/main/res/layout/skeleton_item_container.xml rename app/src/main/res/layout/{skeleton_item_album.xml => skeleton_item_media.xml} (100%) create mode 100644 app/src/main/res/layout/skeleton_item_media_recycler.xml create mode 100644 app/src/main/res/layout/skeleton_item_track.xml diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt index 66e8ea7c..ad210885 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/extensions/OfflineExtension.kt @@ -6,9 +6,11 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.PagingSource import androidx.paging.PagingState +import dev.brahmkshatriya.echo.data.clients.HomeFeedClient import dev.brahmkshatriya.echo.data.clients.SearchClient import dev.brahmkshatriya.echo.data.clients.TrackClient import dev.brahmkshatriya.echo.data.models.MediaItem.Companion.toMediaItem +import dev.brahmkshatriya.echo.data.models.MediaItem.Companion.toMediaItemsContainer import dev.brahmkshatriya.echo.data.models.MediaItemsContainer import dev.brahmkshatriya.echo.data.models.QuickSearchItem import dev.brahmkshatriya.echo.data.models.StreamableAudio @@ -22,63 +24,62 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.IOException -class OfflineExtension(val context: Context) : SearchClient, TrackClient { +class OfflineExtension(val context: Context) : SearchClient, TrackClient, HomeFeedClient { override suspend fun quickSearch(query: String): List = listOf() override suspend fun search(query: String): Flow> = flow { - val result = listOf( - MediaItemsContainer( - "Tracks", - LocalTrack.search(context, query, 1, 50) - .map { it.toMediaItem() } - ), - MediaItemsContainer( - "Artists", - LocalArtist.search(context, query, 1, 50) - .map { it.toMediaItem() } - ), - MediaItemsContainer( - "Albums", - LocalAlbum.search(context, query, 1, 50) - .map { it.toMediaItem() } - ) + val albums = LocalAlbum.search(context, query, 1, 50) + .map { it.toMediaItem() }.ifEmpty { null } + val tracks = LocalTrack.search(context, query, 1, 50) + .map { it.toMediaItem() }.ifEmpty { null } + val artists = LocalArtist.search(context, query, 1, 50) + .map { it.toMediaItem() }.ifEmpty { null } + + val result = listOfNotNull( + tracks?.toMediaItemsContainer("Tracks"), + albums?.toMediaItemsContainer("Albums"), + artists?.toMediaItemsContainer("Artists") ) emit(PagingData.from(result)) } - private fun toPagingSource(dataCallback: suspend (Int, Int) -> List): Flow> { - return Pager( - config = PagingConfig( - pageSize = 10, - enablePlaceholders = false - ), - pagingSourceFactory = { SearchPagingSource(dataCallback) } - ).flow - } - - class SearchPagingSource( - private val dataRequest: suspend (Int, Int) -> List, - private val startPage: Int = 1, - private val pageSize: Int = 10 - ) : PagingSource() { - override fun getRefreshKey(state: PagingState): Int? { + class OfflinePagingSource(val context: Context) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? { return state.anchorPosition?.let { anchorPosition -> state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1) ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1) } } - override suspend fun load(params: LoadParams): LoadResult { - val pageNumber = params.key ?: startPage + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 0 + val pageSize = params.loadSize return try { - val items = dataRequest(pageNumber, params.loadSize) - val nextKey = if (items.isEmpty()) - null - else - pageNumber + (params.loadSize / pageSize) + val items = if (page == 0) { + val albums = LocalAlbum.getAll(context, page, pageSize) + .map { it.toMediaItem() } + val tracks = LocalTrack.getAll(context, page, pageSize) + .map { it.toMediaItem() } + val artists = LocalArtist.getAll(context, page, pageSize) + .map { it.toMediaItem() } + val result = listOfNotNull( + tracks.toMediaItemsContainer("Tracks"), + albums.toMediaItemsContainer("Albums"), + artists.toMediaItemsContainer("Artists") + ) + result + } else { + LocalTrack.getShuffled(context, page, pageSize) + .map { MediaItemsContainer.TrackItem(it) } + } + val nextKey = + if (items.isEmpty()) null + else if (page == 0) 1 + else page + 1 + LoadResult.Page( data = items, - prevKey = if (pageNumber == startPage) null else pageNumber - 1, + prevKey = if (page == 0) null else page - 1, nextKey = nextKey ) } catch (exception: IOException) { @@ -88,6 +89,18 @@ class OfflineExtension(val context: Context) : SearchClient, TrackClient { } override suspend fun getStreamable(track: Track): StreamableAudio { - return LocalStream.getFromTrack(context, track)?.toAudio() ?: throw IOException("Track not found") + return LocalStream.getFromTrack(context, track)?.toAudio() + ?: throw IOException("Track not found") } -} + + override suspend fun getHomeGenres(): List = listOf() + + override suspend fun getHomeFeed(genre: String?) = Pager( + config = PagingConfig( + pageSize = 10, + enablePlaceholders = false + ), + pagingSourceFactory = { OfflinePagingSource(context) } + ).flow + +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt index 36d96b9e..6360f00a 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItem.kt @@ -11,5 +11,8 @@ sealed class MediaItem { fun Album.WithCover.toMediaItem() = AlbumItem(this) fun Artist.WithCover.toMediaItem() = ArtistItem(this) fun Playlist.WithCover.toMediaItem() = PlaylistItem(this) + + fun List.toMediaItemsContainer(title: String, subtitle: String? = null) + = MediaItemsContainer.Category(title, this, subtitle) } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt index 3bab8110..bafd3652 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/models/MediaItemsContainer.kt @@ -1,7 +1,13 @@ package dev.brahmkshatriya.echo.data.models -data class MediaItemsContainer( - val title: String, - val list: List, - val subtitle: String? = null -) \ No newline at end of file +sealed class MediaItemsContainer { + data class Category( + val title: String, + val list: List, + val subtitle: String? = null + ) : MediaItemsContainer() + + data class TrackItem( + val track: Track, + ) : MediaItemsContainer() +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt b/app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt index 3f766d0a..e123f98a 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt @@ -5,6 +5,9 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import dev.brahmkshatriya.echo.data.clients.HomeFeedClient +import dev.brahmkshatriya.echo.data.clients.SearchClient +import dev.brahmkshatriya.echo.data.clients.TrackClient import dev.brahmkshatriya.echo.data.extensions.OfflineExtension import javax.inject.Singleton @@ -12,9 +15,20 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) class PluginModule { + private var offline : OfflineExtension? = null + private fun getOfflineExtension(app: Application) = offline ?: OfflineExtension(app).also { + offline = it + } + + @Provides + @Singleton + fun provideSearchClient(app: Application) : SearchClient = getOfflineExtension(app) + @Provides @Singleton - fun providesOfflineExtension(app: Application) = - OfflineExtension(app) + fun provideHomeClient(app: Application) : HomeFeedClient = getOfflineExtension(app) + @Provides + @Singleton + fun provideTrackClient(app: Application) : TrackClient = getOfflineExtension(app) } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ContainerLoadingAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ContainerLoadingAdapter.kt new file mode 100644 index 00000000..904e56b4 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ContainerLoadingAdapter.kt @@ -0,0 +1,52 @@ +package dev.brahmkshatriya.echo.ui.adapters + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.paging.LoadState +import androidx.paging.LoadStateAdapter +import androidx.recyclerview.widget.RecyclerView +import dev.brahmkshatriya.echo.databinding.ItemErrorBinding +import dev.brahmkshatriya.echo.databinding.SkeletonItemContainerBinding + +class ContainerLoadingAdapter(val retry: () -> Unit) : LoadStateAdapter() { + class ShimmerViewHolder(val container: Container) : + RecyclerView.ViewHolder(container.root) + + sealed class Container(val root: View) { + data class Loading(val binding: SkeletonItemContainerBinding) : Container(binding.root) + data class Error(val binding: ItemErrorBinding) : Container(binding.root) + } + + override fun onCreateViewHolder(parent: ViewGroup, loadState: LoadState): ShimmerViewHolder { + return ShimmerViewHolder( + when (loadState) { + is LoadState.Error, is LoadState.NotLoading -> { + Container.Error( + ItemErrorBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + + is LoadState.Loading -> { + Container.Loading( + SkeletonItemContainerBinding.inflate( + LayoutInflater.from(parent.context), parent, false + ) + ) + } + } + ) + } + + override fun onBindViewHolder(holder: ShimmerViewHolder, loadState: LoadState) { + if (loadState !is LoadState.Error) return + + val binding = (holder.container as Container.Error).binding + binding.error.text = loadState.error.localizedMessage + binding.retry.setOnClickListener { + retry() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt index 17d709f1..e9e874d2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemsContainerAdapter.kt @@ -1,6 +1,7 @@ package dev.brahmkshatriya.echo.ui.adapters import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import androidx.lifecycle.Lifecycle import androidx.paging.PagingDataAdapter @@ -10,44 +11,126 @@ import androidx.recyclerview.widget.LinearLayoutManager.HORIZONTAL import androidx.recyclerview.widget.RecyclerView import dev.brahmkshatriya.echo.data.models.MediaItemsContainer import dev.brahmkshatriya.echo.data.models.Track -import dev.brahmkshatriya.echo.databinding.ItemMediaRecyclerBinding +import dev.brahmkshatriya.echo.databinding.ItemCategoryBinding +import dev.brahmkshatriya.echo.databinding.ItemTrackBinding +import dev.brahmkshatriya.echo.ui.player.PlayerHelper.Companion.toTimeString +import dev.brahmkshatriya.echo.ui.utils.loadInto class MediaItemsContainerAdapter( private val lifecycle: Lifecycle, private val play: (Track) -> Unit, -) : - PagingDataAdapter( - MediaItemsContainerComparator - ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MediaItemsContainerHolder( - ItemMediaRecyclerBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - ) +) : PagingDataAdapter( + MediaItemsContainerComparator +) { + override fun getItemViewType(position: Int): Int { + return getItem(position)?.let { + when (it) { + is MediaItemsContainer.Category -> 0 + is MediaItemsContainer.TrackItem -> 1 + } + } ?: 0 + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) { + 0 -> MediaItemsContainerHolder( + MediaItemsContainerBinding.Category( + ItemCategoryBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + ) + ) + + else -> MediaItemsContainerHolder( + MediaItemsContainerBinding.Track( + ItemTrackBinding + .inflate(LayoutInflater.from(parent.context), parent, false) + ) + ) + } + override fun onBindViewHolder(holder: MediaItemsContainerHolder, position: Int) { val item = getItem(position) ?: return - val binding = holder.binding - binding.textView.text = item.title - binding.recyclerView.layoutManager = - LinearLayoutManager(binding.root.context, HORIZONTAL, false) - val adapter = MediaItemAdapter(play) - binding.recyclerView.adapter = adapter - adapter.submitData(lifecycle, item.list) + when (holder.container) { + is MediaItemsContainerBinding.Category -> { + val binding = holder.container.binding + val category = item as MediaItemsContainer.Category + binding.textView.text = category.title + binding.recyclerView.layoutManager = + LinearLayoutManager(binding.root.context, HORIZONTAL, false) + val adapter = MediaItemAdapter(play) + binding.recyclerView.adapter = adapter + adapter.submitData(lifecycle, category.list) + } + + is MediaItemsContainerBinding.Track -> { + val binding = holder.container.binding + val track = (item as MediaItemsContainer.TrackItem).track + binding.root.setOnClickListener { play(track) } + + binding.title.text = track.title + + track.cover?.loadInto(binding.imageView) + + if (track.album == null) { + binding.album.visibility = View.GONE + } else { + binding.album.visibility = View.VISIBLE + binding.album.text = track.album.title + } + + if (track.artists.isEmpty()) { + binding.artist.visibility = View.GONE + } else { + binding.artist.visibility = View.VISIBLE + binding.artist.text = track.artists.joinToString(" ") { it.name } + } + + if (track.duration == null) { + binding.duration.visibility = View.GONE + } else { + binding.duration.visibility = View.VISIBLE + binding.duration.text = track.duration.toTimeString() + } + } + } + } - class MediaItemsContainerHolder(val binding: ItemMediaRecyclerBinding) : - RecyclerView.ViewHolder(binding.root) + sealed class MediaItemsContainerBinding { + data class Category(val binding: ItemCategoryBinding) : MediaItemsContainerBinding() + data class Track(val binding: ItemTrackBinding) : MediaItemsContainerBinding() + } + + class MediaItemsContainerHolder(val container: MediaItemsContainerBinding) : + RecyclerView.ViewHolder( + when (container) { + is MediaItemsContainerBinding.Category -> container.binding.root + is MediaItemsContainerBinding.Track -> container.binding.root + } + ) companion object MediaItemsContainerComparator : DiffUtil.ItemCallback() { override fun areItemsTheSame( oldItem: MediaItemsContainer, newItem: MediaItemsContainer - ) = oldItem.title == newItem.title + ): Boolean { + return when (oldItem) { + is MediaItemsContainer.Category -> { + val newCategory = newItem as? MediaItemsContainer.Category + oldItem.title == newCategory?.title + } + + is MediaItemsContainer.TrackItem -> { + val newTrack = newItem as? MediaItemsContainer.TrackItem + oldItem.track.uri == newTrack?.track?.uri + } + } + } override fun areContentsTheSame( oldItem: MediaItemsContainer, newItem: MediaItemsContainer - ) = oldItem == newItem + ) = areItemsTheSame(oldItem, newItem) } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ShimmerAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ShimmerAdapter.kt deleted file mode 100644 index 684399b4..00000000 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/ShimmerAdapter.kt +++ /dev/null @@ -1,22 +0,0 @@ -package dev.brahmkshatriya.echo.ui.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import dev.brahmkshatriya.echo.databinding.SkeletonItemAlbumRecyclerBinding - -class ShimmerAdapter : RecyclerView.Adapter() { - class ShimmerViewHolder(binding: SkeletonItemAlbumRecyclerBinding) : - RecyclerView.ViewHolder(binding.root) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ShimmerViewHolder { - val binding = - SkeletonItemAlbumRecyclerBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - return ShimmerViewHolder(binding) - } - - override fun getItemCount(): Int = 3 - - override fun onBindViewHolder(holder: ShimmerViewHolder, position: Int) {} -} \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt index 1291bc94..28c85b8a 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeFragment.kt @@ -13,10 +13,12 @@ import dagger.hilt.android.AndroidEntryPoint import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.databinding.FragmentRecyclerBinding import dev.brahmkshatriya.echo.ui.adapters.HeaderAdapter -import dev.brahmkshatriya.echo.ui.adapters.ShimmerAdapter +import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter +import dev.brahmkshatriya.echo.ui.adapters.ContainerLoadingAdapter import dev.brahmkshatriya.echo.ui.player.PlayerViewModel import dev.brahmkshatriya.echo.ui.utils.autoCleared import dev.brahmkshatriya.echo.ui.utils.dpToPx +import dev.brahmkshatriya.echo.ui.utils.observeFlow import dev.brahmkshatriya.echo.ui.utils.updatePaddingWithSystemInsets @AndroidEntryPoint @@ -39,9 +41,26 @@ class HomeFragment : Fragment() { binding.swipeRefresh.setProgressViewOffset(true, 0, 72.dpToPx()) val headerAdapter = HeaderAdapter(R.string.home) + val mediaItemsContainerAdapter = + MediaItemsContainerAdapter(viewLifecycleOwner.lifecycle, playerViewModel::play) - binding.recyclerView.adapter = ConcatAdapter(headerAdapter, ShimmerAdapter()) + mediaItemsContainerAdapter.withLoadStateFooter(ContainerLoadingAdapter{ + homeViewModel.loadFeed(homeViewModel.genre) + }) + + binding.recyclerView.adapter = ConcatAdapter(headerAdapter, mediaItemsContainerAdapter) binding.recyclerView.layoutManager = LinearLayoutManager(context) + binding.swipeRefresh.setOnRefreshListener { + homeViewModel.loadFeed(homeViewModel.genre) + } + + homeViewModel.feed.observeFlow(viewLifecycleOwner) { + binding.swipeRefresh.isRefreshing = false + if (it == null) return@observeFlow + mediaItemsContainerAdapter.submitData(it) + } + + homeViewModel.loadFeed(homeViewModel.genre) } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeViewModel.kt index 85662bae..10acd80f 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/home/HomeViewModel.kt @@ -1,13 +1,36 @@ package dev.brahmkshatriya.echo.ui.home import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData +import androidx.paging.cachedIn import dagger.hilt.android.lifecycle.HiltViewModel -import dev.brahmkshatriya.echo.data.extensions.OfflineExtension +import dev.brahmkshatriya.echo.data.clients.HomeFeedClient +import dev.brahmkshatriya.echo.data.models.MediaItemsContainer +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class HomeViewModel @Inject constructor( - private val offlineExtension: OfflineExtension -): ViewModel() { + private val homeClient: HomeFeedClient +) : ViewModel() { + private val _feed: MutableStateFlow?> = MutableStateFlow(null) + val feed = _feed.asStateFlow() + + val genre: String? = null + + fun loadFeed(genre: String?) { + viewModelScope.launch(Dispatchers.IO) { + println("Loading Data") + homeClient.getHomeFeed(genre).cachedIn(viewModelScope).collectLatest { + println("Data Received") + _feed.value = it + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt index cbd118a2..f86100cb 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/library/LibraryFragment.kt @@ -11,7 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.databinding.FragmentRecyclerBinding import dev.brahmkshatriya.echo.ui.adapters.HeaderAdapter -import dev.brahmkshatriya.echo.ui.adapters.ShimmerAdapter +import dev.brahmkshatriya.echo.ui.adapters.ContainerLoadingAdapter import dev.brahmkshatriya.echo.ui.player.PlayerViewModel import dev.brahmkshatriya.echo.ui.utils.autoCleared import dev.brahmkshatriya.echo.ui.utils.dpToPx @@ -36,7 +36,7 @@ class LibraryFragment : Fragment() { val headerAdapter = HeaderAdapter(R.string.library) - binding.recyclerView.adapter = ConcatAdapter(headerAdapter, ShimmerAdapter()) + binding.recyclerView.adapter = ConcatAdapter(headerAdapter, ContainerLoadingAdapter{ }) binding.recyclerView.layoutManager = LinearLayoutManager(context) } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt index 23673bc8..8f8dcfcf 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerListener.kt @@ -7,12 +7,17 @@ import androidx.media3.common.C import androidx.media3.common.MediaMetadata import androidx.media3.common.Player import androidx.media3.session.MediaController +import com.google.android.material.checkbox.MaterialCheckBox import dev.brahmkshatriya.echo.data.models.Track import dev.brahmkshatriya.echo.databinding.BottomPlayerBinding import dev.brahmkshatriya.echo.ui.player.PlayerHelper.Companion.toTimeString import dev.brahmkshatriya.echo.ui.utils.loadInto -class PlayerListener(val player: MediaController, val binding: BottomPlayerBinding) : +class PlayerListener( + val player: MediaController, + val binding: BottomPlayerBinding, + val playPauseListener: MaterialCheckBox.OnCheckedStateChangedListener +) : Player.Listener { init { //Poll each second to update the seekbar @@ -22,6 +27,8 @@ class PlayerListener(val player: MediaController, val binding: BottomPlayerBindi binding.collapsedSeekBar.progress = player.currentPosition.toInt() binding.expandedSeekBar.value = player.currentPosition.toFloat() + binding.collapsedSeekBar.secondaryProgress = player.bufferedPosition.toInt() + binding.trackCurrentTime.text = player.currentPosition.toTimeString() handler.postDelayed(this, 1000) @@ -40,19 +47,29 @@ class PlayerListener(val player: MediaController, val binding: BottomPlayerBindi Player.STATE_READY -> { binding.trackPlayPause.isEnabled = true binding.collapsedTrackPlayPause.isEnabled = true + + if(player.duration == C.TIME_UNSET) throw IllegalStateException("Duration is not set") + + binding.collapsedSeekBar.isIndeterminate = false + binding.expandedSeekBar.isEnabled = true + + binding.collapsedSeekBar.max = player.duration.toInt() + 100 + binding.expandedSeekBar.valueTo = player.duration.toFloat() + 100 + + binding.trackTotalTime.text = player.duration.toTimeString() } } } override fun onIsPlayingChanged(isPlaying: Boolean) { - binding.trackPlayPause.isEnabled = false - binding.collapsedTrackPlayPause.isEnabled = false + binding.trackPlayPause.removeOnCheckedStateChangedListener(playPauseListener) + binding.collapsedTrackPlayPause.removeOnCheckedStateChangedListener(playPauseListener) binding.trackPlayPause.isChecked = isPlaying binding.collapsedTrackPlayPause.isChecked = isPlaying - binding.trackPlayPause.isEnabled = true - binding.collapsedTrackPlayPause.isEnabled = true + binding.trackPlayPause.addOnCheckedStateChangedListener(playPauseListener) + binding.collapsedTrackPlayPause.addOnCheckedStateChangedListener(playPauseListener) } @@ -95,19 +112,6 @@ class PlayerListener(val player: MediaController, val binding: BottomPlayerBindi } } - override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) { - if (!playWhenReady) return - if(player.duration == C.TIME_UNSET) return - - binding.collapsedSeekBar.isIndeterminate = false - binding.expandedSeekBar.isEnabled = true - - binding.collapsedSeekBar.max = player.duration.toInt() - binding.expandedSeekBar.valueTo = player.duration.toFloat() - - binding.trackTotalTime.text = player.duration.toTimeString() - } - override fun onPlaylistMetadataChanged(mediaMetadata: MediaMetadata) { super.onPlaylistMetadataChanged(mediaMetadata) println(mediaMetadata) diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerView.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerView.kt index 574bf5c3..3eb6d983 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerView.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerView.kt @@ -9,6 +9,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_COLLAPSED import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED +import com.google.android.material.checkbox.MaterialCheckBox.OnCheckedStateChangedListener import com.google.android.material.checkbox.MaterialCheckBox.STATE_CHECKED import com.google.android.material.checkbox.MaterialCheckBox.STATE_UNCHECKED import dev.brahmkshatriya.echo.MainActivity @@ -74,19 +75,16 @@ class PlayerView( } private fun connect() { - binding.trackPlayPause.addOnCheckedStateChangedListener { _, state -> - when (state) { - STATE_CHECKED -> player.play() - STATE_UNCHECKED -> player.pause() - } - } - binding.collapsedTrackPlayPause.addOnCheckedStateChangedListener { _, state -> + val playPauseListener = OnCheckedStateChangedListener { _, state -> when (state) { STATE_CHECKED -> player.play() STATE_UNCHECKED -> player.pause() } } + binding.trackPlayPause.addOnCheckedStateChangedListener(playPauseListener) + binding.collapsedTrackPlayPause.addOnCheckedStateChangedListener(playPauseListener) + binding.trackNext.setOnClickListener { player.seekToNextMediaItem() } @@ -101,7 +99,7 @@ class PlayerView( } } - val listener = PlayerListener(player, binding) + val listener = PlayerListener(player, binding, playPauseListener) player.addListener(listener) activity.lifecycleScope.launch { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt index cbf01536..96fb26e0 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/player/PlayerViewModel.kt @@ -8,7 +8,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.android.material.bottomsheet.BottomSheetBehavior import dagger.hilt.android.lifecycle.HiltViewModel -import dev.brahmkshatriya.echo.data.extensions.OfflineExtension +import dev.brahmkshatriya.echo.data.clients.TrackClient import dev.brahmkshatriya.echo.data.models.StreamableAudio import dev.brahmkshatriya.echo.data.models.Track import kotlinx.coroutines.Dispatchers @@ -19,7 +19,7 @@ import javax.inject.Inject @HiltViewModel class PlayerViewModel @Inject constructor( - private val offlineExtension: OfflineExtension + private val trackClient: TrackClient ) : ViewModel() { var bottomSheetBehavior: BottomSheetBehavior? = null val playerCollapsed = MutableStateFlow(true) @@ -55,7 +55,7 @@ class PlayerViewModel @Inject constructor( private fun loadStreamable(track: Track) { viewModelScope.launch(Dispatchers.IO) { - audioFlow.value = track to offlineExtension.getStreamable(track) + audioFlow.value = track to trackClient.getStreamable(track) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchViewModel.kt index 4ecc6bc4..d1062ff1 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/search/SearchViewModel.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.paging.PagingData import dagger.hilt.android.lifecycle.HiltViewModel -import dev.brahmkshatriya.echo.data.extensions.OfflineExtension +import dev.brahmkshatriya.echo.data.clients.SearchClient import dev.brahmkshatriya.echo.data.models.MediaItemsContainer import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -15,7 +15,7 @@ import javax.inject.Inject @HiltViewModel class SearchViewModel @Inject constructor( - private val offlineExtension: OfflineExtension + private val searchClient: SearchClient ) : ViewModel() { private val _result: MutableStateFlow?> = MutableStateFlow(null) @@ -25,7 +25,7 @@ class SearchViewModel @Inject constructor( fun search(query: String) { this.query = query viewModelScope.launch(Dispatchers.IO) { - offlineExtension.search(query).collectLatest { + searchClient.search(query).collectLatest { _result.value = it } } diff --git a/app/src/main/res/color/bottom_item_icon.xml b/app/src/main/res/color/bottom_item_icon.xml index 3941be87..badbf080 100644 --- a/app/src/main/res/color/bottom_item_icon.xml +++ b/app/src/main/res/color/bottom_item_icon.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/color/button_play_pause.xml b/app/src/main/res/color/button_play_pause.xml index f885f03d..d17ff78d 100644 --- a/app/src/main/res/color/button_play_pause.xml +++ b/app/src/main/res/color/button_play_pause.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml new file mode 100644 index 00000000..54f461fe --- /dev/null +++ b/app/src/main/res/drawable/ic_refresh.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/bottom_player.xml b/app/src/main/res/layout/bottom_player.xml index a574cfa9..e5f746c8 100644 --- a/app/src/main/res/layout/bottom_player.xml +++ b/app/src/main/res/layout/bottom_player.xml @@ -93,14 +93,14 @@ android:layout_height="48dp" android:layout_marginStart="-2dp" android:layout_marginEnd="-2dp" - app:trackHeight="4dp" + app:labelBehavior="gone" + app:thumbColor="?colorTertiary" app:thumbHeight="16dp" - app:thumbWidth="16dp" - android:value="0.2" app:thumbTrackGapSize="12dp" + app:thumbWidth="16dp" app:trackColorActive="?colorTertiary" - app:thumbColor="?colorTertiary" - app:trackColorInactive="?colorTertiaryContainer" /> + app:trackColorInactive="?colorTertiaryContainer" + app:trackHeight="4dp" /> - + android:visibility="gone" + app:indicatorSize="64dp" /> + + +