From 2cf02ddcea4cfb0c54306810ee39c73e70efd30a Mon Sep 17 00:00:00 2001 From: Karim Abdul-Samad Date: Sun, 26 Jul 2026 19:51:49 -0400 Subject: [PATCH] feat: use material icons --- README.md | 4 +- app/build.gradle.kts | 12 +++- app/proguard-rules.pro | 6 ++ .../ksamad/musicremote/ui/NowPlayingScreen.kt | 57 ++++++++++++++----- deps.json | 11 ++++ docs/TODO.md | 22 +++---- gradle/libs.versions.toml | 1 + 7 files changed, 85 insertions(+), 28 deletions(-) create mode 100644 app/proguard-rules.pro diff --git a/README.md b/README.md index 634131e..cc513be 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ app/ src/test/kotlin/... A plain JVM unit test ``` -## Everyday development (recommended) +## Everyday Development (Recommended) Use the dev shell — it puts a JDK, Gradle, and the Android SDK on your PATH with the right environment variables set: @@ -63,7 +63,7 @@ command-line flow above is fully independent of it.) nix run .#emulate ``` -## Reproducible build with Nix +## Reproducible Build with Nix ```sh nix build # -> ./result/music-remote.apk diff --git a/app/build.gradle.kts b/app/build.gradle.kts index bec5bad..5f553e4 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -23,7 +23,16 @@ android { buildTypes { release { - isMinifyEnabled = false + // R8 code shrinking + resource shrinking. Critical with + // material-icons-extended: it bundles thousands of vector icons, and + // R8 strips everything we don't reference (debug builds can't shrink, + // so the debug APK stays large — that's expected). + isMinifyEnabled = true + isShrinkResources = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro", + ) } } @@ -106,6 +115,7 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + implementation(libs.androidx.material.icons.extended) debugImplementation(libs.androidx.ui.tooling) testImplementation(libs.junit) diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..510ca32 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,6 @@ +# Project-specific ProGuard/R8 rules. +# +# AGP's default rules plus the consumer rules shipped by AndroidX/Compose cover +# this app already (Compose, coroutines, DataStore, our own classes are all +# either kept or safely shrunk/renamed). Add app-specific keeps here if release +# builds ever misbehave. diff --git a/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt b/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt index 3741a87..14d536f 100644 --- a/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt +++ b/app/src/main/kotlin/ca/ksamad/musicremote/ui/NowPlayingScreen.kt @@ -1,7 +1,6 @@ package ca.ksamad.musicremote.ui import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -10,8 +9,17 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.filled.Repeat +import androidx.compose.material.icons.filled.Shuffle +import androidx.compose.material.icons.filled.SkipNext +import androidx.compose.material.icons.filled.SkipPrevious +import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material3.FilledIconButton import androidx.compose.material3.FilterChip +import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Slider @@ -29,7 +37,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.compose.collectAsStateWithLifecycle import ca.ksamad.musicremote.mpd.model.MpdStatus import ca.ksamad.musicremote.mpd.model.PlayerState @@ -91,13 +98,25 @@ fun NowPlayingScreen(vm: PlayerViewModel) { horizontalArrangement = Arrangement.spacedBy(16.dp), ) { IconButton(onClick = vm::previous, modifier = Modifier.size(56.dp)) { - GlyphText("⏮", 28.sp) + Icon( + Icons.Filled.SkipPrevious, + contentDescription = "Previous", + modifier = Modifier.size(36.dp), + ) } FilledIconButton(onClick = vm::togglePlayPause, modifier = Modifier.size(72.dp)) { - GlyphText(if (playing) "⏸" else "▶", 32.sp) + Icon( + if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, + contentDescription = if (playing) "Pause" else "Play", + modifier = Modifier.size(40.dp), + ) } IconButton(onClick = vm::next, modifier = Modifier.size(56.dp)) { - GlyphText("⏭", 28.sp) + Icon( + Icons.Filled.SkipNext, + contentDescription = "Next", + modifier = Modifier.size(36.dp), + ) } } @@ -113,11 +132,25 @@ fun NowPlayingScreen(vm: PlayerViewModel) { selected = status?.repeat == true, onClick = { vm.setRepeat(status?.repeat != true) }, label = { Text("Repeat") }, + leadingIcon = { + Icon( + Icons.Filled.Repeat, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, ) FilterChip( selected = status?.random == true, onClick = { vm.setRandom(status?.random != true) }, label = { Text("Shuffle") }, + leadingIcon = { + Icon( + Icons.Filled.Shuffle, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + }, ) } @@ -181,7 +214,11 @@ private fun VolumeControl(volume: Int?, onSetVolume: (Int) -> Unit) { val shown = dragValue ?: (volume ?: 0).toFloat() Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { - GlyphText("🔈", 18.sp) + Icon( + Icons.Filled.VolumeUp, + contentDescription = "Volume", + modifier = Modifier.size(24.dp), + ) Slider( value = shown, onValueChange = { dragValue = it }, @@ -204,14 +241,6 @@ private fun VolumeControl(volume: Int?, onSetVolume: (Int) -> Unit) { } } -/** A centered glyph, used in place of vector icons to avoid an extra dependency. */ -@Composable -private fun GlyphText(glyph: String, size: androidx.compose.ui.unit.TextUnit) { - Box(contentAlignment = Alignment.Center) { - Text(glyph, fontSize = size) - } -} - private fun formatTime(seconds: Double): String { val total = seconds.toInt().coerceAtLeast(0) val m = total / 60 diff --git a/deps.json b/deps.json index e5f0438..a0979ed 100644 --- a/deps.json +++ b/deps.json @@ -152,6 +152,14 @@ "module": "sha256-ypkx5dgAfp/FkejyIxv5ccyWJnOIUNAg7uFhceaeW+E=", "pom": "sha256-yFXNhvtbSweZk6m8QDD0cGxxZuKtyeZgWt/NlwII4sg=" }, + "androidx/compose/material#material-icons-extended-android/1.7.5": { + "module": "sha256-yXHwWJyYUR2dBMQeAgN4m+ToeZ4SPTYFpmExNtJsbiw=", + "pom": "sha256-mLUsnodvPo6Rpgx2eOjNA0Ih6cZxxO6b/mzrB3mHSLs=" + }, + "androidx/compose/material#material-icons-extended/1.7.5": { + "module": "sha256-Xhps2Xt01WgHdqsOF41f14xMeEaouRV6ksWzeSau7AU=", + "pom": "sha256-sGsqeoplw/M1dIWae3dnUhZbIi8AM4Pp7hRdvBl4/Qw=" + }, "androidx/compose/material#material-ripple-android/1.7.5": { "module": "sha256-pVC0zDNcV4dq2j1Z56ufNa/od3bfNDEMquOqD6wzHK4=", "pom": "sha256-6tiSsY1o2nA9XSA4d5cyWHqoMgJAIYBxOwbvr4wa/Vc=" @@ -170,6 +178,9 @@ "androidx/compose/material/material-icons-core-android/1.7.5/material-icons-core-android-1.7.5": { "aar": "sha256-6C63AxCxPlK+rlZt48G9bELg9s3zhNhXPqc6XRP3xkM=" }, + "androidx/compose/material/material-icons-extended-android/1.7.5/material-icons-extended-android-1.7.5": { + "aar": "sha256-44PfILSH7g2uuHYFTKnIohf8880kfNd22QLq4FroZn0=" + }, "androidx/compose/material/material-ripple-android/1.7.5/material-ripple-android-1.7.5": { "aar": "sha256-/pmfHail4GvAba79JKVgeBtWjW7T+mrRBKGMTa1RPN0=" }, diff --git a/docs/TODO.md b/docs/TODO.md index 0cc54b9..4d36144 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -8,19 +8,19 @@ Status legend: `[ ]` todo · `[~]` in progress · `[x]` done --- -## [ ] 1. Proper icons +## [x] 1. Proper icons -Replace the placeholder unicode glyphs (`GlyphText` in `NowPlayingScreen.kt` — -`⏮ ▶ ⏸ ⏭ 🔈`) with real Material icons. +Done. Replaced the placeholder unicode glyphs in `NowPlayingScreen.kt` with real +Material icons (`Icons.Filled.SkipPrevious/PlayArrow/Pause/SkipNext/VolumeUp`, +plus `Repeat`/`Shuffle` leading icons on the chips) from +`androidx.compose.material:material-icons-extended`. -- Preferred source: **Google Material Symbols / Icons**. In Compose the usual - route is the `androidx.compose.material:material-icons-*` artifacts - (`Icons.Filled.PlayArrow`, `SkipNext`, `VolumeUp`, …). `material-icons-core` - covers the common set; `material-icons-extended` has everything but is large — - prefer core, or import only the specific vector assets we use. -- Platform-bundled drawables (`android.R.drawable.ic_media_*`) exist but look - dated and vary by OEM — avoid; bundle our own for a consistent look. -- Adding the dependency means a `deps.json` regen (see README). +- That artifact bundles thousands of vectors, so the **debug** APK grew ~7 MB + (expected — debug can't shrink). Enabled **R8 + resource shrinking** on the + `release` build type, which strips the unused icons: release APK is ~1.2 MB. +- Considered `material-icons-core` (too small — no media icons) and bundling + individual vector drawables (leaner but manual); the extended dep + R8 was the + best effort/quality trade. ## [ ] 2. Settings menu diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c3821ef..48a1285 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -24,6 +24,7 @@ androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" } +androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } kotlinx-coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } junit = { group = "junit", name = "junit", version.ref = "junit" }