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.AttributeSet
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
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.accent
|
||||||
|
import org.oxycblt.auxio.theme.getInactiveAlpha
|
||||||
|
import org.oxycblt.auxio.theme.getTransparentAccent
|
||||||
|
import org.oxycblt.auxio.theme.toColor
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity() {
|
class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
// Debug placeholder so I can test dark and light modes. Ignore.
|
private val shownFragments = listOf(0, 1)
|
||||||
override fun onAttachedToWindow() {
|
|
||||||
super.onAttachedToWindow()
|
|
||||||
|
|
||||||
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? {
|
override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
|
||||||
|
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||||
|
|
||||||
// Apply the theme
|
// Apply the theme
|
||||||
setTheme(accent.second)
|
setTheme(accent.second)
|
||||||
|
|
||||||
|
@ -27,8 +58,92 @@ class MainActivity : AppCompatActivity() {
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
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.")
|
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
|
package org.oxycblt.auxio.coil
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.databinding.BindingAdapter
|
import androidx.databinding.BindingAdapter
|
||||||
|
@ -15,13 +16,9 @@ private var artistImageFetcher: ArtistImageFetcher? = null
|
||||||
// Get the cover art for a song or album
|
// Get the cover art for a song or album
|
||||||
@BindingAdapter("coverArt")
|
@BindingAdapter("coverArt")
|
||||||
fun ImageView.getCoverArt(song: Song) {
|
fun ImageView.getCoverArt(song: Song) {
|
||||||
val request = ImageRequest.Builder(context)
|
val request = getDefaultRequest(context, this)
|
||||||
.data(song.album.coverUri)
|
.data(song.album.coverUri)
|
||||||
.crossfade(true)
|
.error(R.drawable.ic_song)
|
||||||
.placeholder(android.R.color.transparent)
|
|
||||||
.error(R.drawable.ic_artist)
|
|
||||||
.crossfade(true)
|
|
||||||
.target(this)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
Coil.imageLoader(context).enqueue(request)
|
Coil.imageLoader(context).enqueue(request)
|
||||||
|
@ -29,13 +26,9 @@ fun ImageView.getCoverArt(song: Song) {
|
||||||
|
|
||||||
@BindingAdapter("coverArt")
|
@BindingAdapter("coverArt")
|
||||||
fun ImageView.getCoverArt(album: Album) {
|
fun ImageView.getCoverArt(album: Album) {
|
||||||
val request = ImageRequest.Builder(context)
|
val request = getDefaultRequest(context, this)
|
||||||
.data(album.coverUri)
|
.data(album.coverUri)
|
||||||
.crossfade(true)
|
.error(R.drawable.ic_album)
|
||||||
.placeholder(android.R.color.transparent)
|
|
||||||
.error(R.drawable.ic_artist)
|
|
||||||
.crossfade(true)
|
|
||||||
.target(this)
|
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
Coil.imageLoader(context).enqueue(request)
|
Coil.imageLoader(context).enqueue(request)
|
||||||
|
@ -56,26 +49,25 @@ fun ImageView.getArtistImage(artist: Artist) {
|
||||||
artistImageFetcher = ArtistImageFetcher(context)
|
artistImageFetcher = ArtistImageFetcher(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manually create an image request, as that's the only way to add a fetcher that
|
getDefaultRequest(context, this)
|
||||||
// takes a list of uris AFAIK.
|
|
||||||
ImageRequest.Builder(context)
|
|
||||||
.data(uris)
|
.data(uris)
|
||||||
.fetcher(artistImageFetcher!!)
|
.fetcher(artistImageFetcher!!)
|
||||||
.crossfade(true)
|
|
||||||
.placeholder(android.R.color.transparent)
|
|
||||||
.error(R.drawable.ic_artist)
|
.error(R.drawable.ic_artist)
|
||||||
.target(this)
|
|
||||||
.build()
|
.build()
|
||||||
} else {
|
} else {
|
||||||
ImageRequest.Builder(context)
|
getDefaultRequest(context, this)
|
||||||
.data(artist.albums[0].coverUri)
|
.data(artist.albums[0].coverUri)
|
||||||
.crossfade(true)
|
|
||||||
.placeholder(android.R.color.transparent)
|
|
||||||
.error(R.drawable.ic_artist)
|
.error(R.drawable.ic_artist)
|
||||||
.crossfade(true)
|
|
||||||
.target(this)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
Coil.imageLoader(context).enqueue(request)
|
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 android.view.ViewGroup
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
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.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.recycler.adapters.ArtistAdapter
|
import org.oxycblt.auxio.recycler.adapters.ArtistAdapter
|
||||||
import org.oxycblt.auxio.recycler.applyDivider
|
import org.oxycblt.auxio.recycler.applyDivider
|
||||||
import org.oxycblt.auxio.recycler.viewholders.ClickListener
|
import org.oxycblt.auxio.recycler.viewholders.ClickListener
|
||||||
|
|
||||||
class LibraryFragment : Fragment() {
|
class LibraryFragment : Fragment() {
|
||||||
|
|
||||||
private val libraryModel: LibraryViewModel by lazy {
|
private val musicModel: MusicViewModel by activityViewModels {
|
||||||
ViewModelProvider(this).get(LibraryViewModel::class.java)
|
MusicViewModel.Factory(requireActivity().application)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -30,7 +32,7 @@ class LibraryFragment : Fragment() {
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.libraryRecycler.adapter = ArtistAdapter(
|
binding.libraryRecycler.adapter = ArtistAdapter(
|
||||||
libraryModel.artists.value!!,
|
musicModel.artists.value!!,
|
||||||
ClickListener { artist ->
|
ClickListener { artist ->
|
||||||
Log.d(this::class.simpleName, artist.name)
|
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.Manifest
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.transition.TransitionInflater
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -13,21 +12,16 @@ import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
import org.oxycblt.auxio.databinding.FragmentLoadingBinding
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||||
|
|
||||||
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
|
|
||||||
private val loadingModel: LoadingViewModel by lazy {
|
private val musicModel: MusicViewModel by activityViewModels {
|
||||||
ViewModelProvider(
|
MusicViewModel.Factory(requireActivity().application)
|
||||||
this,
|
|
||||||
LoadingViewModel.Factory(
|
|
||||||
requireActivity().application
|
|
||||||
)
|
|
||||||
).get(LoadingViewModel::class.java)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var binding: FragmentLoadingBinding
|
private lateinit var binding: FragmentLoadingBinding
|
||||||
|
@ -43,23 +37,23 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.lifecycleOwner = this
|
binding.lifecycleOwner = this
|
||||||
binding.loadingModel = loadingModel
|
binding.musicModel = musicModel
|
||||||
|
|
||||||
loadingModel.musicRepoResponse.observe(
|
musicModel.response.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
{ response ->
|
{ response ->
|
||||||
onMusicLoadResponse(response)
|
onMusicLoadResponse(response)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
loadingModel.doRetry.observe(
|
musicModel.doReload.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
{ retry ->
|
{ retry ->
|
||||||
onRetry(retry)
|
onRetry(retry)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
loadingModel.doGrant.observe(
|
musicModel.doGrant.observe(
|
||||||
viewLifecycleOwner,
|
viewLifecycleOwner,
|
||||||
{ grant ->
|
{ grant ->
|
||||||
onGrant(grant)
|
onGrant(grant)
|
||||||
|
@ -75,7 +69,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
if (granted) {
|
if (granted) {
|
||||||
wipeViews()
|
wipeViews()
|
||||||
|
|
||||||
loadingModel.retry()
|
musicModel.reload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -85,7 +79,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
if (checkPerms()) {
|
if (checkPerms()) {
|
||||||
onNoPerms()
|
onNoPerms()
|
||||||
} else {
|
} else {
|
||||||
loadingModel.go()
|
musicModel.go()
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.d(this::class.simpleName, "Fragment created.")
|
Log.d(this::class.simpleName, "Fragment created.")
|
||||||
|
@ -110,28 +104,21 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
repoResponse?.let { response ->
|
repoResponse?.let { response ->
|
||||||
binding.loadingBar.visibility = View.GONE
|
binding.loadingBar.visibility = View.GONE
|
||||||
|
|
||||||
if (response == MusicLoaderResponse.DONE) {
|
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
|
|
||||||
|
|
||||||
binding.errorText.text =
|
binding.errorText.text =
|
||||||
if (response == MusicLoaderResponse.NO_MUSIC)
|
if (response == MusicLoaderResponse.NO_MUSIC)
|
||||||
getString(R.string.error_no_music)
|
getString(R.string.error_no_music)
|
||||||
else
|
else
|
||||||
getString(R.string.error_music_load_failed)
|
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) {
|
if (retry) {
|
||||||
wipeViews()
|
wipeViews()
|
||||||
|
|
||||||
loadingModel.doneWithRetry()
|
musicModel.doneWithReload()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,7 +147,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
||||||
if (grant) {
|
if (grant) {
|
||||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
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.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
@ -7,17 +7,19 @@ import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.databinding.DataBindingUtil
|
import androidx.databinding.DataBindingUtil
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
||||||
|
import org.oxycblt.auxio.music.MusicViewModel
|
||||||
import org.oxycblt.auxio.recycler.adapters.SongAdapter
|
import org.oxycblt.auxio.recycler.adapters.SongAdapter
|
||||||
import org.oxycblt.auxio.recycler.applyDivider
|
import org.oxycblt.auxio.recycler.applyDivider
|
||||||
import org.oxycblt.auxio.recycler.viewholders.ClickListener
|
import org.oxycblt.auxio.recycler.viewholders.ClickListener
|
||||||
|
|
||||||
class SongsFragment : Fragment() {
|
class SongsFragment : Fragment() {
|
||||||
|
|
||||||
private val songsModel: SongsViewModel by lazy {
|
private val musicModel: MusicViewModel by activityViewModels {
|
||||||
ViewModelProvider(this).get(SongsViewModel::class.java)
|
MusicViewModel.Factory(requireActivity().application)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -30,7 +32,7 @@ class SongsFragment : Fragment() {
|
||||||
)
|
)
|
||||||
|
|
||||||
binding.songRecycler.adapter = SongAdapter(
|
binding.songRecycler.adapter = SongAdapter(
|
||||||
songsModel.songs.value!!,
|
musicModel.songs.value!!,
|
||||||
ClickListener { song ->
|
ClickListener { song ->
|
||||||
Log.d(this::class.simpleName, song.name)
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
<layout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
tools:context=".MainActivity">
|
|
||||||
|
|
||||||
<androidx.fragment.app.FragmentContainerView
|
<data>
|
||||||
android:id="@+id/nav_host_fragment"
|
|
||||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
<variable
|
||||||
|
name="musicModel"
|
||||||
|
type="org.oxycblt.auxio.music.MusicViewModel" />
|
||||||
|
</data>
|
||||||
|
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
app:defaultNavHost="true"
|
android:animateLayoutChanges="true">
|
||||||
app:navGraph="@navigation/nav_main"
|
|
||||||
tools:ignore="FragmentTagUsage" />
|
<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>
|
</layout>
|
|
@ -8,8 +8,8 @@
|
||||||
<data>
|
<data>
|
||||||
|
|
||||||
<variable
|
<variable
|
||||||
name="loadingModel"
|
name="musicModel"
|
||||||
type="org.oxycblt.auxio.loading.LoadingViewModel" />
|
type="org.oxycblt.auxio.music.MusicViewModel" />
|
||||||
</data>
|
</data>
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.ConstraintLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
@ -69,7 +69,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fontFamily="@font/inter_semibold"
|
android:fontFamily="@font/inter_semibold"
|
||||||
android:onClick="@{() -> loadingModel.retry()}"
|
android:onClick="@{() -> musicModel.reload()}"
|
||||||
android:text="@string/label_retry"
|
android:text="@string/label_retry"
|
||||||
android:textColor="?attr/colorPrimary"
|
android:textColor="?attr/colorPrimary"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
@ -89,7 +89,7 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:fontFamily="@font/inter_semibold"
|
android:fontFamily="@font/inter_semibold"
|
||||||
android:onClick="@{() -> loadingModel.grant()}"
|
android:onClick="@{() -> musicModel.grant()}"
|
||||||
android:text="@string/label_grant"
|
android:text="@string/label_grant"
|
||||||
android:textColor="?attr/colorPrimary"
|
android:textColor="?attr/colorPrimary"
|
||||||
android:visibility="gone"
|
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_no_music">No music found.</string>
|
||||||
<string name="error_music_load_failed">Music loading failed.</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_retry">Retry</string>
|
||||||
<string name="label_grant">Grant</string>
|
<string name="label_grant">Grant</string>
|
||||||
|
|
Loading…
Reference in a new issue