diff --git a/android/app/src/main/java/com/fabledsword/minstrel/models/QuarantineRef.kt b/android/app/src/main/java/com/fabledsword/minstrel/models/QuarantineRef.kt index e976feac..80635aaa 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/models/QuarantineRef.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/models/QuarantineRef.kt @@ -28,4 +28,13 @@ data class QuarantineRef( "duplicate" -> "Duplicate" else -> "Other" } + + /** + * Cover URL derived from the parent album's + * `/api/albums/{id}/cover` endpoint. Uses the placeholder host + * that `BaseUrlInterceptor` rewrites to the live server URL. + * Empty when the row has no album binding (rare). + */ + val coverUrl: String + get() = if (albumId.isEmpty()) "" else "http://placeholder.invalid/api/albums/$albumId/cover" } diff --git a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt index 64afa193..ccae3eca 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/quarantine/ui/HiddenTab.kt @@ -29,6 +29,7 @@ import com.composables.icons.lucide.Lucide import com.fabledsword.minstrel.models.QuarantineRef import com.fabledsword.minstrel.shared.widgets.EmptyState import com.fabledsword.minstrel.shared.widgets.PullToRefreshScaffold +import com.fabledsword.minstrel.shared.widgets.TrackCoverThumb @Composable fun HiddenTab(viewModel: HiddenTabViewModel = hiltViewModel()) { @@ -73,6 +74,7 @@ private fun HiddenRow(row: QuarantineRef, onUnflag: () -> Unit) { verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp), ) { + TrackCoverThumb(coverUrl = row.coverUrl, contentDescription = row.trackTitle) Column(modifier = Modifier.weight(1f)) { Text( text = row.trackTitle, @@ -88,12 +90,24 @@ private fun HiddenRow(row: QuarantineRef, onUnflag: () -> Unit) { maxLines = 1, overflow = TextOverflow.Ellipsis, ) - AssistChip( - onClick = {}, - enabled = false, - label = { Text(row.reasonLabel) }, + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.padding(top = 4.dp), - ) + ) { + AssistChip( + onClick = {}, + enabled = false, + label = { Text(row.reasonLabel) }, + ) + if (row.createdAt.isNotEmpty()) { + Text( + text = relativeTime(row.createdAt), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } } IconButton(onClick = onUnflag) { Icon( @@ -111,3 +125,36 @@ private fun LoadingCentered() { CircularProgressIndicator(color = MaterialTheme.colorScheme.primary) } } + +// ─── Time helper (duplicated from HistoryTab to keep the widget local) ── + +private const val MINUTES_PER_HOUR = 60L +private const val HOURS_PER_DAY = 24L +private const val DAYS_PER_WEEK = 7L + +private fun relativeTime(iso: String): String { + val parsed = runCatching { java.time.OffsetDateTime.parse(iso) }.getOrNull() + ?: return iso + val local = parsed.atZoneSameInstant(java.time.ZoneId.systemDefault()) + val now = java.time.OffsetDateTime.now(java.time.ZoneId.systemDefault()) + val minutes = java.time.temporal.ChronoUnit.MINUTES.between(local, now) + val hours = java.time.temporal.ChronoUnit.HOURS.between(local, now) + val days = java.time.temporal.ChronoUnit.DAYS.between(local, now) + return when { + minutes < MINUTES_PER_HOUR -> "${minutes.coerceAtLeast(1)}m ago" + hours < HOURS_PER_DAY -> "${hours}h ago" + days < DAYS_PER_WEEK -> { + val dow = local.dayOfWeek.getDisplayName( + java.time.format.TextStyle.SHORT, + java.util.Locale.getDefault(), + ) + val time = local.toLocalTime() + .format(java.time.format.DateTimeFormatter.ofPattern("HH:mm")) + "$dow $time" + } + else -> { + val pattern = if (local.year == now.year) "MMM d" else "MMM d, yyyy" + local.toLocalDate().format(java.time.format.DateTimeFormatter.ofPattern(pattern)) + } + } +}