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.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()
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user