Skip to content

Commit

Permalink
Twelve: Album disc header
Browse files Browse the repository at this point in the history
Change-Id: Ib0e193ed1b33301d7eda209f2e4becbc589c3db4
  • Loading branch information
SebaUbuntu committed Nov 3, 2024
1 parent c3c1f0e commit 431dd45
Show file tree
Hide file tree
Showing 4 changed files with 239 additions and 92 deletions.
235 changes: 143 additions & 92 deletions app/src/main/java/org/lineageos/twelve/fragments/AlbumFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@ import androidx.recyclerview.widget.RecyclerView
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
import org.lineageos.twelve.ext.getParcelable
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
Expand Down Expand Up @@ -65,42 +65,90 @@ class AlbumFragment : Fragment(R.layout.fragment_album) {

// Recyclerview
private val adapter by lazy {
object : SimpleListAdapter<Audio, ListItem>(
object : SimpleListAdapter<AlbumViewModel.AlbumContent, ListItem>(
UniqueItemDiffCallback(),
::ListItem,
) {
private val ViewHolder.trackTextView
get() = view.leadingView!!.findViewById<TextView>(R.id.trackTextView)

override fun ViewHolder.onPrepareView() {
view.setLeadingIconImage(R.drawable.ic_music_note)
view.setLeadingView(R.layout.audio_track_index)

view.setOnClickListener {
item?.let {
viewModel.playAudio(currentList, bindingAdapterPosition)
when (val item = item) {
is AlbumViewModel.AlbumContent.AudioItem -> {
val audios = currentList.mapNotNull {
(it as? AlbumViewModel.AlbumContent.AudioItem)?.audio
}

findNavController().navigate(
R.id.action_albumFragment_to_fragment_now_playing
)
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,
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
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
}
}
}
}
}
Expand Down Expand Up @@ -187,84 +235,87 @@ class AlbumFragment : Fragment(R.layout.fragment_album) {
}

private suspend fun loadData() {
viewModel.album.collectLatest {
linearProgressIndicator.setProgressCompat(it, true)

when (it) {
is RequestStatus.Loading -> {
// Do nothing
}

is RequestStatus.Success -> {
val (album, audios) = it.data
coroutineScope {
launch {
viewModel.album.collectLatest {
linearProgressIndicator.setProgressCompat(it, true)

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
)
}

toolbar.title = album.title
albumTitleTextView.text = album.title
is RequestStatus.Error -> {
Log.e(LOG_TAG, "Error loading album, error: ${it.type}")

album.thumbnail?.uri?.also { uri ->
thumbnailImageView.load(uri)
} ?: album.thumbnail?.bitmap?.also { bitmap ->
thumbnailImageView.load(bitmap)
} ?: thumbnailImageView.setImageResource(R.drawable.ic_album)
toolbar.title = ""
albumTitleTextView.text = ""

artistNameTextView.text = album.artistName
artistNameTextView.setOnClickListener {
findNavController().navigate(
R.id.action_albumFragment_to_fragment_artist,
ArtistFragment.createBundle(album.artistUri)
)
if (it.type == RequestStatus.Error.Type.NOT_FOUND) {
// Get out of here
findNavController().navigateUp()
}
}
}
}
}

album.year?.also { year ->
yearTextView.isVisible = true
yearTextView.text = year.toString()
} ?: run {
yearTextView.isVisible = false
}
launch {
viewModel.albumContent.collectLatest {
adapter.submitList(it)

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()
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,16 @@ import kotlinx.coroutines.Dispatchers
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)
Expand All @@ -34,6 +39,68 @@ class AlbumViewModel(application: Application) : TwelveViewModel(application) {
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
}
Expand Down
25 changes: 25 additions & 0 deletions app/src/main/res/layout/audio_track_index.xml
Original file line number Diff line number Diff line change
@@ -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>
Loading

0 comments on commit 431dd45

Please sign in to comment.