From d55642e035483279d3e7c9e2cb0339b10b74bc7e Mon Sep 17 00:00:00 2001 From: brahmkshatriya <69040506+brahmkshatriya@users.noreply.github.com> Date: Wed, 28 Feb 2024 19:46:43 +0530 Subject: [PATCH] Combined Album stuff --- .../echo/data/extensions/OfflineExtension.kt | 96 +++++++++++-------- .../echo/data/offline/LocalAlbum.kt | 63 +++++++++--- .../echo/data/offline/LocalArtist.kt | 20 ++-- .../echo/data/offline/LocalTrack.kt | 3 + .../echo/data/offline/SortedBy.kt | 43 +++++++++ .../dev/brahmkshatriya/echo/player/Global.kt | 53 +++++++++- .../echo/player/PlaybackService.kt | 8 +- .../echo/player/PlayerHelper.kt | 4 - .../echo/player/PlayerSessionCallback.kt | 7 +- .../echo/player/PlayerViewModel.kt | 29 +----- .../echo/player/ui/ConnectPlayerToUI.kt | 20 ++-- .../echo/player/ui/CreatePlayerUI.kt | 34 +++---- .../echo/player/ui/PlayerListener.kt | 3 + .../echo/ui/MediaItemClickListener.kt | 24 +++-- .../echo/ui/adapters/MediaItemAdapter.kt | 58 ++++++----- .../ui/adapters/MediaItemsContainerAdapter.kt | 4 +- .../echo/ui/adapters/PlaylistAdapter.kt | 12 +-- .../echo/ui/album/AlbumFragment.kt | 53 ++++++---- .../echo/ui/album/AlbumViewModel.kt | 8 ++ .../ui/extension/ExtensionDialogFragment.kt | 2 +- .../echo/ui/home/HomeFragment.kt | 6 +- .../main/res/layout-land/bottom_player.xml | 6 +- app/src/main/res/layout/bottom_player.xml | 4 +- .../res/layout/fragment_collapsing_bar.xml | 65 ++++++++----- app/src/main/res/layout/item_media_album.xml | 1 + .../brahmkshatriya/echo/ExampleUnitTest.kt | 10 +- .../echo/common/clients/AlbumClient.kt | 3 +- .../echo/common/clients/ExtensionClient.kt | 2 +- 28 files changed, 424 insertions(+), 217 deletions(-) create mode 100644 app/src/main/java/dev/brahmkshatriya/echo/data/offline/SortedBy.kt 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 92f0de2a..27d3efc9 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 @@ -7,14 +7,12 @@ import androidx.paging.PagingConfig import androidx.paging.PagingData import androidx.paging.PagingSource import androidx.paging.PagingState +import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.clients.ExtensionClient import dev.brahmkshatriya.echo.common.clients.HomeFeedClient import dev.brahmkshatriya.echo.common.clients.SearchClient import dev.brahmkshatriya.echo.common.clients.TrackClient -import dev.brahmkshatriya.echo.data.offline.LocalAlbum -import dev.brahmkshatriya.echo.data.offline.LocalArtist -import dev.brahmkshatriya.echo.data.offline.LocalStream -import dev.brahmkshatriya.echo.data.offline.LocalTrack +import dev.brahmkshatriya.echo.common.models.Album import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItemsContainer import dev.brahmkshatriya.echo.common.models.ExtensionMetadata @@ -23,13 +21,19 @@ import dev.brahmkshatriya.echo.common.models.QuickSearchItem import dev.brahmkshatriya.echo.common.models.StreamableAudio import dev.brahmkshatriya.echo.common.models.StreamableAudio.Companion.toAudio import dev.brahmkshatriya.echo.common.models.Track +import dev.brahmkshatriya.echo.data.offline.LocalAlbum +import dev.brahmkshatriya.echo.data.offline.LocalArtist +import dev.brahmkshatriya.echo.data.offline.LocalStream +import dev.brahmkshatriya.echo.data.offline.LocalTrack +import dev.brahmkshatriya.echo.data.offline.sortedBy import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import java.io.IOException -class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, TrackClient, HomeFeedClient { +class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, TrackClient, + HomeFeedClient, AlbumClient { - override fun getMetadata() = ExtensionMetadata( + override val metadata = ExtensionMetadata( name = "Offline", version = "1.0.0", description = "Local media library", @@ -41,18 +45,19 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr override suspend fun search(query: String): Flow> = flow { val trimmed = query.trim() - val albums = LocalAlbum.search(context, trimmed, 1, 50) - .map { it.toMediaItem() }.ifEmpty { null } - val tracks = LocalTrack.search(context, trimmed, 1, 50) - .map { it.toMediaItem() }.ifEmpty { null } - val artists = LocalArtist.search(context, trimmed, 1, 50) - .map { it.toMediaItem() }.ifEmpty { null } - - val result = listOfNotNull( - tracks?.toMediaItemsContainer("Tracks"), - albums?.toMediaItemsContainer("Albums"), - artists?.toMediaItemsContainer("Artists") - ) + val albums = + LocalAlbum.search(context, trimmed, 1, 50).map { it.toMediaItem() }.ifEmpty { null } + val tracks = + LocalTrack.search(context, trimmed, 1, 50).map { it.toMediaItem() }.ifEmpty { null } + val artists = + LocalArtist.search(context, trimmed, 1, 50).map { it.toMediaItem() }.ifEmpty { null } + + val result = + listOfNotNull(tracks?.let { it.first().track.title to it.toMediaItemsContainer("Tracks") }, + albums?.let { it.first().album.title to it.toMediaItemsContainer("Albums") }, + artists?.let { it.first().artist.name to it.toMediaItemsContainer("Artists") }).sortedBy( + query + ) { it.first }.map { it.second } emit(PagingData.from(result)) } @@ -69,12 +74,14 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr val pageSize = params.loadSize return try { val items = if (page == 0) { - val albums = LocalAlbum.getAll(context, page, pageSize) - .map { it.toMediaItem() }.ifEmpty { null } - val tracks = LocalTrack.getShuffled(context, page, pageSize) - .map { it.toMediaItem() }.ifEmpty { null } - val artists = LocalArtist.getAll(context, page, pageSize) - .map { it.toMediaItem() }.ifEmpty { null } + val albums = LocalAlbum.getAll(context, page, pageSize).map { it.toMediaItem() } + .ifEmpty { null } + val tracks = + LocalTrack.getShuffled(context, page, pageSize).map { it.toMediaItem() } + .ifEmpty { null } + val artists = + LocalArtist.getAll(context, page, pageSize).map { it.toMediaItem() } + .ifEmpty { null } val result = listOfNotNull( tracks?.toMediaItemsContainer("Tracks"), albums?.toMediaItemsContainer("Albums"), @@ -85,15 +92,12 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr LocalTrack.getAll(context, page, pageSize) .map { MediaItemsContainer.TrackItem(it) } } - val nextKey = - if (items.isEmpty()) null - else if (page == 0) 1 - else page + 1 + val nextKey = if (items.isEmpty()) null + else if (page == 0) 1 + else page + 1 LoadResult.Page( - data = items, - prevKey = if (page == 0) null else page - 1, - nextKey = nextKey + data = items, prevKey = if (page == 0) null else page - 1, nextKey = nextKey ) } catch (exception: IOException) { return LoadResult.Error(exception) @@ -112,13 +116,29 @@ class OfflineExtension(val context: Context) : ExtensionClient, SearchClient, Tr override suspend fun getHomeGenres(): List = listOf() - override suspend fun getHomeFeed(genre: String?) = Pager( - config = PagingConfig( - pageSize = 10, - enablePlaceholders = false - ), - pagingSourceFactory = { OfflinePagingSource(context) } - ).flow + override suspend fun getHomeFeed(genre: String?) = Pager(config = PagingConfig( + pageSize = 10, enablePlaceholders = false + ), pagingSourceFactory = { OfflinePagingSource(context) }).flow + + override suspend fun loadAlbum(small: Album.Small): Album.Full { + return LocalAlbum.get(context, small.uri) + } + + override suspend fun getMediaItems(album: Album.Full): Flow> = + flow { + val artist = album.artists.first() + val tracks = + LocalTrack.search(context, artist.name, 1, 50).filter { it.album?.uri != album.uri } + .map { it.toMediaItem() }.ifEmpty { null } + val albums = + LocalAlbum.search(context, artist.name, 1, 50).filter { it.uri != album.uri } + .map { it.toMediaItem() }.ifEmpty { null } + val result = listOfNotNull( + tracks?.toMediaItemsContainer("More from ${artist.name}"), + albums?.toMediaItemsContainer("Albums") + ) + emit(PagingData.from(result)) + } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalAlbum.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalAlbum.kt index 0e0c9768..4ec45bcc 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalAlbum.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalAlbum.kt @@ -15,12 +15,21 @@ import dev.brahmkshatriya.echo.data.offline.LocalHelper.Companion.createCursor interface LocalAlbum { - companion object{ + companion object { - fun search(context: Context, query: String, page: Int, pageSize: Int): List { - val whereCondition = "${MediaStore.Audio.Albums.ALBUM} LIKE ?" - val selectionArgs = arrayOf("%$query%") + fun search( + context: Context, + query: String, + page: Int, + pageSize: Int + ): List { + val whereCondition = + "${MediaStore.Audio.Media.ARTIST} LIKE ? OR ${MediaStore.Audio.Media.ALBUM} LIKE ?" + val selectionArgs = arrayOf("%$query%", "%$query%") return context.queryAlbums(whereCondition, selectionArgs, page, pageSize) + .sortedBy(query) { + it.title + } } fun getAll(context: Context, page: Int, pageSize: Int): List { @@ -29,14 +38,24 @@ interface LocalAlbum { return context.queryAlbums(whereCondition, selectionArgs, page, pageSize) } - fun getByArtist(context: Context,artist: Artist.Small, page: Int, pageSize: Int): List { - val whereCondition = "${MediaStore.Audio.Media.ARTIST} = ?" - val selectionArgs = arrayOf(artist.name) + fun getByArtist( + context: Context, + artist: Artist.Small, + page: Int, + pageSize: Int + ): List { + val whereCondition = "${MediaStore.Audio.Media.ARTIST} LIKE ?" + val selectionArgs = arrayOf("%${artist.name}%") return context.queryAlbums(whereCondition, selectionArgs, page, pageSize) } - private fun Context.queryAlbums(whereCondition: String, selectionArgs: Array, page: Int, pageSize: Int): MutableList { - val albums = mutableListOf() + private fun Context.queryAlbums( + whereCondition: String, + selectionArgs: Array, + page: Int, + pageSize: Int + ): MutableList { + val albums = mutableListOf() createCursor( contentResolver = contentResolver, collection = MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI, @@ -44,7 +63,8 @@ interface LocalAlbum { MediaStore.Audio.Albums._ID, MediaStore.Audio.Albums.ALBUM, MediaStore.Audio.Albums.ARTIST, - MediaStore.Audio.Albums.NUMBER_OF_SONGS + MediaStore.Audio.Albums.NUMBER_OF_SONGS, + MediaStore.Audio.Albums.FIRST_YEAR ), whereCondition = whereCondition, selectionArgs = selectionArgs, @@ -58,6 +78,7 @@ interface LocalAlbum { val albumColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.ALBUM) val artistColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.ARTIST) val tracksColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.NUMBER_OF_SONGS) + val yearColumn = it.getColumnIndexOrThrow(MediaStore.Audio.Albums.FIRST_YEAR) while (it.moveToNext()) { val uri = Uri.parse("$URI$ALBUM_AUTH${it.getLong(idColumn)}") val coverUri = ContentUris.withAppendedId( @@ -66,17 +87,35 @@ interface LocalAlbum { ) val artistUri = Uri.parse("$URI$ARTIST_AUTH${it.getLong(idColumn)}") albums.add( - Album.WithCover( + Album.Full( uri = uri, title = it.getString(albumColumn), cover = coverUri.toImageHolder(), artists = listOf(Artist.Small(artistUri, it.getString(artistColumn))), - numberOfTracks = it.getInt(tracksColumn) + numberOfTracks = it.getInt(tracksColumn), + releaseDate = it.getString(yearColumn), + tracks = emptyList(), + publisher = null, + duration = null, + description = null ) ) } } return albums } + + fun get(context: Context, uri: Uri): Album.Full { + val id = uri.lastPathSegment!!.toLong() + val whereCondition = "${MediaStore.Audio.Albums._ID} = ?" + val selectionArgs = arrayOf(id.toString()) + val album = context.queryAlbums(whereCondition, selectionArgs, 0, 1).first() + val tracks = LocalTrack.getByAlbum(context, album, 0, 50) + val duration = tracks.sumOf { it.duration ?: 0 } + return album.copy( + tracks = LocalTrack.getByAlbum(context, album, 0, 50), + duration = duration + ) + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalArtist.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalArtist.kt index b9215af6..fc445d85 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalArtist.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalArtist.kt @@ -10,20 +10,28 @@ import dev.brahmkshatriya.echo.data.offline.LocalHelper.Companion.createCursor interface LocalArtist { - companion object{ - fun search(context: Context, query: String, page: Int, pageSize: Int): List { - val whereCondition = "${MediaStore.Audio.Media.ARTIST} LIKE ?" - val selectionArgs = arrayOf("%$query%") + companion object { + fun search( + context: Context, query: String, page: Int, pageSize: Int + ): List { + val whereCondition = + "${MediaStore.Audio.Media.TITLE} LIKE ? OR ${MediaStore.Audio.Media.ARTIST} LIKE ? OR ${MediaStore.Audio.Media.ALBUM} LIKE ?" + val selectionArgs = arrayOf("%$query%", "%$query%", "%$query%") return context.queryArtists(whereCondition, selectionArgs, page, pageSize) + .sortedBy(query) { + it.name + } } - fun getAll(context: Context,page: Int, pageSize: Int): List { + fun getAll(context: Context, page: Int, pageSize: Int): List { val whereCondition = "" val selectionArgs = arrayOf() return context.queryArtists(whereCondition, selectionArgs, page, pageSize) } - private fun Context.queryArtists(whereCondition: String, selectionArgs: Array, page: Int, pageSize: Int): List { + private fun Context.queryArtists( + whereCondition: String, selectionArgs: Array, page: Int, pageSize: Int + ): List { val artists = mutableListOf() createCursor( contentResolver = contentResolver, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt index a7262d70..c628197e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/LocalTrack.kt @@ -24,6 +24,9 @@ interface LocalTrack { val selectionArgs = arrayOf("%$query%", "%$query%", "%$query%") return context.queryTracks(whereCondition, selectionArgs, page, pageSize) + .sortedBy(query) { + it.title + } } fun getAll(context: Context, page: Int, pageSize: Int): List { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/data/offline/SortedBy.kt b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/SortedBy.kt new file mode 100644 index 00000000..b7aa9f75 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/data/offline/SortedBy.kt @@ -0,0 +1,43 @@ +package dev.brahmkshatriya.echo.data.offline + +fun List.sortedBy(query: String, block: (E) -> String): List { + return sortedBy { + val distance = wagnerFischer(block(it), query) + + val bonus = if (block(it).contains(query, true)) -20 else 0 + distance + bonus + } +} + +// taken from https://gist.github.com/jmarchesini/e330088e03daa394cf03ddedb8956fbe +fun wagnerFischer(s: String, t: String): Int { + val m = s.length + val n = t.length + + if (s == t) return 0 + if (s.isEmpty()) return n + if (t.isEmpty()) return m + + val d = Array(m + 1) { IntArray(n + 1) { 0 } } + + (1..m).forEach { i -> + d[i][0] = i + } + + (1..n).forEach { j -> + d[0][j] = j + } + + (1..n).forEach { j -> + (1..m).forEach { i -> + val cost = if (s[i - 1] == t[j - 1]) 0 else 1 + val delCost = d[i - 1][j] + 1 + val addCost = d[i][j - 1] + 1 + val subCost = d[i - 1][j - 1] + cost + + d[i][j] = minOf(delCost, addCost, subCost) + } + } + + return d[m][n] +} diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/Global.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/Global.kt index 9c83a106..2a645ae3 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/Global.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/Global.kt @@ -1,8 +1,59 @@ package dev.brahmkshatriya.echo.player +import androidx.media3.common.MediaItem +import dev.brahmkshatriya.echo.common.models.StreamableAudio import dev.brahmkshatriya.echo.common.models.Track +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch +import java.util.Collections object Global { val queue = mutableListOf>() - fun getTrack(mediaId:String?) = queue.find { it.first == mediaId }?.second + fun getTrack(mediaId: String?) = queue.find { it.first == mediaId }?.second + + private val _clearQueue = MutableSharedFlow() + val clearQueueFlow = _clearQueue.asSharedFlow() + fun clearQueue(scope: CoroutineScope) { + queue.clear() + scope.launch { + _clearQueue.emit(Unit) + } + } + + private val _removeTrack = MutableSharedFlow() + val removeTrackFlow = _removeTrack.asSharedFlow() + fun removeTrack(scope: CoroutineScope, index: Int) { + queue.removeAt(index) + scope.launch { + _removeTrack.emit(index) + if (queue.isEmpty()) _clearQueue.emit(Unit) + } + } + + private val _addTrack = MutableSharedFlow>() + val addTrackFlow = _addTrack.asSharedFlow() + fun addTrack( + scope: CoroutineScope, track: Track, stream: StreamableAudio, positionOffset: Int = 0 + ): Pair { + val item = PlayerHelper.mediaItemBuilder(track, stream) + val mediaId = item.mediaId + val index = queue.size - positionOffset + + queue.add(index, mediaId to track) + scope.launch { + _addTrack.emit(index to item) + } + return index to item + } + + private val _moveTrack = MutableSharedFlow>() + val moveTrackFlow = _moveTrack.asSharedFlow() + fun moveTrack(scope: CoroutineScope, fromIndex: Int, toIndex: Int) { + Collections.swap(queue, fromIndex, toIndex) + scope.launch { + _moveTrack.emit(fromIndex to toIndex) + } + } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt index f280a8f3..b1d772b5 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlaybackService.kt @@ -14,6 +14,10 @@ import dagger.hilt.android.AndroidEntryPoint import dev.brahmkshatriya.echo.MainActivity import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.di.ExtensionFlow +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.plus import javax.inject.Inject @AndroidEntryPoint @@ -58,10 +62,12 @@ class PlaybackService : MediaLibraryService() { setMediaNotificationProvider(notificationProvider) } + private val scope = CoroutineScope(Dispatchers.IO) + Job() + override fun onDestroy() { mediaLibrarySession?.run { player.release() - Global.queue.clear() + Global.clearQueue(scope) release() mediaLibrarySession = null } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt index 2298c6c1..d2369656 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerHelper.kt @@ -17,7 +17,6 @@ interface PlayerHelper { companion object { fun mediaItemBuilder( - queue: MutableList>, track: Track, audio: StreamableAudio ): MediaItem { @@ -40,9 +39,6 @@ interface PlayerHelper { val mediaId = track.uri.toString() item.setMediaId(mediaId) - - queue.add(mediaId to track) - item.setTag(queue.size) return item.build() } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt index 60b00e39..0c9ef53e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerSessionCallback.kt @@ -76,7 +76,6 @@ class PlayerSessionCallback( if (extension !is TrackClient) return default("Extension does not support Streaming Tracks") - val queue = Global.queue return scope.future { differ.submitData(extension.search(query).first()) @@ -87,7 +86,7 @@ class PlayerSessionCallback( if (item is EchoMediaItem.TrackItem) { val track = item.track val stream = extension.getStreamable(track) - PlayerHelper.mediaItemBuilder(queue, track, stream) + Global.addTrack(scope, track, stream).second } else null } } @@ -95,7 +94,7 @@ class PlayerSessionCallback( is MediaItemsContainer.TrackItem -> { val track = it.track val stream = extension.getStreamable(track) - listOf(PlayerHelper.mediaItemBuilder(queue, track, stream)) + listOf(Global.addTrack(scope, track, stream).second) } } }.flatten() @@ -113,7 +112,7 @@ class PlayerSessionCallback( startIndex: Int, startPositionMs: Long ): ListenableFuture { - Global.queue.clear() + Global.clearQueue(scope) return super.onSetMediaItems( mediaSession, controller, diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt index b4d7d490..9912d0f9 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt @@ -2,7 +2,6 @@ package dev.brahmkshatriya.echo.player import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.media3.common.MediaItem import dagger.hilt.android.lifecycle.HiltViewModel import dev.brahmkshatriya.echo.common.clients.TrackClient import dev.brahmkshatriya.echo.common.models.StreamableAudio @@ -12,7 +11,6 @@ import dev.brahmkshatriya.echo.utils.observe import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch -import java.util.Collections import javax.inject.Inject @HiltViewModel @@ -32,11 +30,6 @@ class PlayerViewModel @Inject constructor( } val audioIndexFlow = MutableSharedFlow() - val audioQueueFlow = MutableSharedFlow() - val clearQueueFlow = MutableSharedFlow() - val itemMovedFlow = MutableSharedFlow>() - val itemRemovedFlow: MutableSharedFlow = MutableSharedFlow() - val playPause: MutableSharedFlow = MutableSharedFlow() val seekTo: MutableSharedFlow = MutableSharedFlow() val seekToPrevious: MutableSharedFlow = MutableSharedFlow() @@ -47,14 +40,10 @@ class PlayerViewModel @Inject constructor( return trackClient?.getStreamable(track) ?: return null } - private val queue = Global.queue - private suspend fun loadAndAddToQueue(track: Track): Int { val stream = loadStreamable(track) return stream?.let { - val item = PlayerHelper.mediaItemBuilder(queue, track, it) - audioQueueFlow.emit(item) - queue.size - 1 + Global.addTrack(viewModelScope, track, it).first } ?: -1 } @@ -71,25 +60,15 @@ class PlayerViewModel @Inject constructor( } fun clearQueue() { - queue.clear() - viewModelScope.launch { - clearQueueFlow.emit(Unit) - } + Global.clearQueue(viewModelScope) } fun moveQueueItems(new: Int, old: Int) { - Collections.swap(queue, new, old) - viewModelScope.launch { - itemMovedFlow.emit(new to old) - } + Global.moveTrack(viewModelScope, old, new) } fun removeQueueItem(index: Int) { - queue.removeAt(index) - viewModelScope.launch { - if (queue.size == 0) clearQueueFlow.emit(Unit) - else itemRemovedFlow.emit(index) - } + Global.removeTrack(viewModelScope, index) } // fun radio(track: Track){ diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/ConnectPlayerToUI.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/ConnectPlayerToUI.kt index 87c784cb..2ab4eb18 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/ConnectPlayerToUI.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/ConnectPlayerToUI.kt @@ -3,6 +3,7 @@ package dev.brahmkshatriya.echo.player.ui import androidx.activity.viewModels import androidx.media3.session.MediaBrowser import dev.brahmkshatriya.echo.MainActivity +import dev.brahmkshatriya.echo.player.Global import dev.brahmkshatriya.echo.player.PlayerViewModel import dev.brahmkshatriya.echo.utils.observe @@ -38,21 +39,22 @@ fun connectPlayerToUI(activity: MainActivity, player: MediaBrowser) { observe(playerViewModel.repeat) { player.repeatMode = it } - observe(playerViewModel.audioQueueFlow) { - player.addMediaItem(it) + observe(Global.addTrackFlow) { (index, item) -> + player.addMediaItem(index, item) player.prepare() player.playWhenReady = true } - observe(playerViewModel.clearQueueFlow) { - player.pause() - player.clearMediaItems() - player.stop() - } - observe(playerViewModel.itemMovedFlow) { (new, old) -> + observe(Global.moveTrackFlow) { (new, old) -> player.moveMediaItem(old, new) } - observe(playerViewModel.itemRemovedFlow) { + observe(Global.removeTrackFlow) { player.removeMediaItem(it) } + observe(Global.clearQueueFlow) { + if(player.mediaItemCount == 0) return@observe + player.pause() + player.clearMediaItems() + player.stop() + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/CreatePlayerUI.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/CreatePlayerUI.kt index b7a8150b..c2f3c9d0 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/CreatePlayerUI.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/CreatePlayerUI.kt @@ -7,6 +7,7 @@ import android.view.View import android.view.animation.LinearInterpolator import androidx.activity.viewModels import androidx.appcompat.content.res.AppCompatResources +import androidx.lifecycle.lifecycleScope import androidx.media3.common.Player.REPEAT_MODE_ALL import androidx.media3.common.Player.REPEAT_MODE_OFF import androidx.media3.common.Player.REPEAT_MODE_ONE @@ -30,6 +31,7 @@ import com.google.android.material.slider.Slider import com.google.android.material.slider.Slider.OnSliderTouchListener import dev.brahmkshatriya.echo.MainActivity import dev.brahmkshatriya.echo.R +import dev.brahmkshatriya.echo.player.Global import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.toTimeString import dev.brahmkshatriya.echo.player.PlayerViewModel import dev.brahmkshatriya.echo.ui.adapters.PlaylistAdapter @@ -92,7 +94,7 @@ fun createPlayerUI( if (newState == STATE_SETTLING || newState == STATE_DRAGGING) return PlayerBackButtonHelper.playerSheetState.value = newState if (newState == STATE_HIDDEN) - playerViewModel.clearQueue() + Global.clearQueue(activity.lifecycleScope) } override fun onSlide(bottomSheet: View, slideOffset: Float) { @@ -254,18 +256,8 @@ fun createPlayerUI( activity.apply { - fun playlistCleared() { - container.post { - if (bottomPlayerBehavior.state != STATE_HIDDEN) { - bottomPlayerBehavior.isHideable = true - bottomPlayerBehavior.state = STATE_HIDDEN - } - } - adapter.notifyDataSetChanged() - } - observe(uiViewModel.track) { track -> - track ?: return@observe playlistCleared() + track ?: return@observe playerBinding.collapsedTrackTitle.text = track.title playerBinding.expandedTrackTitle.text = track.title @@ -375,19 +367,21 @@ fun createPlayerUI( } } observe(uiViewModel.playlist) { - playlistBinding.playlistRecycler.apply { - adapter.setCurrent(it) - } + adapter.setCurrent(it) } - observe(playerViewModel.clearQueueFlow) { - playlistCleared() + observe(Global.addTrackFlow) { (index, _) -> + adapter.notifyItemInserted(index) } - observe(playerViewModel.audioQueueFlow) { - (it.localConfiguration?.tag as? Int)?.let { index -> - adapter.notifyItemInserted(index) + observe(Global.clearQueueFlow) { + container.post { + if (bottomPlayerBehavior.state != STATE_HIDDEN) { + bottomPlayerBehavior.isHideable = true + bottomPlayerBehavior.state = STATE_HIDDEN + } } + adapter.notifyDataSetChanged() } } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerListener.kt index 3bc907cb..58c15b02 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerListener.kt @@ -41,6 +41,7 @@ class PlayerListener( override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) { viewModel.track.value = Global.getTrack(mediaItem?.mediaId) + println("onMediaItemTransition: ${player.currentMediaItemIndex}") viewModel.playlist.value = player.currentMediaItemIndex.let { if (it == C.INDEX_UNSET) null else it } } @@ -55,6 +56,7 @@ class PlayerListener( override fun onTimelineChanged(timeline: Timeline, reason: Int) { if (player.currentMediaItem == null) { viewModel.track.value = null + println("onTimelineChanged: null") viewModel.playlist.value = null } updateNavigation() @@ -98,6 +100,7 @@ class PlayerListener( viewModel.totalDuration.value = player.duration.toInt() viewModel.isPlaying.value = player.isPlaying viewModel.buffering.value = player.playbackState == Player.STATE_BUFFERING + println("update: ${player.currentMediaItemIndex}") viewModel.playlist.value = player.currentMediaItemIndex.let { if (it == C.INDEX_UNSET) null else it } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/MediaItemClickListener.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/MediaItemClickListener.kt index 6b7c73d6..a71a7256 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/MediaItemClickListener.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/MediaItemClickListener.kt @@ -1,7 +1,9 @@ package dev.brahmkshatriya.echo.ui +import android.view.View import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels +import androidx.navigation.fragment.FragmentNavigatorExtras import androidx.navigation.fragment.findNavController import dev.brahmkshatriya.echo.NavigationDirections import dev.brahmkshatriya.echo.common.models.EchoMediaItem @@ -9,20 +11,24 @@ import dev.brahmkshatriya.echo.player.PlayerViewModel class MediaItemClickListener( private val fragment: Fragment -) : ClickListener { - override fun onClick(item: EchoMediaItem) { - when (item) { +) : ClickListener> { + override fun onClick(item: Pair) { + val view = item.first + when (val mediaItem = item.second) { is EchoMediaItem.AlbumItem -> NavigationDirections.actionAlbum( - albumWithCover = item.album + albumWithCover = mediaItem.album ).let { - fragment.findNavController().navigate(it) + val transitionName = mediaItem.album.uri.toString() + view.transitionName = transitionName + val extras = FragmentNavigatorExtras(view to transitionName) + fragment.findNavController().navigate(it, extras) } // is EchoMediaItem.ArtistItem -> TODO() // is EchoMediaItem.PlaylistItem -> TODO() is EchoMediaItem.TrackItem -> { val playerViewModel: PlayerViewModel by fragment.activityViewModels() - playerViewModel.play(item.track) + playerViewModel.play(mediaItem.track) } else -> {} @@ -30,14 +36,14 @@ class MediaItemClickListener( } - override fun onLongClick(item: EchoMediaItem) { - when (item) { + override fun onLongClick(item: Pair) { + when (val mediaItem = item.second) { // is EchoMediaItem.AlbumItem -> TODO() // is EchoMediaItem.ArtistItem -> TODO() // is EchoMediaItem.PlaylistItem -> TODO() is EchoMediaItem.TrackItem -> { val playerViewModel: PlayerViewModel by fragment.activityViewModels() - playerViewModel.play(item.track) + playerViewModel.addToQueue(mediaItem.track) } else -> {} diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt index b2a4d6fc..67bd453e 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/MediaItemAdapter.kt @@ -1,7 +1,9 @@ package dev.brahmkshatriya.echo.ui.adapters import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup +import android.widget.ImageView import androidx.core.view.isVisible import androidx.lifecycle.Lifecycle import androidx.paging.PagingData @@ -20,7 +22,7 @@ import dev.brahmkshatriya.echo.ui.ClickListener import dev.brahmkshatriya.echo.utils.loadInto class MediaItemAdapter( - private val listener: ClickListener + private val listener: ClickListener> ) : PagingDataAdapter(MediaItemComparator) { override fun getItemViewType(position: Int): Int { @@ -36,30 +38,29 @@ class MediaItemAdapter( override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = MediaItemHolder( when (viewType) { - 0 -> MediaItemsBinding.Track( - ItemMediaTrackBinding + 0 -> { + val binding = ItemMediaTrackBinding .inflate(LayoutInflater.from(parent.context), parent, false) - ) - - 1 -> MediaItemsBinding.Album( - ItemMediaAlbumBinding - .inflate(LayoutInflater.from(parent.context), parent, false) - ) + MediaItemsBinding.Track(binding, binding.imageView) + } - 2 -> MediaItemsBinding.Artist( - ItemMediaArtistBinding + 1 -> { + val binding = ItemMediaAlbumBinding .inflate(LayoutInflater.from(parent.context), parent, false) - ) + MediaItemsBinding.Album(binding, binding.imageView) + } - 3 -> MediaItemsBinding.Playlist( - ItemMediaPlaylistBinding + 2 -> { + val binding = ItemMediaArtistBinding .inflate(LayoutInflater.from(parent.context), parent, false) - ) + MediaItemsBinding.Artist(binding, binding.imageView) + } - else -> MediaItemsBinding.Track( - ItemMediaTrackBinding + else -> { + val binding = ItemMediaPlaylistBinding .inflate(LayoutInflater.from(parent.context), parent, false) - ) + MediaItemsBinding.Playlist(binding, binding.imageView) + } } ) @@ -69,10 +70,10 @@ class MediaItemAdapter( holder.itemView.apply { setOnClickListener { - listener.onClick(item) + listener.onClick( container.transitionView to item) } setOnLongClickListener { - listener.onLongClick(item) + listener.onLongClick(container.transitionView to item) true } } @@ -181,11 +182,18 @@ class MediaItemAdapter( } ) - sealed class MediaItemsBinding { - data class Track(val binding: ItemMediaTrackBinding) : MediaItemsBinding() - data class Album(val binding: ItemMediaAlbumBinding) : MediaItemsBinding() - data class Artist(val binding: ItemMediaArtistBinding) : MediaItemsBinding() - data class Playlist(val binding: ItemMediaPlaylistBinding) : MediaItemsBinding() + sealed class MediaItemsBinding(open val transitionView: ImageView) { + data class Track(val binding: ItemMediaTrackBinding, override val transitionView: ImageView) : + MediaItemsBinding(transitionView) + + data class Album(val binding: ItemMediaAlbumBinding, override val transitionView: ImageView) : + MediaItemsBinding(transitionView) + + data class Artist(val binding: ItemMediaArtistBinding, override val transitionView: ImageView) : + MediaItemsBinding(transitionView) + + data class Playlist(val binding: ItemMediaPlaylistBinding, override val transitionView: ImageView) : + MediaItemsBinding(transitionView) } companion object MediaItemComparator : DiffUtil.ItemCallback() { 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 d23f4985..3c23521d 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 @@ -24,7 +24,7 @@ import dev.brahmkshatriya.echo.utils.loadInto class MediaItemsContainerAdapter( private val lifecycle: Lifecycle, - private val listener: ClickListener, + private val listener: ClickListener>, ) : PagingDataAdapter( MediaItemsContainerComparator ) { @@ -108,7 +108,7 @@ class MediaItemsContainerAdapter( binding.duration.visibility = View.VISIBLE binding.duration.text = duration.toTimeString() } - track.toMediaItem() + binding.imageView to track.toMediaItem() } } echoMediaItem?.let { diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/PlaylistAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/PlaylistAdapter.kt index 5346659c..6fdfb066 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/PlaylistAdapter.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/PlaylistAdapter.kt @@ -31,8 +31,7 @@ class PlaylistAdapter( callback.onItemClosedClicked(bindingAdapterPosition) } binding.playlistItemDragHandle.setOnTouchListener { _, event -> - if (event.actionMasked != ACTION_DOWN) - return@setOnTouchListener false + if (event.actionMasked != ACTION_DOWN) return@setOnTouchListener false callback.onDragHandleTouched(this) true } @@ -59,12 +58,9 @@ class PlaylistAdapter( private var currentPosition: Int? = null fun setCurrent(position: Int?) { - currentPosition?.let { - notifyItemChanged(it) - } + val old = currentPosition currentPosition = position - currentPosition?.let { - notifyItemChanged(it) - } + old?.let { notifyItemChanged(it) } + currentPosition?.let { notifyItemChanged(it) } } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumFragment.kt index d1962de6..8bdaf3b2 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumFragment.kt @@ -5,16 +5,17 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.view.ViewCompat -import androidx.core.view.WindowInsetsCompat import androidx.core.view.isVisible -import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels import androidx.fragment.app.viewModels +import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.navArgs +import androidx.navigation.ui.setupWithNavController +import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.transition.MaterialContainerTransform import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.models.Album @@ -23,6 +24,7 @@ import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.databinding.FragmentCollapsingBarBinding import dev.brahmkshatriya.echo.databinding.ItemTrackBinding import dev.brahmkshatriya.echo.ui.MediaItemClickListener +import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter import dev.brahmkshatriya.echo.ui.extension.ExtensionViewModel import dev.brahmkshatriya.echo.ui.extension.getAdapterForExtension import dev.brahmkshatriya.echo.utils.autoCleared @@ -41,35 +43,50 @@ class AlbumFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View { binding = FragmentCollapsingBarBinding.inflate(inflater, container, false) + + val album: Album.Small = args.albumWithCover ?: args.albumSmall ?: return binding.root + binding.albumCover.transitionName = album.uri.toString() + sharedElementEnterTransition = MaterialContainerTransform() + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { val album: Album.Small = args.albumWithCover ?: args.albumSmall ?: return - val adapter = TrackAdapter(MediaItemClickListener(this), true) + val clickListener = MediaItemClickListener(this) + val trackAdapter = TrackAdapter(clickListener, false) + val mediaItemsContainerAdapter = MediaItemsContainerAdapter(lifecycle, clickListener) + val concatAdapter = ConcatAdapter(trackAdapter, mediaItemsContainerAdapter) binding.toolbar.title = album.title - (album as? Album.WithCover)?.let { - it.cover.loadInto(binding.albumCover) + binding.appBarLayout.addOnOffsetChangedListener { appbar, verticalOffset -> + val offset = (-verticalOffset) / appbar.totalScrollRange.toFloat() + val inverted = 1 - offset + binding.endIcon.alpha = inverted + binding.albumCover.alpha = inverted + binding.toolbarOutline.alpha = offset } - ViewCompat.setOnApplyWindowInsetsListener(binding.root){ _, insets -> - val systemInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) - println(systemInsets) - binding.albumCoverContainer.updatePadding(top = systemInsets.top) - insets + + (album as? Album.WithCover).let { + it?.cover.loadInto(binding.albumCover, R.drawable.art_album) } - binding.albumCoverContainer.requestApplyInsets() + + binding.toolbar.setupWithNavController(findNavController()) binding.recyclerView.layoutManager = LinearLayoutManager(context) binding.recyclerView.adapter = getAdapterForExtension( - extensionViewModel.getCurrentExtension(), R.string.album, adapter, true + extensionViewModel.getCurrentExtension(), R.string.album, concatAdapter, true ) { client -> if (client == null) return@getAdapterForExtension viewModel.loadAlbum(client, album) - observe(viewModel.albumFlow) { - it ?: return@observe - adapter.submitList(it.tracks) - } + } + + observe(viewModel.albumFlow) { + if (it != null) trackAdapter.submitList(it.tracks) + } + + observe(viewModel.result) { + if (it != null) mediaItemsContainerAdapter.submitData(it) } } } @@ -85,7 +102,7 @@ class TrackAdapter( init { binding.root.setOnClickListener { val track = list?.get(bindingAdapterPosition) ?: return@setOnClickListener - callback.onClick(track.toMediaItem()) + callback.onClick(binding.imageView to track.toMediaItem()) } } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumViewModel.kt index 5680c1b2..41c40f50 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/album/AlbumViewModel.kt @@ -2,17 +2,22 @@ package dev.brahmkshatriya.echo.ui.album import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.PagingData import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.models.Album +import dev.brahmkshatriya.echo.common.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 class AlbumViewModel : ViewModel() { private var initialized = false + private val _result: MutableStateFlow?> = MutableStateFlow(null) + val result = _result.asStateFlow() private val mutableAlbumFlow: MutableStateFlow = MutableStateFlow(null) val albumFlow = mutableAlbumFlow.asStateFlow() @@ -22,6 +27,9 @@ class AlbumViewModel : ViewModel() { viewModelScope.launch(Dispatchers.IO) { albumClient.loadAlbum(album).let { mutableAlbumFlow.value = it + albumClient.getMediaItems(it).collectLatest { data -> + _result.value = data + } } } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionDialogFragment.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionDialogFragment.kt index 0e06685c..30bea4bc 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionDialogFragment.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/extension/ExtensionDialogFragment.kt @@ -48,7 +48,7 @@ class ExtensionDialogFragment : DialogFragment() { binding.buttonToggleGroup, false ).root - val metadata = extension.getMetadata() + val metadata = extension.metadata button.text = metadata.name binding.buttonToggleGroup.addView(button) metadata.iconUrl?.toImageHolder().loadInto(button) 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 108c86b3..261fb3ec 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 @@ -9,6 +9,7 @@ import androidx.fragment.app.activityViewModels import androidx.paging.LoadState import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.transition.Hold import dagger.hilt.android.AndroidEntryPoint import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.HomeFeedClient @@ -31,6 +32,7 @@ class HomeFragment : Fragment() { override fun onCreateView(inflater: LayoutInflater, parent: ViewGroup?, state: Bundle?): View { binding = FragmentRecyclerBinding.inflate(inflater, parent, false) + exitTransition = Hold() return binding.root } @@ -59,9 +61,7 @@ class HomeFragment : Fragment() { observe(homeViewModel.homeFeedFlow.flow) { binding.recyclerView.adapter = getAdapterForExtension( - it, - R.string.home, - concatAdapter + it, R.string.home, concatAdapter ) { client -> binding.swipeRefresh.isEnabled = client != null } diff --git a/app/src/main/res/layout-land/bottom_player.xml b/app/src/main/res/layout-land/bottom_player.xml index 3f92f8d7..68b3a4c0 100644 --- a/app/src/main/res/layout-land/bottom_player.xml +++ b/app/src/main/res/layout-land/bottom_player.xml @@ -77,6 +77,7 @@ android:id="@+id/expandedTrackTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:textAlignment="center" android:textSize="20sp" android:textStyle="bold" tools:text="Nice Track Title" /> @@ -125,6 +126,8 @@ android:layout_marginStart="12dp" android:layout_marginEnd="12dp" android:progress="40" + app:trackThickness="8dp" + app:trackCornerRadius="4dp" app:indicatorColor="?attr/colorTertiaryContainer" app:indicatorTrackGapSize="0dp" app:trackColor="?attr/colorSurfaceContainerHigh" /> @@ -143,7 +146,7 @@ app:thumbWidth="16dp" app:trackColorActive="?attr/colorTertiary" app:trackColorInactive="@android:color/transparent" - app:trackHeight="4dp" /> + app:trackHeight="8dp" /> @@ -329,6 +332,7 @@ android:id="@+id/collapsedTrackTitle" android:layout_width="match_parent" android:layout_height="wrap_content" + android:ellipsize="end" android:maxLines="1" android:textStyle="bold" tools:text="Nice Track Title" /> diff --git a/app/src/main/res/layout/bottom_player.xml b/app/src/main/res/layout/bottom_player.xml index b1e52155..daf9b678 100644 --- a/app/src/main/res/layout/bottom_player.xml +++ b/app/src/main/res/layout/bottom_player.xml @@ -93,6 +93,7 @@ android:id="@+id/expandedTrackTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:textAlignment="center" android:textSize="20sp" android:textStyle="bold" tools:text="Nice Track Title" /> @@ -330,6 +331,7 @@ android:id="@+id/collapsedTrackTitle" android:layout_width="match_parent" android:layout_height="wrap_content" + android:ellipsize="end" android:maxLines="1" android:textStyle="bold" tools:text="Nice Track Title" /> @@ -363,10 +365,10 @@ android:id="@+id/collapsedTrackPlayPause" android:layout_width="48dp" android:layout_height="48dp" + android:background="@drawable/ripple_44dp" android:button="@drawable/anim_play_pause" android:contentDescription="@string/play_pause" android:enabled="false" - android:background="@drawable/ripple_44dp" app:buttonTint="@color/button_play_pause" /> diff --git a/app/src/main/res/layout/fragment_collapsing_bar.xml b/app/src/main/res/layout/fragment_collapsing_bar.xml index 6621d5b0..bb05cf8c 100644 --- a/app/src/main/res/layout/fragment_collapsing_bar.xml +++ b/app/src/main/res/layout/fragment_collapsing_bar.xml @@ -1,12 +1,13 @@ @@ -14,44 +15,50 @@ + app:maxLines="2" + app:titleCollapseMode="scale"> + android:layout_height="wrap_content" + android:layout_gravity="bottom|end" + android:layout_marginEnd="28dp" + android:layout_marginBottom="20dp" + android:contentDescription="@string/album" + android:src="@drawable/ic_album" /> + android:layout_marginBottom="72dp" + android:fitsSystemWindows="true"> @@ -61,27 +68,35 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" - app:title="@string/app_name" + android:background="@android:color/transparent" app:layout_collapseMode="pin" app:menu="@menu/back_more_menu" - app:navigationIcon="@drawable/ic_arrow_back" app:navigationContentDescription="@string/close" - android:paddingEnd="16dp" - android:paddingStart="16dp" - android:background="@android:color/transparent"/> + tools:title="@tools:sample/lorem/random" /> + + + android:paddingBottom="96dp" + app:layout_behavior="@string/appbar_scrolling_view_behavior" + tools:listitem="@layout/item_track" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_media_album.xml b/app/src/main/res/layout/item_media_album.xml index 15952c88..304ffc89 100644 --- a/app/src/main/res/layout/item_media_album.xml +++ b/app/src/main/res/layout/item_media_album.xml @@ -40,6 +40,7 @@ android:layout_width="128dp" android:layout_height="128dp" android:layout_marginTop="8dp" + android:transitionName="albumViewItem" android:background="?attr/colorSurfaceContainer" android:scaleType="fitCenter" app:shapeAppearance="@style/ShapeAppearance.Material3.Corner.Small" diff --git a/app/src/test/java/dev/brahmkshatriya/echo/ExampleUnitTest.kt b/app/src/test/java/dev/brahmkshatriya/echo/ExampleUnitTest.kt index 2fe04369..536e19e5 100644 --- a/app/src/test/java/dev/brahmkshatriya/echo/ExampleUnitTest.kt +++ b/app/src/test/java/dev/brahmkshatriya/echo/ExampleUnitTest.kt @@ -1,6 +1,14 @@ package dev.brahmkshatriya.echo -class ExampleUnitTest { +import dev.brahmkshatriya.echo.data.offline.sortedBy +import org.junit.Test +class ExampleUnitTest { + @Test + fun testLevenshtein() { + val query = "tears" + val target = listOf("Humans in the evening","tears in the evening","Skrillex", "Fox Stevenson", "steven fox") + println("${target.sortedBy(query) { it }}") + } } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/AlbumClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/AlbumClient.kt index 838ce5a5..483eeaa9 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/AlbumClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/AlbumClient.kt @@ -7,6 +7,5 @@ import kotlinx.coroutines.flow.Flow interface AlbumClient { suspend fun loadAlbum(small: Album.Small): Album.Full - suspend fun getMediaItems(album: Album.Small): Flow> - + suspend fun getMediaItems(album: Album.Full): Flow> } \ No newline at end of file diff --git a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ExtensionClient.kt b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ExtensionClient.kt index 0b56c0ad..02447650 100644 --- a/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ExtensionClient.kt +++ b/common/src/main/java/dev/brahmkshatriya/echo/common/clients/ExtensionClient.kt @@ -3,5 +3,5 @@ package dev.brahmkshatriya.echo.common.clients import dev.brahmkshatriya.echo.common.models.ExtensionMetadata interface ExtensionClient { - fun getMetadata(): ExtensionMetadata + val metadata: ExtensionMetadata } \ No newline at end of file