feat: use material icons

This commit is contained in:
2026-07-27 00:16:46 -04:00
parent 5930630b2e
commit 2cf02ddcea
7 changed files with 85 additions and 28 deletions
+2 -2
View File
@@ -27,7 +27,7 @@ app/
src/test/kotlin/... A plain JVM unit test 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 Use the dev shell — it puts a JDK, Gradle, and the Android SDK on your PATH with the
right environment variables set: right environment variables set:
@@ -63,7 +63,7 @@ command-line flow above is fully independent of it.)
nix run .#emulate nix run .#emulate
``` ```
## Reproducible build with Nix ## Reproducible Build with Nix
```sh ```sh
nix build # -> ./result/music-remote.apk nix build # -> ./result/music-remote.apk
+11 -1
View File
@@ -23,7 +23,16 @@ android {
buildTypes { buildTypes {
release { 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.graphics)
implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3) implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.tooling)
testImplementation(libs.junit) testImplementation(libs.junit)
+6
View File
@@ -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.
@@ -1,7 +1,6 @@
package ca.ksamad.musicremote.ui package ca.ksamad.musicremote.ui
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size 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.FilledIconButton
import androidx.compose.material3.FilterChip import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Slider 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.TextAlign
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import ca.ksamad.musicremote.mpd.model.MpdStatus import ca.ksamad.musicremote.mpd.model.MpdStatus
import ca.ksamad.musicremote.mpd.model.PlayerState import ca.ksamad.musicremote.mpd.model.PlayerState
@@ -91,13 +98,25 @@ fun NowPlayingScreen(vm: PlayerViewModel) {
horizontalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
IconButton(onClick = vm::previous, modifier = Modifier.size(56.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)) { 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)) { 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, selected = status?.repeat == true,
onClick = { vm.setRepeat(status?.repeat != true) }, onClick = { vm.setRepeat(status?.repeat != true) },
label = { Text("Repeat") }, label = { Text("Repeat") },
leadingIcon = {
Icon(
Icons.Filled.Repeat,
contentDescription = null,
modifier = Modifier.size(18.dp),
)
},
) )
FilterChip( FilterChip(
selected = status?.random == true, selected = status?.random == true,
onClick = { vm.setRandom(status?.random != true) }, onClick = { vm.setRandom(status?.random != true) },
label = { Text("Shuffle") }, 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() val shown = dragValue ?: (volume ?: 0).toFloat()
Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
GlyphText("🔈", 18.sp) Icon(
Icons.Filled.VolumeUp,
contentDescription = "Volume",
modifier = Modifier.size(24.dp),
)
Slider( Slider(
value = shown, value = shown,
onValueChange = { dragValue = it }, 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 { private fun formatTime(seconds: Double): String {
val total = seconds.toInt().coerceAtLeast(0) val total = seconds.toInt().coerceAtLeast(0)
val m = total / 60 val m = total / 60
+11
View File
@@ -152,6 +152,14 @@
"module": "sha256-ypkx5dgAfp/FkejyIxv5ccyWJnOIUNAg7uFhceaeW+E=", "module": "sha256-ypkx5dgAfp/FkejyIxv5ccyWJnOIUNAg7uFhceaeW+E=",
"pom": "sha256-yFXNhvtbSweZk6m8QDD0cGxxZuKtyeZgWt/NlwII4sg=" "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": { "androidx/compose/material#material-ripple-android/1.7.5": {
"module": "sha256-pVC0zDNcV4dq2j1Z56ufNa/od3bfNDEMquOqD6wzHK4=", "module": "sha256-pVC0zDNcV4dq2j1Z56ufNa/od3bfNDEMquOqD6wzHK4=",
"pom": "sha256-6tiSsY1o2nA9XSA4d5cyWHqoMgJAIYBxOwbvr4wa/Vc=" "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": { "androidx/compose/material/material-icons-core-android/1.7.5/material-icons-core-android-1.7.5": {
"aar": "sha256-6C63AxCxPlK+rlZt48G9bELg9s3zhNhXPqc6XRP3xkM=" "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": { "androidx/compose/material/material-ripple-android/1.7.5/material-ripple-android-1.7.5": {
"aar": "sha256-/pmfHail4GvAba79JKVgeBtWjW7T+mrRBKGMTa1RPN0=" "aar": "sha256-/pmfHail4GvAba79JKVgeBtWjW7T+mrRBKGMTa1RPN0="
}, },
+11 -11
View File
@@ -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` Done. Replaced the placeholder unicode glyphs in `NowPlayingScreen.kt` with real
`⏮ ▶ ⏸ ⏭ 🔈`) with real Material icons. 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 - That artifact bundles thousands of vectors, so the **debug** APK grew ~7 MB
route is the `androidx.compose.material:material-icons-*` artifacts (expected — debug can't shrink). Enabled **R8 + resource shrinking** on the
(`Icons.Filled.PlayArrow`, `SkipNext`, `VolumeUp`, …). `material-icons-core` `release` build type, which strips the unused icons: release APK is ~1.2 MB.
covers the common set; `material-icons-extended` has everything but is large — - Considered `material-icons-core` (too small — no media icons) and bundling
prefer core, or import only the specific vector assets we use. individual vector drawables (leaner but manual); the extended dep + R8 was the
- Platform-bundled drawables (`android.R.drawable.ic_media_*`) exist but look best effort/quality trade.
dated and vary by OEM — avoid; bundle our own for a consistent look.
- Adding the dependency means a `deps.json` regen (see README).
## [ ] 2. Settings menu ## [ ] 2. Settings menu
+1
View File
@@ -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 = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } 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" } 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" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }