Release: web UX overhaul + Android native port + server polish #59

Merged
bvandeusen merged 298 commits from dev into main 2026-06-01 16:11:21 -04:00
Showing only changes of commit 5b050022f1 - Show all commits
@@ -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()
}