Twelve: Album disc header
Change-Id: Ib0e193ed1b33301d7eda209f2e4becbc589c3db4
diff --git a/app/src/main/java/org/lineageos/twelve/fragments/AlbumFragment.kt b/app/src/main/java/org/lineageos/twelve/fragments/AlbumFragment.kt
index a946bad..fe8d007 100644
--- a/app/src/main/java/org/lineageos/twelve/fragments/AlbumFragment.kt
+++ b/app/src/main/java/org/lineageos/twelve/fragments/AlbumFragment.kt
@@ -27,6 +27,7 @@
import coil3.load
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.progressindicator.LinearProgressIndicator
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.lineageos.twelve.R
@@ -34,7 +35,6 @@
import org.lineageos.twelve.ext.getViewProperty
import org.lineageos.twelve.ext.setProgressCompat
import org.lineageos.twelve.ext.updatePadding
-import org.lineageos.twelve.models.Audio
import org.lineageos.twelve.models.RequestStatus
import org.lineageos.twelve.ui.recyclerview.SimpleListAdapter
import org.lineageos.twelve.ui.recyclerview.UniqueItemDiffCallback
@@ -65,42 +65,90 @@
// Recyclerview
private val adapter by lazy {
- object : SimpleListAdapter<Audio, ListItem>(
+ object : SimpleListAdapter<AlbumViewModel.AlbumContent, ListItem>(
UniqueItemDiffCallback(),
::ListItem,
) {
- override fun ViewHolder.onPrepareView() {
- view.setLeadingIconImage(R.drawable.ic_music_note)
- view.setOnClickListener {
- item?.let {
- viewModel.playAudio(currentList, bindingAdapterPosition)
+ private val ViewHolder.trackTextView
+ get() = view.leadingView!!.findViewById<TextView>(R.id.trackTextView)
- findNavController().navigate(
- R.id.action_albumFragment_to_fragment_now_playing
- )
+ override fun ViewHolder.onPrepareView() {
+ view.setLeadingView(R.layout.audio_track_index)
+
+ view.setOnClickListener {
+ when (val item = item) {
+ is AlbumViewModel.AlbumContent.AudioItem -> {
+ val audios = currentList.mapNotNull {
+ (it as? AlbumViewModel.AlbumContent.AudioItem)?.audio
+ }
+
+ viewModel.playAudio(audios, audios.indexOf(item.audio))
+
+ findNavController().navigate(
+ R.id.action_albumFragment_to_fragment_now_playing
+ )
+ }
+
+ else -> {}
}
}
- view.setOnLongClickListener {
- item?.let {
- findNavController().navigate(
- R.id.action_albumFragment_to_fragment_audio_bottom_sheet_dialog,
- AudioBottomSheetDialogFragment.createBundle(
- it.uri,
- fromAlbum = true,
- )
- )
- }
- true
+ view.setOnLongClickListener {
+ when (val item = item) {
+ is AlbumViewModel.AlbumContent.AudioItem -> {
+ findNavController().navigate(
+ R.id.action_albumFragment_to_fragment_audio_bottom_sheet_dialog,
+ AudioBottomSheetDialogFragment.createBundle(
+ item.audio.uri,
+ fromAlbum = true,
+ )
+ )
+
+ true
+ }
+
+ else -> false
+ }
}
}
- override fun ViewHolder.onBindView(item: Audio) {
- view.headlineText = item.title
- view.supportingText = item.artistName
- view.trailingSupportingText = TimestampFormatter.formatTimestampMillis(
- item.durationMs
- )
+ override fun ViewHolder.onBindView(item: AlbumViewModel.AlbumContent) {
+ when (item) {
+ is AlbumViewModel.AlbumContent.DiscHeader -> {
+ view.setLeadingIconImage(R.drawable.ic_album)
+ view.leadingViewIsVisible = false
+ view.setHeadlineText(
+ R.string.album_disc_header,
+ item.discNumber,
+ )
+ view.supportingText = null
+ view.trailingSupportingText = null
+ view.isClickable = false
+ view.isLongClickable = false
+ }
+
+ is AlbumViewModel.AlbumContent.AudioItem -> {
+ item.audio.trackNumber?.also {
+ view.leadingIconImage = null
+ trackTextView.text = getString(
+ R.string.track_number,
+ it
+ )
+ view.leadingViewIsVisible = true
+ } ?: run {
+ view.setLeadingIconImage(R.drawable.ic_music_note)
+ view.leadingViewIsVisible = false
+ }
+
+ view.headlineText = item.audio.title
+ view.supportingText = item.audio.artistName
+ view.trailingSupportingText = TimestampFormatter.formatTimestampMillis(
+ item.audio.durationMs
+ )
+ view.isClickable = true
+ view.isLongClickable = true
+ }
+ }
}
}
}
@@ -187,84 +235,87 @@
}
private suspend fun loadData() {
- viewModel.album.collectLatest {
- linearProgressIndicator.setProgressCompat(it, true)
+ coroutineScope {
+ launch {
+ viewModel.album.collectLatest {
+ linearProgressIndicator.setProgressCompat(it, true)
- when (it) {
- is RequestStatus.Loading -> {
- // Do nothing
+ when (it) {
+ is RequestStatus.Loading -> {
+ // Do nothing
+ }
+
+ is RequestStatus.Success -> {
+ val (album, audios) = it.data
+
+ toolbar.title = album.title
+ albumTitleTextView.text = album.title
+
+ album.thumbnail?.uri?.also { uri ->
+ thumbnailImageView.load(uri)
+ } ?: album.thumbnail?.bitmap?.also { bitmap ->
+ thumbnailImageView.load(bitmap)
+ } ?: thumbnailImageView.setImageResource(R.drawable.ic_album)
+
+ artistNameTextView.text = album.artistName
+ artistNameTextView.setOnClickListener {
+ findNavController().navigate(
+ R.id.action_albumFragment_to_fragment_artist,
+ ArtistFragment.createBundle(album.artistUri)
+ )
+ }
+
+ album.year?.also { year ->
+ yearTextView.isVisible = true
+ yearTextView.text = year.toString()
+ } ?: run {
+ yearTextView.isVisible = false
+ }
+
+ val totalDurationMs = audios.sumOf { audio ->
+ audio.durationMs
+ }
+ val totalDurationMinutes = totalDurationMs / 1000 / 60
+
+ val tracksCount = resources.getQuantityString(
+ R.plurals.tracks_count,
+ audios.size,
+ audios.size
+ )
+ val tracksDuration = resources.getQuantityString(
+ R.plurals.tracks_duration,
+ totalDurationMinutes,
+ totalDurationMinutes
+ )
+ tracksInfoTextView.text = getString(
+ R.string.tracks_info,
+ tracksCount, tracksDuration
+ )
+ }
+
+ is RequestStatus.Error -> {
+ Log.e(LOG_TAG, "Error loading album, error: ${it.type}")
+
+ toolbar.title = ""
+ albumTitleTextView.text = ""
+
+ if (it.type == RequestStatus.Error.Type.NOT_FOUND) {
+ // Get out of here
+ findNavController().navigateUp()
+ }
+ }
+ }
}
+ }
- is RequestStatus.Success -> {
- val (album, audios) = it.data
+ launch {
+ viewModel.albumContent.collectLatest {
+ adapter.submitList(it)
- toolbar.title = album.title
- albumTitleTextView.text = album.title
-
- album.thumbnail?.uri?.also { uri ->
- thumbnailImageView.load(uri)
- } ?: album.thumbnail?.bitmap?.also { bitmap ->
- thumbnailImageView.load(bitmap)
- } ?: thumbnailImageView.setImageResource(R.drawable.ic_album)
-
- artistNameTextView.text = album.artistName
- artistNameTextView.setOnClickListener {
- findNavController().navigate(
- R.id.action_albumFragment_to_fragment_artist,
- ArtistFragment.createBundle(album.artistUri)
- )
- }
-
- album.year?.also { year ->
- yearTextView.isVisible = true
- yearTextView.text = year.toString()
- } ?: run {
- yearTextView.isVisible = false
- }
-
- val totalDurationMs = audios.sumOf { audio ->
- audio.durationMs
- }
- val totalDurationMinutes = totalDurationMs / 1000 / 60
-
- val tracksCount = resources.getQuantityString(
- R.plurals.tracks_count,
- audios.size,
- audios.size
- )
- val tracksDuration = resources.getQuantityString(
- R.plurals.tracks_duration,
- totalDurationMinutes,
- totalDurationMinutes
- )
- tracksInfoTextView.text = getString(
- R.string.tracks_info,
- tracksCount, tracksDuration
- )
-
- adapter.submitList(audios)
-
- val isEmpty = audios.isEmpty()
+ val isEmpty = it.isEmpty()
recyclerView.isVisible = !isEmpty
noElementsNestedScrollView.isVisible = isEmpty
}
-
- is RequestStatus.Error -> {
- Log.e(LOG_TAG, "Error loading album, error: ${it.type}")
-
- toolbar.title = ""
- albumTitleTextView.text = ""
-
- adapter.submitList(listOf())
-
- recyclerView.isVisible = false
- noElementsNestedScrollView.isVisible = true
-
- if (it.type == RequestStatus.Error.Type.NOT_FOUND) {
- // Get out of here
- findNavController().navigateUp()
- }
- }
}
}
}
diff --git a/app/src/main/java/org/lineageos/twelve/viewmodels/AlbumViewModel.kt b/app/src/main/java/org/lineageos/twelve/viewmodels/AlbumViewModel.kt
index 8a99120..1a71036 100644
--- a/app/src/main/java/org/lineageos/twelve/viewmodels/AlbumViewModel.kt
+++ b/app/src/main/java/org/lineageos/twelve/viewmodels/AlbumViewModel.kt
@@ -12,11 +12,16 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
+import org.lineageos.twelve.models.Audio
import org.lineageos.twelve.models.RequestStatus
+import org.lineageos.twelve.models.UniqueItem
+import kotlin.reflect.safeCast
class AlbumViewModel(application: Application) : TwelveViewModel(application) {
private val albumUri = MutableStateFlow<Uri?>(null)
@@ -34,6 +39,68 @@
RequestStatus.Loading()
)
+ sealed interface AlbumContent : UniqueItem<AlbumContent> {
+ data class DiscHeader(val discNumber: Int) : AlbumContent {
+ override fun areItemsTheSame(other: AlbumContent) =
+ DiscHeader::class.safeCast(other)?.let {
+ discNumber == it.discNumber
+ } ?: false
+
+ override fun areContentsTheSame(other: AlbumContent) = true
+ }
+
+ class AudioItem(val audio: Audio) : AlbumContent {
+ override fun areItemsTheSame(other: AlbumContent) = AudioItem::class.safeCast(
+ other
+ )?.let {
+ audio.areItemsTheSame(it.audio)
+ } ?: false
+
+ override fun areContentsTheSame(other: AlbumContent) = AudioItem::class.safeCast(
+ other
+ )?.let {
+ audio.areContentsTheSame(it.audio)
+ } ?: false
+ }
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ val albumContent = album
+ .mapLatest {
+ when (it) {
+ is RequestStatus.Loading -> null
+
+ is RequestStatus.Success -> {
+ val discToTracks = it.data.second.groupBy { audio ->
+ audio.discNumber ?: 1
+ }
+
+ mutableListOf<AlbumContent>().apply {
+ discToTracks.keys.sorted().forEach { discNumber ->
+ add(AlbumContent.DiscHeader(discNumber))
+
+ discToTracks[discNumber]?.let { tracks ->
+ addAll(
+ tracks.map { audio ->
+ AlbumContent.AudioItem(audio)
+ }
+ )
+ }
+ }
+ }.toList()
+ }
+
+ is RequestStatus.Error -> listOf()
+ }
+ }
+ .filterNotNull()
+ .flowOn(Dispatchers.IO)
+ .stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(),
+ listOf()
+ )
+
fun loadAlbum(albumUri: Uri) {
this.albumUri.value = albumUri
}
diff --git a/app/src/main/res/layout/audio_track_index.xml b/app/src/main/res/layout/audio_track_index.xml
new file mode 100644
index 0000000..616563a
--- /dev/null
+++ b/app/src/main/res/layout/audio_track_index.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ SPDX-FileCopyrightText: 2024 The LineageOS Project
+ SPDX-License-Identifier: Apache-2.0
+-->
+<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ style="@style/Widget.Material3.CardView.Filled"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ app:cardBackgroundColor="?attr/colorPrimaryContainer"
+ app:cardCornerRadius="4dp">
+
+ <TextView
+ android:id="@+id/trackTextView"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:gravity="center"
+ android:textAlignment="gravity"
+ android:textAppearance="?attr/textAppearanceLabelMedium"
+ android:textColor="?attr/colorOnPrimaryContainer"
+ tools:text="99" />
+
+</com.google.android.material.card.MaterialCardView>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2b4f3df..cc39008 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -143,4 +143,8 @@
<string name="now_playing_widget_dummy_title">Title</string>
<string name="now_playing_widget_dummy_artist">Artist</string>
<string name="now_playing_widget_dummy_album">Album</string>
+
+ <!-- Album fragment -->
+ <string name="album_disc_header">Disc %1$d</string>
+ <string name="track_number" translatable="false">%1$d</string>
</resources>