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.
This commit is contained in:
@@ -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()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user