diff --git a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt index 02b8fc909..d5b32e584 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/Detail.kt @@ -35,21 +35,13 @@ 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. + * 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. */ -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. - */ - data class Properties( - val bitrateKbps: Int?, - val sampleRateHz: Int?, - val resolvedMimeType: MimeType - ) -} +data class SongProperties( + val bitrateKbps: Int?, + val sampleRateHz: Int?, + val resolvedMimeType: MimeType +) diff --git a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt index d6a3666c7..378cfe15f 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -59,15 +59,15 @@ class DetailViewModel(application: Application) : // --- SONG --- - private val _currentSong = MutableStateFlow(null) - /** - * The current [DetailSong] to display. Null if there is nothing to show. - * - * TODO: De-couple Song and Properties? - */ - val currentSong: StateFlow + private val _currentSong = MutableStateFlow(null) + /** The current [Song] to display. Null if there is nothing to show. */ + val currentSong: StateFlow get() = _currentSong + private val _songProperties = MutableStateFlow(null) + /** The [SongProperties] of the currently shown [Song]. Null if not loaded yet. */ + val songProperties: StateFlow = _songProperties + // --- ALBUM --- private val _currentAlbum = MutableStateFlow(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(uid).also(::loadProperties) } /** @@ -202,7 +197,7 @@ class DetailViewModel(application: Application) : return } logD("Opening Album [uid: $uid]") - _currentAlbum.value = requireMusic(uid).also { refreshAlbumList(it) } + _currentAlbum.value = requireMusic(uid).also(::refreshAlbumList) } /** @@ -216,7 +211,7 @@ class DetailViewModel(application: Application) : return } logD("Opening Artist [uid: $uid]") - _currentArtist.value = requireMusic(uid).also { refreshArtistList(it) } + _currentArtist.value = requireMusic(uid).also(::refreshArtistList) } /** @@ -230,7 +225,7 @@ class DetailViewModel(application: Application) : return } logD("Opening Genre [uid: $uid]") - _currentGenre.value = requireMusic(uid).also { refreshGenreList(it) } + _currentGenre.value = requireMusic(uid).also(::refreshGenreList) } private fun 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) { diff --git a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt index e7444ab16..cf2d516d7 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -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() { 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() { } 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) }