Add playback fragment
Add a fragment to show songs that are currently being played.
This commit is contained in:
parent
21ff93d5f0
commit
a72cb48c71
18 changed files with 266 additions and 103 deletions
|
@ -17,8 +17,8 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ
|
||||||
|
|
||||||
/songs/
|
/songs/
|
||||||
|
|
||||||
|
- Search when LibraryFragment isnt enabled
|
||||||
- ? Sorting ?
|
- ? Sorting ?
|
||||||
- ? Search ?
|
|
||||||
- ? Fast Scrolling ?
|
- ? Fast Scrolling ?
|
||||||
|
|
||||||
/library/
|
/library/
|
||||||
|
@ -28,9 +28,14 @@ TODOs surrounded with !s are things I tried to do, but failed for reasons includ
|
||||||
- ? Add Nested Nav to Library ViewPager fragment [Hold fire on this until everything else is added, there could be sneaky bugs later on if you add it now] ?
|
- ? Add Nested Nav to Library ViewPager fragment [Hold fire on this until everything else is added, there could be sneaky bugs later on if you add it now] ?
|
||||||
- ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] !
|
- ! Move Adapter functionality to ListAdapter [RecyclerView scrolls to middle/bottom when data is re-sorted] !
|
||||||
|
|
||||||
|
/playback/
|
||||||
|
-
|
||||||
|
|
||||||
|
/other/
|
||||||
|
- Highlight recycler items when they are being played
|
||||||
|
|
||||||
/bugs/
|
/bugs/
|
||||||
- Fix issue where fast navigations will cause the app to not display anything
|
- Fix issue where fast navigations will cause the app to not display anything
|
||||||
|
|
||||||
To be added:
|
To be added:
|
||||||
/prefs/
|
/prefs/
|
||||||
/playback/
|
|
|
@ -47,9 +47,9 @@ dependencies {
|
||||||
// --- SUPPORT ---
|
// --- SUPPORT ---
|
||||||
|
|
||||||
// General
|
// General
|
||||||
implementation 'androidx.core:core-ktx:1.3.1'
|
implementation 'androidx.core:core-ktx:1.3.2'
|
||||||
implementation 'androidx.activity:activity:1.2.0-alpha08'
|
implementation 'androidx.activity:activity:1.2.0-beta01'
|
||||||
implementation 'androidx.fragment:fragment:1.3.0-alpha08'
|
implementation 'androidx.fragment:fragment:1.3.0-beta01'
|
||||||
|
|
||||||
// Layout
|
// Layout
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.1'
|
||||||
|
@ -71,7 +71,7 @@ dependencies {
|
||||||
implementation 'io.coil-kt:coil:0.13.0'
|
implementation 'io.coil-kt:coil:0.13.0'
|
||||||
|
|
||||||
// Material
|
// Material
|
||||||
implementation 'com.google.android.material:material:1.3.0-alpha02'
|
implementation 'com.google.android.material:material:1.3.0-alpha03'
|
||||||
|
|
||||||
// Lint
|
// Lint
|
||||||
ktlint "com.pinterest:ktlint:0.37.2"
|
ktlint "com.pinterest:ktlint:0.37.2"
|
||||||
|
|
|
@ -41,7 +41,7 @@ class MainFragment : Fragment() {
|
||||||
val binding = FragmentMainBinding.inflate(inflater)
|
val binding = FragmentMainBinding.inflate(inflater)
|
||||||
|
|
||||||
// If musicModel was cleared while the app was closed [Likely due to Auxio being suspended
|
// If musicModel was cleared while the app was closed [Likely due to Auxio being suspended
|
||||||
// in the background], then navigate back to loading to reload the music.
|
// in the background], then navigate back to LoadingFragment to reload the music.
|
||||||
if (musicModel.response.value == null) {
|
if (musicModel.response.value == null) {
|
||||||
findNavController().navigate(MainFragmentDirections.actionReturnToLoading())
|
findNavController().navigate(MainFragmentDirections.actionReturnToLoading())
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ class MainFragment : Fragment() {
|
||||||
|
|
||||||
// --- UI SETUP ---
|
// --- UI SETUP ---
|
||||||
|
|
||||||
binding.lifecycleOwner = viewLifecycleOwner
|
binding.lifecycleOwner = this
|
||||||
binding.mainViewPager.adapter = PagerAdapter()
|
binding.mainViewPager.adapter = PagerAdapter()
|
||||||
|
|
||||||
// Link the ViewPager & Tab View
|
// Link the ViewPager & Tab View
|
||||||
|
|
|
@ -13,6 +13,7 @@ import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
|
import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
|
||||||
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
|
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.recycler.ClickListener
|
import org.oxycblt.auxio.recycler.ClickListener
|
||||||
import org.oxycblt.auxio.theme.applyDivider
|
import org.oxycblt.auxio.theme.applyDivider
|
||||||
import org.oxycblt.auxio.theme.disable
|
import org.oxycblt.auxio.theme.disable
|
||||||
|
@ -21,6 +22,7 @@ class AlbumDetailFragment : Fragment() {
|
||||||
|
|
||||||
private val args: AlbumDetailFragmentArgs by navArgs()
|
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||||
private val detailModel: DetailViewModel by activityViewModels()
|
private val detailModel: DetailViewModel by activityViewModels()
|
||||||
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -45,7 +47,7 @@ class AlbumDetailFragment : Fragment() {
|
||||||
|
|
||||||
val songAdapter = DetailSongAdapter(
|
val songAdapter = DetailSongAdapter(
|
||||||
ClickListener {
|
ClickListener {
|
||||||
Log.d(this::class.simpleName, it.name)
|
playbackModel.updateSong(it)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -17,13 +17,15 @@ import androidx.transition.TransitionManager
|
||||||
import org.oxycblt.auxio.MainFragmentDirections
|
import org.oxycblt.auxio.MainFragmentDirections
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
|
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
|
||||||
import org.oxycblt.auxio.library.recycler.LibraryAdapter
|
import org.oxycblt.auxio.library.adapters.LibraryAdapter
|
||||||
import org.oxycblt.auxio.library.recycler.SearchAdapter
|
import org.oxycblt.auxio.library.adapters.SearchAdapter
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.BaseModel
|
import org.oxycblt.auxio.music.BaseModel
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.recycler.ShowMode
|
import org.oxycblt.auxio.recycler.ShowMode
|
||||||
import org.oxycblt.auxio.theme.applyColor
|
import org.oxycblt.auxio.theme.applyColor
|
||||||
import org.oxycblt.auxio.theme.applyDivider
|
import org.oxycblt.auxio.theme.applyDivider
|
||||||
|
@ -36,6 +38,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private val libraryModel: LibraryViewModel by activityViewModels()
|
private val libraryModel: LibraryViewModel by activityViewModels()
|
||||||
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
|
@ -173,6 +176,13 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun navToItem(baseModel: BaseModel) {
|
private fun navToItem(baseModel: BaseModel) {
|
||||||
|
// If the item is a song [That was selected through search], then update the playback
|
||||||
|
// to that song instead of doing any naviagation
|
||||||
|
if (baseModel is Song) {
|
||||||
|
playbackModel.updateSong(baseModel)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!libraryModel.isNavigating) {
|
if (!libraryModel.isNavigating) {
|
||||||
libraryModel.updateNavigationStatus(true)
|
libraryModel.updateNavigationStatus(true)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.library.recycler
|
package org.oxycblt.auxio.library.adapters
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
@ -13,7 +13,8 @@ import org.oxycblt.auxio.recycler.viewholders.ArtistViewHolder
|
||||||
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder
|
import org.oxycblt.auxio.recycler.viewholders.GenreViewHolder
|
||||||
|
|
||||||
// A ListAdapter that can contain three different types of ViewHolders depending
|
// A ListAdapter that can contain three different types of ViewHolders depending
|
||||||
// the showmode given. It cannot display multiple types of viewholders *at once*.
|
// the ShowMode given.
|
||||||
|
// It cannot display multiple ViewHolders *at once* however. That's what SearchAdapter is for.
|
||||||
class LibraryAdapter(
|
class LibraryAdapter(
|
||||||
private val showMode: ShowMode,
|
private val showMode: ShowMode,
|
||||||
private val doOnClick: (BaseModel) -> Unit
|
private val doOnClick: (BaseModel) -> Unit
|
|
@ -1,4 +1,4 @@
|
||||||
package org.oxycblt.auxio.library.recycler
|
package org.oxycblt.auxio.library.adapters
|
||||||
|
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.recyclerview.widget.ListAdapter
|
import androidx.recyclerview.widget.ListAdapter
|
|
@ -20,62 +20,44 @@ import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||||
|
|
||||||
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
|
|
||||||
// LoadingFragment is [hopefully] going to be the first one to have to create musicModel,
|
|
||||||
// so pass a factory instance so that the model has access to the application resources.
|
|
||||||
private val musicModel: MusicViewModel by activityViewModels {
|
private val musicModel: MusicViewModel by activityViewModels {
|
||||||
MusicViewModel.Factory(requireActivity().application)
|
MusicViewModel.Factory(requireActivity().application)
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var binding: FragmentLoadingBinding
|
|
||||||
private lateinit var permLauncher: ActivityResultLauncher<String>
|
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
savedInstanceState: Bundle?
|
savedInstanceState: Bundle?
|
||||||
): View? {
|
): View? {
|
||||||
binding = FragmentLoadingBinding.inflate(inflater)
|
val binding = FragmentLoadingBinding.inflate(inflater)
|
||||||
|
|
||||||
binding.lifecycleOwner = this
|
|
||||||
binding.musicModel = musicModel
|
|
||||||
|
|
||||||
musicModel.response.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{ response ->
|
|
||||||
onMusicLoadResponse(response)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
musicModel.doReload.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{ retry ->
|
|
||||||
onRetry(retry)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
musicModel.doGrant.observe(
|
|
||||||
viewLifecycleOwner,
|
|
||||||
{ grant ->
|
|
||||||
onGrant(grant)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Set up the permission launcher, as its disallowed outside of onCreate.
|
// Set up the permission launcher, as its disallowed outside of onCreate.
|
||||||
permLauncher =
|
val permLauncher =
|
||||||
registerForActivityResult(
|
registerForActivityResult(
|
||||||
ActivityResultContracts.RequestPermission()
|
ActivityResultContracts.RequestPermission()
|
||||||
) { granted: Boolean ->
|
) { granted: Boolean ->
|
||||||
// If its actually granted, restart the loading process again.
|
// If its actually granted, restart the loading process again.
|
||||||
if (granted) {
|
if (granted) {
|
||||||
wipeViews()
|
wipeViews(binding)
|
||||||
|
|
||||||
musicModel.reload()
|
musicModel.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- UI SETUP ---
|
||||||
|
|
||||||
|
binding.lifecycleOwner = this
|
||||||
|
binding.musicModel = musicModel
|
||||||
|
|
||||||
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
musicModel.response.observe(viewLifecycleOwner) { onMusicLoadResponse(it, binding) }
|
||||||
|
musicModel.doReload.observe(viewLifecycleOwner) { onRetry(it, binding) }
|
||||||
|
musicModel.doGrant.observe(viewLifecycleOwner) { onGrant(it, permLauncher) }
|
||||||
|
|
||||||
// Force an error screen if the permissions are denied or the prompt needs to be shown.
|
// Force an error screen if the permissions are denied or the prompt needs to be shown.
|
||||||
if (checkPerms()) {
|
if (checkPerms()) {
|
||||||
onNoPerms()
|
onNoPerms(binding)
|
||||||
} else {
|
} else {
|
||||||
musicModel.go()
|
musicModel.go()
|
||||||
}
|
}
|
||||||
|
@ -96,13 +78,15 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
) == PackageManager.PERMISSION_DENIED
|
) == PackageManager.PERMISSION_DENIED
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onMusicLoadResponse(response: MusicLoaderResponse?) {
|
private fun onMusicLoadResponse(
|
||||||
|
response: MusicLoaderResponse?,
|
||||||
|
binding: FragmentLoadingBinding
|
||||||
|
) {
|
||||||
if (response == MusicLoaderResponse.DONE) {
|
if (response == MusicLoaderResponse.DONE) {
|
||||||
findNavController().navigate(
|
findNavController().navigate(
|
||||||
LoadingFragmentDirections.actionToMain()
|
LoadingFragmentDirections.actionToMain()
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
binding.let { binding ->
|
|
||||||
binding.loadingErrorText.text =
|
binding.loadingErrorText.text =
|
||||||
if (response == MusicLoaderResponse.NO_MUSIC)
|
if (response == MusicLoaderResponse.NO_MUSIC)
|
||||||
getString(R.string.error_no_music)
|
getString(R.string.error_no_music)
|
||||||
|
@ -112,15 +96,13 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
// If the response wasn't a success, then show the specific error message
|
// If the response wasn't a success, then show the specific error message
|
||||||
// depending on which error response was given, along with a retry button
|
// depending on which error response was given, along with a retry button
|
||||||
binding.loadingBar.visibility = View.GONE
|
binding.loadingBar.visibility = View.GONE
|
||||||
|
|
||||||
binding.loadingErrorText.visibility = View.VISIBLE
|
binding.loadingErrorText.visibility = View.VISIBLE
|
||||||
binding.loadingErrorIcon.visibility = View.VISIBLE
|
binding.loadingErrorIcon.visibility = View.VISIBLE
|
||||||
binding.loadingRetryButton.visibility = View.VISIBLE
|
binding.loadingRetryButton.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun onNoPerms() {
|
private fun onNoPerms(binding: FragmentLoadingBinding) {
|
||||||
// If there are no perms, switch out the view elements as if an error screen was being
|
// If there are no perms, switch out the view elements as if an error screen was being
|
||||||
// shown, but show the label that Auxio needs to read external storage to function,
|
// shown, but show the label that Auxio needs to read external storage to function,
|
||||||
// along with a GRANT button
|
// along with a GRANT button
|
||||||
|
@ -133,15 +115,15 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
binding.loadingErrorText.text = getString(R.string.error_no_perms)
|
binding.loadingErrorText.text = getString(R.string.error_no_perms)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onRetry(retry: Boolean) {
|
private fun onRetry(retry: Boolean, binding: FragmentLoadingBinding) {
|
||||||
if (retry) {
|
if (retry) {
|
||||||
wipeViews()
|
wipeViews(binding)
|
||||||
|
|
||||||
musicModel.doneWithReload()
|
musicModel.doneWithReload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onGrant(grant: Boolean) {
|
private fun onGrant(grant: Boolean, permLauncher: ActivityResultLauncher<String>) {
|
||||||
if (grant) {
|
if (grant) {
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
|
|
||||||
|
@ -150,7 +132,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe views and switch back to the plain ProgressBar
|
// Wipe views and switch back to the plain ProgressBar
|
||||||
private fun wipeViews() {
|
private fun wipeViews(binding: FragmentLoadingBinding) {
|
||||||
binding.loadingBar.visibility = View.VISIBLE
|
binding.loadingBar.visibility = View.VISIBLE
|
||||||
binding.loadingErrorText.visibility = View.GONE
|
binding.loadingErrorText.visibility = View.GONE
|
||||||
binding.loadingErrorIcon.visibility = View.GONE
|
binding.loadingErrorIcon.visibility = View.GONE
|
||||||
|
|
|
@ -13,9 +13,9 @@ sealed class BaseModel {
|
||||||
data class Song(
|
data class Song(
|
||||||
override val id: Long = -1,
|
override val id: Long = -1,
|
||||||
override var name: String,
|
override var name: String,
|
||||||
val albumId: Long,
|
val albumId: Long = -1,
|
||||||
val track: Int,
|
val track: Int = -1,
|
||||||
val duration: Long,
|
val duration: Long = 0,
|
||||||
) : BaseModel() {
|
) : BaseModel() {
|
||||||
lateinit var album: Album
|
lateinit var album: Album
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
|
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
|
||||||
|
class CompactPlaybackFragment : Fragment() {
|
||||||
|
private val musicModel: MusicViewModel by activityViewModels {
|
||||||
|
MusicViewModel.Factory(requireActivity().application)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
|
override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?
|
||||||
|
): View? {
|
||||||
|
val binding = FragmentCompactPlaybackBinding.inflate(inflater)
|
||||||
|
|
||||||
|
binding.lifecycleOwner = this
|
||||||
|
|
||||||
|
// Put a placeholder song in the binding & hide the playback fragment initially,
|
||||||
|
// as for some reason the attach event doesn't register anymore w/LiveData
|
||||||
|
binding.song = musicModel.songs.value!![0]
|
||||||
|
binding.root.visibility = View.GONE
|
||||||
|
|
||||||
|
playbackModel.currentSong.observe(viewLifecycleOwner) {
|
||||||
|
if (it == null) {
|
||||||
|
Log.d(this::class.simpleName, "Hiding playback bar due to no song being played.")
|
||||||
|
|
||||||
|
binding.root.visibility = View.GONE
|
||||||
|
} else {
|
||||||
|
Log.d(this::class.simpleName, "Updating song display to ${it.name}")
|
||||||
|
|
||||||
|
binding.song = it
|
||||||
|
|
||||||
|
binding.root.visibility = View.VISIBLE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.d(this::class.simpleName, "Fragment Created")
|
||||||
|
|
||||||
|
return binding.root
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package org.oxycblt.auxio.playback
|
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
|
|
||||||
|
class PlaybackViewModel : ViewModel() {
|
||||||
|
private val mCurrentSong = MutableLiveData<Song>()
|
||||||
|
val currentSong: LiveData<Song> get() = mCurrentSong
|
||||||
|
|
||||||
|
fun updateSong(song: Song) {
|
||||||
|
mCurrentSong.value = song
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,15 +9,17 @@ import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
||||||
import org.oxycblt.auxio.music.MusicViewModel
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
|
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||||
import org.oxycblt.auxio.recycler.ClickListener
|
import org.oxycblt.auxio.recycler.ClickListener
|
||||||
import org.oxycblt.auxio.theme.applyDivider
|
import org.oxycblt.auxio.theme.applyDivider
|
||||||
|
|
||||||
class SongsFragment : Fragment() {
|
class SongsFragment : Fragment() {
|
||||||
|
|
||||||
private val musicModel: MusicViewModel by activityViewModels {
|
private val musicModel: MusicViewModel by activityViewModels {
|
||||||
MusicViewModel.Factory(requireActivity().application)
|
MusicViewModel.Factory(requireActivity().application)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
inflater: LayoutInflater,
|
inflater: LayoutInflater,
|
||||||
container: ViewGroup?,
|
container: ViewGroup?,
|
||||||
|
@ -31,7 +33,7 @@ class SongsFragment : Fragment() {
|
||||||
adapter = SongAdapter(
|
adapter = SongAdapter(
|
||||||
musicModel.songs.value!!,
|
musicModel.songs.value!!,
|
||||||
ClickListener { song ->
|
ClickListener { song ->
|
||||||
Log.d(this::class.simpleName, song.name)
|
playbackModel.updateSong(song)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
applyDivider()
|
applyDivider()
|
||||||
|
|
|
@ -87,7 +87,7 @@ fun resolveAttr(context: Context, @AttrRes attr: Int): Int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply a color to a Menu Item
|
// Apply a color to a Menu Item
|
||||||
fun MenuItem.applyColor(@ColorRes color: Int) {
|
fun MenuItem.applyColor(@ColorInt color: Int) {
|
||||||
SpannableString(title).apply {
|
SpannableString(title).apply {
|
||||||
setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
|
setSpan(ForegroundColorSpan(color), 0, length, SpannableString.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||||
title = this
|
title = this
|
||||||
|
|
78
app/src/main/res/layout/fragment_compact_playback.xml
Normal file
78
app/src/main/res/layout/fragment_compact_playback.xml
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
tools:context=".playback.CompactPlaybackFragment">
|
||||||
|
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<variable
|
||||||
|
name="song"
|
||||||
|
type="org.oxycblt.auxio.music.Song" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
android:animateLayoutChanges="true"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content">
|
||||||
|
|
||||||
|
<ProgressBar
|
||||||
|
android:id="@+id/song_progress"
|
||||||
|
style="?android:attr/progressBarStyleHorizontal"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/playback_progress_size"
|
||||||
|
android:clickable="false"
|
||||||
|
android:progressBackgroundTint="?android:attr/colorControlNormal"
|
||||||
|
android:progressTint="?android:attr/colorPrimary"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:progress="70" />
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/album_cover"
|
||||||
|
android:layout_width="@dimen/cover_size_compact"
|
||||||
|
android:layout_height="@dimen/cover_size_compact"
|
||||||
|
android:contentDescription="@{@string/description_album_cover(song.name)}"
|
||||||
|
android:layout_margin="@dimen/margin_smallish"
|
||||||
|
app:coverArt="@{song}"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/song_progress"
|
||||||
|
tools:src="@drawable/ic_song" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/song_name"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:fontFamily="@font/inter_semibold"
|
||||||
|
android:text="@{song.name}"
|
||||||
|
android:layout_marginStart="@dimen/margin_smallish"
|
||||||
|
android:textAppearance="@style/TextAppearance.SmallHeader"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
app:layout_constraintBottom_toTopOf="@+id/song_info"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintVertical_chainStyle="packed"
|
||||||
|
tools:text="Song Name" />
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/song_info"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/margin_smallish"
|
||||||
|
android:ellipsize="marquee"
|
||||||
|
android:singleLine="true"
|
||||||
|
android:marqueeRepeatLimit="marquee_forever"
|
||||||
|
android:textAppearance="@style/TextAppearance.MaterialComponents.Caption"
|
||||||
|
android:text="@{@string/format_info(song.album.name, song.album.artist.name)}"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toEndOf="@+id/album_cover"
|
||||||
|
app:layout_constraintTop_toBottomOf="@+id/song_name"
|
||||||
|
tools:text="Artist / Album" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
</layout>
|
|
@ -15,6 +15,13 @@
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" />
|
android:layout_weight="1" />
|
||||||
|
|
||||||
|
<androidx.fragment.app.FragmentContainerView
|
||||||
|
android:id="@+id/playback_bar"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:name="org.oxycblt.auxio.playback.CompactPlaybackFragment"
|
||||||
|
android:elevation="@dimen/elevation_normal" />
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
<com.google.android.material.tabs.TabLayout
|
||||||
android:id="@+id/main_tabs"
|
android:id="@+id/main_tabs"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
@ -29,7 +36,8 @@
|
||||||
app:tabIndicator="@drawable/indicator"
|
app:tabIndicator="@drawable/indicator"
|
||||||
app:tabIndicatorColor="?android:attr/colorPrimary"
|
app:tabIndicatorColor="?android:attr/colorPrimary"
|
||||||
app:tabMode="fixed"
|
app:tabMode="fixed"
|
||||||
app:tabRippleColor="@color/selection_color" />
|
app:tabRippleColor="@color/selection_color"
|
||||||
|
tools:background="@color/control_color" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</layout>
|
</layout>
|
|
@ -12,10 +12,10 @@
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_to_main"
|
android:id="@+id/action_to_main"
|
||||||
app:destination="@id/main_fragment"
|
app:destination="@id/main_fragment"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:popUpTo="@id/loading_fragment"
|
app:popUpTo="@id/loading_fragment"
|
||||||
app:popUpToInclusive="true"
|
app:popUpToInclusive="true"
|
||||||
app:launchSingleTop="true" />
|
app:launchSingleTop="true" />
|
||||||
|
@ -27,24 +27,24 @@
|
||||||
tools:layout="@layout/fragment_main">
|
tools:layout="@layout/fragment_main">
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_artist"
|
android:id="@+id/action_show_artist"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:destination="@id/artist_detail_fragment" />
|
app:destination="@id/artist_detail_fragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_album"
|
android:id="@+id/action_show_album"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:destination="@id/album_detail_fragment" />
|
app:destination="@id/album_detail_fragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_genre"
|
android:id="@+id/action_show_genre"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:destination="@id/genreDetailFragment" />
|
app:destination="@id/genreDetailFragment" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_return_to_loading"
|
android:id="@+id/action_return_to_loading"
|
||||||
|
@ -62,10 +62,10 @@
|
||||||
app:argType="long" />
|
app:argType="long" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_album"
|
android:id="@+id/action_show_album"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:destination="@id/album_detail_fragment" />
|
app:destination="@id/album_detail_fragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -81,10 +81,10 @@
|
||||||
app:argType="boolean" />
|
app:argType="boolean" />
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_parent_artist"
|
android:id="@+id/action_show_parent_artist"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:destination="@id/artist_detail_fragment" />
|
app:destination="@id/artist_detail_fragment" />
|
||||||
</fragment>
|
</fragment>
|
||||||
<fragment
|
<fragment
|
||||||
|
@ -94,10 +94,10 @@
|
||||||
tools:layout="@layout/fragment_genre_detail">
|
tools:layout="@layout/fragment_genre_detail">
|
||||||
<action
|
<action
|
||||||
android:id="@+id/action_show_artist"
|
android:id="@+id/action_show_artist"
|
||||||
app:enterAnim="@anim/fragment_fade_enter"
|
app:enterAnim="@anim/nav_default_enter_anim"
|
||||||
app:exitAnim="@anim/fragment_fade_exit"
|
app:exitAnim="@anim/nav_default_exit_anim"
|
||||||
app:popEnterAnim="@anim/fragment_fade_enter"
|
app:popEnterAnim="@anim/nav_default_pop_enter_anim"
|
||||||
app:popExitAnim="@anim/fragment_fade_exit"
|
app:popExitAnim="@anim/nav_default_pop_exit_anim"
|
||||||
app:destination="@id/artist_detail_fragment" />
|
app:destination="@id/artist_detail_fragment" />
|
||||||
<argument
|
<argument
|
||||||
android:name="genreId"
|
android:name="genreId"
|
||||||
|
|
|
@ -5,12 +5,14 @@
|
||||||
<dimen name="padding_medium">16dp</dimen>
|
<dimen name="padding_medium">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="margin_small">8dp</dimen>
|
<dimen name="margin_small">8dp</dimen>
|
||||||
|
<dimen name="margin_smallish">10dp</dimen>
|
||||||
<dimen name="margin_medium">16dp</dimen>
|
<dimen name="margin_medium">16dp</dimen>
|
||||||
|
|
||||||
<dimen name="status_icon_size">48dp</dimen>
|
<dimen name="status_icon_size">48dp</dimen>
|
||||||
|
|
||||||
<dimen name="tab_menu_size">40dp</dimen>
|
<dimen name="tab_menu_size">40dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="cover_size_small">36dp</dimen>
|
||||||
<dimen name="cover_size_compact">44dp</dimen>
|
<dimen name="cover_size_compact">44dp</dimen>
|
||||||
<dimen name="cover_size_normal">56dp</dimen>
|
<dimen name="cover_size_normal">56dp</dimen>
|
||||||
<dimen name="cover_size_large">68dp</dimen>
|
<dimen name="cover_size_large">68dp</dimen>
|
||||||
|
@ -26,5 +28,7 @@
|
||||||
|
|
||||||
<dimen name="divider_ripple_size">18dp</dimen>
|
<dimen name="divider_ripple_size">18dp</dimen>
|
||||||
|
|
||||||
|
<dimen name="playback_progress_size">2dp</dimen>
|
||||||
|
|
||||||
<dimen name="elevation_normal">4dp</dimen>
|
<dimen name="elevation_normal">4dp</dimen>
|
||||||
</resources>
|
</resources>
|
|
@ -27,6 +27,10 @@
|
||||||
<item name="android:textSize">@dimen/detail_header_size_max</item>
|
<item name="android:textSize">@dimen/detail_header_size_max</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="TextAppearance.SmallHeader" parent="TextAppearance.MaterialComponents.Body2">
|
||||||
|
<item name="android:fontFamily">@font/inter_semibold</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
<style name="AppThemeOverlay.Popup" parent="ThemeOverlay.AppCompat.DayNight">
|
<style name="AppThemeOverlay.Popup" parent="ThemeOverlay.AppCompat.DayNight">
|
||||||
<item name="android:colorBackground">@color/background</item>
|
<item name="android:colorBackground">@color/background</item>
|
||||||
<item name="colorControlHighlight">@color/selection_color</item>
|
<item name="colorControlHighlight">@color/selection_color</item>
|
||||||
|
|
Loading…
Reference in a new issue