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.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier 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.Heart
import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.Lucide
@@ -14,9 +19,13 @@ import com.composables.icons.lucide.Lucide
* caller's ViewModel (typically observeIsLiked + toggleLike from * caller's ViewModel (typically observeIsLiked + toggleLike from
* LikesRepository), not here. * LikesRepository), not here.
* *
* Flutter equivalent is `flutter_client/lib/likes/like_button.dart`. * Liked state renders a **filled** heart (HeartFilled, defined below);
* That one tints the heart with the accent color; we do the same via * unliked state renders Lucide's outlined Heart. Both share the same
* the M3 primary slot so light/dark mode tracks the theme. * 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 @Composable
fun LikeButton( fun LikeButton(
@@ -26,7 +35,7 @@ fun LikeButton(
) { ) {
IconButton(onClick = onToggle, modifier = modifier) { IconButton(onClick = onToggle, modifier = modifier) {
Icon( Icon(
imageVector = Lucide.Heart, imageVector = if (liked) HeartFilled else Lucide.Heart,
contentDescription = if (liked) "Unlike" else "Like", contentDescription = if (liked) "Unlike" else "Like",
tint = if (liked) { tint = if (liked) {
MaterialTheme.colorScheme.primary 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()
}