detail: respond to automatic rescanning

Make the detail UIs respond to automatic rescanning events.

This currently has no effect, but is instead a preparation step for the
seamless addition of automatic rescanning.
This commit is contained in:
OxygenCobalt 2022-06-06 20:05:12 -06:00
parent 0a18883a6a
commit 8d7aa7936b
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
8 changed files with 85 additions and 8 deletions

View file

@ -69,6 +69,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
// -- VIEWMODEL SETUP ---
launch { detailModel.currentAlbum.collect(::handleItemChange) }
launch { detailModel.albumData.collect(detailAdapter.data::submitList) }
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
@ -127,6 +128,12 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
}
private fun handleItemChange(album: Album?) {
if (album == null) {
findNavController().navigateUp()
}
}
private fun handleNavigation(item: Music?) {
val binding = requireBinding()
when (item) {

View file

@ -66,6 +66,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
// --- VIEWMODEL SETUP ---
launch { detailModel.currentArtist.collect(::handleItemChange) }
launch { detailModel.artistData.collect(detailAdapter.data::submitList) }
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
@ -104,6 +105,12 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
})
}
private fun handleItemChange(artist: Artist?) {
if (artist == null) {
findNavController().navigateUp()
}
}
private fun handleNavigation(item: Music?) {
val binding = requireBinding()

View file

@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
* - Menu triggers for each fragment
* @author OxygenCobalt
*/
class DetailViewModel : ViewModel() {
class DetailViewModel : ViewModel(), MusicStore.Callback {
private val musicStore = MusicStore.getInstance()
private val settingsManager = SettingsManager.getInstance()
@ -116,6 +116,10 @@ class DetailViewModel : ViewModel() {
refreshGenreData(genre)
}
init {
musicStore.addCallback(this)
}
private fun refreshAlbumData(album: Album) {
logD("Refreshing album data")
val data = mutableListOf<Item>(album)
@ -157,4 +161,38 @@ class DetailViewModel : ViewModel() {
data.addAll(genreSort.songs(genre.songs))
_genreData.value = data
}
// --- CALLBACKS ---
override fun onLibraryChanged(library: MusicStore.Library?) {
if (library != null) {
val album = currentAlbum.value
if (album != null) {
val newAlbum = library.sanitize(album).also { _currentAlbum.value = it }
if (newAlbum != null) {
refreshAlbumData(newAlbum)
}
}
val artist = currentArtist.value
if (artist != null) {
val newArtist = library.sanitize(artist).also { _currentArtist.value = it }
if (newArtist != null) {
refreshArtistData(newArtist)
}
}
val genre = currentGenre.value
if (genre != null) {
val newGenre = library.sanitize(genre).also { _currentGenre.value = it }
if (newGenre != null) {
refreshGenreData(newGenre)
}
}
}
}
override fun onCleared() {
musicStore.removeCallback(this)
}
}

View file

@ -65,6 +65,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
// --- VIEWMODEL SETUP ---
launch { detailModel.currentGenre.collect(::handleItemChange) }
launch { detailModel.genreData.collect(detailAdapter.data::submitList) }
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
@ -101,6 +102,12 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
showItem = { it != R.id.option_sort_disc && it != R.id.option_sort_track })
}
private fun handleItemChange(genre: Genre?) {
if (genre == null) {
findNavController().navigateUp()
}
}
private fun handleNavigation(item: Music?) {
when (item) {
is Song -> {

View file

@ -148,7 +148,8 @@ class Indexer {
// If we have canceled the loading process, we want to revert to a previous completion
// whenever possible to prevent state inconsistency.
val state =
indexingState?.let { State.Indexing(it) } ?: lastResponse?.let { State.Complete(it) }
indexingState?.let { State.Indexing(it) }
?: lastResponse?.let { State.Complete(it) }
for (callback in callbacks) {
callback.onIndexerStateChanged(state)
@ -180,7 +181,8 @@ class Indexer {
emitIndexing(Indexing.Indeterminate, generation)
// Establish the backend to use when initially loading songs.
val mediaStoreBackend = when {
val mediaStoreBackend =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Api30MediaStoreBackend()
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend()
else -> Api21MediaStoreBackend()
@ -229,7 +231,9 @@ class Indexer {
"Successfully queried media database " +
"in ${System.currentTimeMillis() - start}ms")
backend.buildSongs(context, cursor) { indexing -> emitIndexing(indexing, generation) }
backend.buildSongs(context, cursor) { indexing ->
emitIndexing(indexing, generation)
}
}
// Deduplicate songs to prevent (most) deformed music clones

View file

@ -77,6 +77,12 @@ class MusicStore private constructor() {
songs.find { it.fileName == displayName }
}
/** "Sanitize" a music object from a previous library iteration. */
fun sanitize(song: Song) = songs.find { it.id == song.id }
fun sanitize(album: Album) = albums.find { it.id == album.id }
fun sanitize(artist: Artist) = artists.find { it.id == artist.id }
fun sanitize(genre: Genre) = genres.find { it.id == genre.id }
}
/** A callback for awaiting the loading of music. */

View file

@ -21,6 +21,13 @@ import android.os.Build
import java.io.File
import org.oxycblt.auxio.util.logW
/**
* Represents a directory excluded from the music loading process. This is a in-code
* representation of a typical document tree URI scheme, designed to not only provide
* support for external volumes, but also provide it in a way compatible with older
* android versions.
* @author OxygenCobalt
*/
data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
override fun toString(): String = "${volume}:$relativePath"
@ -50,6 +57,9 @@ data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
val volume = Volume.fromString(split.getOrNull(0) ?: return null)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && volume is Volume.Secondary) {
// While Android Q provides a stable way of accessing volumes, we can't trust
// that DATA provides a stable volume scheme on older versions, so external
// volumes are not supported.
logW("Cannot use secondary volumes below API 29")
return null
}

View file

@ -81,8 +81,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
}
private fun updateAlbumCount(albums: List<Album>) {
requireBinding().aboutAlbumCount.textSafe =
getString(R.string.fmt_album_count, albums.size)
requireBinding().aboutAlbumCount.textSafe = getString(R.string.fmt_album_count, albums.size)
}
private fun updateArtistCount(artists: List<Artist>) {
@ -91,8 +90,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
}
private fun updateGenreCount(genres: List<Genre>) {
requireBinding().aboutGenreCount.textSafe =
getString(R.string.fmt_genre_count, genres.size)
requireBinding().aboutGenreCount.textSafe = getString(R.string.fmt_genre_count, genres.size)
}
/** Go through the process of opening a [link] in a browser. */