all: add viewmodel contexts where useful
Make some ViewModel instances AndroidViewModels in order to make some code less insane. I don't like doing this, as I want to keep ViewModel instances clean of android things, but this just makes a lot of functionality easier to implement.
This commit is contained in:
parent
543a3ebffb
commit
107f7bee27
6 changed files with 71 additions and 52 deletions
|
@ -17,10 +17,10 @@
|
|||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.content.Context
|
||||
import android.app.Application
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
@ -40,6 +40,7 @@ import org.oxycblt.auxio.settings.SettingsManager
|
|||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.application
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
@ -51,7 +52,8 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* - Menu triggers for each fragment
|
||||
* @author OxygenCobalt
|
||||
*/
|
||||
class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||
class DetailViewModel(application: Application) :
|
||||
AndroidViewModel(application), MusicStore.Callback {
|
||||
data class DetailSong(
|
||||
val song: Song,
|
||||
val bitrateKbps: Int?,
|
||||
|
@ -109,11 +111,11 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
currentGenre.value?.let(::refreshGenreData)
|
||||
}
|
||||
|
||||
fun setSongId(context: Context, id: Long) {
|
||||
fun setSongId(id: Long) {
|
||||
if (_currentSong.value?.run { song.id } == id) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val song = requireNotNull(library.songs.find { it.id == id }) { "Invalid song id provided" }
|
||||
generateDetailSong(context, song)
|
||||
generateDetailSong(song)
|
||||
}
|
||||
|
||||
fun clearSong() {
|
||||
|
@ -152,14 +154,15 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
musicStore.addCallback(this)
|
||||
}
|
||||
|
||||
private fun generateDetailSong(context: Context, song: Song) {
|
||||
private fun generateDetailSong(song: Song) {
|
||||
viewModelScope.launch {
|
||||
_currentSong.value =
|
||||
withContext(Dispatchers.IO) {
|
||||
val extractor = MediaExtractor()
|
||||
|
||||
try {
|
||||
extractor.setDataSource(context, song.uri, emptyMap())
|
||||
@Suppress("BlockingMethodInNonBlockingContext")
|
||||
extractor.setDataSource(application, song.uri, emptyMap())
|
||||
} catch (e: Exception) {
|
||||
logW("Unable to extract song attributes.")
|
||||
logW(e.stackTraceToString())
|
||||
|
@ -250,14 +253,13 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
|
||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||
if (library != null) {
|
||||
// TODO: Add when we have a context
|
||||
// val song = currentSong.value
|
||||
// if (song != null) {
|
||||
// val newSong = library.sanitize(song.song)
|
||||
// if (newSong != null) {
|
||||
// generateDetailSong(newSong)
|
||||
// }
|
||||
// }
|
||||
val song = currentSong.value
|
||||
if (song != null) {
|
||||
val newSong = library.sanitize(song.song)
|
||||
if (newSong != null) {
|
||||
generateDetailSong(newSong)
|
||||
}
|
||||
}
|
||||
|
||||
val album = currentAlbum.value
|
||||
if (album != null) {
|
||||
|
|
|
@ -44,7 +44,7 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
|
||||
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
detailModel.setSongId(requireContext(), requireNotNull(arguments).getLong(ARG_ID))
|
||||
detailModel.setSongId(requireNotNull(arguments).getLong(ARG_ID))
|
||||
launch { detailModel.currentSong.collect(::updateSong) }
|
||||
}
|
||||
|
||||
|
|
|
@ -28,15 +28,12 @@ import androidx.core.view.postDelayed
|
|||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.FragmentSearchBinding
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.Indexer
|
||||
import org.oxycblt.auxio.music.IndexerViewModel
|
||||
import org.oxycblt.auxio.music.Music
|
||||
import org.oxycblt.auxio.music.MusicParent
|
||||
import org.oxycblt.auxio.music.Song
|
||||
|
@ -47,6 +44,7 @@ import org.oxycblt.auxio.ui.MenuItemListener
|
|||
import org.oxycblt.auxio.ui.NavigationViewModel
|
||||
import org.oxycblt.auxio.ui.ViewBindingFragment
|
||||
import org.oxycblt.auxio.ui.newMenu
|
||||
import org.oxycblt.auxio.util.androidViewModels
|
||||
import org.oxycblt.auxio.util.applySpans
|
||||
import org.oxycblt.auxio.util.getSystemServiceSafe
|
||||
import org.oxycblt.auxio.util.launch
|
||||
|
@ -61,10 +59,9 @@ class SearchFragment :
|
|||
MenuItemListener,
|
||||
Toolbar.OnMenuItemClickListener {
|
||||
// SearchViewModel is only scoped to this Fragment
|
||||
private val searchModel: SearchViewModel by viewModels()
|
||||
private val searchModel: SearchViewModel by androidViewModels()
|
||||
private val playbackModel: PlaybackViewModel by activityViewModels()
|
||||
private val navModel: NavigationViewModel by activityViewModels()
|
||||
private val indexerModel: IndexerViewModel by activityViewModels()
|
||||
|
||||
private val searchAdapter = SearchAdapter(this)
|
||||
private var imm: InputMethodManager? = null
|
||||
|
@ -87,7 +84,7 @@ class SearchFragment :
|
|||
binding.searchEditText.apply {
|
||||
addTextChangedListener { text ->
|
||||
// Run the search with the updated text as the query
|
||||
searchModel.search(context, text?.toString())
|
||||
searchModel.search(text?.toString())
|
||||
}
|
||||
|
||||
if (!launchedKeyboard) {
|
||||
|
@ -109,7 +106,6 @@ class SearchFragment :
|
|||
// --- VIEWMODEL SETUP ---
|
||||
|
||||
launch { searchModel.searchResults.collect(::updateResults) }
|
||||
launch { indexerModel.state.collect(::handleIndexerState) }
|
||||
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||
}
|
||||
|
||||
|
@ -124,7 +120,7 @@ class SearchFragment :
|
|||
R.id.submenu_filtering -> {}
|
||||
else -> {
|
||||
if (item.itemId != R.id.submenu_filtering) {
|
||||
searchModel.updateFilterModeWithId(requireContext(), item.itemId)
|
||||
searchModel.updateFilterModeWithId(item.itemId)
|
||||
item.isChecked = true
|
||||
}
|
||||
}
|
||||
|
@ -171,12 +167,6 @@ class SearchFragment :
|
|||
requireImm().hide()
|
||||
}
|
||||
|
||||
private fun handleIndexerState(state: Indexer.State?) {
|
||||
if (state is Indexer.State.Complete && state.response is Indexer.Response.Ok) {
|
||||
searchModel.refresh(requireContext())
|
||||
}
|
||||
}
|
||||
|
||||
private fun requireImm(): InputMethodManager {
|
||||
requireAttached()
|
||||
val instance = imm
|
||||
|
|
|
@ -17,8 +17,10 @@
|
|||
|
||||
package org.oxycblt.auxio.search
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.annotation.IdRes
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import java.text.Normalizer
|
||||
|
@ -33,16 +35,15 @@ import org.oxycblt.auxio.ui.DisplayMode
|
|||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.application
|
||||
import org.oxycblt.auxio.util.logD
|
||||
|
||||
/**
|
||||
* The [ViewModel] for search functionality
|
||||
* The [ViewModel] for search functionality.
|
||||
* @author OxygenCobalt
|
||||
*
|
||||
* TODO: Add a context to this ViewModel, not because I want to, but because it just makes the code
|
||||
* easier to work with.
|
||||
*/
|
||||
class SearchViewModel : ViewModel() {
|
||||
class SearchViewModel(application: Application) :
|
||||
AndroidViewModel(application), MusicStore.Callback {
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
|
@ -64,7 +65,7 @@ class SearchViewModel : ViewModel() {
|
|||
/**
|
||||
* Use [query] to perform a search of the music library. Will push results to [searchResults].
|
||||
*/
|
||||
fun search(context: Context, query: String?) {
|
||||
fun search(query: String?) {
|
||||
lastQuery = query
|
||||
|
||||
val library = musicStore.library
|
||||
|
@ -84,28 +85,28 @@ class SearchViewModel : ViewModel() {
|
|||
// Note: a filter mode of null means to not filter at all.
|
||||
|
||||
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ARTISTS) {
|
||||
library.artists.filterByOrNull(context, query)?.let { artists ->
|
||||
library.artists.filterByOrNull(query)?.let { artists ->
|
||||
results.add(Header(-1, R.string.lbl_artists))
|
||||
results.addAll(sort.artists(artists))
|
||||
}
|
||||
}
|
||||
|
||||
if (_filterMode == null || _filterMode == DisplayMode.SHOW_ALBUMS) {
|
||||
library.albums.filterByOrNull(context, query)?.let { albums ->
|
||||
library.albums.filterByOrNull(query)?.let { albums ->
|
||||
results.add(Header(-2, R.string.lbl_albums))
|
||||
results.addAll(sort.albums(albums))
|
||||
}
|
||||
}
|
||||
|
||||
if (_filterMode == null || _filterMode == DisplayMode.SHOW_GENRES) {
|
||||
library.genres.filterByOrNull(context, query)?.let { genres ->
|
||||
library.genres.filterByOrNull(query)?.let { genres ->
|
||||
results.add(Header(-3, R.string.lbl_genres))
|
||||
results.addAll(sort.genres(genres))
|
||||
}
|
||||
}
|
||||
|
||||
if (_filterMode == null || _filterMode == DisplayMode.SHOW_SONGS) {
|
||||
library.songs.filterByOrNull(context, query)?.let { songs ->
|
||||
library.songs.filterByOrNull(query)?.let { songs ->
|
||||
results.add(Header(-4, R.string.lbl_songs))
|
||||
results.addAll(sort.songs(songs))
|
||||
}
|
||||
|
@ -115,15 +116,10 @@ class SearchViewModel : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
/** Re-search the library using the last query. Will push results to [searchResults]. */
|
||||
fun refresh(context: Context) {
|
||||
search(context, lastQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current filter mode with a menu [id]. New value will be pushed to [filterMode].
|
||||
*/
|
||||
fun updateFilterModeWithId(context: Context, @IdRes id: Int) {
|
||||
fun updateFilterModeWithId(@IdRes id: Int) {
|
||||
_filterMode =
|
||||
when (id) {
|
||||
R.id.option_filter_songs -> DisplayMode.SHOW_SONGS
|
||||
|
@ -137,20 +133,20 @@ class SearchViewModel : ViewModel() {
|
|||
|
||||
settingsManager.searchFilterMode = _filterMode
|
||||
|
||||
refresh(context)
|
||||
search(lastQuery)
|
||||
}
|
||||
|
||||
/**
|
||||
* Shortcut that will run a ignoreCase filter on a list and only return a value if the resulting
|
||||
* list is empty.
|
||||
*/
|
||||
private fun <T : Music> List<T>.filterByOrNull(context: Context, value: String): List<T>? {
|
||||
private fun <T : Music> List<T>.filterByOrNull(value: String): List<T>? {
|
||||
val filtered = filter {
|
||||
// Compare normalized names, which are names with unicode characters that are
|
||||
// normalized to their non-unicode forms. This is just for quality-of-life,
|
||||
// and I hope it doesn't bork search functionality for other languages.
|
||||
it.resolveNameNormalized(context).contains(value, ignoreCase = true) ||
|
||||
it.resolveNameNormalized(context).contains(value, ignoreCase = true)
|
||||
it.resolveNameNormalized(application).contains(value, ignoreCase = true) ||
|
||||
it.resolveNameNormalized(application).contains(value, ignoreCase = true)
|
||||
}
|
||||
|
||||
return filtered.ifEmpty { null }
|
||||
|
@ -185,4 +181,16 @@ class SearchViewModel : ViewModel() {
|
|||
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||
if (library != null) {
|
||||
// Make sure our query is up to date with the music library.
|
||||
search(lastQuery)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
musicStore.removeCallback(this)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
|
||||
package org.oxycblt.auxio.util
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.database.Cursor
|
||||
|
@ -31,7 +32,11 @@ import androidx.core.graphics.Insets
|
|||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
|
@ -168,6 +173,20 @@ fun Fragment.launch(
|
|||
viewLifecycleOwner.lifecycleScope.launch { viewLifecycleOwner.repeatOnLifecycle(state, block) }
|
||||
}
|
||||
|
||||
fun Fragment.androidViewModelFactory() =
|
||||
ViewModelProvider.AndroidViewModelFactory(requireContext().applicationContext as Application)
|
||||
|
||||
inline fun <reified T : AndroidViewModel> Fragment.androidViewModels() =
|
||||
viewModels<T> { ViewModelProvider.AndroidViewModelFactory(requireActivity().application) }
|
||||
|
||||
inline fun <reified T : AndroidViewModel> Fragment.activityAndroidViewModels() =
|
||||
activityViewModels<T> {
|
||||
ViewModelProvider.AndroidViewModelFactory(requireActivity().application)
|
||||
}
|
||||
|
||||
val AndroidViewModel.application: Application
|
||||
get() = getApplication()
|
||||
|
||||
/**
|
||||
* Combines the called flow with the given flow and then collects them both into [block]. This is a
|
||||
* bit of a dumb hack with [combine], as when we have to combine flows, we often just want to call
|
||||
|
|
|
@ -14,6 +14,6 @@
|
|||
<string name="cdc_wav">Microsoft WAVE</string>
|
||||
|
||||
<!-- Note: These are stopgap measures until we make the path code rely on components! -->
|
||||
<string name="fmt_primary_path">Internal:%s</string>
|
||||
<string name="fmt_secondary_path">SDCARD:%s</string>
|
||||
<string name="fmt_primary_path">Internal/%s</string>
|
||||
<string name="fmt_secondary_path">SDCARD/%s</string>
|
||||
</resources>
|
Loading…
Reference in a new issue