feat(android): filled heart for liked state, outlined for unliked
android / Build + lint + test (push) Successful in 3m35s
android / Build signed release APK (push) Has been skipped

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.
This commit is contained in:
2026-06-01 02:10:21 -04:00
parent 25c5b80add
commit 5b050022f1
@@ -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()
}