Change music storage to shared object
Change MusicViewModel from a ViewModel to a shared object, so that it can be used everywhere in the app instead of just in fragments.
This commit is contained in:
parent
3376b57f8e
commit
96c30b3f93
15 changed files with 253 additions and 233 deletions
|
@ -14,7 +14,8 @@ 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.music.MusicViewModel
|
||||
import org.oxycblt.auxio.loading.LoadingViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.songs.SongsFragment
|
||||
import org.oxycblt.auxio.theme.accent
|
||||
import org.oxycblt.auxio.theme.getInactiveAlpha
|
||||
|
@ -22,8 +23,8 @@ import org.oxycblt.auxio.theme.getTransparentAccent
|
|||
import org.oxycblt.auxio.theme.toColor
|
||||
|
||||
class MainFragment : Fragment() {
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
private val loadingModel: LoadingViewModel by activityViewModels {
|
||||
LoadingViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
private val shownFragments = listOf(0, 1)
|
||||
|
@ -39,9 +40,9 @@ class MainFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentMainBinding.inflate(inflater)
|
||||
|
||||
// If musicModel was cleared while the app was closed [Likely due to Auxio being suspended
|
||||
// If the music was cleared while the app was closed [Likely due to Auxio being suspended
|
||||
// in the background], then navigate back to LoadingFragment to reload the music.
|
||||
if (musicModel.response.value == null) {
|
||||
if (MusicStore.getInstance().songs.isEmpty()) {
|
||||
findNavController().navigate(MainFragmentDirections.actionReturnToLoading())
|
||||
|
||||
return null
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.navigation.fragment.navArgs
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentAlbumDetailBinding
|
||||
import org.oxycblt.auxio.detail.adapters.DetailSongAdapter
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.theme.applyDivider
|
||||
import org.oxycblt.auxio.theme.disable
|
||||
|
@ -22,7 +22,6 @@ class AlbumDetailFragment : Fragment() {
|
|||
private val args: AlbumDetailFragmentArgs by navArgs()
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
|
@ -31,22 +30,21 @@ class AlbumDetailFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentAlbumDetailBinding.inflate(inflater)
|
||||
|
||||
// If DetailViewModel isn't already storing the album, get it from MusicViewModel
|
||||
// If DetailViewModel isn't already storing the album, get it from MusicStore
|
||||
// using the ID given by the navigation arguments.
|
||||
if (detailModel.currentAlbum.value == null ||
|
||||
detailModel.currentAlbum.value?.id != args.albumId
|
||||
) {
|
||||
val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
detailModel.updateAlbum(
|
||||
musicModel.albums.value!!.find {
|
||||
MusicStore.getInstance().albums.find {
|
||||
it.id == args.albumId
|
||||
}!!
|
||||
)
|
||||
}
|
||||
|
||||
val songAdapter = DetailSongAdapter {
|
||||
playbackModel.update(it, musicModel.songs.value!!)
|
||||
playbackModel.update(it)
|
||||
}
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
|
|
@ -11,7 +11,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.FragmentArtistDetailBinding
|
||||
import org.oxycblt.auxio.detail.adapters.DetailAlbumAdapter
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.theme.applyDivider
|
||||
import org.oxycblt.auxio.theme.disable
|
||||
|
||||
|
@ -30,15 +30,13 @@ class ArtistDetailFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentArtistDetailBinding.inflate(inflater)
|
||||
|
||||
// If DetailViewModel isn't already storing the artist, get it from MusicViewModel
|
||||
// If DetailViewModel isn't already storing the artist, get it from MusicStore
|
||||
// using the ID given by the navigation arguments
|
||||
if (detailModel.currentArtist.value == null ||
|
||||
detailModel.currentArtist.value?.id != args.artistId
|
||||
) {
|
||||
val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
detailModel.updateArtist(
|
||||
musicModel.artists.value!!.find {
|
||||
MusicStore.getInstance().artists.find {
|
||||
it.id == args.artistId
|
||||
}!!
|
||||
)
|
||||
|
|
|
@ -11,7 +11,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.databinding.FragmentGenreDetailBinding
|
||||
import org.oxycblt.auxio.detail.adapters.DetailArtistAdapter
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.theme.applyDivider
|
||||
import org.oxycblt.auxio.theme.disable
|
||||
|
||||
|
@ -27,15 +27,13 @@ class GenreDetailFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentGenreDetailBinding.inflate(inflater)
|
||||
|
||||
// If DetailViewModel isn't already storing the genre, get it from MusicViewModel
|
||||
// If DetailViewModel isn't already storing the genre, get it from MusicStore
|
||||
// using the ID given by the navigation arguments
|
||||
if (detailModel.currentGenre.value == null ||
|
||||
detailModel.currentGenre.value?.id != args.genreId
|
||||
) {
|
||||
val musicModel: MusicViewModel by activityViewModels()
|
||||
|
||||
detailModel.updateGenre(
|
||||
musicModel.genres.value!!.find {
|
||||
MusicStore.getInstance().genres.find {
|
||||
it.id == args.genreId
|
||||
}!!
|
||||
)
|
||||
|
|
|
@ -23,10 +23,9 @@ import org.oxycblt.auxio.music.Album
|
|||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.recycler.ShowMode
|
||||
import org.oxycblt.auxio.theme.applyColor
|
||||
import org.oxycblt.auxio.theme.applyDivider
|
||||
import org.oxycblt.auxio.theme.resolveAttr
|
||||
|
@ -34,10 +33,6 @@ import org.oxycblt.auxio.theme.resolveAttr
|
|||
// A Fragment to show all the music in the Library.
|
||||
class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
||||
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
private val libraryModel: LibraryViewModel by activityViewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
|
@ -48,6 +43,8 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
): View? {
|
||||
val binding = FragmentLibraryBinding.inflate(inflater)
|
||||
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
val libraryAdapter = LibraryAdapter(libraryModel.showMode.value!!) {
|
||||
navToItem(it)
|
||||
}
|
||||
|
@ -137,13 +134,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
// Update the adapter with the new data
|
||||
libraryAdapter.updateData(
|
||||
mode.getSortedBaseModelList(
|
||||
when (libraryModel.showMode.value) {
|
||||
ShowMode.SHOW_GENRES -> musicModel.genres.value!!
|
||||
ShowMode.SHOW_ARTISTS -> musicModel.artists.value!!
|
||||
ShowMode.SHOW_ALBUMS -> musicModel.albums.value!!
|
||||
|
||||
else -> musicModel.artists.value!!
|
||||
}
|
||||
musicStore.getListForShowMode(libraryModel.showMode.value!!)
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -179,7 +170,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
override fun onQueryTextSubmit(query: String): Boolean = false
|
||||
|
||||
override fun onQueryTextChange(query: String): Boolean {
|
||||
libraryModel.updateSearchQuery(query, musicModel)
|
||||
libraryModel.updateSearchQuery(query)
|
||||
|
||||
return false
|
||||
}
|
||||
|
@ -190,7 +181,7 @@ class LibraryFragment : Fragment(), SearchView.OnQueryTextListener {
|
|||
// If the item is a song [That was selected through search], then update the playback
|
||||
// to that song instead of doing any navigation
|
||||
if (baseModel is Song) {
|
||||
playbackModel.update(baseModel, musicModel.songs.value!!)
|
||||
playbackModel.update(baseModel)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@ import kotlinx.coroutines.launch
|
|||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.BaseModel
|
||||
import org.oxycblt.auxio.music.Header
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.recycler.ShowMode
|
||||
import org.oxycblt.auxio.recycler.SortMode
|
||||
|
||||
|
@ -44,7 +44,7 @@ class LibraryViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
fun updateSearchQuery(query: String, musicModel: MusicViewModel) {
|
||||
fun updateSearchQuery(query: String) {
|
||||
// Don't bother if the query is blank.
|
||||
if (query == "") {
|
||||
resetQuery()
|
||||
|
@ -52,16 +52,17 @@ class LibraryViewModel : ViewModel() {
|
|||
return
|
||||
}
|
||||
|
||||
// Search MusicViewModel for all the items [Artists, Albums, Songs] that contain
|
||||
// Search MusicStore for all the items [Artists, Albums, Songs] that contain
|
||||
// the query, and update the LiveData with those items. This is done on a separate
|
||||
// thread as it can be a very long operation for large music libraries.
|
||||
viewModelScope.launch {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
val combined = mutableListOf<BaseModel>()
|
||||
val children = showMode.value!!.getChildren()
|
||||
|
||||
// If the Library ShowMode supports it, include artists / genres in the search.
|
||||
if (children.contains(ShowMode.SHOW_GENRES)) {
|
||||
val genres = musicModel.genres.value!!.filter { it.name.contains(query, true) }
|
||||
val genres = musicStore.genres.filter { it.name.contains(query, true) }
|
||||
|
||||
if (genres.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_GENRES.constant))
|
||||
|
@ -70,7 +71,7 @@ class LibraryViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
if (children.contains(ShowMode.SHOW_ARTISTS)) {
|
||||
val artists = musicModel.artists.value!!.filter { it.name.contains(query, true) }
|
||||
val artists = musicStore.artists.filter { it.name.contains(query, true) }
|
||||
|
||||
if (artists.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_ARTISTS.constant))
|
||||
|
@ -79,14 +80,14 @@ class LibraryViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
// Albums & Songs are always included.
|
||||
val albums = musicModel.albums.value!!.filter { it.name.contains(query, true) }
|
||||
val albums = musicStore.albums.filter { it.name.contains(query, true) }
|
||||
|
||||
if (albums.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_ALBUMS.constant))
|
||||
combined.addAll(albums)
|
||||
}
|
||||
|
||||
val songs = musicModel.songs.value!!.filter { it.name.contains(query, true) }
|
||||
val songs = musicStore.songs.filter { it.name.contains(query, true) }
|
||||
|
||||
if (songs.isNotEmpty()) {
|
||||
combined.add(Header(id = ShowMode.SHOW_SONGS.constant))
|
||||
|
|
|
@ -14,13 +14,12 @@ import androidx.fragment.app.activityViewModels
|
|||
import androidx.navigation.fragment.findNavController
|
||||
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 musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
private val loadingModel: LoadingViewModel by activityViewModels {
|
||||
LoadingViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -39,18 +38,18 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
if (granted) {
|
||||
returnToLoading(binding)
|
||||
|
||||
musicModel.reload()
|
||||
loadingModel.reload()
|
||||
}
|
||||
}
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.lifecycleOwner = this
|
||||
binding.musicModel = musicModel
|
||||
binding.loadingModel = loadingModel
|
||||
|
||||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
musicModel.response.observe(viewLifecycleOwner) {
|
||||
loadingModel.response.observe(viewLifecycleOwner) {
|
||||
if (it == MusicLoaderResponse.DONE) {
|
||||
findNavController().navigate(
|
||||
LoadingFragmentDirections.actionToMain()
|
||||
|
@ -69,17 +68,17 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
}
|
||||
}
|
||||
|
||||
musicModel.doReload.observe(viewLifecycleOwner) {
|
||||
loadingModel.doReload.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
returnToLoading(binding)
|
||||
musicModel.doneWithReload()
|
||||
loadingModel.doneWithReload()
|
||||
}
|
||||
}
|
||||
|
||||
musicModel.doGrant.observe(viewLifecycleOwner) {
|
||||
loadingModel.doGrant.observe(viewLifecycleOwner) {
|
||||
if (it) {
|
||||
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
musicModel.doneWithGrant()
|
||||
loadingModel.doneWithGrant()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,7 +89,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
binding.loadingGrantButton.visibility = View.VISIBLE
|
||||
binding.loadingErrorText.text = getString(R.string.error_no_perms)
|
||||
} else {
|
||||
musicModel.go()
|
||||
loadingModel.go()
|
||||
}
|
||||
|
||||
Log.d(this::class.simpleName, "Fragment created.")
|
||||
|
@ -109,7 +108,7 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
|
|||
) == PackageManager.PERMISSION_DENIED
|
||||
}
|
||||
|
||||
// Remove the loading ui_indicator and show the error groups
|
||||
// Remove the loading indicator and show the error groups
|
||||
private fun showError(binding: FragmentLoadingBinding) {
|
||||
binding.loadingBar.visibility = View.GONE
|
||||
binding.loadingErrorIcon.visibility = View.VISIBLE
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
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.MusicStore
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
|
||||
class LoadingViewModel(private val app: Application) : ViewModel() {
|
||||
// Coroutine
|
||||
private val loadingJob = Job()
|
||||
private val ioScope = CoroutineScope(
|
||||
loadingJob + Dispatchers.IO
|
||||
)
|
||||
|
||||
// UI control
|
||||
private val mResponse = MutableLiveData<MusicLoaderResponse>()
|
||||
val response: LiveData<MusicLoaderResponse> get() = mResponse
|
||||
|
||||
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 reload() for all other loads.
|
||||
fun go() {
|
||||
if (!started) {
|
||||
started = true
|
||||
doLoad()
|
||||
}
|
||||
}
|
||||
|
||||
private fun doLoad() {
|
||||
ioScope.launch {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
val response = musicStore.load(app)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
mResponse.value = response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UI communication functions
|
||||
// LoadingFragment uses these so that button presses can update the ViewModel.
|
||||
// all doneWithX functions are to reset the value so that LoadingFragment doesn't
|
||||
// repeat commands if the view is recreated.
|
||||
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(LoadingViewModel::class.java)) {
|
||||
return LoadingViewModel(application) as T
|
||||
}
|
||||
|
||||
throw IllegalArgumentException("Unknown ViewModel class.")
|
||||
}
|
||||
}
|
||||
}
|
100
app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt
Normal file
100
app/src/main/java/org/oxycblt/auxio/music/MusicStore.kt
Normal file
|
@ -0,0 +1,100 @@
|
|||
package org.oxycblt.auxio.music
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.processing.MusicLoader
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
import org.oxycblt.auxio.music.processing.MusicSorter
|
||||
import org.oxycblt.auxio.recycler.ShowMode
|
||||
|
||||
// Storage for Music Data. Only use getInstance() to access this object.
|
||||
class MusicStore private constructor() {
|
||||
private var mGenres = listOf<Genre>()
|
||||
val genres: List<Genre> get() = mGenres
|
||||
|
||||
private var mArtists = listOf<Artist>()
|
||||
val artists: List<Artist> get() = mArtists
|
||||
|
||||
private var mAlbums = listOf<Album>()
|
||||
val albums: List<Album> get() = mAlbums
|
||||
|
||||
private var mSongs = listOf<Song>()
|
||||
val songs: List<Song> get() = mSongs
|
||||
|
||||
suspend fun load(app: Application): MusicLoaderResponse {
|
||||
Log.i(this::class.simpleName, "Starting initial music load...")
|
||||
|
||||
val start = System.currentTimeMillis()
|
||||
|
||||
// Get the placeholder strings, which are used by MusicLoader & MusicSorter for
|
||||
// any music that doesn't have metadata.
|
||||
val genrePlaceholder = app.getString(R.string.placeholder_genre)
|
||||
val artistPlaceholder = app.getString(R.string.placeholder_artist)
|
||||
val albumPlaceholder = app.getString(R.string.placeholder_album)
|
||||
|
||||
val loader = MusicLoader(
|
||||
app.contentResolver,
|
||||
|
||||
genrePlaceholder,
|
||||
artistPlaceholder,
|
||||
albumPlaceholder
|
||||
)
|
||||
|
||||
if (loader.response == MusicLoaderResponse.DONE) {
|
||||
// If the loading succeeds, then sort the songs and update the value
|
||||
val sorter = MusicSorter(
|
||||
loader.genres,
|
||||
loader.artists,
|
||||
loader.albums,
|
||||
loader.songs,
|
||||
|
||||
genrePlaceholder,
|
||||
artistPlaceholder,
|
||||
albumPlaceholder
|
||||
)
|
||||
|
||||
mSongs = sorter.songs.toList()
|
||||
mAlbums = sorter.albums.toList()
|
||||
mArtists = sorter.artists.toList()
|
||||
mGenres = sorter.genres.toList()
|
||||
|
||||
val elapsed = System.currentTimeMillis() - start
|
||||
|
||||
Log.i(
|
||||
this::class.simpleName,
|
||||
"Music load completed successfully in ${elapsed}ms."
|
||||
)
|
||||
}
|
||||
|
||||
return loader.response
|
||||
}
|
||||
|
||||
fun getListForShowMode(showMode: ShowMode): List<BaseModel> {
|
||||
return when (showMode) {
|
||||
ShowMode.SHOW_GENRES -> mGenres
|
||||
ShowMode.SHOW_ARTISTS -> mArtists
|
||||
ShowMode.SHOW_ALBUMS -> mAlbums
|
||||
ShowMode.SHOW_SONGS -> mSongs
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: MusicStore? = null
|
||||
|
||||
fun getInstance(): MusicStore {
|
||||
val currentInstance = INSTANCE
|
||||
|
||||
if (currentInstance != null) {
|
||||
return currentInstance
|
||||
}
|
||||
|
||||
synchronized(this) {
|
||||
val newInstance = MusicStore()
|
||||
INSTANCE = newInstance
|
||||
return newInstance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
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.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.music.processing.MusicLoader
|
||||
import org.oxycblt.auxio.music.processing.MusicLoaderResponse
|
||||
import org.oxycblt.auxio.music.processing.MusicSorter
|
||||
|
||||
// ViewModel for music storage.
|
||||
// TODO: Move genre usage to songs [If there's a way to find songs without a genre]
|
||||
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 reload() 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()
|
||||
|
||||
// Get the placeholder strings, which are used by MusicLoader & MusicSorter for
|
||||
// any music that doesn't have metadata.
|
||||
val genrePlaceholder = app.getString(R.string.placeholder_genre)
|
||||
val artistPlaceholder = app.getString(R.string.placeholder_artist)
|
||||
val albumPlaceholder = app.getString(R.string.placeholder_album)
|
||||
|
||||
val loader = MusicLoader(
|
||||
app.contentResolver,
|
||||
|
||||
genrePlaceholder,
|
||||
artistPlaceholder,
|
||||
albumPlaceholder
|
||||
)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (loader.response == MusicLoaderResponse.DONE) {
|
||||
// If the loading succeeds, then sort the songs and update the value
|
||||
val sorter = MusicSorter(
|
||||
loader.genres,
|
||||
loader.artists,
|
||||
loader.albums,
|
||||
loader.songs,
|
||||
|
||||
genrePlaceholder,
|
||||
artistPlaceholder,
|
||||
albumPlaceholder
|
||||
)
|
||||
|
||||
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
|
||||
// LoadingFragment uses these so that button presses can update the ViewModel.
|
||||
// all doneWithX functions are to reset the value so that LoadingFragment doesn't
|
||||
// repeat commands if the view is recreated.
|
||||
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.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -13,14 +13,9 @@ import androidx.navigation.fragment.findNavController
|
|||
import org.oxycblt.auxio.MainFragmentDirections
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentCompactPlaybackBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import kotlin.time.seconds
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
|
||||
class CompactPlaybackFragment : Fragment() {
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -44,7 +39,7 @@ class CompactPlaybackFragment : Fragment() {
|
|||
|
||||
// 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.song = MusicStore.getInstance().songs[0]
|
||||
binding.playbackModel = playbackModel
|
||||
binding.root.visibility = View.GONE
|
||||
|
||||
|
|
|
@ -78,12 +78,9 @@ class PlaybackFragment : Fragment(), SeekBar.OnSeekBarChangeListener {
|
|||
binding.playbackSkipPrev.disable(requireContext())
|
||||
}
|
||||
|
||||
Log.d(this::class.simpleName, it.toString())
|
||||
|
||||
if (it < playbackModel.queue.value!!.lastIndex) {
|
||||
binding.playbackSkipNext.enable(requireContext())
|
||||
} else {
|
||||
Log.d(this::class.simpleName, "Fucking stupid retard.")
|
||||
binding.playbackSkipNext.disable(requireContext())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.lifecycle.ViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.music.toDuration
|
||||
|
||||
|
@ -40,11 +41,13 @@ class PlaybackViewModel : ViewModel() {
|
|||
}
|
||||
|
||||
// Update the current song while changing the queue to All Songs.
|
||||
fun update(song: Song, allSongs: List<Song>) {
|
||||
fun update(song: Song) {
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
updatePlayback(song)
|
||||
|
||||
mQueue.value = allSongs.toMutableList()
|
||||
mCurrentIndex.value = allSongs.indexOf(song)
|
||||
mQueue.value = musicStore.songs.toMutableList()
|
||||
mCurrentIndex.value = musicStore.songs.indexOf(song)
|
||||
}
|
||||
|
||||
private fun updatePlayback(song: Song) {
|
||||
|
|
|
@ -8,15 +8,11 @@ import android.view.ViewGroup
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.oxycblt.auxio.databinding.FragmentSongsBinding
|
||||
import org.oxycblt.auxio.music.MusicViewModel
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.playback.PlaybackViewModel
|
||||
import org.oxycblt.auxio.theme.applyDivider
|
||||
|
||||
class SongsFragment : Fragment() {
|
||||
private val musicModel: MusicViewModel by activityViewModels {
|
||||
MusicViewModel.Factory(requireActivity().application)
|
||||
}
|
||||
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateView(
|
||||
|
@ -26,14 +22,16 @@ class SongsFragment : Fragment() {
|
|||
): View? {
|
||||
val binding = FragmentSongsBinding.inflate(inflater)
|
||||
|
||||
val musicStore = MusicStore.getInstance()
|
||||
|
||||
// TODO: Add option to search songs if LibraryFragment isn't enabled
|
||||
// TODO: Maybe add fast scrolling or sorting
|
||||
|
||||
// --- UI SETUP ---
|
||||
|
||||
binding.songRecycler.apply {
|
||||
adapter = SongAdapter(musicModel.songs.value!!) {
|
||||
playbackModel.update(it, musicModel.songs.value!!)
|
||||
adapter = SongAdapter(musicStore.songs) {
|
||||
playbackModel.update(it)
|
||||
}
|
||||
applyDivider()
|
||||
setHasFixedSize(true)
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
<data>
|
||||
|
||||
<variable
|
||||
name="musicModel"
|
||||
type="org.oxycblt.auxio.music.MusicViewModel" />
|
||||
name="loadingModel"
|
||||
type="org.oxycblt.auxio.loading.LoadingViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
@ -66,7 +66,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:onClick="@{() -> musicModel.reload()}"
|
||||
android:onClick="@{() -> loadingModel.reload()}"
|
||||
android:text="@string/label_retry"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
|
@ -85,7 +85,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="@font/inter_semibold"
|
||||
android:onClick="@{() -> musicModel.grant()}"
|
||||
android:onClick="@{() -> loadingModel.grant()}"
|
||||
android:text="@string/label_grant"
|
||||
android:textColor="?attr/colorPrimary"
|
||||
android:visibility="gone"
|
||||
|
|
Loading…
Reference in a new issue