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