refactor(android): migrate History/Liked/Search/Playlist rows to shared TrackRow

This commit is contained in:
2026-05-29 22:44:09 -04:00
parent bbd483603d
commit 6582d7699e
5 changed files with 88 additions and 169 deletions
@@ -1,11 +1,9 @@
package com.fabledsword.minstrel.history.ui package com.fabledsword.minstrel.history.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
@@ -14,9 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -27,7 +23,7 @@ import com.fabledsword.minstrel.history.data.HistoryEntry
import com.fabledsword.minstrel.history.data.HistoryRepository import com.fabledsword.minstrel.history.data.HistoryRepository
import com.fabledsword.minstrel.models.TrackRef import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.TrackRow
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@@ -171,48 +167,30 @@ private fun HistoryRow(
onNavigateToAlbum: (String) -> Unit, onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit, onNavigateToArtist: (String) -> Unit,
) { ) {
val titleColor = if (nowPlaying) { TrackRow(
MaterialTheme.colorScheme.primary title = entry.track.title,
} else { artist = entry.track.artistName.ifEmpty { entry.track.albumTitle },
MaterialTheme.colorScheme.onSurface trackId = entry.track.id,
} onClick = onClick,
Row( nowPlaying = nowPlaying,
modifier = Modifier enabled = entry.track.streamUrl.isNotEmpty(),
.fillMaxWidth()
.clickable(enabled = entry.track.streamUrl.isNotEmpty(), onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
) { leading = {
TrackCoverThumb(coverUrl = entry.track.coverUrl, contentDescription = entry.track.title) TrackCoverThumb(coverUrl = entry.track.coverUrl, contentDescription = entry.track.title)
Column(modifier = Modifier.weight(1f)) { },
trailing = {
Text( Text(
text = entry.track.title, text = relativeTime(entry.playedAt),
style = MaterialTheme.typography.bodyLarge,
color = titleColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = entry.track.artistName.ifEmpty { entry.track.albumTitle },
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
} TrackActionsButton(
CachedDot(entry.track.id) track = entry.track,
Text( onNavigateToAlbum = onNavigateToAlbum,
text = relativeTime(entry.playedAt), onNavigateToArtist = onNavigateToArtist,
style = MaterialTheme.typography.bodySmall, )
color = MaterialTheme.colorScheme.onSurfaceVariant, },
) )
TrackActionsButton(
track = entry.track,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
} }
// ─── Time helpers ──────────────────────────────────────────────────── // ─── Time helpers ────────────────────────────────────────────────────
@@ -1,6 +1,5 @@
package com.fabledsword.minstrel.likes.ui package com.fabledsword.minstrel.likes.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
@@ -20,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -37,7 +35,7 @@ import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.nav.AlbumDetail import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.player.PlayerController import com.fabledsword.minstrel.player.PlayerController
import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.TrackRow
import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.EmptyState
import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow import com.fabledsword.minstrel.shared.widgets.HorizontalScrollRow
import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.LikeButton
@@ -259,42 +257,24 @@ private fun LikedTrackRow(
onNavigateToAlbum: (String) -> Unit, onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit, onNavigateToArtist: (String) -> Unit,
) { ) {
val titleColor = if (nowPlaying) { TrackRow(
MaterialTheme.colorScheme.primary title = track.title,
} else { artist = track.artistName.ifEmpty { track.albumTitle },
MaterialTheme.colorScheme.onSurface trackId = track.id,
} onClick = onClick,
Row( nowPlaying = nowPlaying,
modifier = Modifier enabled = track.streamUrl.isNotEmpty(),
.fillMaxWidth()
.clickable(enabled = track.streamUrl.isNotEmpty(), onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
) { leading = {
TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title) TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title)
Column(modifier = Modifier.weight(1f)) { },
Text( trailing = {
text = track.title, LikeButton(liked = true, onToggle = onUnlike)
style = MaterialTheme.typography.bodyLarge, TrackActionsButton(
color = titleColor, track = track,
maxLines = 1, onNavigateToAlbum = onNavigateToAlbum,
overflow = TextOverflow.Ellipsis, onNavigateToArtist = onNavigateToArtist,
) )
Text( },
text = track.artistName.ifEmpty { track.albumTitle }, )
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
CachedDot(track.id)
LikeButton(liked = true, onToggle = onUnlike)
TrackActionsButton(
track = track,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
} }
@@ -4,7 +4,6 @@ package com.fabledsword.minstrel.playlists.ui
import androidx.compose.animation.Crossfade import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -73,7 +72,7 @@ import com.fabledsword.minstrel.likes.data.LikesRepository
import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef import com.fabledsword.minstrel.playlists.data.PlaylistDetailRef
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
import com.fabledsword.minstrel.shared.formatDuration import com.fabledsword.minstrel.shared.formatDuration
import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.TrackRow
import com.fabledsword.minstrel.shared.widgets.LikeButton import com.fabledsword.minstrel.shared.widgets.LikeButton
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.SkeletonTrackRow import com.fabledsword.minstrel.shared.widgets.SkeletonTrackRow
@@ -497,55 +496,38 @@ private fun TrackRow(
) { ) {
val enabled = row.isAvailable val enabled = row.isAvailable
val rowAlpha = if (enabled) 1f else UNAVAILABLE_ALPHA val rowAlpha = if (enabled) 1f else UNAVAILABLE_ALPHA
val baseTitleColor = if (nowPlaying) { TrackRow(
MaterialTheme.colorScheme.primary title = row.title,
} else { artist = row.artistName.ifEmpty { row.albumTitle },
MaterialTheme.colorScheme.onSurface trackId = row.trackId.orEmpty(),
} onClick = onClick,
Row( nowPlaying = nowPlaying,
modifier = Modifier enabled = enabled,
.fillMaxWidth() contentAlpha = rowAlpha,
.clickable(enabled = enabled, onClick = onClick) leading = {
.padding(horizontal = 16.dp, vertical = 8.dp), TrackCoverThumb(
verticalAlignment = Alignment.CenterVertically, coverUrl = row.coverUrl,
) { contentDescription = row.title,
TrackCoverThumb( modifier = Modifier.padding(end = 12.dp),
coverUrl = row.coverUrl,
contentDescription = row.title,
modifier = Modifier.padding(end = 12.dp),
)
Column(modifier = Modifier.weight(1f)) {
Text(
text = row.title,
style = MaterialTheme.typography.bodyLarge,
color = baseTitleColor.copy(alpha = rowAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
) )
},
trailing = {
Text( Text(
text = row.artistName.ifEmpty { row.albumTitle }, text = formatDuration(row.durationSec, zero = "--:--"),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha), color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha),
maxLines = 1, modifier = Modifier.padding(start = 12.dp),
overflow = TextOverflow.Ellipsis,
) )
} if (enabled) {
row.trackId?.let { CachedDot(it) } LikeButton(liked = liked, onToggle = onToggleLike)
Text( TrackActionsButton(
text = formatDuration(row.durationSec, zero = "--:--"), track = row.toTrackRef(),
style = MaterialTheme.typography.bodySmall, onNavigateToAlbum = onNavigateToAlbum,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha), onNavigateToArtist = onNavigateToArtist,
modifier = Modifier.padding(start = 12.dp), )
) }
if (enabled) { },
LikeButton(liked = liked, onToggle = onToggleLike) )
TrackActionsButton(
track = row.toTrackRef(),
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
}
} }
private const val SKELETON_PLAYLIST_ROWS = 8 private const val SKELETON_PLAYLIST_ROWS = 8
@@ -6,7 +6,6 @@
package com.fabledsword.minstrel.search.ui package com.fabledsword.minstrel.search.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@@ -42,7 +41,6 @@ import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -59,7 +57,7 @@ import com.fabledsword.minstrel.models.TrackRef
import com.fabledsword.minstrel.nav.AlbumDetail import com.fabledsword.minstrel.nav.AlbumDetail
import com.fabledsword.minstrel.nav.ArtistDetail import com.fabledsword.minstrel.nav.ArtistDetail
import com.fabledsword.minstrel.nav.Search as SearchRoute import com.fabledsword.minstrel.nav.Search as SearchRoute
import com.fabledsword.minstrel.shared.widgets.CachedDot import com.fabledsword.minstrel.shared.widgets.TrackRow
import com.fabledsword.minstrel.shared.widgets.LoadingCentered import com.fabledsword.minstrel.shared.widgets.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
@@ -278,47 +276,25 @@ private fun TrackResultRow(
onNavigateToAlbum: (String) -> Unit, onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit, onNavigateToArtist: (String) -> Unit,
) { ) {
val titleColor = if (nowPlaying) { TrackRow(
MaterialTheme.colorScheme.primary title = track.title,
} else { artist = track.artistName.ifEmpty { track.albumTitle },
MaterialTheme.colorScheme.onSurface trackId = track.id,
} onClick = onClick,
Row( nowPlaying = nowPlaying,
modifier = Modifier enabled = track.streamUrl.isNotEmpty(),
.fillMaxWidth()
.clickable(enabled = track.streamUrl.isNotEmpty(), onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
) { leading = {
TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title) TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title)
Column(modifier = Modifier.weight(1f)) { },
Text( trailing = {
text = track.title, TrackActionsButton(
style = MaterialTheme.typography.bodyLarge, track = track,
color = titleColor, onNavigateToAlbum = onNavigateToAlbum,
maxLines = 1, onNavigateToArtist = onNavigateToArtist,
overflow = TextOverflow.Ellipsis,
) )
val secondary = track.artistName.takeIf { it.isNotEmpty() } },
?: track.albumTitle.takeIf { it.isNotEmpty() } )
if (secondary != null) {
Text(
text = secondary,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
CachedDot(track.id)
TrackActionsButton(
track = track,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
} }
@Composable @Composable
@@ -1,6 +1,7 @@
package com.fabledsword.minstrel.shared.widgets package com.fabledsword.minstrel.shared.widgets
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
@@ -40,6 +41,7 @@ fun TrackRow(
nowPlaying: Boolean = false, nowPlaying: Boolean = false,
enabled: Boolean = true, enabled: Boolean = true,
contentAlpha: Float = 1f, contentAlpha: Float = 1f,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
leading: @Composable () -> Unit = {}, leading: @Composable () -> Unit = {},
trailing: @Composable RowScope.() -> Unit = {}, trailing: @Composable RowScope.() -> Unit = {},
) { ) {
@@ -54,6 +56,7 @@ fun TrackRow(
.clickable(enabled = enabled, onClick = onClick) .clickable(enabled = enabled, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp), .padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = horizontalArrangement,
) { ) {
leading() leading()
Column(modifier = Modifier.weight(1f)) { Column(modifier = Modifier.weight(1f)) {