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 6ec1e2611..c15a66ed4 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/DetailViewModel.kt @@ -15,7 +15,7 @@ * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ - + package org.oxycblt.auxio.detail import androidx.lifecycle.ViewModel @@ -30,6 +30,7 @@ import org.oxycblt.auxio.R import org.oxycblt.auxio.detail.list.DiscDivider import org.oxycblt.auxio.detail.list.DiscHeader import org.oxycblt.auxio.detail.list.EditHeader +import org.oxycblt.auxio.detail.list.SongProperty import org.oxycblt.auxio.detail.list.SortHeader import org.oxycblt.auxio.list.BasicHeader import org.oxycblt.auxio.list.Item @@ -42,6 +43,7 @@ import org.oxycblt.auxio.music.MusicRepository import org.oxycblt.auxio.music.MusicType import org.oxycblt.auxio.playback.PlaySong import org.oxycblt.auxio.playback.PlaybackSettings +import org.oxycblt.auxio.playback.formatDurationMs import org.oxycblt.auxio.util.Event import org.oxycblt.auxio.util.MutableEvent import org.oxycblt.auxio.util.unlikelyToBeNull @@ -70,6 +72,7 @@ constructor( detailGeneratorFactory: DetailGenerator.Factory ) : ViewModel(), DetailGenerator.Invalidator { private val _toShow = MutableEvent() + /** * A [Show] command that is awaiting a view capable of responding to it. Null if none currently. */ @@ -78,26 +81,34 @@ constructor( // --- SONG --- - private var currentSongJob: Job? = null - private val _currentSong = MutableStateFlow(null) + /** The current [Song] to display. Null if there is nothing to show. */ val currentSong: StateFlow get() = _currentSong + private val _currentSongProperties = MutableStateFlow>(listOf()) + + /** The current properties of [currentSong]. Empty if nothing to show. */ + val currentSongProperties: StateFlow> + get() = _currentSongProperties + // --- ALBUM --- private val _currentAlbum = MutableStateFlow(null) + /** The current [Album] to display. Null if there is nothing to show. */ val currentAlbum: StateFlow get() = _currentAlbum private val _albumSongList = MutableStateFlow(listOf()) + /** The current list data derived from [currentAlbum]. */ val albumSongList: StateFlow> get() = _albumSongList private val _albumSongInstructions = MutableEvent() + /** Instructions for updating [albumSongList] in the UI. */ val albumSongInstructions: Event get() = _albumSongInstructions @@ -113,15 +124,18 @@ constructor( // --- ARTIST --- private val _currentArtist = MutableStateFlow(null) + /** The current [Artist] to display. Null if there is nothing to show. */ val currentArtist: StateFlow get() = _currentArtist private val _artistSongList = MutableStateFlow(listOf()) + /** The current list derived from [currentArtist]. */ val artistSongList: StateFlow> = _artistSongList private val _artistSongInstructions = MutableEvent() + /** Instructions for updating [artistSongList] in the UI. */ val artistSongInstructions: Event get() = _artistSongInstructions @@ -137,15 +151,18 @@ constructor( // --- GENRE --- private val _currentGenre = MutableStateFlow(null) + /** The current [Genre] to display. Null if there is nothing to show. */ val currentGenre: StateFlow get() = _currentGenre private val _genreSongList = MutableStateFlow(listOf()) + /** The current list data derived from [currentGenre]. */ val genreSongList: StateFlow> = _genreSongList private val _genreSongInstructions = MutableEvent() + /** Instructions for updating [artistSongList] in the UI. */ val genreSongInstructions: Event get() = _genreSongInstructions @@ -161,20 +178,24 @@ constructor( // --- PLAYLIST --- private val _currentPlaylist = MutableStateFlow(null) + /** The current [Playlist] to display. Null if there is nothing to do. */ val currentPlaylist: StateFlow get() = _currentPlaylist private val _playlistSongList = MutableStateFlow(listOf()) + /** The current list data derived from [currentPlaylist] */ val playlistSongList: StateFlow> = _playlistSongList private val _playlistSongInstructions = MutableEvent() + /** Instructions for updating [playlistSongList] in the UI. */ val playlistSongInstructions: Event get() = _playlistSongInstructions private val _editedPlaylist = MutableStateFlow?>(null) + /** * The new playlist songs created during the current editing session. Null if no editing session * is occurring. @@ -204,18 +225,23 @@ constructor( val album = detailGenerator.album(currentAlbum.value?.uid ?: return) refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace) } + MusicType.ARTISTS -> { val artist = detailGenerator.artist(currentArtist.value?.uid ?: return) refreshDetail( - artist, _currentArtist, _artistSongList, _artistSongInstructions, replace) + artist, _currentArtist, _artistSongList, _artistSongInstructions, replace + ) } + MusicType.GENRES -> { val genre = detailGenerator.genre(currentGenre.value?.uid ?: return) refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace) } + MusicType.PLAYLISTS -> { refreshPlaylist(currentPlaylist.value?.uid ?: return) } + else -> error("Unexpected music type $type") } } @@ -253,7 +279,8 @@ constructor( Show.SongArtistDecision(song) } else { Show.ArtistDetails(song.artists.first()) - }) + } + ) /** * Navigate to the details of one of the [Artist]s of an [Album] using the corresponding choice @@ -267,7 +294,8 @@ constructor( Show.AlbumArtistDecision(album) } else { Show.ArtistDetails(album.artists.first()) - }) + } + ) /** * Navigate to the details of an [Artist]. @@ -499,10 +527,104 @@ constructor( } else { L.d("Playlist will be empty after removal, removing header") UpdateInstructions.Remove(at - 1, 3) - }) + } + ) } - private fun refreshAudioInfo(song: Song) {} + private fun refreshAudioInfo(song: Song) { + _currentSongProperties.value = + buildList { + add(SongProperty(R.string.lbl_name, SongProperty.Value.MusicName(song))) + add(SongProperty(R.string.lbl_album, SongProperty.Value.MusicName(song.album))) + add( + SongProperty( + R.string.lbl_artists, + SongProperty.Value.MusicNames(song.artists) + ) + ) + add( + SongProperty( + R.string.lbl_genres, + SongProperty.Value.MusicNames(song.genres) + ) + ) + song.date?.let { + add( + SongProperty( + R.string.lbl_date, + SongProperty.Value.ItemDate(it) + ) + ) + } + song.track?.let { + add( + SongProperty( + R.string.lbl_track, + SongProperty.Value.Number(it, null) + ) + ) + } + song.disc?.let { + add( + SongProperty( + R.string.lbl_disc, + SongProperty.Value.Number(it.number, it.name) + ) + ) + } + add( + SongProperty( + R.string.lbl_path, + SongProperty.Value.ItemPath(song.path) + ) + ) + add( + SongProperty( + R.string.lbl_size, SongProperty.Value.Size(song.size) + ) + ) + add( + SongProperty( + R.string.lbl_duration, + SongProperty.Value.Duration(song.durationMs) + ) + ) + add( + SongProperty( + R.string.lbl_format, + SongProperty.Value.ItemFormat(song.format) + ) + ) + add( + SongProperty( + R.string.lbl_bitrate, + SongProperty.Value.Bitrate(song.bitrateKbps) + ) + ) + add( + SongProperty( + R.string.lbl_sample_rate, + SongProperty.Value.SampleRate(song.sampleRateHz) + ) + ) + song.replayGainAdjustment.track?.let { + add( + SongProperty( + R.string.lbl_replaygain_track, + SongProperty.Value.Decibels(it) + ) + ) + } + song.replayGainAdjustment.album?.let { + add( + SongProperty( + R.string.lbl_replaygain_album, + SongProperty.Value.Decibels(it) + ) + ) + } + } + } private inline fun refreshDetail( detail: Detail?, @@ -531,6 +653,7 @@ constructor( newList.add(header) section.items } + is DetailSection.Discs -> { val header = SortHeader(section.stringRes) if (newList.isNotEmpty()) { @@ -571,9 +694,10 @@ constructor( if (edited == null) { val playlist = detailGenerator.playlist(uid) refreshDetail( - playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null) { - EditHeader(it) - } + playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null + ) { + EditHeader(it) + } return } val list = mutableListOf() 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 44e8e6fa2..286d2b242 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/SongDetailDialog.kt @@ -28,7 +28,9 @@ import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.DialogSongDetailBinding +import org.oxycblt.auxio.detail.list.SongProperty import org.oxycblt.auxio.detail.list.SongPropertyAdapter +import org.oxycblt.auxio.list.adapter.UpdateInstructions import org.oxycblt.auxio.music.resolve import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment import org.oxycblt.auxio.util.collectImmediately @@ -66,84 +68,18 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment T.zipName(context: Context): String { - val name = name - return if (name is Name.Known && name.sort != null) { - getString(R.string.fmt_zipped_names, name.resolve(context), name.sort) - } else { - name.resolve(context) + if (song == null) { + findNavController().navigateUp() + return } } - private fun List.zipNames(context: Context) = - concatLocalized(context) { it.zipName(context) } + private fun updateSongProperties(songProperties: List) { + detailAdapter.update(songProperties, UpdateInstructions.Replace(0)) + } } diff --git a/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt index fdb5da79a..469cc719e 100644 --- a/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt +++ b/app/src/main/java/org/oxycblt/auxio/detail/list/SongPropertyAdapter.kt @@ -18,16 +18,28 @@ package org.oxycblt.auxio.detail.list +import android.text.Editable +import android.text.format.Formatter import android.view.View import android.view.ViewGroup import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView +import org.oxycblt.auxio.R import org.oxycblt.auxio.databinding.ItemSongPropertyBinding import org.oxycblt.auxio.list.adapter.FlexibleListAdapter import org.oxycblt.auxio.list.adapter.SimpleDiffCallback import org.oxycblt.auxio.list.recycler.DialogRecyclerView +import org.oxycblt.auxio.music.resolve +import org.oxycblt.auxio.music.resolveNames +import org.oxycblt.auxio.playback.formatDurationMs +import org.oxycblt.auxio.playback.replaygain.formatDb import org.oxycblt.auxio.util.context import org.oxycblt.auxio.util.inflater +import org.oxycblt.musikr.Music +import org.oxycblt.musikr.fs.Format +import org.oxycblt.musikr.fs.Path +import org.oxycblt.musikr.tag.Date +import org.oxycblt.musikr.tag.Name /** * An adapter for [SongProperty] instances. @@ -52,7 +64,21 @@ class SongPropertyAdapter : * @param value The value of the property. * @author Alexander Capehart (OxygenCobalt) */ -data class SongProperty(@StringRes val name: Int, val value: String) +data class SongProperty(@StringRes val name: Int, val value: Value) { + sealed interface Value { + data class MusicName(val music: Music) : Value + data class MusicNames(val name: List) : Value + data class Number(val value: Int, val subtitle: String?) : Value + data class ItemDate(val date: Date) : Value + data class ItemPath(val path: Path) : Value + data class Size(val sizeBytes: Long) : Value + data class Duration(val durationMs: Long) : Value + data class ItemFormat(val format: Format) : Value + data class Bitrate(val kbps: Int) : Value + data class SampleRate(val hz: Int) : Value + data class Decibels(val value: Float) : Value + } +} /** * A [RecyclerView.ViewHolder] that displays a [SongProperty]. Use [from] to create an instance. @@ -64,7 +90,57 @@ class SongPropertyViewHolder private constructor(private val binding: ItemSongPr fun bind(property: SongProperty) { val context = binding.context binding.propertyName.hint = context.getString(property.name) - binding.propertyValue.setText(property.value) + when (property.value) { + is SongProperty.Value.MusicName -> { + val music = property.value.music + binding.propertyValue.setText(music.name.resolve(context)) + } + is SongProperty.Value.MusicNames -> { + val names = property.value.name.resolveNames(context) + binding.propertyValue.setText(names) + } + is SongProperty.Value.Number -> { + val value = context.getString(R.string.fmt_number, property.value.value) + val subtitle = property.value.subtitle + binding.propertyValue.setText(if (subtitle != null) { + context.getString(R.string.fmt_zipped_names, value, subtitle) + } else { + value + }) + } + is SongProperty.Value.ItemDate -> { + val date = property.value.date + binding.propertyValue.setText(date.resolve(context)) + } + is SongProperty.Value.ItemPath -> { + val path = property.value.path + binding.propertyValue.setText(path.resolve(context)) + } + is SongProperty.Value.Size -> { + val size = property.value.sizeBytes + binding.propertyValue.setText(Formatter.formatFileSize(context, size)) + } + is SongProperty.Value.Duration -> { + val duration = property.value.durationMs + binding.propertyValue.setText(duration.formatDurationMs(true)) + } + is SongProperty.Value.ItemFormat -> { + val format = property.value.format + binding.propertyValue.setText(format.resolve(context)) + } + is SongProperty.Value.Bitrate -> { + val kbps = property.value.kbps + binding.propertyValue.setText(context.getString(R.string.fmt_bitrate, kbps)) + } + is SongProperty.Value.SampleRate -> { + val hz = property.value.hz + binding.propertyValue.setText(context.getString(R.string.fmt_sample_rate, hz)) + } + is SongProperty.Value.Decibels -> { + val value = property.value.value + binding.propertyValue.setText(value.formatDb(context)) + } + } } companion object { diff --git a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt index bf6c16927..217ae9ee9 100644 --- a/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt +++ b/app/src/main/java/org/oxycblt/auxio/music/MusicUtil.kt @@ -25,6 +25,7 @@ import kotlin.math.max import org.oxycblt.auxio.R import org.oxycblt.auxio.util.concatLocalized import org.oxycblt.musikr.Music +import org.oxycblt.musikr.fs.Format import org.oxycblt.musikr.tag.Date import org.oxycblt.musikr.tag.Disc import org.oxycblt.musikr.tag.Name @@ -152,3 +153,19 @@ fun ReleaseType.resolve(context: Context) = is ReleaseType.Mixtape -> context.getString(R.string.lbl_mixtape) is ReleaseType.Demo -> context.getString(R.string.lbl_demo) } + +fun Format.resolve(context: Context): String = + when (this) { + is Format.MPEG3 -> context.getString(R.string.cdc_mp3) + is Format.MPEG4 -> containing?.let { context.getString(R.string.cnt_mp4, it.resolve(context)) } + ?: context.getString(R.string.cdc_mp4) + is Format.AAC -> context.getString(R.string.cdc_aac) + is Format.ALAC -> context.getString(R.string.cdc_alac) + is Format.Ogg -> containing?.let { context.getString(R.string.cnt_ogg, it.resolve(context)) } + ?: context.getString(R.string.cdc_ogg) + is Format.Opus -> context.getString(R.string.cdc_opus) + is Format.Vorbis -> context.getString(R.string.cdc_vorbis) + is Format.FLAC -> context.getString(R.string.cdc_flac) + is Format.Wav -> context.getString(R.string.cdc_wav) + is Format.Unknown -> extension ?: context.getString(R.string.cdc_unknown) + } \ No newline at end of file diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index a45ef58ce..5031e41d2 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -14,6 +14,7 @@ Vorbis Opus Microsoft WAVE + Ogg %s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8ce492580..9c1a1875f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -378,14 +378,20 @@ MPEG-1 audio MPEG-4 audio + + MPEG-4 containing %s + + Advanced Audio Coding (AAC) + + Apple Lossless Audio Codec (ALAC) Ogg audio Matroska audio - - Advanced Audio Coding (AAC) Free Lossless Audio Codec (FLAC) + + Unknown