feat(android): system-playlist placeholder cards on Home (audit v3 §4.4, part of #28)
The Home Playlists row now always renders the For You + Discover + 3× Songs-like slots: a real PlaylistCard when the system playlist has generated, otherwise a PlaylistPlaceholderCard showing its build state — building / failed / seed-needed / pending — derived from GET /api/me/system-playlists-status. Mirrors Flutter's _buildPlaylistsRow + PlaylistPlaceholderCard. * SystemPlaylistsStatus domain + wire; MeApi.getSystemPlaylistsStatus; HomeRepository.getSystemPlaylistsStatus. * HomeViewModel fetches the status in refresh() and exposes it as a StateFlow; HomeScreen collects it and threads it to the row builder. * PlaylistPlaceholderCard widget (sized to match PlaylistCard) with a status chip + variant subtitle. * buildPlaylistsRow / variantFor port the slot logic; user playlists follow the fixed system slots. The offline-pool cards (§4.3) are NOT in this commit — Android has no local recently-played cache (history is fetch-on-visit), so the "Recently played" pool has no offline source. Deferring that half until the history snapshot cache lands; see #28 notes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ package com.fabledsword.minstrel.api.endpoints
|
|||||||
|
|
||||||
import com.fabledsword.minstrel.models.wire.ListenBrainzStatusWire
|
import com.fabledsword.minstrel.models.wire.ListenBrainzStatusWire
|
||||||
import com.fabledsword.minstrel.models.wire.MyProfileWire
|
import com.fabledsword.minstrel.models.wire.MyProfileWire
|
||||||
|
import com.fabledsword.minstrel.models.wire.SystemPlaylistsStatusWire
|
||||||
import kotlinx.serialization.SerialName
|
import kotlinx.serialization.SerialName
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import retrofit2.http.Body
|
import retrofit2.http.Body
|
||||||
@@ -43,6 +44,14 @@ interface MeApi {
|
|||||||
@GET("api/me/listenbrainz")
|
@GET("api/me/listenbrainz")
|
||||||
suspend fun getListenBrainz(): ListenBrainzStatusWire
|
suspend fun getListenBrainz(): ListenBrainzStatusWire
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caller's most recent system-playlist build state. Drives the
|
||||||
|
* Home placeholder cards (building / failed / pending / seed-needed)
|
||||||
|
* for system playlists that haven't generated yet.
|
||||||
|
*/
|
||||||
|
@GET("api/me/system-playlists-status")
|
||||||
|
suspend fun getSystemPlaylistsStatus(): SystemPlaylistsStatusWire
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PUT against the same `/api/me/listenbrainz` endpoint with one
|
* PUT against the same `/api/me/listenbrainz` endpoint with one
|
||||||
* of two shapes — `{token: ...}` stashes a new token,
|
* of two shapes — `{token: ...}` stashes a new token,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.fabledsword.minstrel.home.data
|
package com.fabledsword.minstrel.home.data
|
||||||
|
|
||||||
import com.fabledsword.minstrel.api.endpoints.HomeApi
|
import com.fabledsword.minstrel.api.endpoints.HomeApi
|
||||||
|
import com.fabledsword.minstrel.api.endpoints.MeApi
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
|
import com.fabledsword.minstrel.cache.db.dao.CachedAlbumDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
|
import com.fabledsword.minstrel.cache.db.dao.CachedArtistDao
|
||||||
import com.fabledsword.minstrel.cache.db.dao.CachedHomeIndexDao
|
import com.fabledsword.minstrel.cache.db.dao.CachedHomeIndexDao
|
||||||
@@ -10,6 +11,7 @@ import com.fabledsword.minstrel.library.data.toDomain
|
|||||||
import com.fabledsword.minstrel.metadata.MetadataProvider
|
import com.fabledsword.minstrel.metadata.MetadataProvider
|
||||||
import com.fabledsword.minstrel.models.AlbumRef
|
import com.fabledsword.minstrel.models.AlbumRef
|
||||||
import com.fabledsword.minstrel.models.ArtistRef
|
import com.fabledsword.minstrel.models.ArtistRef
|
||||||
|
import com.fabledsword.minstrel.models.SystemPlaylistsStatus
|
||||||
import com.fabledsword.minstrel.models.TrackRef
|
import com.fabledsword.minstrel.models.TrackRef
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
@@ -45,6 +47,20 @@ class HomeRepository @Inject constructor(
|
|||||||
retrofit: Retrofit,
|
retrofit: Retrofit,
|
||||||
) {
|
) {
|
||||||
private val api: HomeApi = retrofit.create()
|
private val api: HomeApi = retrofit.create()
|
||||||
|
private val meApi: MeApi = retrofit.create()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caller's system-playlist build state for the Home placeholder
|
||||||
|
* cards. One-shot GET; the caller refreshes alongside the index.
|
||||||
|
*/
|
||||||
|
suspend fun getSystemPlaylistsStatus(): SystemPlaylistsStatus =
|
||||||
|
meApi.getSystemPlaylistsStatus().let {
|
||||||
|
SystemPlaylistsStatus(
|
||||||
|
inFlight = it.inFlight,
|
||||||
|
lastRunAt = it.lastRunAt,
|
||||||
|
lastError = it.lastError,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun observeRecentlyAddedAlbums(): Flow<List<AlbumRef>> =
|
fun observeRecentlyAddedAlbums(): Flow<List<AlbumRef>> =
|
||||||
homeIndexDao.observeBySection(SECTION_RECENTLY_ADDED_ALBUMS).map { hydrateAlbums(it) }
|
homeIndexDao.observeBySection(SECTION_RECENTLY_ADDED_ALBUMS).map { hydrateAlbums(it) }
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ import com.fabledsword.minstrel.library.widgets.ArtistCard
|
|||||||
import com.fabledsword.minstrel.models.AlbumRef
|
import com.fabledsword.minstrel.models.AlbumRef
|
||||||
import com.fabledsword.minstrel.models.ArtistRef
|
import com.fabledsword.minstrel.models.ArtistRef
|
||||||
import com.fabledsword.minstrel.models.PlaylistRef
|
import com.fabledsword.minstrel.models.PlaylistRef
|
||||||
|
import com.fabledsword.minstrel.models.SystemPlaylistsStatus
|
||||||
import com.fabledsword.minstrel.models.TrackRef
|
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
|
||||||
@@ -57,6 +58,7 @@ import com.fabledsword.minstrel.nav.Home
|
|||||||
import com.fabledsword.minstrel.nav.PlaylistDetail
|
import com.fabledsword.minstrel.nav.PlaylistDetail
|
||||||
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
import com.fabledsword.minstrel.playlists.data.PlaylistsRepository
|
||||||
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
|
import com.fabledsword.minstrel.playlists.widgets.PlaylistCard
|
||||||
|
import com.fabledsword.minstrel.playlists.widgets.PlaylistPlaceholderCard
|
||||||
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.MainAppBarActions
|
import com.fabledsword.minstrel.shared.widgets.MainAppBarActions
|
||||||
@@ -66,8 +68,10 @@ import com.fabledsword.minstrel.shared.widgets.SkeletonArtistTile
|
|||||||
import com.fabledsword.minstrel.shared.widgets.SkeletonSectionHeader
|
import com.fabledsword.minstrel.shared.widgets.SkeletonSectionHeader
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -113,6 +117,11 @@ class HomeViewModel @Inject constructor(
|
|||||||
private val player: com.fabledsword.minstrel.player.PlayerController,
|
private val player: com.fabledsword.minstrel.player.PlayerController,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
private val systemStatusInternal = MutableStateFlow(SystemPlaylistsStatus())
|
||||||
|
|
||||||
|
/** System-playlist build status for the Home placeholder cards. */
|
||||||
|
val systemStatus: StateFlow<SystemPlaylistsStatus> = systemStatusInternal.asStateFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
@@ -140,8 +149,13 @@ class HomeViewModel @Inject constructor(
|
|||||||
fun refresh(): Job = viewModelScope.launch {
|
fun refresh(): Job = viewModelScope.launch {
|
||||||
val home = launch { runCatching { homeRepository.refreshIndex() } }
|
val home = launch { runCatching { homeRepository.refreshIndex() } }
|
||||||
val lists = launch { runCatching { playlistsRepository.refreshList() } }
|
val lists = launch { runCatching { playlistsRepository.refreshList() } }
|
||||||
|
val status = launch {
|
||||||
|
runCatching { homeRepository.getSystemPlaylistsStatus() }
|
||||||
|
.onSuccess { systemStatusInternal.value = it }
|
||||||
|
}
|
||||||
home.join()
|
home.join()
|
||||||
lists.join()
|
lists.join()
|
||||||
|
status.join()
|
||||||
}
|
}
|
||||||
|
|
||||||
val uiState: StateFlow<HomeUiState> = combineHomeFlows().stateIn(
|
val uiState: StateFlow<HomeUiState> = combineHomeFlows().stateIn(
|
||||||
@@ -203,6 +217,7 @@ fun HomeScreen(
|
|||||||
},
|
},
|
||||||
) { inner ->
|
) { inner ->
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val systemStatus by viewModel.systemStatus.collectAsStateWithLifecycle()
|
||||||
PullToRefreshScaffold(
|
PullToRefreshScaffold(
|
||||||
onRefresh = { viewModel.refresh().join() },
|
onRefresh = { viewModel.refresh().join() },
|
||||||
modifier = Modifier.fillMaxSize().padding(inner),
|
modifier = Modifier.fillMaxSize().padding(inner),
|
||||||
@@ -222,6 +237,7 @@ fun HomeScreen(
|
|||||||
)
|
)
|
||||||
is HomeUiState.Success -> HomeSuccessContent(
|
is HomeUiState.Success -> HomeSuccessContent(
|
||||||
sections = s.sections,
|
sections = s.sections,
|
||||||
|
systemStatus = systemStatus,
|
||||||
onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) },
|
onAlbumClick = { id -> navController.navigate(AlbumDetail(id)) },
|
||||||
onArtistClick = { id -> navController.navigate(ArtistDetail(id)) },
|
onArtistClick = { id -> navController.navigate(ArtistDetail(id)) },
|
||||||
onPlaylistClick = { id -> navController.navigate(PlaylistDetail(id)) },
|
onPlaylistClick = { id -> navController.navigate(PlaylistDetail(id)) },
|
||||||
@@ -280,6 +296,7 @@ private fun SkeletonArtistRow() {
|
|||||||
@Composable
|
@Composable
|
||||||
private fun HomeSuccessContent(
|
private fun HomeSuccessContent(
|
||||||
sections: HomeSections,
|
sections: HomeSections,
|
||||||
|
systemStatus: SystemPlaylistsStatus,
|
||||||
onAlbumClick: (String) -> Unit,
|
onAlbumClick: (String) -> Unit,
|
||||||
onArtistClick: (String) -> Unit,
|
onArtistClick: (String) -> Unit,
|
||||||
onPlaylistClick: (String) -> Unit,
|
onPlaylistClick: (String) -> Unit,
|
||||||
@@ -289,8 +306,11 @@ private fun HomeSuccessContent(
|
|||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
contentPadding = PaddingValues(vertical = 8.dp),
|
contentPadding = PaddingValues(vertical = 8.dp),
|
||||||
) {
|
) {
|
||||||
if (sections.playlists.isNotEmpty()) {
|
item {
|
||||||
item { PlaylistsRow(sections.playlists, onPlaylistClick) }
|
// Always rendered: real system/user playlists, with
|
||||||
|
// placeholder cards filling the For You / Discover /
|
||||||
|
// 3× Songs-like slots that haven't generated yet.
|
||||||
|
PlaylistsRow(buildPlaylistsRow(sections.playlists, systemStatus), onPlaylistClick)
|
||||||
}
|
}
|
||||||
recentlyAddedSection(sections.recentlyAddedAlbums, onAlbumClick)
|
recentlyAddedSection(sections.recentlyAddedAlbums, onAlbumClick)
|
||||||
rediscoverSection(
|
rediscoverSection(
|
||||||
@@ -443,16 +463,66 @@ private fun AlbumsRow(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun PlaylistsRow(
|
private fun PlaylistsRow(
|
||||||
playlists: List<PlaylistRef>,
|
rowItems: List<PlaylistRowItem>,
|
||||||
onPlaylistClick: (String) -> Unit,
|
onPlaylistClick: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
HorizontalScrollRow(title = "Playlists") {
|
HorizontalScrollRow(title = "Playlists") {
|
||||||
items(items = playlists, key = { it.id }) { playlist ->
|
itemsIndexed(items = rowItems) { _, item ->
|
||||||
PlaylistCard(playlist = playlist, onClick = { onPlaylistClick(playlist.id) })
|
when (item) {
|
||||||
|
is PlaylistRowItem.Real -> PlaylistCard(
|
||||||
|
playlist = item.playlist,
|
||||||
|
onClick = { onPlaylistClick(item.playlist.id) },
|
||||||
|
)
|
||||||
|
is PlaylistRowItem.Placeholder -> PlaylistPlaceholderCard(
|
||||||
|
label = item.label,
|
||||||
|
variant = item.variant,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Either a real playlist tile or a not-yet-generated placeholder slot. */
|
||||||
|
private sealed interface PlaylistRowItem {
|
||||||
|
data class Real(val playlist: PlaylistRef) : PlaylistRowItem
|
||||||
|
data class Placeholder(val label: String, val variant: String) : PlaylistRowItem
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds the Home Playlists row: For You + Discover + 3× Songs-like
|
||||||
|
* fixed slots (real card when generated, placeholder otherwise),
|
||||||
|
* then any user-owned playlists. Mirrors Flutter's `_buildPlaylistsRow`.
|
||||||
|
*/
|
||||||
|
private fun buildPlaylistsRow(
|
||||||
|
owned: List<PlaylistRef>,
|
||||||
|
status: SystemPlaylistsStatus,
|
||||||
|
): List<PlaylistRowItem> {
|
||||||
|
val out = mutableListOf<PlaylistRowItem>()
|
||||||
|
out += owned.firstOrNull { it.systemVariant == "for_you" }
|
||||||
|
?.let { PlaylistRowItem.Real(it) }
|
||||||
|
?: PlaylistRowItem.Placeholder("For You", variantFor("for-you", status))
|
||||||
|
out += owned.firstOrNull { it.systemVariant == "discover" }
|
||||||
|
?.let { PlaylistRowItem.Real(it) }
|
||||||
|
?: PlaylistRowItem.Placeholder("Discover", variantFor("discover", status))
|
||||||
|
val songsLike = owned.filter { it.systemVariant == "songs_like_artist" }.take(3)
|
||||||
|
for (i in 0 until SONGS_LIKE_SLOTS) {
|
||||||
|
out += songsLike.getOrNull(i)
|
||||||
|
?.let { PlaylistRowItem.Real(it) }
|
||||||
|
?: PlaylistRowItem.Placeholder("Songs like…", variantFor("songs-like", status))
|
||||||
|
}
|
||||||
|
owned.filter { it.systemVariant == null }.forEach { out += PlaylistRowItem.Real(it) }
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun variantFor(slot: String, s: SystemPlaylistsStatus): String = when {
|
||||||
|
s.inFlight -> "building"
|
||||||
|
s.lastError != null -> "failed"
|
||||||
|
slot == "songs-like" && s.lastRunAt != null -> "seed-needed"
|
||||||
|
else -> "pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SONGS_LIKE_SLOTS = 3
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ArtistsRow(
|
private fun ArtistsRow(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package com.fabledsword.minstrel.models
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caller's most recent system_playlist_runs state, driving the Home
|
||||||
|
* placeholder cards for not-yet-generated system playlists. Mirrors
|
||||||
|
* `flutter_client/lib/models/system_playlists_status.dart` and the
|
||||||
|
* server's `systemPlaylistsStatusResp`.
|
||||||
|
*
|
||||||
|
* Zero values (inFlight=false, both timestamps null) mean the user
|
||||||
|
* has never had a build attempted — the placeholders read as
|
||||||
|
* "pending".
|
||||||
|
*/
|
||||||
|
data class SystemPlaylistsStatus(
|
||||||
|
val inFlight: Boolean = false,
|
||||||
|
val lastRunAt: String? = null,
|
||||||
|
val lastError: String? = null,
|
||||||
|
)
|
||||||
+14
@@ -0,0 +1,14 @@
|
|||||||
|
package com.fabledsword.minstrel.models.wire
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire shape for `GET /api/me/system-playlists-status`.
|
||||||
|
*/
|
||||||
|
@Serializable
|
||||||
|
data class SystemPlaylistsStatusWire(
|
||||||
|
@SerialName("in_flight") val inFlight: Boolean = false,
|
||||||
|
@SerialName("last_run_at") val lastRunAt: String? = null,
|
||||||
|
@SerialName("last_error") val lastError: String? = null,
|
||||||
|
)
|
||||||
+115
@@ -0,0 +1,115 @@
|
|||||||
|
package com.fabledsword.minstrel.playlists.widgets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.composables.icons.lucide.ListMusic
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.RefreshCw
|
||||||
|
import com.composables.icons.lucide.TriangleAlert
|
||||||
|
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder tile for a system playlist that hasn't generated yet.
|
||||||
|
* Mirrors `flutter_client/lib/playlists/widgets/playlist_placeholder_card.dart`.
|
||||||
|
* Sized to match [PlaylistCard] so the Home Playlists row stays
|
||||||
|
* visually consistent.
|
||||||
|
*
|
||||||
|
* [variant] is one of building / failed / pending / seed-needed,
|
||||||
|
* derived by the caller from the system-playlists build status:
|
||||||
|
* - building → a run is in flight
|
||||||
|
* - failed → the last run errored
|
||||||
|
* - seed-needed → songs-like slot with no seed artist liked yet
|
||||||
|
* - pending → never built / waiting for the next scheduled run
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun PlaylistPlaceholderCard(
|
||||||
|
label: String,
|
||||||
|
variant: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier.width(176.dp).padding(horizontal = 8.dp)) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(144.dp)
|
||||||
|
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm))
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = iconFor(variant),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(40.dp),
|
||||||
|
)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.padding(6.dp),
|
||||||
|
shape = RoundedCornerShape(FabledSwordFlatTokens.radiusSm),
|
||||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = STATUS_BG_ALPHA),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = statusLabelFor(variant),
|
||||||
|
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = subtitleFor(variant),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val STATUS_BG_ALPHA = 0.85f
|
||||||
|
|
||||||
|
private fun iconFor(variant: String) = when (variant) {
|
||||||
|
"failed" -> Lucide.TriangleAlert
|
||||||
|
"building" -> Lucide.RefreshCw
|
||||||
|
else -> Lucide.ListMusic
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun statusLabelFor(variant: String): String = when (variant) {
|
||||||
|
"building" -> "Building"
|
||||||
|
"failed" -> "Failed"
|
||||||
|
"seed-needed" -> "Needs a seed"
|
||||||
|
else -> "Pending"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun subtitleFor(variant: String): String = when (variant) {
|
||||||
|
"building" -> "Generating now…"
|
||||||
|
"failed" -> "Last build failed"
|
||||||
|
"seed-needed" -> "Like an artist to seed this"
|
||||||
|
else -> "Coming soon"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user