Skip to content

Commit

Permalink
- Add HomeFeed in OfflineExtension
Browse files Browse the repository at this point in the history
- Create ContainerLoadingAdapter
- Switch to clients instead of OfflineExtension
- Fix some issues with player view
  • Loading branch information
brahmkshatriya committed Feb 5, 2024
1 parent 474b259 commit 647c032
Show file tree
Hide file tree
Showing 28 changed files with 565 additions and 198 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<QuickSearchItem> = listOf()

override suspend fun search(query: String): Flow<PagingData<MediaItemsContainer>> = 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 <TData : Any> toPagingSource(dataCallback: suspend (Int, Int) -> List<TData>): Flow<PagingData<TData>> {
return Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false
),
pagingSourceFactory = { SearchPagingSource(dataCallback) }
).flow
}

class SearchPagingSource<TData : Any>(
private val dataRequest: suspend (Int, Int) -> List<TData>,
private val startPage: Int = 1,
private val pageSize: Int = 10
) : PagingSource<Int, TData>() {
override fun getRefreshKey(state: PagingState<Int, TData>): Int? {
class OfflinePagingSource(val context: Context) : PagingSource<Int, MediaItemsContainer>() {
override fun getRefreshKey(state: PagingState<Int, MediaItemsContainer>): Int? {
return state.anchorPosition?.let { anchorPosition ->
state.closestPageToPosition(anchorPosition)?.prevKey?.plus(1)
?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(1)
}
}

override suspend fun load(params: LoadParams<Int>): LoadResult<Int, TData> {
val pageNumber = params.key ?: startPage
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, MediaItemsContainer> {
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) {
Expand All @@ -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<String> = listOf()

override suspend fun getHomeFeed(genre: String?) = Pager(
config = PagingConfig(
pageSize = 10,
enablePlaceholders = false
),
pagingSourceFactory = { OfflinePagingSource(context) }
).flow

}
Original file line number Diff line number Diff line change
Expand Up @@ -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<MediaItem>.toMediaItemsContainer(title: String, subtitle: String? = null)
= MediaItemsContainer.Category(title, this, subtitle)
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
package dev.brahmkshatriya.echo.data.models

data class MediaItemsContainer(
val title: String,
val list: List<MediaItem>,
val subtitle: String? = null
)
sealed class MediaItemsContainer {
data class Category(
val title: String,
val list: List<MediaItem>,
val subtitle: String? = null
) : MediaItemsContainer()

data class TrackItem(
val track: Track,
) : MediaItemsContainer()
}
18 changes: 16 additions & 2 deletions app/src/main/java/dev/brahmkshatriya/echo/di/PluginModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,30 @@ 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

@Module
@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)
}
Original file line number Diff line number Diff line change
@@ -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<ContainerLoadingAdapter.ShimmerViewHolder>() {
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()
}
}
}
Loading

0 comments on commit 647c032

Please sign in to comment.