detail: reimplement song details
This commit is contained in:
parent
50b7c24c03
commit
6850a3443f
6 changed files with 248 additions and 88 deletions
|
@ -15,7 +15,7 @@
|
||||||
* You should have received a copy of the GNU General Public License
|
* You should have received a copy of the GNU General Public License
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
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.DiscDivider
|
||||||
import org.oxycblt.auxio.detail.list.DiscHeader
|
import org.oxycblt.auxio.detail.list.DiscHeader
|
||||||
import org.oxycblt.auxio.detail.list.EditHeader
|
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.detail.list.SortHeader
|
||||||
import org.oxycblt.auxio.list.BasicHeader
|
import org.oxycblt.auxio.list.BasicHeader
|
||||||
import org.oxycblt.auxio.list.Item
|
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.music.MusicType
|
||||||
import org.oxycblt.auxio.playback.PlaySong
|
import org.oxycblt.auxio.playback.PlaySong
|
||||||
import org.oxycblt.auxio.playback.PlaybackSettings
|
import org.oxycblt.auxio.playback.PlaybackSettings
|
||||||
|
import org.oxycblt.auxio.playback.formatDurationMs
|
||||||
import org.oxycblt.auxio.util.Event
|
import org.oxycblt.auxio.util.Event
|
||||||
import org.oxycblt.auxio.util.MutableEvent
|
import org.oxycblt.auxio.util.MutableEvent
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
@ -70,6 +72,7 @@ constructor(
|
||||||
detailGeneratorFactory: DetailGenerator.Factory
|
detailGeneratorFactory: DetailGenerator.Factory
|
||||||
) : ViewModel(), DetailGenerator.Invalidator {
|
) : ViewModel(), DetailGenerator.Invalidator {
|
||||||
private val _toShow = MutableEvent<Show>()
|
private val _toShow = MutableEvent<Show>()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
|
* A [Show] command that is awaiting a view capable of responding to it. Null if none currently.
|
||||||
*/
|
*/
|
||||||
|
@ -78,26 +81,34 @@ constructor(
|
||||||
|
|
||||||
// --- SONG ---
|
// --- SONG ---
|
||||||
|
|
||||||
private var currentSongJob: Job? = null
|
|
||||||
|
|
||||||
private val _currentSong = MutableStateFlow<Song?>(null)
|
private val _currentSong = MutableStateFlow<Song?>(null)
|
||||||
|
|
||||||
/** The current [Song] to display. Null if there is nothing to show. */
|
/** The current [Song] to display. Null if there is nothing to show. */
|
||||||
val currentSong: StateFlow<Song?>
|
val currentSong: StateFlow<Song?>
|
||||||
get() = _currentSong
|
get() = _currentSong
|
||||||
|
|
||||||
|
private val _currentSongProperties = MutableStateFlow<List<SongProperty>>(listOf())
|
||||||
|
|
||||||
|
/** The current properties of [currentSong]. Empty if nothing to show. */
|
||||||
|
val currentSongProperties: StateFlow<List<SongProperty>>
|
||||||
|
get() = _currentSongProperties
|
||||||
|
|
||||||
// --- ALBUM ---
|
// --- ALBUM ---
|
||||||
|
|
||||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||||
|
|
||||||
/** The current [Album] to display. Null if there is nothing to show. */
|
/** The current [Album] to display. Null if there is nothing to show. */
|
||||||
val currentAlbum: StateFlow<Album?>
|
val currentAlbum: StateFlow<Album?>
|
||||||
get() = _currentAlbum
|
get() = _currentAlbum
|
||||||
|
|
||||||
private val _albumSongList = MutableStateFlow(listOf<Item>())
|
private val _albumSongList = MutableStateFlow(listOf<Item>())
|
||||||
|
|
||||||
/** The current list data derived from [currentAlbum]. */
|
/** The current list data derived from [currentAlbum]. */
|
||||||
val albumSongList: StateFlow<List<Item>>
|
val albumSongList: StateFlow<List<Item>>
|
||||||
get() = _albumSongList
|
get() = _albumSongList
|
||||||
|
|
||||||
private val _albumSongInstructions = MutableEvent<UpdateInstructions>()
|
private val _albumSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
|
||||||
/** Instructions for updating [albumSongList] in the UI. */
|
/** Instructions for updating [albumSongList] in the UI. */
|
||||||
val albumSongInstructions: Event<UpdateInstructions>
|
val albumSongInstructions: Event<UpdateInstructions>
|
||||||
get() = _albumSongInstructions
|
get() = _albumSongInstructions
|
||||||
|
@ -113,15 +124,18 @@ constructor(
|
||||||
// --- ARTIST ---
|
// --- ARTIST ---
|
||||||
|
|
||||||
private val _currentArtist = MutableStateFlow<Artist?>(null)
|
private val _currentArtist = MutableStateFlow<Artist?>(null)
|
||||||
|
|
||||||
/** The current [Artist] to display. Null if there is nothing to show. */
|
/** The current [Artist] to display. Null if there is nothing to show. */
|
||||||
val currentArtist: StateFlow<Artist?>
|
val currentArtist: StateFlow<Artist?>
|
||||||
get() = _currentArtist
|
get() = _currentArtist
|
||||||
|
|
||||||
private val _artistSongList = MutableStateFlow(listOf<Item>())
|
private val _artistSongList = MutableStateFlow(listOf<Item>())
|
||||||
|
|
||||||
/** The current list derived from [currentArtist]. */
|
/** The current list derived from [currentArtist]. */
|
||||||
val artistSongList: StateFlow<List<Item>> = _artistSongList
|
val artistSongList: StateFlow<List<Item>> = _artistSongList
|
||||||
|
|
||||||
private val _artistSongInstructions = MutableEvent<UpdateInstructions>()
|
private val _artistSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
|
||||||
/** Instructions for updating [artistSongList] in the UI. */
|
/** Instructions for updating [artistSongList] in the UI. */
|
||||||
val artistSongInstructions: Event<UpdateInstructions>
|
val artistSongInstructions: Event<UpdateInstructions>
|
||||||
get() = _artistSongInstructions
|
get() = _artistSongInstructions
|
||||||
|
@ -137,15 +151,18 @@ constructor(
|
||||||
// --- GENRE ---
|
// --- GENRE ---
|
||||||
|
|
||||||
private val _currentGenre = MutableStateFlow<Genre?>(null)
|
private val _currentGenre = MutableStateFlow<Genre?>(null)
|
||||||
|
|
||||||
/** The current [Genre] to display. Null if there is nothing to show. */
|
/** The current [Genre] to display. Null if there is nothing to show. */
|
||||||
val currentGenre: StateFlow<Genre?>
|
val currentGenre: StateFlow<Genre?>
|
||||||
get() = _currentGenre
|
get() = _currentGenre
|
||||||
|
|
||||||
private val _genreSongList = MutableStateFlow(listOf<Item>())
|
private val _genreSongList = MutableStateFlow(listOf<Item>())
|
||||||
|
|
||||||
/** The current list data derived from [currentGenre]. */
|
/** The current list data derived from [currentGenre]. */
|
||||||
val genreSongList: StateFlow<List<Item>> = _genreSongList
|
val genreSongList: StateFlow<List<Item>> = _genreSongList
|
||||||
|
|
||||||
private val _genreSongInstructions = MutableEvent<UpdateInstructions>()
|
private val _genreSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
|
||||||
/** Instructions for updating [artistSongList] in the UI. */
|
/** Instructions for updating [artistSongList] in the UI. */
|
||||||
val genreSongInstructions: Event<UpdateInstructions>
|
val genreSongInstructions: Event<UpdateInstructions>
|
||||||
get() = _genreSongInstructions
|
get() = _genreSongInstructions
|
||||||
|
@ -161,20 +178,24 @@ constructor(
|
||||||
// --- PLAYLIST ---
|
// --- PLAYLIST ---
|
||||||
|
|
||||||
private val _currentPlaylist = MutableStateFlow<Playlist?>(null)
|
private val _currentPlaylist = MutableStateFlow<Playlist?>(null)
|
||||||
|
|
||||||
/** The current [Playlist] to display. Null if there is nothing to do. */
|
/** The current [Playlist] to display. Null if there is nothing to do. */
|
||||||
val currentPlaylist: StateFlow<Playlist?>
|
val currentPlaylist: StateFlow<Playlist?>
|
||||||
get() = _currentPlaylist
|
get() = _currentPlaylist
|
||||||
|
|
||||||
private val _playlistSongList = MutableStateFlow(listOf<Item>())
|
private val _playlistSongList = MutableStateFlow(listOf<Item>())
|
||||||
|
|
||||||
/** The current list data derived from [currentPlaylist] */
|
/** The current list data derived from [currentPlaylist] */
|
||||||
val playlistSongList: StateFlow<List<Item>> = _playlistSongList
|
val playlistSongList: StateFlow<List<Item>> = _playlistSongList
|
||||||
|
|
||||||
private val _playlistSongInstructions = MutableEvent<UpdateInstructions>()
|
private val _playlistSongInstructions = MutableEvent<UpdateInstructions>()
|
||||||
|
|
||||||
/** Instructions for updating [playlistSongList] in the UI. */
|
/** Instructions for updating [playlistSongList] in the UI. */
|
||||||
val playlistSongInstructions: Event<UpdateInstructions>
|
val playlistSongInstructions: Event<UpdateInstructions>
|
||||||
get() = _playlistSongInstructions
|
get() = _playlistSongInstructions
|
||||||
|
|
||||||
private val _editedPlaylist = MutableStateFlow<List<Song>?>(null)
|
private val _editedPlaylist = MutableStateFlow<List<Song>?>(null)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The new playlist songs created during the current editing session. Null if no editing session
|
* The new playlist songs created during the current editing session. Null if no editing session
|
||||||
* is occurring.
|
* is occurring.
|
||||||
|
@ -204,18 +225,23 @@ constructor(
|
||||||
val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
|
val album = detailGenerator.album(currentAlbum.value?.uid ?: return)
|
||||||
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
|
refreshDetail(album, _currentAlbum, _albumSongList, _albumSongInstructions, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicType.ARTISTS -> {
|
MusicType.ARTISTS -> {
|
||||||
val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
|
val artist = detailGenerator.artist(currentArtist.value?.uid ?: return)
|
||||||
refreshDetail(
|
refreshDetail(
|
||||||
artist, _currentArtist, _artistSongList, _artistSongInstructions, replace)
|
artist, _currentArtist, _artistSongList, _artistSongInstructions, replace
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicType.GENRES -> {
|
MusicType.GENRES -> {
|
||||||
val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
|
val genre = detailGenerator.genre(currentGenre.value?.uid ?: return)
|
||||||
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
|
refreshDetail(genre, _currentGenre, _genreSongList, _genreSongInstructions, replace)
|
||||||
}
|
}
|
||||||
|
|
||||||
MusicType.PLAYLISTS -> {
|
MusicType.PLAYLISTS -> {
|
||||||
refreshPlaylist(currentPlaylist.value?.uid ?: return)
|
refreshPlaylist(currentPlaylist.value?.uid ?: return)
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> error("Unexpected music type $type")
|
else -> error("Unexpected music type $type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -253,7 +279,8 @@ constructor(
|
||||||
Show.SongArtistDecision(song)
|
Show.SongArtistDecision(song)
|
||||||
} else {
|
} else {
|
||||||
Show.ArtistDetails(song.artists.first())
|
Show.ArtistDetails(song.artists.first())
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to the details of one of the [Artist]s of an [Album] using the corresponding choice
|
* 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)
|
Show.AlbumArtistDecision(album)
|
||||||
} else {
|
} else {
|
||||||
Show.ArtistDetails(album.artists.first())
|
Show.ArtistDetails(album.artists.first())
|
||||||
})
|
}
|
||||||
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate to the details of an [Artist].
|
* Navigate to the details of an [Artist].
|
||||||
|
@ -499,10 +527,104 @@ constructor(
|
||||||
} else {
|
} else {
|
||||||
L.d("Playlist will be empty after removal, removing header")
|
L.d("Playlist will be empty after removal, removing header")
|
||||||
UpdateInstructions.Remove(at - 1, 3)
|
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 <T : MusicParent> refreshDetail(
|
private inline fun <T : MusicParent> refreshDetail(
|
||||||
detail: Detail<T>?,
|
detail: Detail<T>?,
|
||||||
|
@ -531,6 +653,7 @@ constructor(
|
||||||
newList.add(header)
|
newList.add(header)
|
||||||
section.items
|
section.items
|
||||||
}
|
}
|
||||||
|
|
||||||
is DetailSection.Discs -> {
|
is DetailSection.Discs -> {
|
||||||
val header = SortHeader(section.stringRes)
|
val header = SortHeader(section.stringRes)
|
||||||
if (newList.isNotEmpty()) {
|
if (newList.isNotEmpty()) {
|
||||||
|
@ -571,9 +694,10 @@ constructor(
|
||||||
if (edited == null) {
|
if (edited == null) {
|
||||||
val playlist = detailGenerator.playlist(uid)
|
val playlist = detailGenerator.playlist(uid)
|
||||||
refreshDetail(
|
refreshDetail(
|
||||||
playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null) {
|
playlist, _currentPlaylist, _playlistSongList, _playlistSongInstructions, null
|
||||||
EditHeader(it)
|
) {
|
||||||
}
|
EditHeader(it)
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val list = mutableListOf<Item>()
|
val list = mutableListOf<Item>()
|
||||||
|
|
|
@ -28,7 +28,9 @@ import androidx.navigation.fragment.navArgs
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||||
|
import org.oxycblt.auxio.detail.list.SongProperty
|
||||||
import org.oxycblt.auxio.detail.list.SongPropertyAdapter
|
import org.oxycblt.auxio.detail.list.SongPropertyAdapter
|
||||||
|
import org.oxycblt.auxio.list.adapter.UpdateInstructions
|
||||||
import org.oxycblt.auxio.music.resolve
|
import org.oxycblt.auxio.music.resolve
|
||||||
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
|
||||||
import org.oxycblt.auxio.util.collectImmediately
|
import org.oxycblt.auxio.util.collectImmediately
|
||||||
|
@ -66,84 +68,18 @@ class SongDetailDialog : ViewBindingMaterialDialogFragment<DialogSongDetailBindi
|
||||||
detailModel.setSong(args.songUid)
|
detailModel.setSong(args.songUid)
|
||||||
detailModel.toShow.consume()
|
detailModel.toShow.consume()
|
||||||
collectImmediately(detailModel.currentSong, ::updateSong)
|
collectImmediately(detailModel.currentSong, ::updateSong)
|
||||||
|
collectImmediately(detailModel.currentSongProperties, ::updateSongProperties)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSong(song: Song?) {
|
private fun updateSong(song: Song?) {
|
||||||
// if (song == null) {
|
|
||||||
L.d("No song to show, navigating away")
|
L.d("No song to show, navigating away")
|
||||||
findNavController().navigateUp()
|
if (song == null) {
|
||||||
return
|
findNavController().navigateUp()
|
||||||
// }
|
return
|
||||||
//
|
|
||||||
// if (info != null) {
|
|
||||||
// val context = requireContext()
|
|
||||||
// detailAdapter.update(
|
|
||||||
// buildList {
|
|
||||||
// add(SongProperty(R.string.lbl_name, song.zipName(context)))
|
|
||||||
// add(SongProperty(R.string.lbl_album, song.album.zipName(context)))
|
|
||||||
// add(SongProperty(R.string.lbl_artists,
|
|
||||||
// song.artists.zipNames(context)))
|
|
||||||
// add(SongProperty(R.string.lbl_genres,
|
|
||||||
// song.genres.resolveNames(context)))
|
|
||||||
// song.date?.let { add(SongProperty(R.string.lbl_date,
|
|
||||||
// it.resolve(context))) }
|
|
||||||
// song.track?.let {
|
|
||||||
// add(SongProperty(R.string.lbl_track,
|
|
||||||
// getString(R.string.fmt_number, it)))
|
|
||||||
// }
|
|
||||||
// song.disc?.let {
|
|
||||||
// val formattedNumber = getString(R.string.fmt_number, it.number)
|
|
||||||
// val zipped =
|
|
||||||
// if (it.name != null) {
|
|
||||||
// getString(R.string.fmt_zipped_names, formattedNumber,
|
|
||||||
// it.name)
|
|
||||||
// } else {
|
|
||||||
// formattedNumber
|
|
||||||
// }
|
|
||||||
// add(SongProperty(R.string.lbl_disc, zipped))
|
|
||||||
// }
|
|
||||||
// add(SongProperty(R.string.lbl_path, song.path.resolve(context)))
|
|
||||||
// // info.format.resolveName(context)?.let {
|
|
||||||
// // add(SongProperty(R.string.lbl_format, it))
|
|
||||||
// // }
|
|
||||||
// add(
|
|
||||||
// SongProperty(
|
|
||||||
// R.string.lbl_size, Formatter.formatFileSize(context,
|
|
||||||
// song.size)))
|
|
||||||
// add(SongProperty(R.string.lbl_duration,
|
|
||||||
// song.durationMs.formatDurationMs(true)))
|
|
||||||
// info.bitrateKbps?.let {
|
|
||||||
// add(SongProperty(R.string.lbl_bitrate,
|
|
||||||
// getString(R.string.fmt_bitrate, it)))
|
|
||||||
// }
|
|
||||||
// info.sampleRateHz?.let {
|
|
||||||
// add(
|
|
||||||
// SongProperty(
|
|
||||||
// R.string.lbl_sample_rate,
|
|
||||||
// getString(R.string.fmt_sample_rate, it)))
|
|
||||||
// }
|
|
||||||
// song.replayGainAdjustment.track?.let {
|
|
||||||
// add(SongProperty(R.string.lbl_replaygain_track,
|
|
||||||
// it.formatDb(context)))
|
|
||||||
// }
|
|
||||||
// song.replayGainAdjustment.album?.let {
|
|
||||||
// add(SongProperty(R.string.lbl_replaygain_album,
|
|
||||||
// it.formatDb(context)))
|
|
||||||
// }
|
|
||||||
// },
|
|
||||||
// UpdateInstructions.Replace(0))
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T : Music> 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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun <T : Music> List<T>.zipNames(context: Context) =
|
private fun updateSongProperties(songProperties: List<SongProperty>) {
|
||||||
concatLocalized(context) { it.zipName(context) }
|
detailAdapter.update(songProperties, UpdateInstructions.Replace(0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,16 +18,28 @@
|
||||||
|
|
||||||
package org.oxycblt.auxio.detail.list
|
package org.oxycblt.auxio.detail.list
|
||||||
|
|
||||||
|
import android.text.Editable
|
||||||
|
import android.text.format.Formatter
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.databinding.ItemSongPropertyBinding
|
import org.oxycblt.auxio.databinding.ItemSongPropertyBinding
|
||||||
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
import org.oxycblt.auxio.list.adapter.FlexibleListAdapter
|
||||||
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
import org.oxycblt.auxio.list.adapter.SimpleDiffCallback
|
||||||
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
|
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.context
|
||||||
import org.oxycblt.auxio.util.inflater
|
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.
|
* An adapter for [SongProperty] instances.
|
||||||
|
@ -52,7 +64,21 @@ class SongPropertyAdapter :
|
||||||
* @param value The value of the property.
|
* @param value The value of the property.
|
||||||
* @author Alexander Capehart (OxygenCobalt)
|
* @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<Music>) : 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.
|
* 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) {
|
fun bind(property: SongProperty) {
|
||||||
val context = binding.context
|
val context = binding.context
|
||||||
binding.propertyName.hint = context.getString(property.name)
|
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 {
|
companion object {
|
||||||
|
|
|
@ -25,6 +25,7 @@ import kotlin.math.max
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.util.concatLocalized
|
import org.oxycblt.auxio.util.concatLocalized
|
||||||
import org.oxycblt.musikr.Music
|
import org.oxycblt.musikr.Music
|
||||||
|
import org.oxycblt.musikr.fs.Format
|
||||||
import org.oxycblt.musikr.tag.Date
|
import org.oxycblt.musikr.tag.Date
|
||||||
import org.oxycblt.musikr.tag.Disc
|
import org.oxycblt.musikr.tag.Disc
|
||||||
import org.oxycblt.musikr.tag.Name
|
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.Mixtape -> context.getString(R.string.lbl_mixtape)
|
||||||
is ReleaseType.Demo -> context.getString(R.string.lbl_demo)
|
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)
|
||||||
|
}
|
|
@ -14,6 +14,7 @@
|
||||||
<string name="cdc_vorbis">Vorbis</string>
|
<string name="cdc_vorbis">Vorbis</string>
|
||||||
<string name="cdc_opus">Opus</string>
|
<string name="cdc_opus">Opus</string>
|
||||||
<string name="cdc_wav">Microsoft WAVE</string>
|
<string name="cdc_wav">Microsoft WAVE</string>
|
||||||
|
<string name="cnt_ogg">Ogg %s</string>
|
||||||
|
|
||||||
<!-- Supporter Namespace | Sponsor usernames -->
|
<!-- Supporter Namespace | Sponsor usernames -->
|
||||||
</resources>
|
</resources>
|
|
@ -378,14 +378,20 @@
|
||||||
<string name="cdc_mp3">MPEG-1 audio</string>
|
<string name="cdc_mp3">MPEG-1 audio</string>
|
||||||
<!-- "Audio" can optionally be translated -->
|
<!-- "Audio" can optionally be translated -->
|
||||||
<string name="cdc_mp4">MPEG-4 audio</string>
|
<string name="cdc_mp4">MPEG-4 audio</string>
|
||||||
|
<!-- i.e MPEG-4 containing some codec -->
|
||||||
|
<string name="cnt_mp4">MPEG-4 containing %s</string>
|
||||||
|
<!-- "Advanced Audio Coding" can optionally be translated -->
|
||||||
|
<string name="cdc_aac">Advanced Audio Coding (AAC)</string>
|
||||||
|
<!-- "Apple Lossless Audio Codec" can optionally be translated -->
|
||||||
|
<string name="cdc_alac">Apple Lossless Audio Codec (ALAC)</string>
|
||||||
<!-- "Audio" can optionally be translated -->
|
<!-- "Audio" can optionally be translated -->
|
||||||
<string name="cdc_ogg">Ogg audio</string>
|
<string name="cdc_ogg">Ogg audio</string>
|
||||||
<!-- "Audio" can optionally be translated -->
|
<!-- "Audio" can optionally be translated -->
|
||||||
<string name="cdc_mka">Matroska audio</string>
|
<string name="cdc_mka">Matroska audio</string>
|
||||||
<!-- "Advanced Audio Coding" can optionally be translated -->
|
|
||||||
<string name="cdc_aac">Advanced Audio Coding (AAC)</string>
|
|
||||||
<!-- "Free Lossless Audio Codec" can optionally be translated -->
|
<!-- "Free Lossless Audio Codec" can optionally be translated -->
|
||||||
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
|
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
|
||||||
|
<!-- As in unknown format -->
|
||||||
|
<string name="cdc_unknown">Unknown</string>
|
||||||
|
|
||||||
<!-- Color Label namespace | Accent names -->
|
<!-- Color Label namespace | Accent names -->
|
||||||
<eat-comment />
|
<eat-comment />
|
||||||
|
|
Loading…
Reference in a new issue