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 c4ed382e..d1181c66 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/Global.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/Global.kt @@ -17,10 +17,10 @@ object Global { private val _clearQueue = MutableSharedFlow() val clearQueueFlow = _clearQueue.asSharedFlow() fun clearQueue(scope: CoroutineScope) { - _queue.clear() scope.launch { _clearQueue.emit(Unit) } + _queue.clear() } private val _removeTrack = MutableSharedFlow() @@ -33,7 +33,7 @@ object Global { } } - private val _addTrack = MutableSharedFlow>() + private val _addTrack = MutableSharedFlow>() val addTrackFlow = _addTrack.asSharedFlow() fun addTrack( scope: CoroutineScope, track: Track, stream: StreamableAudio, positionOffset: Int = 0 @@ -44,7 +44,7 @@ object Global { _queue.add(index, mediaId to track) scope.launch { - _addTrack.emit(index to item) + _addTrack.emit(Triple(index, item, track)) } return index to item } 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 e90966e9..84cb1e90 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/PlayerViewModel.kt @@ -35,6 +35,7 @@ class PlayerViewModel @Inject constructor( val seekToPrevious: MutableSharedFlow = MutableSharedFlow() val seekToNext: MutableSharedFlow = MutableSharedFlow() val repeat: MutableSharedFlow = MutableSharedFlow() + val shuffle: MutableSharedFlow>> = MutableSharedFlow() private suspend fun loadStreamable(track: Track): StreamableAudio? { return trackClient?.getStreamable(track) ?: return null @@ -54,11 +55,28 @@ class PlayerViewModel @Inject constructor( } fun play(tracks: List) { + clearQueue() viewModelScope.launch(Dispatchers.IO) { - tracks.forEach { - loadAndAddToQueue(it) + tracks.forEachIndexed { index, track -> + if (index == 0) audioIndexFlow.emit(loadAndAddToQueue(track)) + else loadAndAddToQueue(track) } - audioIndexFlow.emit(0) + } + } + + private var oldList: List>? = null + fun shuffle(shuffled: Boolean) { + println("Shuffling: $shuffled") + val list = if (shuffled) { + (0.. i to j } + .also { oldList = it.asReversed() } + } else oldList ?: return + println(list) + viewModelScope.launch(Dispatchers.IO) { + shuffle.emit(list) + } + list.forEach { (i, j) -> + moveQueueItems(i, j) } } 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 afd3483a..226c17a9 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 @@ -34,6 +34,7 @@ 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 +import dev.brahmkshatriya.echo.utils.dpToPx import dev.brahmkshatriya.echo.utils.emit import dev.brahmkshatriya.echo.utils.loadInto import dev.brahmkshatriya.echo.utils.observe @@ -222,14 +223,14 @@ fun createPlayerUI( val new = viewHolder.bindingAdapterPosition val old = target.bindingAdapterPosition playerViewModel.moveQueueItems(new, old) - adapter?.notifyItemMoved(new, old) + adapter?.moveItems(old, new) return true } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { val pos = viewHolder.bindingAdapterPosition playerViewModel.removeQueueItem(pos) - adapter?.notifyItemRemoved(pos) + adapter?.removeItem(pos) } } val touchHelper = ItemTouchHelper(callback) @@ -244,9 +245,10 @@ fun createPlayerUI( override fun onItemClosedClicked(position: Int) { playerViewModel.removeQueueItem(position) - adapter?.notifyItemRemoved(position) + adapter?.removeItem(position) } }) + adapter.submitList(Global.queue.map { it.second }) playlistBinding.playlistRecycler.apply { layoutManager = linearLayoutManager @@ -258,6 +260,17 @@ fun createPlayerUI( playerViewModel.clearQueue() } + playlistBinding.playlistShuffle.apply { + val stroke = 1.dpToPx() + strokeWidth = if(uiViewModel.shuffled.value) stroke else 0 + setOnClickListener { + uiViewModel.shuffled.value = !uiViewModel.shuffled.value + val shuffled = uiViewModel.shuffled.value + playerViewModel.shuffle(shuffled) + strokeWidth = if(shuffled) stroke else 0 + } + } + uiViewModel.view = WeakReference(playerBinding.collapsedTrackCover) activity.apply { @@ -373,15 +386,15 @@ fun createPlayerUI( } } observe(uiViewModel.playlist) { - adapter.setCurrent(Global.queue, it) + adapter.setCurrent(it) } - observe(Global.addTrackFlow) { (index, _) -> - adapter.addItem(Global.queue, index) + observe(Global.addTrackFlow) { (index, _, item) -> + adapter.addItem(index, item) } observe(Global.clearQueueFlow) { - adapter.removeItems(Global.queue) + adapter.emptyItems() PlayerBackButtonHelper.playlistState.value = STATE_COLLAPSED PlayerBackButtonHelper.playerSheetState.value = STATE_HIDDEN container.post { @@ -391,5 +404,11 @@ fun createPlayerUI( } } } + + observe(playerViewModel.shuffle) { + it.forEach { (i, j) -> + adapter.moveItems(i, j) + } + } } } \ No newline at end of file diff --git a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerUIViewModel.kt b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerUIViewModel.kt index daa64e5b..a210a168 100644 --- a/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerUIViewModel.kt +++ b/app/src/main/java/dev/brahmkshatriya/echo/player/ui/PlayerUIViewModel.kt @@ -21,6 +21,7 @@ class PlayerUIViewModel : ViewModel() { val isPlaying = MutableStateFlow(false) val nextEnabled = MutableStateFlow(false) val previousEnabled = MutableStateFlow(false) + val shuffled = MutableStateFlow(false) var view: WeakReference = WeakReference(null) } \ No newline at end of file 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 47421ad6..7a1fd689 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 @@ -5,17 +5,16 @@ import android.view.LayoutInflater import android.view.MotionEvent.ACTION_DOWN import android.view.ViewGroup import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.databinding.ItemPlaylistItemBinding -import dev.brahmkshatriya.echo.player.Global import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.toTimeString import dev.brahmkshatriya.echo.utils.loadInto class PlaylistAdapter( - val callback: Callback, - var list: List> = Global.queue, + val callback: Callback ) : RecyclerView.Adapter() { open class Callback { @@ -42,6 +41,12 @@ class PlaylistAdapter( } } + class DiffCallback : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Track, newItem: Track) = oldItem.uri == newItem.uri + override fun areContentsTheSame(oldItem: Track, newItem: Track) = oldItem == newItem + + } + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( ItemPlaylistItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) ) @@ -50,7 +55,7 @@ class PlaylistAdapter( override fun onBindViewHolder(holder: ViewHolder, position: Int) { val binding = holder.binding - val track = list[position].second + val track = list[position] binding.playlistItemTitle.text = track.title track.cover.loadInto(binding.playlistItemImageView, R.drawable.art_music) binding.playlistCurrentItem.isVisible = position == currentPosition @@ -65,27 +70,42 @@ class PlaylistAdapter( binding.playlistItemAuthor.text = subtitle } - private fun submitList(list: List>) { - this.list = list - } private var currentPosition: Int? = null - fun setCurrent(list: List>, position: Int?) { - submitList(list) + fun setCurrent(position: Int?) { val old = currentPosition currentPosition = position old?.let { notifyItemChanged(it) } currentPosition?.let { notifyItemChanged(it) } } - fun addItem(queue: List>, index: Int) { - submitList(queue) + val list = mutableListOf() + + fun addItem(index: Int, track: Track) { + list.add(index, track) notifyItemInserted(index) } - @SuppressLint("NotifyDataSetChanged") - fun removeItems(queue: List>) { - submitList(queue) - notifyDataSetChanged() + fun removeItem(index: Int) { + list.removeAt(index) + notifyItemRemoved(index) + } + + fun moveItems(fromIndex: Int, toIndex: Int) { + val item = list.removeAt(fromIndex) + list.add(toIndex, item) + notifyItemMoved(fromIndex, toIndex) + } + + fun emptyItems() { + val oldSize = list.size + list.clear() + notifyItemRangeRemoved(0, oldSize) + } + + fun submitList(tracks: List) { + emptyItems() + list.addAll(tracks) + notifyItemRangeInserted(0, tracks.size) } } diff --git a/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/TrackAdapter.kt b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/TrackAdapter.kt new file mode 100644 index 00000000..142582e3 --- /dev/null +++ b/app/src/main/java/dev/brahmkshatriya/echo/ui/adapters/TrackAdapter.kt @@ -0,0 +1,62 @@ +package dev.brahmkshatriya.echo.ui.adapters + +import android.annotation.SuppressLint +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.core.view.isVisible +import androidx.recyclerview.widget.RecyclerView +import dev.brahmkshatriya.echo.R +import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem +import dev.brahmkshatriya.echo.common.models.Track +import dev.brahmkshatriya.echo.databinding.ItemTrackSmallBinding +import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.toTimeString +import dev.brahmkshatriya.echo.ui.MediaItemClickListener +import dev.brahmkshatriya.echo.utils.loadInto + +class TrackAdapter( + private val callback: MediaItemClickListener, + private val albumVisible: Boolean = true, +) : RecyclerView.Adapter() { + + var list: List? = null + + inner class ViewHolder(val binding: ItemTrackSmallBinding) : + RecyclerView.ViewHolder(binding.root) { + init { + binding.root.setOnClickListener { + val track = list?.get(bindingAdapterPosition) ?: return@setOnClickListener + callback.onClick(binding.imageView to track.toMediaItem()) + } + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( + ItemTrackSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false) + ) + + override fun getItemCount() = list?.count() ?: 0 + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val binding = holder.binding + val track = list?.get(position) ?: return + binding.itemNumber.text = + binding.root.context.getString(R.string.number_dot, (position + 1)) + binding.itemTitle.text = track.title + track.cover.loadInto(binding.imageView, R.drawable.art_music) + var subtitle = "" + track.duration?.toTimeString()?.let { + subtitle += it + } + track.artists.joinToString(", ") { it.name }.let { + if (it.isNotBlank()) subtitle += if (subtitle.isNotBlank()) " • $it" else it + } + binding.itemSubtitle.isVisible = subtitle.isNotEmpty() + binding.itemSubtitle.text = subtitle + } + + @SuppressLint("NotifyDataSetChanged") + fun submitList(tracks: List) { + list = tracks + notifyDataSetChanged() + } +} \ No newline at end of file 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 d06920f1..8552e90c 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 @@ -1,15 +1,12 @@ package dev.brahmkshatriya.echo.ui.album -import android.annotation.SuppressLint import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.Toast import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnPreDraw -import androidx.core.view.isVisible import androidx.core.view.updatePadding import androidx.fragment.app.Fragment import androidx.fragment.app.activityViewModels @@ -19,20 +16,17 @@ 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 com.google.android.material.transition.MaterialElevationScale import dev.brahmkshatriya.echo.R import dev.brahmkshatriya.echo.common.clients.AlbumClient import dev.brahmkshatriya.echo.common.models.Album -import dev.brahmkshatriya.echo.common.models.EchoMediaItem.Companion.toMediaItem -import dev.brahmkshatriya.echo.common.models.Track import dev.brahmkshatriya.echo.databinding.FragmentCollapsingBarBinding -import dev.brahmkshatriya.echo.databinding.ItemTrackSmallBinding -import dev.brahmkshatriya.echo.player.PlayerHelper.Companion.toTimeString +import dev.brahmkshatriya.echo.player.PlayerViewModel import dev.brahmkshatriya.echo.player.ui.PlayerBackButtonHelper import dev.brahmkshatriya.echo.ui.MediaItemClickListener import dev.brahmkshatriya.echo.ui.adapters.MediaItemsContainerAdapter +import dev.brahmkshatriya.echo.ui.adapters.TrackAdapter import dev.brahmkshatriya.echo.ui.extension.ExtensionViewModel import dev.brahmkshatriya.echo.ui.extension.getAdapterForExtension import dev.brahmkshatriya.echo.utils.autoCleared @@ -42,21 +36,26 @@ import dev.brahmkshatriya.echo.utils.updatePaddingWithPlayerAndSystemInsets class AlbumFragment : Fragment() { + private val args: AlbumFragmentArgs by navArgs() + private var binding: FragmentCollapsingBarBinding by autoCleared() + private val viewModel: AlbumViewModel by viewModels() private val extensionViewModel: ExtensionViewModel by activityViewModels() - private val args: AlbumFragmentArgs by navArgs() + private val playerViewModel: PlayerViewModel by activityViewModels() private val clickListener = MediaItemClickListener(this) private val trackAdapter = TrackAdapter(clickListener, false) private val mediaItemsContainerAdapter = MediaItemsContainerAdapter(this, clickListener) private val header = AlbumHeaderAdapter(object : AlbumHeaderAdapter.AlbumHeaderListener { override fun onPlayClicked(album: Album.Full) { - Toast.makeText(context, "Todo", Toast.LENGTH_SHORT).show() + playerViewModel.play(album.tracks) } override fun onShuffleClicked(album: Album.Full) { - Toast.makeText(context, "Todo", Toast.LENGTH_SHORT).show() + album.tracks.forEach { + playerViewModel.addToQueue(it) + } } }) private val concatAdapter = ConcatAdapter(header, trackAdapter, mediaItemsContainerAdapter) @@ -131,52 +130,4 @@ class AlbumFragment : Fragment() { if (it != null) mediaItemsContainerAdapter.submit(it) } } -} - -class TrackAdapter( - private val callback: MediaItemClickListener, - private val albumVisible: Boolean = true, -) : RecyclerView.Adapter() { - - var list: List? = null - - inner class ViewHolder(val binding: ItemTrackSmallBinding) : - RecyclerView.ViewHolder(binding.root) { - init { - binding.root.setOnClickListener { - val track = list?.get(bindingAdapterPosition) ?: return@setOnClickListener - callback.onClick(binding.imageView to track.toMediaItem()) - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ViewHolder( - ItemTrackSmallBinding.inflate(LayoutInflater.from(parent.context), parent, false) - ) - - override fun getItemCount() = list?.count() ?: 0 - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val binding = holder.binding - val track = list?.get(position) ?: return - binding.itemNumber.text = - binding.root.context.getString(R.string.number_dot, (position + 1)) - binding.itemTitle.text = track.title - track.cover.loadInto(binding.imageView, R.drawable.art_music) - var subtitle = "" - track.duration?.toTimeString()?.let { - subtitle += it - } - track.artists.joinToString(", ") { it.name }.let { - if (it.isNotBlank()) subtitle += if (subtitle.isNotBlank()) " • $it" else it - } - binding.itemSubtitle.isVisible = subtitle.isNotEmpty() - binding.itemSubtitle.text = subtitle - } - - @SuppressLint("NotifyDataSetChanged") - fun submitList(tracks: List) { - list = tracks - notifyDataSetChanged() - } -} +} \ No newline at end of file diff --git a/app/src/main/res/layout/item_album_info.xml b/app/src/main/res/layout/item_album_info.xml index 79d01190..4de16f22 100644 --- a/app/src/main/res/layout/item_album_info.xml +++ b/app/src/main/res/layout/item_album_info.xml @@ -78,26 +78,28 @@ android:orientation="horizontal"> + android:text="@string/shuffle" + app:iconTint="?attr/colorTertiary" + android:textColor="?attr/colorTertiary" + app:icon="@drawable/ic_shuffle" /> - + android:text="@string/play" + app:backgroundTint="?attr/colorTertiary" + app:icon="@drawable/ic_play" />