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
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -14,9 +12,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.models.TrackRef
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.LoadingCentered
import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold
@@ -171,48 +167,30 @@ private fun HistoryRow(
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
) {
val titleColor = if (nowPlaying) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = entry.track.streamUrl.isNotEmpty(), onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
TrackRow(
title = entry.track.title,
artist = entry.track.artistName.ifEmpty { entry.track.albumTitle },
trackId = entry.track.id,
onClick = onClick,
nowPlaying = nowPlaying,
enabled = entry.track.streamUrl.isNotEmpty(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
TrackCoverThumb(coverUrl = entry.track.coverUrl, contentDescription = entry.track.title)
Column(modifier = Modifier.weight(1f)) {
leading = {
TrackCoverThumb(coverUrl = entry.track.coverUrl, contentDescription = entry.track.title)
},
trailing = {
Text(
text = entry.track.title,
style = MaterialTheme.typography.bodyLarge,
color = titleColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = entry.track.artistName.ifEmpty { entry.track.albumTitle },
text = relativeTime(entry.playedAt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
CachedDot(entry.track.id)
Text(
text = relativeTime(entry.playedAt),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
TrackActionsButton(
track = entry.track,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
TrackActionsButton(
track = entry.track,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
},
)
}
// ─── Time helpers ────────────────────────────────────────────────────
@@ -1,6 +1,5 @@
package com.fabledsword.minstrel.likes.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
@@ -20,7 +19,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.ArtistDetail
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.HorizontalScrollRow
import com.fabledsword.minstrel.shared.widgets.LikeButton
@@ -259,42 +257,24 @@ private fun LikedTrackRow(
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
) {
val titleColor = if (nowPlaying) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = track.streamUrl.isNotEmpty(), onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
TrackRow(
title = track.title,
artist = track.artistName.ifEmpty { track.albumTitle },
trackId = track.id,
onClick = onClick,
nowPlaying = nowPlaying,
enabled = track.streamUrl.isNotEmpty(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title)
Column(modifier = Modifier.weight(1f)) {
Text(
text = track.title,
style = MaterialTheme.typography.bodyLarge,
color = titleColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
leading = {
TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title)
},
trailing = {
LikeButton(liked = true, onToggle = onUnlike)
TrackActionsButton(
track = track,
onNavigateToAlbum = onNavigateToAlbum,
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.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.PlaylistsRepository
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.PullToRefreshScaffold
import com.fabledsword.minstrel.shared.widgets.SkeletonTrackRow
@@ -497,55 +496,38 @@ private fun TrackRow(
) {
val enabled = row.isAvailable
val rowAlpha = if (enabled) 1f else UNAVAILABLE_ALPHA
val baseTitleColor = if (nowPlaying) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = enabled, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
TrackCoverThumb(
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,
TrackRow(
title = row.title,
artist = row.artistName.ifEmpty { row.albumTitle },
trackId = row.trackId.orEmpty(),
onClick = onClick,
nowPlaying = nowPlaying,
enabled = enabled,
contentAlpha = rowAlpha,
leading = {
TrackCoverThumb(
coverUrl = row.coverUrl,
contentDescription = row.title,
modifier = Modifier.padding(end = 12.dp),
)
},
trailing = {
Text(
text = row.artistName.ifEmpty { row.albumTitle },
text = formatDuration(row.durationSec, zero = "--:--"),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(start = 12.dp),
)
}
row.trackId?.let { CachedDot(it) }
Text(
text = formatDuration(row.durationSec, zero = "--:--"),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = rowAlpha),
modifier = Modifier.padding(start = 12.dp),
)
if (enabled) {
LikeButton(liked = liked, onToggle = onToggleLike)
TrackActionsButton(
track = row.toTrackRef(),
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
}
if (enabled) {
LikeButton(liked = liked, onToggle = onToggleLike)
TrackActionsButton(
track = row.toTrackRef(),
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
}
},
)
}
private const val SKELETON_PLAYLIST_ROWS = 8
@@ -6,7 +6,6 @@
package com.fabledsword.minstrel.search.ui
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
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.graphics.Color
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
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.ArtistDetail
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.MainAppBarActions
import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb
@@ -278,47 +276,25 @@ private fun TrackResultRow(
onNavigateToAlbum: (String) -> Unit,
onNavigateToArtist: (String) -> Unit,
) {
val titleColor = if (nowPlaying) {
MaterialTheme.colorScheme.primary
} else {
MaterialTheme.colorScheme.onSurface
}
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = track.streamUrl.isNotEmpty(), onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
TrackRow(
title = track.title,
artist = track.artistName.ifEmpty { track.albumTitle },
trackId = track.id,
onClick = onClick,
nowPlaying = nowPlaying,
enabled = track.streamUrl.isNotEmpty(),
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title)
Column(modifier = Modifier.weight(1f)) {
Text(
text = track.title,
style = MaterialTheme.typography.bodyLarge,
color = titleColor,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
leading = {
TrackCoverThumb(coverUrl = track.coverUrl, contentDescription = track.title)
},
trailing = {
TrackActionsButton(
track = track,
onNavigateToAlbum = onNavigateToAlbum,
onNavigateToArtist = onNavigateToArtist,
)
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
@@ -1,6 +1,7 @@
package com.fabledsword.minstrel.shared.widgets
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -40,6 +41,7 @@ fun TrackRow(
nowPlaying: Boolean = false,
enabled: Boolean = true,
contentAlpha: Float = 1f,
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
leading: @Composable () -> Unit = {},
trailing: @Composable RowScope.() -> Unit = {},
) {
@@ -54,6 +56,7 @@ fun TrackRow(
.clickable(enabled = enabled, onClick = onClick)
.padding(horizontal = 16.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = horizontalArrangement,
) {
leading()
Column(modifier = Modifier.weight(1f)) {