Heavily refactor Library Management
Remove MusicRepository/All Library models and replace it with a single shared ViewModel, Move MainFragment into MainActivity, and remove the LoadingFragment -> MainFragment navigation path.
This commit is contained in:
parent
e9ee9d1ef1
commit
031815d746
17 changed files with 360 additions and 494 deletions
|
@ -5,20 +5,51 @@ import android.os.Bundle
|
|||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.oxycblt.auxio.databinding.ActivityMainBinding
|
||||
import org.oxycblt.auxio.library.LibraryFragment
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
import org.oxycblt.auxio.library.SongsFragment
|
||||
import org.oxycblt.auxio.theme.accent
|
||||
import org.oxycblt.auxio.theme.getInactiveAlpha
|
||||
import org.oxycblt.auxio.theme.getTransparentAccent
|
||||
import org.oxycblt.auxio.theme.toColor
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
// Debug placeholder so I can test dark and light modes. Ignore.
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
private val shownFragments = listOf(0, 1)
|
||||
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
private val libraryFragment: LibraryFragment by lazy { LibraryFragment() }
|
||||
private val songsFragment: SongsFragment by lazy { SongsFragment() }
|
||||
|
||||
private val tabIcons = listOf(
|
||||
R.drawable.ic_library,
|
||||
R.drawable.ic_song
|
||||
)
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private lateinit var permLauncher: ActivityResultLauncher<String>
|
||||
|
||||
private val musicModel: MusicViewModel by lazy {
|
||||
ViewModelProvider(
|
||||
this, MusicViewModel.Factory(application)
|
||||
).get(MusicViewModel::class.java)
|
||||
}
|
||||
|
||||
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
|
||||
// Apply the theme
|
||||
setTheme(accent.second)
|
||||
|
||||
|
@ -27,8 +58,92 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_main)
|
||||
binding = DataBindingUtil.setContentView<ActivityMainBinding>(
|
||||
this, R.layout.activity_main
|
||||
)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
|
||||
val adapter = PagerAdapter(this)
|
||||
binding.viewPager.adapter = adapter
|
||||
|
||||
val colorActive = accent.first.toColor(baseContext)
|
||||
val colorInactive = getTransparentAccent(
|
||||
baseContext,
|
||||
accent.first,
|
||||
getInactiveAlpha(accent.first)
|
||||
)
|
||||
|
||||
// Link the ViewPager & Tab View
|
||||
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
|
||||
tab.icon = ContextCompat.getDrawable(baseContext, tabIcons[position])
|
||||
|
||||
// Set the icon tint to deselected if its not the default tab
|
||||
if (position > 0) {
|
||||
tab.icon?.setTint(colorInactive)
|
||||
}
|
||||
|
||||
// Init the fragment
|
||||
fragmentAt(position)
|
||||
}.attach()
|
||||
|
||||
// Set up the selected/deselected colors
|
||||
binding.tabs.addOnTabSelectedListener(
|
||||
object : TabLayout.OnTabSelectedListener {
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
tab.icon?.setTint(colorActive)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {
|
||||
tab.icon?.setTint(colorInactive)
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
musicModel.response.observe(
|
||||
this, {
|
||||
if (it == MusicLoaderResponse.DONE) {
|
||||
binding.loadingFragment.visibility = View.GONE
|
||||
binding.viewPager.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
musicModel.go()
|
||||
|
||||
Log.d(this::class.simpleName, "Activity Created.")
|
||||
}
|
||||
|
||||
private fun fragmentAt(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> libraryFragment
|
||||
1 -> songsFragment
|
||||
|
||||
else -> libraryFragment
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = shownFragments.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
Log.d(this::class.simpleName, "Switching to fragment $position.")
|
||||
|
||||
if (shownFragments.contains(position)) {
|
||||
return fragmentAt(position)
|
||||
}
|
||||
|
||||
// Not sure how this would happen but it might
|
||||
Log.e(
|
||||
this::class.simpleName,
|
||||
"Attempted to index a fragment that shouldn't be shown. Returning libraryFragment."
|
||||
)
|
||||
|
||||
return libraryFragment
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,117 +0,0 @@
|
|||
package org.oxycblt.auxio
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
import org.oxycblt.auxio.databinding.FragmentMainBinding
|
||||
import org.oxycblt.auxio.library.LibraryFragment
|
||||
import org.oxycblt.auxio.songs.SongsFragment
|
||||
import org.oxycblt.auxio.theme.accent
|
||||
import org.oxycblt.auxio.theme.getInactiveAlpha
|
||||
import org.oxycblt.auxio.theme.getTransparentAccent
|
||||
import org.oxycblt.auxio.theme.toColor
|
||||
|
||||
class MainFragment : Fragment() {
|
||||
|
||||
private val shownFragments = listOf(0, 1)
|
||||
|
||||
private val libraryFragment: LibraryFragment by lazy { LibraryFragment() }
|
||||
private val songsFragment: SongsFragment by lazy { SongsFragment() }
|
||||
|
||||
private val tabIcons = listOf(
|
||||
R.drawable.ic_library,
|
||||
R.drawable.ic_song
|
||||
)
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val binding = DataBindingUtil.inflate<FragmentMainBinding>(
|
||||
inflater, R.layout.fragment_main, container, false
|
||||
)
|
||||
|
||||
val adapter = PagerAdapter(requireActivity())
|
||||
binding.viewPager.adapter = adapter
|
||||
|
||||
val colorActive = accent.first.toColor(requireContext())
|
||||
val colorInactive = getTransparentAccent(
|
||||
requireContext(),
|
||||
accent.first,
|
||||
getInactiveAlpha(accent.first)
|
||||
)
|
||||
|
||||
// Link the ViewPager & Tab View
|
||||
TabLayoutMediator(binding.tabs, binding.viewPager) { tab, position ->
|
||||
tab.icon = ContextCompat.getDrawable(requireContext(), tabIcons[position])
|
||||
|
||||
// Set the icon tint to deselected if its not the default tab
|
||||
if (position > 0) {
|
||||
tab.icon?.setTint(colorInactive)
|
||||
}
|
||||
|
||||
// Init the fragment
|
||||
fragmentAt(position)
|
||||
}.attach()
|
||||
|
||||
// Set up the selected/deselected colors
|
||||
binding.tabs.addOnTabSelectedListener(
|
||||
object : TabLayout.OnTabSelectedListener {
|
||||
|
||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
||||
tab.icon?.setTint(colorActive)
|
||||
}
|
||||
|
||||
override fun onTabUnselected(tab: TabLayout.Tab) {
|
||||
tab.icon?.setTint(colorInactive)
|
||||
}
|
||||
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
Log.d(this::class.simpleName, "Fragment Created.")
|
||||
|
||||
return binding.root
|
||||
}
|
||||
|
||||
private fun fragmentAt(position: Int): Fragment {
|
||||
return when (position) {
|
||||
0 -> libraryFragment
|
||||
1 -> songsFragment
|
||||
|
||||
else -> libraryFragment
|
||||
}
|
||||
}
|
||||
|
||||
private inner class PagerAdapter(activity: FragmentActivity) : FragmentStateAdapter(activity) {
|
||||
override fun getItemCount(): Int = shownFragments.size
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
Log.d(this::class.simpleName, "Switching to fragment $position.")
|
||||
|
||||
if (shownFragments.contains(position)) {
|
||||
return fragmentAt(position)
|
||||
}
|
||||
|
||||
// Not sure how this would happen but it might
|
||||
Log.e(
|
||||
this::class.simpleName,
|
||||
"Attempted to index a fragment that shouldn't be shown. Returning libraryFragment."
|
||||
)
|
||||
|
||||
return libraryFragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
package org.oxycblt.auxio.coil
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.widget.ImageView
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
@ -15,13 +16,9 @@ private var artistImageFetcher: ArtistImageFetcher? = null
|
|||
// Get the cover art for a song or album
|
||||
@BindingAdapter("coverArt")
|
||||
fun ImageView.getCoverArt(song: Song) {
|
||||
val request = ImageRequest.Builder(context)
|
||||
val request = getDefaultRequest(context, this)
|
||||
.data(song.album.coverUri)
|
||||
.crossfade(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.error(R.drawable.ic_artist)
|
||||
.crossfade(true)
|
||||
.target(this)
|
||||
.error(R.drawable.ic_song)
|
||||
.build()
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
|
@ -29,13 +26,9 @@ fun ImageView.getCoverArt(song: Song) {
|
|||
|
||||
@BindingAdapter("coverArt")
|
||||
fun ImageView.getCoverArt(album: Album) {
|
||||
val request = ImageRequest.Builder(context)
|
||||
val request = getDefaultRequest(context, this)
|
||||
.data(album.coverUri)
|
||||
.crossfade(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.error(R.drawable.ic_artist)
|
||||
.crossfade(true)
|
||||
.target(this)
|
||||
.error(R.drawable.ic_album)
|
||||
.build()
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
|
@ -56,26 +49,25 @@ fun ImageView.getArtistImage(artist: Artist) {
|
|||
artistImageFetcher = ArtistImageFetcher(context)
|
||||
}
|
||||
|
||||
// Manually create an image request, as that's the only way to add a fetcher that
|
||||
// takes a list of uris AFAIK.
|
||||
ImageRequest.Builder(context)
|
||||
getDefaultRequest(context, this)
|
||||
.data(uris)
|
||||
.fetcher(artistImageFetcher!!)
|
||||
.crossfade(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.error(R.drawable.ic_artist)
|
||||
.target(this)
|
||||
.build()
|
||||
} else {
|
||||
ImageRequest.Builder(context)
|
||||
getDefaultRequest(context, this)
|
||||
.data(artist.albums[0].coverUri)
|
||||
.crossfade(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.error(R.drawable.ic_artist)
|
||||
.crossfade(true)
|
||||
.target(this)
|
||||
.build()
|
||||
}
|
||||
|
||||
Coil.imageLoader(context).enqueue(request)
|
||||
}
|
||||
|
||||
// Get the base request used across the app.
|
||||
private fun getDefaultRequest(context: Context, imageView: ImageView): ImageRequest.Builder {
|
||||
return ImageRequest.Builder(context)
|
||||
.crossfade(true)
|
||||
.placeholder(android.R.color.transparent)
|
||||
.target(imageView)
|
||||
}
|
||||
|
|
|
@ -7,17 +7,19 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentLibraryBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.recycler.adapters.ArtistAdapter
|
||||
import org.oxycblt.auxio.recycler.applyDivider
|
||||
import org.oxycblt.auxio.recycler.viewholders.ClickListener
|
||||
|
||||
class LibraryFragment : Fragment() {
|
||||
|
||||
private val libraryModel: LibraryViewModel by lazy {
|
||||
ViewModelProvider(this).get(LibraryViewModel::class.java)
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -30,7 +32,7 @@ class LibraryFragment : Fragment() {
|
|||
)
|
||||
|
||||
binding.libraryRecycler.adapter = ArtistAdapter(
|
||||
libraryModel.artists.value!!,
|
||||
musicModel.artists.value!!,
|
||||
ClickListener { artist ->
|
||||
Log.d(this::class.simpleName, artist.name)
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
package org.oxycblt.auxio.library
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.models.Album
|
||||
import org.oxycblt.auxio.music.models.Artist
|
||||
|
||||
class LibraryViewModel : ViewModel() {
|
||||
|
||||
private val mArtists = MutableLiveData<List<Artist>>()
|
||||
private var mAlbums = MutableLiveData<List<Album>>()
|
||||
|
||||
val artists: LiveData<List<Artist>> get() = mArtists
|
||||
val albums: LiveData<List<Album>> get() = mAlbums
|
||||
|
||||
init {
|
||||
val repo = MusicRepository.getInstance()
|
||||
|
||||
mArtists.value = repo.artists
|
||||
mAlbums.value = repo.albums
|
||||
|
||||
Log.d(this::class.simpleName, "ViewModel created.")
|
||||
}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
package org.oxycblt.auxio.loading
|
||||
package org.oxycblt.auxio.library
|
||||
|
||||
import android.Manifest
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.transition.TransitionInflater
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -13,21 +12,16 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||
import androidx.core.content.ContextCompat
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
|
||||
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||
|
||||
private val loadingModel: LoadingViewModel by lazy {
|
||||
ViewModelProvider(
|
||||
this,
|
||||
LoadingViewModel.Factory(
|
||||
requireActivity().application
|
||||
)
|
||||
).get(LoadingViewModel::class.java)
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
private lateinit var binding: FragmentLoadingBinding
|
||||
|
@ -43,23 +37,23 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
)
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
binding.loadingModel = loadingModel
|
||||
binding.musicModel = musicModel
|
||||
|
||||
loadingModel.musicRepoResponse.observe(
|
||||
musicModel.response.observe(
|
||||
viewLifecycleOwner,
|
||||
{ response ->
|
||||
onMusicLoadResponse(response)
|
||||
}
|
||||
)
|
||||
|
||||
loadingModel.doRetry.observe(
|
||||
musicModel.doReload.observe(
|
||||
viewLifecycleOwner,
|
||||
{ retry ->
|
||||
onRetry(retry)
|
||||
}
|
||||
)
|
||||
|
||||
loadingModel.doGrant.observe(
|
||||
musicModel.doGrant.observe(
|
||||
viewLifecycleOwner,
|
||||
{ grant ->
|
||||
onGrant(grant)
|
||||
|
@ -75,7 +69,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
if (granted) {
|
||||
wipeViews()
|
||||
|
||||
loadingModel.retry()
|
||||
musicModel.reload()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,7 +79,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
if (checkPerms()) {
|
||||
onNoPerms()
|
||||
} else {
|
||||
loadingModel.go()
|
||||
musicModel.go()
|
||||
}
|
||||
|
||||
Log.d(this::class.simpleName, "Fragment created.")
|
||||
|
@ -110,28 +104,21 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
repoResponse?.let { response ->
|
||||
binding.loadingBar.visibility = View.GONE
|
||||
|
||||
if (response == MusicLoaderResponse.DONE) {
|
||||
val inflater = TransitionInflater.from(requireContext())
|
||||
exitTransition = inflater.inflateTransition(R.transition.transition_to_main)
|
||||
|
||||
this.findNavController().navigate(
|
||||
LoadingFragmentDirections.actionToMain()
|
||||
)
|
||||
} else {
|
||||
// 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
|
||||
binding.errorText.visibility = View.VISIBLE
|
||||
binding.statusIcon.visibility = View.VISIBLE
|
||||
binding.retryButton.visibility = View.VISIBLE
|
||||
|
||||
if (response != MusicLoaderResponse.DONE) {
|
||||
binding.errorText.text =
|
||||
if (response == MusicLoaderResponse.NO_MUSIC)
|
||||
getString(R.string.error_no_music)
|
||||
else
|
||||
getString(R.string.error_music_load_failed)
|
||||
|
||||
// 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
|
||||
binding.errorText.visibility = View.VISIBLE
|
||||
binding.statusIcon.visibility = View.VISIBLE
|
||||
binding.retryButton.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
loadingModel.doneWithResponse()
|
||||
musicModel.doneWithResponse()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -152,7 +139,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
if (retry) {
|
||||
wipeViews()
|
||||
|
||||
loadingModel.doneWithRetry()
|
||||
musicModel.doneWithReload()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -160,7 +147,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
if (grant) {
|
||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
|
||||
loadingModel.doneWithGrant()
|
||||
musicModel.doneWithGrant()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package org.oxycblt.auxio.songs
|
||||
package org.oxycblt.auxio.library
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
@ -7,17 +7,19 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.databinding.DataBindingUtil
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.recycler.adapters.SongAdapter
|
||||
import org.oxycblt.auxio.recycler.applyDivider
|
||||
import org.oxycblt.auxio.recycler.viewholders.ClickListener
|
||||
|
||||
class SongsFragment : Fragment() {
|
||||
|
||||
private val songsModel: SongsViewModel by lazy {
|
||||
ViewModelProvider(this).get(SongsViewModel::class.java)
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -30,7 +32,7 @@ class SongsFragment : Fragment() {
|
|||
)
|
||||
|
||||
binding.songRecycler.adapter = SongAdapter(
|
||||
songsModel.songs.value!!,
|
||||
musicModel.songs.value!!,
|
||||
ClickListener { song ->
|
||||
Log.d(this::class.simpleName, song.name)
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package org.oxycblt.auxio.loading
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
|
||||
class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||
|
||||
private val loadingJob = Job()
|
||||
private val ioScope = CoroutineScope(
|
||||
loadingJob + Dispatchers.IO
|
||||
)
|
||||
|
||||
private val mMusicRepoResponse = MutableLiveData<MusicLoaderResponse>()
|
||||
val musicRepoResponse: LiveData<MusicLoaderResponse> get() = mMusicRepoResponse
|
||||
|
||||
private val mDoRetry = MutableLiveData<Boolean>()
|
||||
val doRetry: LiveData<Boolean> get() = mDoRetry
|
||||
|
||||
private val mDoGrant = MutableLiveData<Boolean>()
|
||||
val doGrant: LiveData<Boolean> get() = mDoGrant
|
||||
|
||||
private var started = false
|
||||
|
||||
// Start the music loading. It has already been called, one needs to call retry() instead.
|
||||
fun go() {
|
||||
if (!started) {
|
||||
started = true
|
||||
|
||||
startMusicRepo()
|
||||
}
|
||||
}
|
||||
|
||||
// Start the music loading sequence.
|
||||
private fun startMusicRepo() {
|
||||
val repo = MusicRepository.getInstance()
|
||||
|
||||
// Allow MusicRepository to scan the file system on the IO thread
|
||||
ioScope.launch {
|
||||
val response = repo.init(app)
|
||||
|
||||
// Then actually notify listeners of the response in the Main thread
|
||||
withContext(Dispatchers.Main) {
|
||||
mMusicRepoResponse.value = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Functions for communicating between LoadingFragment & LoadingViewModel
|
||||
|
||||
fun doneWithResponse() {
|
||||
mMusicRepoResponse.value = null
|
||||
}
|
||||
|
||||
fun retry() {
|
||||
startMusicRepo()
|
||||
|
||||
mDoRetry.value = true
|
||||
}
|
||||
|
||||
fun doneWithRetry() {
|
||||
mDoRetry.value = false
|
||||
}
|
||||
|
||||
fun grant() {
|
||||
mDoGrant.value = true
|
||||
}
|
||||
|
||||
fun doneWithGrant() {
|
||||
mDoGrant.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
// Cancel the current loading job if the app has been stopped
|
||||
loadingJob.cancel()
|
||||
}
|
||||
|
||||
class Factory(private val application: Application) : ViewModelProvider.Factory {
|
||||
@Suppress("unchecked_cast")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) {
|
||||
return LoadingViewModel(application) as T
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Unknown ViewModel class.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.models.Album
|
||||
import org.oxycblt.auxio.music.models.Artist
|
||||
import org.oxycblt.auxio.music.models.Genre
|
||||
import org.oxycblt.auxio.music.models.Song
|
||||
import org.oxycblt.auxio.music.processing.MusicLoader
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
import org.oxycblt.auxio.music.processing.MusicSorter
|
||||
|
||||
// Storage for music data.
|
||||
class MusicRepository {
|
||||
|
||||
lateinit var genres: List<Genre>
|
||||
lateinit var artists: List<Artist>
|
||||
lateinit var albums: List<Album>
|
||||
lateinit var songs: List<Song>
|
||||
|
||||
fun init(app: Application): MusicLoaderResponse {
|
||||
Log.i(this::class.simpleName, "Starting initial music load...")
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
val loader = MusicLoader(app.contentResolver)
|
||||
|
||||
if (loader.response == MusicLoaderResponse.DONE) {
|
||||
// If the loading succeeds, then process the songs and set them.
|
||||
val sorter = MusicSorter(
|
||||
loader.genres,
|
||||
loader.artists,
|
||||
loader.albums,
|
||||
loader.songs,
|
||||
|
||||
app.getString(R.string.placeholder_unknown_genre),
|
||||
app.getString(R.string.placeholder_unknown_artist),
|
||||
app.getString(R.string.placeholder_unknown_album)
|
||||
)
|
||||
|
||||
songs = sorter.songs.toList()
|
||||
albums = sorter.albums.toList()
|
||||
artists = sorter.artists.toList()
|
||||
genres = sorter.genres.toList()
|
||||
|
||||
val elapsed = System.currentTimeMillis() - start
|
||||
|
||||
Log.i(
|
||||
this::class.simpleName,
|
||||
"Music load completed successfully in ${elapsed}ms."
|
||||
)
|
||||
}
|
||||
|
||||
return loader.response
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: MusicRepository? = null
|
||||
|
||||
fun getInstance(): MusicRepository {
|
||||
val tempInstance = INSTANCE
|
||||
|
||||
if (tempInstance != null) {
|
||||
Log.d(
|
||||
this::class.simpleName,
|
||||
"Passed an existing instance of MusicRepository."
|
||||
)
|
||||
|
||||
return tempInstance
|
||||
}
|
||||
|
||||
synchronized(this) {
|
||||
val newInstance = MusicRepository()
|
||||
INSTANCE = newInstance
|
||||
|
||||
Log.d(
|
||||
this::class.simpleName,
|
||||
"Created an instance of MusicRepository."
|
||||
)
|
||||
|
||||
return newInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
142
app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
Normal file
142
app/src/main/java/org/oxycblt/auxio/music/MusicViewModel.kt
Normal file
|
@ -0,0 +1,142 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import kotlinx.coroutines.*
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.models.Album
|
||||
import org.oxycblt.auxio.music.models.Artist
|
||||
import org.oxycblt.auxio.music.models.Genre
|
||||
import org.oxycblt.auxio.music.models.Song
|
||||
import org.oxycblt.auxio.music.processing.MusicLoader
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
import org.oxycblt.auxio.music.processing.MusicSorter
|
||||
|
||||
// Storage for music data.
|
||||
class MusicViewModel(private val app: Application) : ViewModel() {
|
||||
|
||||
// Coroutine
|
||||
private val loadingJob = Job()
|
||||
private val ioScope = CoroutineScope(
|
||||
loadingJob + Dispatchers.IO
|
||||
)
|
||||
|
||||
// Values
|
||||
private val mGenres = MutableLiveData<List<Genre>>()
|
||||
val genres: LiveData<List<Genre>> get() = mGenres
|
||||
|
||||
private val mArtists = MutableLiveData<List<Artist>>()
|
||||
val artists: LiveData<List<Artist>> get() = mArtists
|
||||
|
||||
private val mAlbums = MutableLiveData<List<Album>>()
|
||||
val albums: LiveData<List<Album>> get() = mAlbums
|
||||
|
||||
private val mSongs = MutableLiveData<List<Song>>()
|
||||
val songs: LiveData<List<Song>> get() = mSongs
|
||||
|
||||
private val mResponse = MutableLiveData<MusicLoaderResponse>()
|
||||
val response: LiveData<MusicLoaderResponse> get() = mResponse
|
||||
|
||||
// UI control
|
||||
private val mRedo = MutableLiveData<Boolean>()
|
||||
val doReload: LiveData<Boolean> get() = mRedo
|
||||
|
||||
private val mDoGrant = MutableLiveData<Boolean>()
|
||||
val doGrant: LiveData<Boolean> get() = mDoGrant
|
||||
|
||||
private var started = false
|
||||
|
||||
// Start the music loading sequence.
|
||||
// This should only be ran once, use redo() for all other loads.
|
||||
fun go() {
|
||||
if (!started) {
|
||||
started = true
|
||||
doLoad()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLoad() {
|
||||
Log.i(this::class.simpleName, "Starting initial music load...")
|
||||
|
||||
ioScope.launch {
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
val loader = MusicLoader(app.contentResolver)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (loader.response == MusicLoaderResponse.DONE) {
|
||||
// If the loading succeeds, then process the songs and set them.
|
||||
val sorter = MusicSorter(
|
||||
loader.genres,
|
||||
loader.artists,
|
||||
loader.albums,
|
||||
loader.songs,
|
||||
|
||||
app.getString(R.string.placeholder_unknown_genre),
|
||||
app.getString(R.string.placeholder_unknown_artist),
|
||||
app.getString(R.string.placeholder_unknown_album)
|
||||
)
|
||||
|
||||
mSongs.value = sorter.songs.toList()
|
||||
mAlbums.value = sorter.albums.toList()
|
||||
mArtists.value = sorter.artists.toList()
|
||||
mGenres.value = sorter.genres.toList()
|
||||
}
|
||||
|
||||
mResponse.value = loader.response
|
||||
|
||||
val elapsed = System.currentTimeMillis() - start
|
||||
|
||||
Log.i(
|
||||
this::class.simpleName,
|
||||
"Music load completed successfully in ${elapsed}ms."
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI communication functions
|
||||
fun doneWithResponse() {
|
||||
mResponse.value = null
|
||||
}
|
||||
|
||||
fun reload() {
|
||||
mRedo.value = true
|
||||
|
||||
doLoad()
|
||||
}
|
||||
|
||||
fun doneWithReload() {
|
||||
mRedo.value = false
|
||||
}
|
||||
|
||||
fun grant() {
|
||||
mDoGrant.value = true
|
||||
}
|
||||
|
||||
fun doneWithGrant() {
|
||||
mDoGrant.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
// Cancel the current loading job if the app has been stopped
|
||||
loadingJob.cancel()
|
||||
}
|
||||
|
||||
class Factory(private val application: Application) : ViewModelProvider.Factory {
|
||||
@Suppress("unchecked_cast")
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
if (modelClass.isAssignableFrom(MusicViewModel::class.java)) {
|
||||
return MusicViewModel(application) as T
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Unknown ViewModel class.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package org.oxycblt.auxio.songs
|
||||
|
||||
import android.util.Log
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.oxycblt.auxio.music.MusicRepository
|
||||
import org.oxycblt.auxio.music.models.Song
|
||||
|
||||
class SongsViewModel : ViewModel() {
|
||||
|
||||
private val mSongs = MutableLiveData<List<Song>>()
|
||||
val songs: LiveData<List<Song>> get() = mSongs
|
||||
|
||||
init {
|
||||
val repo = MusicRepository.getInstance()
|
||||
|
||||
mSongs.value = repo.songs
|
||||
|
||||
Log.d(this::class.simpleName, "ViewModel created.")
|
||||
}
|
||||
}
|
|
@ -1,15 +1,53 @@
|
|||
<?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=".MainActivity">
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
<data>
|
||||
|
||||
<variable
|
||||
name="musicModel"
|
||||
type="org.oxycblt.auxio.music.MusicViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:navGraph="@navigation/nav_main"
|
||||
tools:ignore="FragmentTagUsage" />
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tabs"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/loading_fragment"
|
||||
android:name="org.oxycblt.auxio.loading.LoadingFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@+id/tabs"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_menu_size"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:tabGravity="fill"
|
||||
app:tabIconTint="?android:attr/colorPrimary"
|
||||
app:tabIconTintMode="src_in"
|
||||
app:tabIndicator="@drawable/indicator"
|
||||
app:tabIndicatorColor="?android:attr/colorPrimary"
|
||||
app:tabMode="fixed"
|
||||
app:tabRippleColor="@color/selection_color" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -8,8 +8,8 @@
|
|||
<data>
|
||||
|
||||
<variable
|
||||
name="loadingModel"
|
||||
type="org.oxycblt.auxio.loading.LoadingViewModel" />
|
||||
name="musicModel"
|
||||
type="org.oxycblt.auxio.music.MusicViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
@ -69,7 +69,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:onClick="@{() -> loadingModel.retry()}"
|
||||
android:onClick="@{() -> musicModel.reload()}"
|
||||
android:text="@string/label_retry"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
|
@ -89,7 +89,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:onClick="@{() -> loadingModel.grant()}"
|
||||
android:onClick="@{() -> musicModel.grant()}"
|
||||
android:text="@string/label_grant"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
|
|
|
@ -1,33 +0,0 @@
|
|||
<?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">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/viewPager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<com.google.android.material.tabs.TabLayout
|
||||
android:id="@+id/tabs"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/tab_menu_size"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="?android:attr/windowBackground"
|
||||
android:elevation="@dimen/elevation_normal"
|
||||
app:tabIndicatorColor="?android:attr/colorPrimary"
|
||||
app:tabGravity="fill"
|
||||
app:tabMode="fixed"
|
||||
app:tabIconTint="?android:attr/colorPrimary"
|
||||
app:tabIconTintMode="src_in"
|
||||
app:tabIndicator="@drawable/indicator"
|
||||
app:tabRippleColor="@color/selection_color" />
|
||||
|
||||
</LinearLayout>
|
||||
</layout>
|
|
@ -1,24 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation 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"
|
||||
android:id="@+id/nav_main"
|
||||
app:startDestination="@id/loadingFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/loadingFragment"
|
||||
android:name="org.oxycblt.auxio.loading.LoadingFragment"
|
||||
android:label="LoadingFragment"
|
||||
tools:layout="@layout/fragment_loading">
|
||||
<action
|
||||
android:id="@+id/action_to_main"
|
||||
app:destination="@id/mainFragment"
|
||||
app:popUpTo="@id/loadingFragment"
|
||||
app:popUpToInclusive="true" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/mainFragment"
|
||||
android:name="org.oxycblt.auxio.MainFragment"
|
||||
android:label="LibraryFragment"
|
||||
tools:layout="@layout/fragment_main" />
|
||||
</navigation>
|
|
@ -1,3 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<fade xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="@android:integer/config_longAnimTime"></fade>
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<string name="error_no_music">No music found.</string>
|
||||
<string name="error_music_load_failed">Music loading failed.</string>
|
||||
<string name="error_no_perms">Auxio needs permission to access to your music library.</string>
|
||||
<string name="error_no_perms">Permissions to read storage are needed.</string>
|
||||
|
||||
<string name="label_retry">Retry</string>
|
||||
<string name="label_grant">Grant</string>
|
||||
|
|
Loading…
Reference in a new issue