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:
parent
0a18883a6a
commit
8d7aa7936b
8 changed files with 85 additions and 8 deletions
|
@ -69,6 +69,7 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
|
|
||||||
// -- VIEWMODEL SETUP ---
|
// -- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
launch { detailModel.currentAlbum.collect(::handleItemChange) }
|
||||||
launch { detailModel.albumData.collect(detailAdapter.data::submitList) }
|
launch { detailModel.albumData.collect(detailAdapter.data::submitList) }
|
||||||
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
||||||
|
@ -127,6 +128,12 @@ class AlbumDetailFragment : DetailFragment(), AlbumDetailAdapter.Listener {
|
||||||
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
|
unlikelyToBeNull(detailModel.currentAlbum.value).artist.id))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleItemChange(album: Album?) {
|
||||||
|
if (album == null) {
|
||||||
|
findNavController().navigateUp()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun handleNavigation(item: Music?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
when (item) {
|
when (item) {
|
||||||
|
|
|
@ -66,6 +66,7 @@ class ArtistDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
launch { detailModel.currentArtist.collect(::handleItemChange) }
|
||||||
launch { detailModel.artistData.collect(detailAdapter.data::submitList) }
|
launch { detailModel.artistData.collect(detailAdapter.data::submitList) }
|
||||||
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
val binding = requireBinding()
|
val binding = requireBinding()
|
||||||
|
|
||||||
|
|
|
@ -41,7 +41,7 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* - Menu triggers for each fragment
|
* - Menu triggers for each fragment
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel : ViewModel() {
|
class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
|
@ -116,6 +116,10 @@ class DetailViewModel : ViewModel() {
|
||||||
refreshGenreData(genre)
|
refreshGenreData(genre)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
musicStore.addCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
private fun refreshAlbumData(album: Album) {
|
private fun refreshAlbumData(album: Album) {
|
||||||
logD("Refreshing album data")
|
logD("Refreshing album data")
|
||||||
val data = mutableListOf<Item>(album)
|
val data = mutableListOf<Item>(album)
|
||||||
|
@ -157,4 +161,38 @@ class DetailViewModel : ViewModel() {
|
||||||
data.addAll(genreSort.songs(genre.songs))
|
data.addAll(genreSort.songs(genre.songs))
|
||||||
_genreData.value = data
|
_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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,6 +65,7 @@ class GenreDetailFragment : DetailFragment(), DetailAdapter.Listener {
|
||||||
|
|
||||||
// --- VIEWMODEL SETUP ---
|
// --- VIEWMODEL SETUP ---
|
||||||
|
|
||||||
|
launch { detailModel.currentGenre.collect(::handleItemChange) }
|
||||||
launch { detailModel.genreData.collect(detailAdapter.data::submitList) }
|
launch { detailModel.genreData.collect(detailAdapter.data::submitList) }
|
||||||
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
launch { navModel.exploreNavigationItem.collect(::handleNavigation) }
|
||||||
launch { playbackModel.song.collectWith(playbackModel.parent, ::updatePlayback) }
|
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 })
|
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?) {
|
private fun handleNavigation(item: Music?) {
|
||||||
when (item) {
|
when (item) {
|
||||||
is Song -> {
|
is Song -> {
|
||||||
|
|
|
@ -148,7 +148,8 @@ class Indexer {
|
||||||
// If we have canceled the loading process, we want to revert to a previous completion
|
// If we have canceled the loading process, we want to revert to a previous completion
|
||||||
// whenever possible to prevent state inconsistency.
|
// whenever possible to prevent state inconsistency.
|
||||||
val state =
|
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) {
|
for (callback in callbacks) {
|
||||||
callback.onIndexerStateChanged(state)
|
callback.onIndexerStateChanged(state)
|
||||||
|
@ -180,7 +181,8 @@ class Indexer {
|
||||||
emitIndexing(Indexing.Indeterminate, generation)
|
emitIndexing(Indexing.Indeterminate, generation)
|
||||||
|
|
||||||
// Establish the backend to use when initially loading songs.
|
// 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.R -> Api30MediaStoreBackend()
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend()
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q -> Api29MediaStoreBackend()
|
||||||
else -> Api21MediaStoreBackend()
|
else -> Api21MediaStoreBackend()
|
||||||
|
@ -229,7 +231,9 @@ class Indexer {
|
||||||
"Successfully queried media database " +
|
"Successfully queried media database " +
|
||||||
"in ${System.currentTimeMillis() - start}ms")
|
"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
|
// Deduplicate songs to prevent (most) deformed music clones
|
||||||
|
|
|
@ -77,6 +77,12 @@ class MusicStore private constructor() {
|
||||||
|
|
||||||
songs.find { it.fileName == displayName }
|
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. */
|
/** A callback for awaiting the loading of music. */
|
||||||
|
|
|
@ -21,6 +21,13 @@ import android.os.Build
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import org.oxycblt.auxio.util.logW
|
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) {
|
data class ExcludedDirectory(val volume: Volume, val relativePath: String) {
|
||||||
override fun toString(): String = "${volume}:$relativePath"
|
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)
|
val volume = Volume.fromString(split.getOrNull(0) ?: return null)
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q && volume is Volume.Secondary) {
|
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")
|
logW("Cannot use secondary volumes below API 29")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
|
@ -81,8 +81,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateAlbumCount(albums: List<Album>) {
|
private fun updateAlbumCount(albums: List<Album>) {
|
||||||
requireBinding().aboutAlbumCount.textSafe =
|
requireBinding().aboutAlbumCount.textSafe = getString(R.string.fmt_album_count, albums.size)
|
||||||
getString(R.string.fmt_album_count, albums.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateArtistCount(artists: List<Artist>) {
|
private fun updateArtistCount(artists: List<Artist>) {
|
||||||
|
@ -91,8 +90,7 @@ class AboutFragment : ViewBindingFragment<FragmentAboutBinding>() {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateGenreCount(genres: List<Genre>) {
|
private fun updateGenreCount(genres: List<Genre>) {
|
||||||
requireBinding().aboutGenreCount.textSafe =
|
requireBinding().aboutGenreCount.textSafe = getString(R.string.fmt_genre_count, genres.size)
|
||||||
getString(R.string.fmt_genre_count, genres.size)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Go through the process of opening a [link] in a browser. */
|
/** Go through the process of opening a [link] in a browser. */
|
||||||
|
|
Loading…
Reference in a new issue