detail: decouple detail song and properties
Decouple DetailSong into two fields for the current song and current properties. Combining them was a technical decision that no longer makes sense, and separating them makes life much easier.
This commit is contained in:
parent
b103eb4749
commit
9ab729a069
3 changed files with 43 additions and 55 deletions
|
@ -34,22 +34,14 @@ data class SortHeader(@StringRes val titleRes: Int) : Item
|
|||
*/
|
||||
data class DiscHeader(val disc: Int) : Item
|
||||
|
||||
/**
|
||||
* A [Song] extension that adds information about it's file properties.
|
||||
* @param song The internal song
|
||||
* @param properties The properties of the song file. Null if parsing is ongoing.
|
||||
*/
|
||||
data class DetailSong(val song: Song, val properties: Properties?) {
|
||||
/**
|
||||
* The properties of a [Song]'s file.
|
||||
* @param bitrateKbps The bit rate, in kilobytes-per-second. Null if it could not be parsed.
|
||||
* @param sampleRateHz The sample rate, in hertz.
|
||||
* @param resolvedMimeType The known mime type of the [Song] after it's file format was
|
||||
* determined.
|
||||
* @param resolvedMimeType The known mime type of the [Song] after it's file format was determined.
|
||||
*/
|
||||
data class Properties(
|
||||
data class SongProperties(
|
||||
val bitrateKbps: Int?,
|
||||
val sampleRateHz: Int?,
|
||||
val resolvedMimeType: MimeType
|
||||
)
|
||||
}
|
||||
|
|
|
@ -59,15 +59,15 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
// --- SONG ---
|
||||
|
||||
private val _currentSong = MutableStateFlow<DetailSong?>(null)
|
||||
/**
|
||||
* The current [DetailSong] to display. Null if there is nothing to show.
|
||||
*
|
||||
* TODO: De-couple Song and Properties?
|
||||
*/
|
||||
val currentSong: StateFlow<DetailSong?>
|
||||
private val _currentSong = MutableStateFlow<Song?>(null)
|
||||
/** The current [Song] to display. Null if there is nothing to show. */
|
||||
val currentSong: StateFlow<Song?>
|
||||
get() = _currentSong
|
||||
|
||||
private val _songProperties = MutableStateFlow<SongProperties?>(null)
|
||||
/** The [SongProperties] of the currently shown [Song]. Null if not loaded yet. */
|
||||
val songProperties: StateFlow<SongProperties?> = _songProperties
|
||||
|
||||
// --- ALBUM ---
|
||||
|
||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||
|
@ -149,13 +149,8 @@ class DetailViewModel(application: Application) :
|
|||
|
||||
val song = currentSong.value
|
||||
if (song != null) {
|
||||
val newSong = library.sanitize(song.song)
|
||||
if (newSong != null) {
|
||||
loadDetailSong(newSong)
|
||||
} else {
|
||||
_currentSong.value = null
|
||||
}
|
||||
logD("Updated song to $newSong")
|
||||
_currentSong.value = library.sanitize(song)?.also(::loadProperties)
|
||||
logD("Updated song to ${currentSong.value}")
|
||||
}
|
||||
|
||||
val album = currentAlbum.value
|
||||
|
@ -183,12 +178,12 @@ class DetailViewModel(application: Application) :
|
|||
* @param uid The UID of the [Song] to load. Must be valid.
|
||||
*/
|
||||
fun setSongUid(uid: Music.UID) {
|
||||
if (_currentSong.value?.run { song.uid } == uid) {
|
||||
if (_currentSong.value?.uid == uid) {
|
||||
// Nothing to do.
|
||||
return
|
||||
}
|
||||
logD("Opening Song [uid: $uid]")
|
||||
loadDetailSong(requireMusic(uid))
|
||||
_currentSong.value = requireMusic<Song>(uid).also(::loadProperties)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -202,7 +197,7 @@ class DetailViewModel(application: Application) :
|
|||
return
|
||||
}
|
||||
logD("Opening Album [uid: $uid]")
|
||||
_currentAlbum.value = requireMusic<Album>(uid).also { refreshAlbumList(it) }
|
||||
_currentAlbum.value = requireMusic<Album>(uid).also(::refreshAlbumList)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -216,7 +211,7 @@ class DetailViewModel(application: Application) :
|
|||
return
|
||||
}
|
||||
logD("Opening Artist [uid: $uid]")
|
||||
_currentArtist.value = requireMusic<Artist>(uid).also { refreshArtistList(it) }
|
||||
_currentArtist.value = requireMusic<Artist>(uid).also(::refreshArtistList)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -230,7 +225,7 @@ class DetailViewModel(application: Application) :
|
|||
return
|
||||
}
|
||||
logD("Opening Genre [uid: $uid]")
|
||||
_currentGenre.value = requireMusic<Genre>(uid).also { refreshGenreList(it) }
|
||||
_currentGenre.value = requireMusic<Genre>(uid).also(::refreshGenreList)
|
||||
}
|
||||
|
||||
private fun <T : Music> requireMusic(uid: Music.UID): T =
|
||||
|
@ -240,19 +235,19 @@ class DetailViewModel(application: Application) :
|
|||
* Start a new job to load a [DetailSong] based on the properties of the given [Song]'s file.
|
||||
* @param song The song to load.
|
||||
*/
|
||||
private fun loadDetailSong(song: Song) {
|
||||
private fun loadProperties(song: Song) {
|
||||
// Clear any previous job in order to avoid stale data from appearing in the UI.
|
||||
currentSongJob?.cancel()
|
||||
_currentSong.value = DetailSong(song, null)
|
||||
_songProperties.value = null
|
||||
currentSongJob =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val info = loadProperties(song)
|
||||
val properties = this@DetailViewModel.loadPropertiesImpl(song)
|
||||
yield()
|
||||
_currentSong.value = DetailSong(song, info)
|
||||
_songProperties.value = properties
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadProperties(song: Song): DetailSong.Properties {
|
||||
private fun loadPropertiesImpl(song: Song): SongProperties {
|
||||
// While we would use ExoPlayer to extract this information, it doesn't support
|
||||
// common data like bit rate in progressive data sources due to there being no
|
||||
// demand. Thus, we are stuck with the inferior OS-provided MediaExtractor.
|
||||
|
@ -266,7 +261,7 @@ class DetailViewModel(application: Application) :
|
|||
// that we can show.
|
||||
logW("Unable to extract song attributes.")
|
||||
logW(e.stackTraceToString())
|
||||
return DetailSong.Properties(null, null, song.mimeType)
|
||||
return SongProperties(null, null, song.mimeType)
|
||||
}
|
||||
|
||||
// Get the first track from the extractor (This is basically always the only
|
||||
|
@ -310,7 +305,7 @@ class DetailViewModel(application: Application) :
|
|||
MimeType(song.mimeType.fromExtension, formatMimeType)
|
||||
}
|
||||
|
||||
return DetailSong.Properties(bitrate, sampleRate, resolvedMimeType)
|
||||
return SongProperties(bitrate, sampleRate, resolvedMimeType)
|
||||
}
|
||||
|
||||
private fun refreshAlbumList(album: Album) {
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.navigation.fragment.findNavController
|
|||
import androidx.navigation.fragment.navArgs
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.playback.formatDurationMs
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.androidActivityViewModels
|
||||
|
@ -53,10 +54,10 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
super.onBindingCreated(binding, savedInstanceState)
|
||||
// DetailViewModel handles most initialization from the navigation argument.
|
||||
detailModel.setSongUid(args.itemUid)
|
||||
collectImmediately(detailModel.currentSong, ::updateSong)
|
||||
collectImmediately(detailModel.currentSong, detailModel.songProperties, ::updateSong)
|
||||
}
|
||||
|
||||
private fun updateSong(song: DetailSong?) {
|
||||
private fun updateSong(song: Song?, properties: SongProperties?) {
|
||||
if (song == null) {
|
||||
// Song we were showing no longer exists.
|
||||
findNavController().navigateUp()
|
||||
|
@ -64,28 +65,28 @@ class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
|||
}
|
||||
|
||||
val binding = requireBinding()
|
||||
if (song.properties != null) {
|
||||
if (properties != null) {
|
||||
// Finished loading Song properties, populate and show the list of Song information.
|
||||
binding.detailLoading.isInvisible = true
|
||||
binding.detailContainer.isInvisible = false
|
||||
|
||||
val context = requireContext()
|
||||
binding.detailFileName.setText(song.song.path.name)
|
||||
binding.detailRelativeDir.setText(song.song.path.parent.resolveName(context))
|
||||
binding.detailFormat.setText(song.properties.resolvedMimeType.resolveName(context))
|
||||
binding.detailSize.setText(Formatter.formatFileSize(context, song.song.size))
|
||||
binding.detailDuration.setText(song.song.durationMs.formatDurationMs(true))
|
||||
binding.detailFileName.setText(song.path.name)
|
||||
binding.detailRelativeDir.setText(song.path.parent.resolveName(context))
|
||||
binding.detailFormat.setText(properties.resolvedMimeType.resolveName(context))
|
||||
binding.detailSize.setText(Formatter.formatFileSize(context, song.size))
|
||||
binding.detailDuration.setText(song.durationMs.formatDurationMs(true))
|
||||
|
||||
if (song.properties.bitrateKbps != null) {
|
||||
if (properties.bitrateKbps != null) {
|
||||
binding.detailBitrate.setText(
|
||||
getString(R.string.fmt_bitrate, song.properties.bitrateKbps))
|
||||
getString(R.string.fmt_bitrate, properties.bitrateKbps))
|
||||
} else {
|
||||
binding.detailBitrate.setText(R.string.def_bitrate)
|
||||
}
|
||||
|
||||
if (song.properties.sampleRateHz != null) {
|
||||
if (properties.sampleRateHz != null) {
|
||||
binding.detailSampleRate.setText(
|
||||
getString(R.string.fmt_sample_rate, song.properties.sampleRateHz))
|
||||
getString(R.string.fmt_sample_rate, properties.sampleRateHz))
|
||||
} else {
|
||||
binding.detailSampleRate.setText(R.string.def_sample_rate)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue