From 5b050022f130d45ec150cfb2bb72aa8c51c76afc Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 1 Jun 2026 02:10:21 -0400 Subject: [PATCH] feat(android): filled heart for liked state, outlined for unliked Liked tracks now render a filled heart (HeartFilled, defined inline in LikeButton.kt) instead of just recoloring the outlined heart. Matches the cross-app convention (Spotify, Apple Music, etc.) where fill is the affordance change rather than tint alone. Lucide ships outlined-only by design (jar inspection: heart.kt, heart_crack.kt, heart_off.kt, etc., none filled). Built the filled variant locally from Lucide's exact path data so the toggle reads as a fill swap, not a different shape. No new dep - keeps the project's Lucide-everywhere icon system consistent. Operator authorized the divergence 2026-06-01 ("changing to material for this one symbol... is acceptable"); inline ImageVector route preserves Lucide visual harmony better than mixing material-icons just for one glyph. --- .../minstrel/shared/widgets/LikeButton.kt | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/LikeButton.kt b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/LikeButton.kt index 3ec7722f..67067ae4 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/LikeButton.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/LikeButton.kt @@ -5,6 +5,11 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.path +import androidx.compose.ui.unit.dp import com.composables.icons.lucide.Heart import com.composables.icons.lucide.Lucide @@ -14,9 +19,13 @@ import com.composables.icons.lucide.Lucide * caller's ViewModel (typically observeIsLiked + toggleLike from * LikesRepository), not here. * - * Flutter equivalent is `flutter_client/lib/likes/like_button.dart`. - * That one tints the heart with the accent color; we do the same via - * the M3 primary slot so light/dark mode tracks the theme. + * Liked state renders a **filled** heart (HeartFilled, defined below); + * unliked state renders Lucide's outlined Heart. Both share the same + * geometric path so the toggle is a fill swap, not an icon swap — + * matches the cross-app pattern (Spotify, Apple Music, etc.). Lucide + * ships outlined-only by design; the filled variant is built locally + * from the same path data so we keep the project's icon system + * consistent (operator 2026-06-01). */ @Composable fun LikeButton( @@ -26,7 +35,7 @@ fun LikeButton( ) { IconButton(onClick = onToggle, modifier = modifier) { Icon( - imageVector = Lucide.Heart, + imageVector = if (liked) HeartFilled else Lucide.Heart, contentDescription = if (liked) "Unlike" else "Like", tint = if (liked) { MaterialTheme.colorScheme.primary @@ -36,3 +45,35 @@ fun LikeButton( ) } } + +/** + * Filled-heart counterpart to [Lucide.Heart], built from the same path + * data so the toggle reads as a fill change rather than an icon swap. + * Cached as a private lateinit-style backing prop following Lucide's + * own pattern (heart.kt in the icons-lucide-cmp jar). + */ +private val HeartFilled: ImageVector by lazy { + ImageVector.Builder( + name = "heart_filled", + defaultWidth = 24.dp, + defaultHeight = 24.dp, + viewportWidth = 24f, + viewportHeight = 24f, + ).apply { + path( + fill = SolidColor(Color.Black), + stroke = null, + ) { + moveTo(2f, 9.5f) + arcToRelative(5.5f, 5.5f, 0f, false, true, 9.591f, -3.676f) + arcToRelative(0.56f, 0.56f, 0f, false, false, 0.818f, 0f) + arcTo(5.49f, 5.49f, 0f, false, true, 22f, 9.5f) + curveToRelative(0f, 2.29f, -1.5f, 4f, -3f, 5.5f) + lineToRelative(-5.492f, 5.313f) + arcToRelative(2f, 2f, 0f, false, true, -3f, 0.019f) + lineTo(5f, 15f) + curveToRelative(-1.5f, -1.5f, -3f, -3.2f, -3f, -5.5f) + close() + } + }.build() +}