feat: use material icons
This commit is contained in:
@@ -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
@@ -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)
|
||||||
|
|||||||
Vendored
+6
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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" }
|
||||||
|
|||||||
Reference in New Issue
Block a user