From 4222d59fed1c78ebae13197d692dc794d1b16af3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 10 Oct 2026 01:53:30 +0000 Subject: [PATCH] android: Material Symbols (rounded) icons, like desktop Desktop already draws Material Symbols Rounded (scripts in desktop/scripts/gen-icons.mjs). Android used androidx material-icons-extended (the older Material Icons, filled/outlined styles). scripts/gen-android-icons.mjs now generates ui/icons/HocketIcons.kt from the same Iconify dataset: the 96 glyphs the app uses, as lazy Compose ImageVectors, with filled and outlined rounded variants so the bottom bar keeps marking the selected place by fill. HocketIcons mirrors androidx's Icons layout, so call sites only swap the receiver. Path data is re-spelled with explicit separators; every Material Symbols path renders pixel-identical before and after in Chromium. Drops the material-icons-extended dependency. Closes #41 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01NuB6eB6f9VjDfxTSEfsRQy --- android/README.md | 9 + android/app/build.gradle.kts | 1 - .../app/hocket/ui/components/ActionSheet.kt | 103 +++----- .../app/hocket/ui/components/JobsIndicator.kt | 32 +-- .../app/hocket/ui/components/RatingStars.kt | 6 +- .../java/app/hocket/ui/components/Rows.kt | 26 +- .../hocket/ui/components/SelectionToolbar.kt | 11 +- .../app/hocket/ui/components/SwipeActions.kt | 21 +- .../java/app/hocket/ui/icons/HocketIcons.kt | 225 ++++++++++++++++++ .../java/app/hocket/ui/lyrics/LyricsPage.kt | 11 +- .../java/app/hocket/ui/nav/AccountSheet.kt | 17 +- .../main/java/app/hocket/ui/nav/AppRoot.kt | 15 -- .../main/java/app/hocket/ui/nav/BottomBar.kt | 59 ++--- .../java/app/hocket/ui/player/HandoffSheet.kt | 12 +- .../app/hocket/ui/player/MiniPlayerBar.kt | 9 +- .../app/hocket/ui/player/NowPlayingPage.kt | 30 +-- .../java/app/hocket/ui/player/Transport.kt | 12 +- .../java/app/hocket/ui/queue/QueuePanel.kt | 21 +- .../java/app/hocket/ui/queue/RecentQueues.kt | 20 +- .../hocket/ui/screens/detail/DetailScreens.kt | 30 +-- .../ui/screens/downloads/DownloadsScreen.kt | 14 +- .../ui/screens/filters/FilterBuilderScreen.kt | 21 +- .../ui/screens/filters/FiltersScreen.kt | 17 +- .../screens/library/AvailableOfflineScreen.kt | 5 +- .../ui/screens/library/LibraryScreen.kt | 13 +- .../hocket/ui/screens/search/SearchScreen.kt | 8 +- .../settings/SettingsCategoryScreens.kt | 11 +- .../ui/screens/settings/SettingsCommon.kt | 5 +- .../ui/screens/settings/SettingsScreen.kt | 44 ++-- .../ui/screens/settings/SubSettingsScreens.kt | 5 +- .../ui/screens/setup/ServerSetupScreen.kt | 10 +- .../hocket/ui/screens/stats/StatsScreen.kt | 5 +- .../hocket/ui/a11y/AccessibilityChecksTest.kt | 5 +- .../app/hocket/ui/icons/HocketIconsTest.kt | 40 ++++ android/gradle/libs.versions.toml | 2 - scripts/gen-android-icons.mjs | 186 +++++++++++++++ 36 files changed, 660 insertions(+), 401 deletions(-) create mode 100644 android/app/src/main/java/app/hocket/ui/icons/HocketIcons.kt create mode 100644 android/app/src/test/java/app/hocket/ui/icons/HocketIconsTest.kt create mode 100644 scripts/gen-android-icons.mjs diff --git a/android/README.md b/android/README.md index d654becb..ae57163e 100644 --- a/android/README.md +++ b/android/README.md @@ -202,6 +202,15 @@ syllable lyrics and a streamed read with a ranged re-open against a real Navidro 33+ needs `jdk.internal.ref`, which the JDK does not export. It is skipped when the host `.so` is absent. +## Icons + +Icons are Material Symbols (rounded), the same glyphs as desktop, generated into +`ui/icons/HocketIcons.kt` by `scripts/gen-android-icons.mjs` (from desktop's +`@iconify-json/material-symbols` dev dependency, so `pnpm install` in `desktop/` first). The object +mirrors androidx's `Icons` layout: `HocketIcons.Filled.Home`, `HocketIcons.Outlined.Home`, +`HocketIcons.AutoMirrored.Filled.ArrowBack`. To add one, map its Compose-style name to a Material +Symbols name in the script, list it under its group, and re-run; the output is committed. + ## Gesture and motion conventions - Anything the finger drives settles with a spring (`ui/theme/Motion.kt`): low-bouncy for the diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 4dbdbfd6..1db746df 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -92,7 +92,6 @@ dependencies { implementation(libs.compose.animation) implementation(libs.compose.material3) implementation(libs.compose.material3.window.size) - implementation(libs.compose.material.icons.extended) implementation(libs.coil.compose) implementation(libs.reorderable) implementation(libs.kotlinx.coroutines.android) diff --git a/android/app/src/main/java/app/hocket/ui/components/ActionSheet.kt b/android/app/src/main/java/app/hocket/ui/components/ActionSheet.kt index 41a488f2..d16728d1 100644 --- a/android/app/src/main/java/app/hocket/ui/components/ActionSheet.kt +++ b/android/app/src/main/java/app/hocket/ui/components/ActionSheet.kt @@ -11,40 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.Redo -import androidx.compose.material.icons.automirrored.filled.Undo -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Album -import androidx.compose.material.icons.filled.AllInclusive -import androidx.compose.material.icons.filled.Bedtime -import androidx.compose.material.icons.filled.Cast -import androidx.compose.material.icons.filled.ClearAll -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.DownloadDone -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.FavoriteBorder -import androidx.compose.material.icons.filled.History -import androidx.compose.material.icons.filled.Lyrics -import androidx.compose.material.icons.filled.Pause -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.PlaylistAdd -import androidx.compose.material.icons.filled.PlaylistAddCheck -import androidx.compose.material.icons.filled.PlaylistPlay -import androidx.compose.material.icons.filled.PlaylistRemove -import androidx.compose.material.icons.filled.PushPin -import androidx.compose.material.icons.filled.QueueMusic -import androidx.compose.material.icons.filled.Remove -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.Star -import androidx.compose.material.icons.filled.StarOutline -import androidx.compose.material.icons.filled.Stop -import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.ListItem @@ -78,43 +44,44 @@ import app.hocket.core.api.ActionTarget import app.hocket.core.api.Playlist import app.hocket.core.api.QueryResult import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons /** Material Symbols names the core's action registry emits (`actions/defs.rs`), mapped to icons. */ fun actionIcon(name: String): ImageVector = when (name) { - "play_arrow" -> Icons.Filled.PlayArrow - "play_pause" -> Icons.Filled.PlayArrow - "pause" -> Icons.Filled.Pause - "stop" -> Icons.Filled.Stop - "skip_next" -> Icons.Filled.SkipNext - "skip_previous" -> Icons.Filled.SkipPrevious - "shuffle", "shuffle_on" -> Icons.Filled.Shuffle - "repeat" -> Icons.Filled.Repeat - "all_inclusive" -> Icons.Filled.AllInclusive - "playlist_play" -> Icons.Filled.PlaylistPlay - "playlist_add" -> Icons.Filled.PlaylistAdd - "playlist_add_check" -> Icons.Filled.PlaylistAddCheck - "playlist_remove", "remove_from_queue" -> Icons.Filled.PlaylistRemove - "queue_music" -> Icons.Filled.QueueMusic - "favorite" -> Icons.Filled.Favorite - "heart_minus", "heart_broken" -> Icons.Filled.FavoriteBorder - "star", "star_rate" -> Icons.Filled.Star - "star_outline" -> Icons.Filled.StarOutline - "download" -> Icons.Filled.Download - "download_done" -> Icons.Filled.DownloadDone - "delete", "delete_forever" -> Icons.Filled.Delete - "remove" -> Icons.Filled.Remove - "clear_all" -> Icons.Filled.ClearAll - "album" -> Icons.Filled.Album - "artist", "person" -> Icons.Filled.Person - "history" -> Icons.Filled.History - "push_pin" -> Icons.Filled.PushPin - "keep_off" -> Icons.Outlined.PushPin - "bedtime" -> Icons.Filled.Bedtime - "cast" -> Icons.Filled.Cast - "lyrics" -> Icons.Filled.Lyrics - "undo" -> Icons.AutoMirrored.Filled.Undo - "redo" -> Icons.AutoMirrored.Filled.Redo - else -> Icons.Filled.Add + "play_arrow" -> HocketIcons.Filled.PlayArrow + "play_pause" -> HocketIcons.Filled.PlayArrow + "pause" -> HocketIcons.Filled.Pause + "stop" -> HocketIcons.Filled.Stop + "skip_next" -> HocketIcons.Filled.SkipNext + "skip_previous" -> HocketIcons.Filled.SkipPrevious + "shuffle", "shuffle_on" -> HocketIcons.Filled.Shuffle + "repeat" -> HocketIcons.Filled.Repeat + "all_inclusive" -> HocketIcons.Filled.AllInclusive + "playlist_play" -> HocketIcons.Filled.PlaylistPlay + "playlist_add" -> HocketIcons.Filled.PlaylistAdd + "playlist_add_check" -> HocketIcons.Filled.PlaylistAddCheck + "playlist_remove", "remove_from_queue" -> HocketIcons.Filled.PlaylistRemove + "queue_music" -> HocketIcons.Filled.QueueMusic + "favorite" -> HocketIcons.Filled.Favorite + "heart_minus", "heart_broken" -> HocketIcons.Filled.FavoriteBorder + "star", "star_rate" -> HocketIcons.Filled.Star + "star_outline" -> HocketIcons.Filled.StarOutline + "download" -> HocketIcons.Filled.Download + "download_done" -> HocketIcons.Filled.DownloadDone + "delete", "delete_forever" -> HocketIcons.Filled.Delete + "remove" -> HocketIcons.Filled.Remove + "clear_all" -> HocketIcons.Filled.ClearAll + "album" -> HocketIcons.Filled.Album + "artist", "person" -> HocketIcons.Filled.Person + "history" -> HocketIcons.Filled.History + "push_pin" -> HocketIcons.Filled.PushPin + "keep_off" -> HocketIcons.Outlined.PushPin + "bedtime" -> HocketIcons.Filled.Bedtime + "cast" -> HocketIcons.Filled.Cast + "lyrics" -> HocketIcons.Filled.Lyrics + "undo" -> HocketIcons.AutoMirrored.Filled.Undo + "redo" -> HocketIcons.AutoMirrored.Filled.Redo + else -> HocketIcons.Filled.Add } /** diff --git a/android/app/src/main/java/app/hocket/ui/components/JobsIndicator.kt b/android/app/src/main/java/app/hocket/ui/components/JobsIndicator.kt index a1b46911..09c2899c 100644 --- a/android/app/src/main/java/app/hocket/ui/components/JobsIndicator.kt +++ b/android/app/src/main/java/app/hocket/ui/components/JobsIndicator.kt @@ -11,15 +11,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Cancel -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.ErrorOutline -import androidx.compose.material.icons.filled.Pause -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Refresh -import androidx.compose.material.icons.filled.Sync import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -44,6 +35,7 @@ import app.hocket.core.Commands import app.hocket.core.api.Job import app.hocket.core.api.JobState import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons /** * The one progress indicator near search (design: job queue). Idle: a plain activity icon; running: @@ -60,8 +52,8 @@ fun JobsIndicator(onClick: () -> Unit, modifier: Modifier = Modifier) { IconButton(onClick = onClick, modifier = modifier.semantics { contentDescription = desc }) { when { active.isNotEmpty() -> LoadingIndicator(modifier = Modifier.size(28.dp)) - hasProblems -> Icon(Icons.Filled.ErrorOutline, null, tint = MaterialTheme.colorScheme.error) - else -> Icon(Icons.Filled.Sync, null) + hasProblems -> Icon(HocketIcons.Filled.ErrorOutline, null, tint = MaterialTheme.colorScheme.error) + else -> Icon(HocketIcons.Filled.Sync, null) } } } @@ -100,14 +92,14 @@ fun JobsSheet(onDismiss: () -> Unit) { } items(problems, key = { it.id }) { p -> Row(Modifier.fillMaxWidth().padding(horizontal = 24.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Filled.ErrorOutline, null, tint = MaterialTheme.colorScheme.error) + Icon(HocketIcons.Filled.ErrorOutline, null, tint = MaterialTheme.colorScheme.error) Spacer(Modifier.size(12.dp)) Column(Modifier.weight(1f)) { Text(p.summary, style = MaterialTheme.typography.bodyLarge) p.detail?.let { Text(it, style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } } - if (p.retryable) IconButton(onClick = { client.dispatch(Commands.retryProblem(p.id)) }) { Icon(Icons.Filled.Refresh, stringResource(R.string.action_retry)) } - IconButton(onClick = { client.dispatch(Commands.dismissProblem(p.id)) }) { Icon(Icons.Filled.Close, stringResource(R.string.action_dismiss)) } + if (p.retryable) IconButton(onClick = { client.dispatch(Commands.retryProblem(p.id)) }) { Icon(HocketIcons.Filled.Refresh, stringResource(R.string.action_retry)) } + IconButton(onClick = { client.dispatch(Commands.dismissProblem(p.id)) }) { Icon(HocketIcons.Filled.Close, stringResource(R.string.action_dismiss)) } } } } @@ -139,15 +131,15 @@ private fun JobRow(job: Job) { } when (job.state) { JobState.Running -> { - IconButton(onClick = { client.dispatch(Commands.pauseJob(job.id)) }) { Icon(Icons.Filled.Pause, stringResource(R.string.jobs_pause)) } - if (job.cancellable) IconButton(onClick = { client.dispatch(Commands.cancelJob(job.id)) }) { Icon(Icons.Filled.Cancel, stringResource(R.string.action_cancel)) } + IconButton(onClick = { client.dispatch(Commands.pauseJob(job.id)) }) { Icon(HocketIcons.Filled.Pause, stringResource(R.string.jobs_pause)) } + if (job.cancellable) IconButton(onClick = { client.dispatch(Commands.cancelJob(job.id)) }) { Icon(HocketIcons.Filled.Cancel, stringResource(R.string.action_cancel)) } } JobState.Paused, JobState.Queued -> { - IconButton(onClick = { client.dispatch(Commands.resumeJob(job.id)) }) { Icon(Icons.Filled.PlayArrow, stringResource(R.string.jobs_resume)) } - if (job.cancellable) IconButton(onClick = { client.dispatch(Commands.cancelJob(job.id)) }) { Icon(Icons.Filled.Cancel, stringResource(R.string.action_cancel)) } + IconButton(onClick = { client.dispatch(Commands.resumeJob(job.id)) }) { Icon(HocketIcons.Filled.PlayArrow, stringResource(R.string.jobs_resume)) } + if (job.cancellable) IconButton(onClick = { client.dispatch(Commands.cancelJob(job.id)) }) { Icon(HocketIcons.Filled.Cancel, stringResource(R.string.action_cancel)) } } - JobState.Failed, JobState.Cancelled -> IconButton(onClick = { client.dispatch(Commands.retryJob(job.id)) }) { Icon(Icons.Filled.Refresh, stringResource(R.string.action_retry)) } - JobState.Done -> Icon(Icons.Filled.CheckCircle, stringResource(R.string.jobs_done), tint = MaterialTheme.colorScheme.primary) + JobState.Failed, JobState.Cancelled -> IconButton(onClick = { client.dispatch(Commands.retryJob(job.id)) }) { Icon(HocketIcons.Filled.Refresh, stringResource(R.string.action_retry)) } + JobState.Done -> Icon(HocketIcons.Filled.CheckCircle, stringResource(R.string.jobs_done), tint = MaterialTheme.colorScheme.primary) } } if (job.state == JobState.Running || job.state == JobState.Paused) { diff --git a/android/app/src/main/java/app/hocket/ui/components/RatingStars.kt b/android/app/src/main/java/app/hocket/ui/components/RatingStars.kt index dd06be90..d36485cf 100644 --- a/android/app/src/main/java/app/hocket/ui/components/RatingStars.kt +++ b/android/app/src/main/java/app/hocket/ui/components/RatingStars.kt @@ -3,9 +3,6 @@ package app.hocket.ui.components import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.outlined.StarOutline import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -32,6 +29,7 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import app.hocket.R import app.hocket.ui.a11y.Spoken +import app.hocket.ui.icons.HocketIcons import kotlin.math.roundToInt /** @@ -69,7 +67,7 @@ fun RatingStars(rating: Int, onRate: (Int) -> Unit, modifier: Modifier = Modifie for (i in 1..5) { val filled = i <= rating IconButton(onClick = { haptics.performHapticFeedback(HapticFeedbackType.Confirm); onRate(if (i == rating) 0 else i) }, modifier = Modifier.size(starSize + 12.dp)) { - Icon(if (filled) Icons.Filled.Star else Icons.Outlined.StarOutline, contentDescription = null, tint = if (filled) tint else MaterialTheme.colorScheme.outline, modifier = Modifier.size(starSize)) + Icon(if (filled) HocketIcons.Filled.Star else HocketIcons.Outlined.StarOutline, contentDescription = null, tint = if (filled) tint else MaterialTheme.colorScheme.outline, modifier = Modifier.size(starSize)) } } } diff --git a/android/app/src/main/java/app/hocket/ui/components/Rows.kt b/android/app/src/main/java/app/hocket/ui/components/Rows.kt index 1fb6b262..0482288a 100644 --- a/android/app/src/main/java/app/hocket/ui/components/Rows.kt +++ b/android/app/src/main/java/app/hocket/ui/components/Rows.kt @@ -21,13 +21,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.DownloadForOffline -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.GraphicEq -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.outlined.OfflineBolt import androidx.compose.ui.platform.testTag import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -68,6 +61,7 @@ import app.hocket.core.api.Genre import app.hocket.core.api.OfflineState import app.hocket.core.api.Playlist import app.hocket.core.api.TrackSummary +import app.hocket.ui.icons.HocketIcons /** * Long-press enters selection mode; tap toggles while selection is active (design: lists). @@ -232,7 +226,7 @@ fun TrackRow( Box(Modifier.size(48.dp)) { Artwork(track.coverArt, ArtworkSizes.THUMB, null, Modifier.size(48.dp), RoundedCornerShape(ListArtCorner)) if (selected) Box(Modifier.size(48.dp).clip(RoundedCornerShape(ListArtCorner)), contentAlignment = Alignment.Center) { - Surface(shape = CircleShape, color = MaterialTheme.colorScheme.primary) { Icon(Icons.Filled.Check, null, Modifier.padding(4.dp), tint = MaterialTheme.colorScheme.onPrimary) } + Surface(shape = CircleShape, color = MaterialTheme.colorScheme.primary) { Icon(HocketIcons.Filled.Check, null, Modifier.padding(4.dp), tint = MaterialTheme.colorScheme.onPrimary) } } } Spacer(Modifier.width(14.dp)) @@ -240,7 +234,7 @@ fun TrackRow( Column(Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { if (nowPlaying) { - Icon(Icons.Filled.GraphicEq, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) + Icon(HocketIcons.Filled.GraphicEq, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(6.dp)) } Text(track.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis, @@ -252,13 +246,13 @@ fun TrackRow( } } if (track.loved) { - Icon(Icons.Filled.Favorite, null, tint = MaterialTheme.colorScheme.tertiary, modifier = Modifier.size(16.dp)) + Icon(HocketIcons.Filled.Favorite, null, tint = MaterialTheme.colorScheme.tertiary, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(8.dp)) } Text(formatClock(track.durationMs), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) trailing?.invoke() if (onMore != null) { - IconButton(onClick = onMore) { Icon(Icons.Filled.MoreVert, stringResource(R.string.action_more_for, track.title)) } + IconButton(onClick = onMore) { Icon(HocketIcons.Filled.MoreVert, stringResource(R.string.action_more_for, track.title)) } } } } @@ -278,8 +272,8 @@ fun TrackRow( @Composable fun OfflineBadge(state: OfflineState, describe: Boolean = true) { val (icon, text, tint) = when (state) { - OfflineState.Downloaded -> Triple(Icons.Filled.DownloadForOffline, R.string.badge_downloaded, MaterialTheme.colorScheme.primary) - OfflineState.Cached -> Triple(Icons.Outlined.OfflineBolt, R.string.badge_cached, MaterialTheme.colorScheme.onSurfaceVariant) + OfflineState.Downloaded -> Triple(HocketIcons.Filled.DownloadForOffline, R.string.badge_downloaded, MaterialTheme.colorScheme.primary) + OfflineState.Cached -> Triple(HocketIcons.Outlined.OfflineBolt, R.string.badge_cached, MaterialTheme.colorScheme.onSurfaceVariant) else -> return } Icon(icon, if (describe) stringResource(text) else null, tint = tint, modifier = Modifier.size(16.dp).testTag("offlineBadge.${state.string}")) @@ -304,7 +298,7 @@ fun AlbumCard( // The press ripple is drawn on the cover only (Navic): the text below stays calm. Artwork(album.coverArt, ArtworkSizes.GRID, null, Modifier.fillMaxWidth().aspectRatio(1f).clip(RoundedCornerShape(GridArtCorner)).indication(interaction, ripple()), RoundedCornerShape(GridArtCorner)) if (selected) Box(Modifier.padding(8.dp)) { - Surface(shape = CircleShape, color = MaterialTheme.colorScheme.primary) { Icon(Icons.Filled.Check, null, Modifier.padding(4.dp), tint = MaterialTheme.colorScheme.onPrimary) } + Surface(shape = CircleShape, color = MaterialTheme.colorScheme.primary) { Icon(HocketIcons.Filled.Check, null, Modifier.padding(4.dp), tint = MaterialTheme.colorScheme.onPrimary) } } } Spacer(Modifier.height(8.dp)) @@ -337,7 +331,7 @@ fun ArtistRow(artist: Artist, onClick: () -> Unit, modifier: Modifier = Modifier Text(artist.name, style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis) Text(stringResource(R.string.library_count_albums, artist.albumCount.toInt()), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) } - if (selected) Icon(Icons.Filled.Check, null, tint = MaterialTheme.colorScheme.primary) + if (selected) Icon(HocketIcons.Filled.Check, null, tint = MaterialTheme.colorScheme.primary) } } } @@ -360,7 +354,7 @@ fun PlaylistRow(playlist: Playlist, onClick: () -> Unit, modifier: Modifier = Mo if (playlist.isSmart) Badge(stringResource(R.string.badge_smart)) } } - if (selected) Icon(Icons.Filled.Check, null, tint = MaterialTheme.colorScheme.primary) + if (selected) Icon(HocketIcons.Filled.Check, null, tint = MaterialTheme.colorScheme.primary) } } } diff --git a/android/app/src/main/java/app/hocket/ui/components/SelectionToolbar.kt b/android/app/src/main/java/app/hocket/ui/components/SelectionToolbar.kt index c5dfd277..514edfc6 100644 --- a/android/app/src/main/java/app/hocket/ui/components/SelectionToolbar.kt +++ b/android/app/src/main/java/app/hocket/ui/components/SelectionToolbar.kt @@ -9,10 +9,6 @@ import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.SelectAll import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.HorizontalFloatingToolbar @@ -39,6 +35,7 @@ import app.hocket.core.ActionIds import app.hocket.core.Commands import app.hocket.core.api.ActionDescriptor import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons import kotlinx.coroutines.flow.collectLatest /** @@ -83,13 +80,13 @@ fun SelectionToolbar(modifier: Modifier = Modifier, onSelectAll: (() -> Unit)? = colors = FloatingToolbarDefaults.vibrantFloatingToolbarColors(), modifier = Modifier.semantics { contentDescription = count }, leadingContent = { - IconButton(onClick = { client.clearSelection() }) { Icon(Icons.Filled.Close, stringResource(R.string.action_clear_selection)) } + IconButton(onClick = { client.clearSelection() }) { Icon(HocketIcons.Filled.Close, stringResource(R.string.action_clear_selection)) } Text(count, style = MaterialTheme.typography.labelLarge) }, trailingContent = { - if (onSelectAll != null) IconButton(onClick = onSelectAll) { Icon(Icons.Filled.SelectAll, stringResource(R.string.action_select_all)) } + if (onSelectAll != null) IconButton(onClick = onSelectAll) { Icon(HocketIcons.Filled.SelectAll, stringResource(R.string.action_select_all)) } Box { - IconButton(onClick = { menu = true }) { Icon(Icons.Filled.MoreVert, stringResource(R.string.action_more)) } + IconButton(onClick = { menu = true }) { Icon(HocketIcons.Filled.MoreVert, stringResource(R.string.action_more)) } DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) { toolbarActions.drop(4).forEach { a -> DropdownMenuItem(text = { Text(label(a)) }, leadingIcon = { Icon(icon(a), null) }, enabled = a.enabled, onClick = { menu = false; run(a) }, diff --git a/android/app/src/main/java/app/hocket/ui/components/SwipeActions.kt b/android/app/src/main/java/app/hocket/ui/components/SwipeActions.kt index 409379e9..f4caa7e6 100644 --- a/android/app/src/main/java/app/hocket/ui/components/SwipeActions.kt +++ b/android/app/src/main/java/app/hocket/ui/components/SwipeActions.kt @@ -13,14 +13,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material.icons.automirrored.filled.PlaylistPlay -import androidx.compose.material.icons.automirrored.filled.QueueMusic -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.FavoriteBorder import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -60,6 +52,7 @@ import app.hocket.core.api.ActionTarget import app.hocket.core.api.OfflineState import app.hocket.core.api.TrackSummary import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.screens.settings.actionLabel import app.hocket.ui.screens.settings.setting import kotlinx.coroutines.launch @@ -111,16 +104,16 @@ fun trackSwipeAction(id: String, track: TrackSummary, target: ActionTarget, onAd val scheme = MaterialTheme.colorScheme val run = { action: String -> { client.dispatch(Commands.runAction(action, target)) } } return when (id) { - ActionIds.REMOVE_FROM_QUEUE -> SwipeAction(id, actionLabel(id), Icons.Filled.Delete, scheme.errorContainer, scheme.onErrorContainer, dismisses = true, run(id)) - ActionIds.PLAY_NEXT -> SwipeAction(id, actionLabel(id), Icons.AutoMirrored.Filled.PlaylistPlay, scheme.primaryContainer, scheme.onPrimaryContainer, dismisses = false, run(id)) - ActionIds.PLAY_LATER -> SwipeAction(id, actionLabel(id), Icons.AutoMirrored.Filled.QueueMusic, scheme.secondaryContainer, scheme.onSecondaryContainer, dismisses = false, run(id)) + ActionIds.REMOVE_FROM_QUEUE -> SwipeAction(id, actionLabel(id), HocketIcons.Filled.Delete, scheme.errorContainer, scheme.onErrorContainer, dismisses = true, run(id)) + ActionIds.PLAY_NEXT -> SwipeAction(id, actionLabel(id), HocketIcons.AutoMirrored.Filled.PlaylistPlay, scheme.primaryContainer, scheme.onPrimaryContainer, dismisses = false, run(id)) + ActionIds.PLAY_LATER -> SwipeAction(id, actionLabel(id), HocketIcons.AutoMirrored.Filled.QueueMusic, scheme.secondaryContainer, scheme.onSecondaryContainer, dismisses = false, run(id)) ActionIds.LOVE -> { val action = if (track.loved) ActionIds.UNLOVE else ActionIds.LOVE - SwipeAction(action, actionLabel(action), if (track.loved) Icons.Filled.FavoriteBorder else Icons.Filled.Favorite, scheme.tertiaryContainer, scheme.onTertiaryContainer, dismisses = false, run(action)) + SwipeAction(action, actionLabel(action), if (track.loved) HocketIcons.Filled.FavoriteBorder else HocketIcons.Filled.Favorite, scheme.tertiaryContainer, scheme.onTertiaryContainer, dismisses = false, run(action)) } - ActionIds.ADD_TO_PLAYLIST -> SwipeAction(id, actionLabel(id), Icons.AutoMirrored.Filled.PlaylistAdd, scheme.secondaryContainer, scheme.onSecondaryContainer, dismisses = false, onAddToPlaylist) + ActionIds.ADD_TO_PLAYLIST -> SwipeAction(id, actionLabel(id), HocketIcons.AutoMirrored.Filled.PlaylistAdd, scheme.secondaryContainer, scheme.onSecondaryContainer, dismisses = false, onAddToPlaylist) ActionIds.DOWNLOAD -> if (track.offline == OfflineState.Downloaded) null - else SwipeAction(id, actionLabel(id), Icons.Filled.Download, scheme.primaryContainer, scheme.onPrimaryContainer, dismisses = false, run(id)) + else SwipeAction(id, actionLabel(id), HocketIcons.Filled.Download, scheme.primaryContainer, scheme.onPrimaryContainer, dismisses = false, run(id)) else -> null } } diff --git a/android/app/src/main/java/app/hocket/ui/icons/HocketIcons.kt b/android/app/src/main/java/app/hocket/ui/icons/HocketIcons.kt new file mode 100644 index 00000000..1831569f --- /dev/null +++ b/android/app/src/main/java/app/hocket/ui/icons/HocketIcons.kt @@ -0,0 +1,225 @@ +// Generated by scripts/gen-android-icons.mjs from @iconify-json/material-symbols +// (Material Symbols, Apache-2.0, rounded). Do not edit; edit the script's lists. +package app.hocket.ui.icons + +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.addPathNodes +import androidx.compose.ui.unit.dp + +/** The app's icons: Material Symbols, rounded, as 24dp vectors (tinted by `Icon` like any other). */ +object HocketIcons { + object Filled { + val AccountCircle: ImageVector by lazy { symbol("Filled.AccountCircle", "M 5.85 17.1 q 1.275 -0.975 2.85 -1.537 T 12 15 t 3.3 0.563 t 2.85 1.537 q 0.875 -1.025 1.363 -2.325 T 20 12 q 0 -3.325 -2.337 -5.663 T 12 4 T 6.337 6.338 T 4 12 q 0 1.475 0.488 2.775 T 5.85 17.1 m 3.663 -5.113 Q 8.5 10.976 8.5 9.5 t 1.013 -2.488 T 12 6 t 2.488 1.013 T 15.5 9.5 t -1.012 2.488 T 12 13 t -2.488 -1.012 M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22") } + val Add: ImageVector by lazy { symbol("Filled.Add", "M 11 13 H 6 q -0.425 0 -0.712 -0.288 T 5 12 t 0.288 -0.712 T 6 11 h 5 V 6 q 0 -0.425 0.288 -0.712 T 12 5 t 0.713 0.288 T 13 6 v 5 h 5 q 0.425 0 0.713 0.288 T 19 12 t -0.288 0.713 T 18 13 h -5 v 5 q 0 0.425 -0.288 0.713 T 12 19 t -0.712 -0.288 T 11 18 z") } + val Album: ImageVector by lazy { symbol("Filled.Album", "M 12 16.5 q 1.875 0 3.188 -1.312 T 16.5 12 t -1.312 -3.187 T 12 7.5 T 8.813 8.813 T 7.5 12 t 1.313 3.188 T 12 16.5 m -0.712 -3.787 Q 11 12.425 11 12 t 0.288 -0.712 T 12 11 t 0.713 0.288 T 13 12 t -0.288 0.713 T 12 13 t -0.712 -0.288 M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22") } + val AllInclusive: ImageVector by lazy { symbol("Filled.AllInclusive", "M 5.5 17.5 q -2.3 0 -3.9 -1.6 T 0 12 t 1.6 -3.9 t 3.9 -1.6 q 0.925 0 1.775 0.325 T 8.8 7.75 l 1.7 1.55 L 9 10.65 l -1.55 -1.4 q -0.4 -0.35 -0.9 -0.55 T 5.5 8.5 q -1.45 0 -2.475 1.025 T 2 12 t 1.025 2.475 T 5.5 15.5 q 0.55 0 1.05 -0.2 t 0.9 -0.55 l 7.75 -7 q 0.675 -0.6 1.525 -0.925 T 18.5 6.5 q 2.3 0 3.9 1.6 T 24 12 t -1.6 3.9 t -3.9 1.6 q -0.925 0 -1.775 -0.325 T 15.2 16.25 l -1.7 -1.55 l 1.5 -1.35 l 1.55 1.4 q 0.4 0.35 0.9 0.55 t 1.05 0.2 q 1.45 0 2.475 -1.025 T 22 12 t -1.025 -2.475 T 18.5 8.5 q -0.55 0 -1.05 0.2 t -0.9 0.55 l -7.75 7 q -0.675 0.6 -1.525 0.925 T 5.5 17.5") } + val ArrowDropDown: ImageVector by lazy { symbol("Filled.ArrowDropDown", "M 11.475 14.475 L 7.85 10.85 q -0.075 -0.075 -0.112 -0.162 T 7.7 10.5 q 0 -0.2 0.138 -0.35 T 8.2 10 h 7.6 q 0.225 0 0.363 0.15 t 0.137 0.35 q 0 0.05 -0.15 0.35 l -3.625 3.625 q -0.125 0.125 -0.25 0.175 T 12 14.7 t -0.275 -0.05 t -0.25 -0.175") } + val BarChart: ImageVector by lazy { symbol("Filled.BarChart", "M 17 20 q -0.425 0 -0.712 -0.288 T 16 19 v -5 q 0 -0.425 0.288 -0.712 T 17 13 h 2 q 0.425 0 0.713 0.288 T 20 14 v 5 q 0 0.425 -0.288 0.713 T 19 20 z m -6 0 q -0.425 0 -0.712 -0.288 T 10 19 V 5 q 0 -0.425 0.288 -0.712 T 11 4 h 2 q 0.425 0 0.713 0.288 T 14 5 v 14 q 0 0.425 -0.288 0.713 T 13 20 z m -6 0 q -0.425 0 -0.712 -0.288 T 4 19 v -9 q 0 -0.425 0.288 -0.712 T 5 9 h 2 q 0.425 0 0.713 0.288 T 8 10 v 9 q 0 0.425 -0.288 0.713 T 7 20 z") } + val BatterySaver: ImageVector by lazy { symbol("Filled.BatterySaver", "M 16 19 h -2 q -0.425 0 -0.712 -0.288 T 13 18 t 0.288 -0.712 T 14 17 h 2 v -2 q 0 -0.425 0.288 -0.712 T 17 14 t 0.713 0.288 T 18 15 v 2 h 2 q 0.425 0 0.713 0.288 T 21 18 t -0.288 0.713 T 20 19 h -2 v 2 q 0 0.425 -0.288 0.713 T 17 22 t -0.712 -0.288 T 16 21 z m -8 3 q -0.425 0 -0.712 -0.288 T 7 21 V 5 q 0 -0.425 0.288 -0.712 T 8 4 h 2 V 3 q 0 -0.425 0.288 -0.712 T 11 2 h 2 q 0.425 0 0.713 0.288 T 14 3 v 1 h 2 q 0.425 0 0.713 0.288 T 17 5 v 6.325 q 0 0.275 -0.2 0.488 t -0.475 0.237 q -1.05 0.125 -1.95 0.563 t -1.6 1.137 q -0.8 0.8 -1.287 1.888 T 11 18 q 0 0.75 0.175 1.45 t 0.525 1.35 q 0.2 0.425 0 0.813 t -0.625 0.387 z") } + val Bedtime: ImageVector by lazy { symbol("Filled.Bedtime", "M 12.1 22 q -2.1 0 -3.937 -0.8 t -3.2 -2.162 t -2.163 -3.2 T 2 11.9 q 0 -3.2 1.8 -5.8 t 4.825 -3.65 q 0.55 -0.2 1.025 0.138 t 0.45 0.912 q -0.075 2.125 0.675 4.05 t 2.25 3.425 t 3.425 2.25 t 4.05 0.675 q 0.65 -0.025 0.963 0.438 t 0.112 1.037 q -1.1 3 -3.687 4.813 T 12.1 22") } + val Cancel: ImageVector by lazy { symbol("Filled.Cancel", "m 12 13.4 l 2.9 2.9 q 0.275 0.275 0.7 0.275 t 0.7 -0.275 t 0.275 -0.7 t -0.275 -0.7 L 13.4 12 l 2.9 -2.9 q 0.275 -0.275 0.275 -0.7 t -0.275 -0.7 t -0.7 -0.275 t -0.7 0.275 L 12 10.6 L 9.1 7.7 q -0.275 -0.275 -0.7 -0.275 t -0.7 0.275 t -0.275 0.7 t 0.275 0.7 l 2.9 2.9 l -2.9 2.9 q -0.275 0.275 -0.275 0.7 t 0.275 0.7 t 0.7 0.275 t 0.7 -0.275 z m 0 8.6 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22") } + val Cast: ImageVector by lazy { symbol("Filled.Cast", "M 22 18 q 0 0.825 -0.587 1.413 T 20 20 h -4.175 q -0.35 0 -0.587 -0.262 t -0.263 -0.613 q -0.15 -2.375 -1.087 -4.437 t -2.45 -3.663 T 7.912 8.4 T 3.576 7.1 q -0.65 -0.075 -1.112 -0.512 T 2 5.5 t 0.463 -1.075 T 3.575 4 H 20 q 0.825 0 1.413 0.588 T 22 6 z M 8 20 q -0.4 0 -0.7 -0.238 t -0.375 -0.637 Q 6.65 17.55 5.513 16.45 T 2.8 15.075 q -0.375 -0.05 -0.587 -0.362 T 2 14 q 0 -0.425 0.275 -0.712 t 0.65 -0.238 q 2.35 0.3 4.013 1.975 t 1.987 4.025 q 0.05 0.4 -0.225 0.675 T 8 20 m 4 0 q -0.425 0 -0.712 -0.275 t -0.338 -0.7 q -0.35 -3.2 -2.612 -5.425 t -5.463 -2.55 q -0.4 -0.05 -0.638 -0.35 T 2 10 q 0 -0.425 0.263 -0.725 t 0.637 -0.25 q 4.025 0.325 6.85 3.125 t 3.2 6.825 q 0.05 0.425 -0.238 0.725 T 12 20 m -8.75 0 q -0.525 0 -0.888 -0.363 T 2 18.75 t 0.363 -0.888 t 0.887 -0.362 t 0.888 0.363 t 0.362 0.887 t -0.363 0.888 T 3.25 20") } + val Category: ImageVector by lazy { symbol("Filled.Category", "M 7.425 9.475 L 11.15 3.4 q 0.15 -0.25 0.375 -0.363 T 12 2.925 t 0.475 0.113 t 0.375 0.362 l 3.725 6.075 q 0.15 0.25 0.15 0.525 t -0.125 0.5 t -0.35 0.363 t -0.525 0.137 h -7.45 q -0.3 0 -0.525 -0.137 T 7.4 10.5 t -0.125 -0.5 t 0.15 -0.525 M 17.5 22 q -1.875 0 -3.187 -1.312 T 13 17.5 t 1.313 -3.187 T 17.5 13 t 3.188 1.313 T 22 17.5 t -1.312 3.188 T 17.5 22 M 3 20.5 v -6 q 0 -0.425 0.288 -0.712 T 4 13.5 h 6 q 0.425 0 0.713 0.288 T 11 14.5 v 6 q 0 0.425 -0.288 0.713 T 10 21.5 H 4 q -0.425 0 -0.712 -0.288 T 3 20.5") } + val Check: ImageVector by lazy { symbol("Filled.Check", "m 9.55 15.15 l 8.475 -8.475 q 0.3 -0.3 0.7 -0.3 t 0.7 0.3 t 0.3 0.713 t -0.3 0.712 l -9.175 9.2 q -0.3 0.3 -0.7 0.3 t -0.7 -0.3 L 4.55 13 q -0.3 -0.3 -0.288 -0.712 t 0.313 -0.713 t 0.713 -0.3 t 0.712 0.3 z") } + val CheckCircle: ImageVector by lazy { symbol("Filled.CheckCircle", "m 10.6 13.8 l -2.15 -2.15 q -0.275 -0.275 -0.7 -0.275 t -0.7 0.275 t -0.275 0.7 t 0.275 0.7 L 9.9 15.9 q 0.3 0.3 0.7 0.3 t 0.7 -0.3 l 5.65 -5.65 q 0.275 -0.275 0.275 -0.7 t -0.275 -0.7 t -0.7 -0.275 t -0.7 0.275 z M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22") } + val ChevronRight: ImageVector by lazy { symbol("Filled.ChevronRight", "M 12.6 12 L 8.7 8.1 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 t 0.7 -0.275 t 0.7 0.275 l 4.6 4.6 q 0.15 0.15 0.213 0.325 t 0.062 0.375 t -0.062 0.375 t -0.213 0.325 l -4.6 4.6 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 t -0.275 -0.7 t 0.275 -0.7 z") } + val ClearAll: ImageVector by lazy { symbol("Filled.ClearAll", "M 4 17 q -0.425 0 -0.712 -0.288 T 3 16 t 0.288 -0.712 T 4 15 h 12 q 0.425 0 0.713 0.288 T 17 16 t -0.288 0.713 T 16 17 z m 2 -4 q -0.425 0 -0.712 -0.288 T 5 12 t 0.288 -0.712 T 6 11 h 12 q 0.425 0 0.713 0.288 T 19 12 t -0.288 0.713 T 18 13 z m 2 -4 q -0.425 0 -0.712 -0.288 T 7 8 t 0.288 -0.712 T 8 7 h 12 q 0.425 0 0.713 0.288 T 21 8 t -0.288 0.713 T 20 9 z") } + val Close: ImageVector by lazy { symbol("Filled.Close", "m 12 13.4 l -4.9 4.9 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 t -0.275 -0.7 t 0.275 -0.7 l 4.9 -4.9 l -4.9 -4.9 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 t 0.7 -0.275 t 0.7 0.275 l 4.9 4.9 l 4.9 -4.9 q 0.275 -0.275 0.7 -0.275 t 0.7 0.275 t 0.275 0.7 t -0.275 0.7 L 13.4 12 l 4.9 4.9 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 z") } + val Computer: ImageVector by lazy { symbol("Filled.Computer", "M 2 21 q -0.425 0 -0.712 -0.288 T 1 20 t 0.288 -0.712 T 2 19 h 20 q 0.425 0 0.713 0.288 T 23 20 t -0.288 0.713 T 22 21 z m 2 -3 q -0.825 0 -1.412 -0.587 T 2 16 V 5 q 0 -0.825 0.588 -1.412 T 4 3 h 16 q 0.825 0 1.413 0.588 T 22 5 v 11 q 0 0.825 -0.587 1.413 T 20 18 z") } + val Delete: ImageVector by lazy { symbol("Filled.Delete", "M 7 21 q -0.825 0 -1.412 -0.587 T 5 19 V 6 q -0.425 0 -0.712 -0.288 T 4 5 t 0.288 -0.712 T 5 4 h 4 q 0 -0.425 0.288 -0.712 T 10 3 h 4 q 0.425 0 0.713 0.288 T 15 4 h 4 q 0.425 0 0.713 0.288 T 20 5 t -0.288 0.713 T 19 6 v 13 q 0 0.825 -0.587 1.413 T 17 21 z m 3.713 -4.288 Q 11 16.426 11 16 V 9 q 0 -0.425 -0.288 -0.712 T 10 8 t -0.712 0.288 T 9 9 v 7 q 0 0.425 0.288 0.713 T 10 17 t 0.713 -0.288 m 4 0 Q 15 16.426 15 16 V 9 q 0 -0.425 -0.288 -0.712 T 14 8 t -0.712 0.288 T 13 9 v 7 q 0 0.425 0.288 0.713 T 14 17 t 0.713 -0.288") } + val Devices: ImageVector by lazy { symbol("Filled.Devices", "M 11 20 H 3 q -0.425 0 -0.712 -0.288 T 2 19 t 0.288 -0.712 T 3 18 h 8 q 0.425 0 0.713 0.288 T 12 19 t -0.288 0.713 T 11 20 m -6 -3 q -0.825 0 -1.412 -0.587 T 3 15 V 6 q 0 -0.825 0.588 -1.412 T 5 4 h 14 q 0.825 0 1.413 0.588 T 21 6 h -5.5 q -1.45 0 -2.475 1.025 T 12 9.5 V 16 q 0 0.425 -0.288 0.713 T 11 17 z m 10.5 3 q -0.625 0 -1.062 -0.437 T 14 18.5 v -9 q 0 -0.625 0.438 -1.062 T 15.5 8 h 5 q 0.625 0 1.063 0.438 T 22 9.5 v 9 q 0 0.625 -0.437 1.063 T 20.5 20 z m 2.5 -7.5 q 0.325 0 0.538 -0.225 t 0.212 -0.525 q 0 -0.325 -0.213 -0.537 T 18 11 q -0.3 0 -0.525 0.213 t -0.225 0.537 q 0 0.3 0.225 0.525 T 18 12.5") } + val Download: ImageVector by lazy { symbol("Filled.Download", "M 11.625 15.513 q -0.175 -0.063 -0.325 -0.213 l -3.6 -3.6 q -0.3 -0.3 -0.288 -0.7 t 0.288 -0.7 q 0.3 -0.3 0.713 -0.312 t 0.712 0.287 L 11 12.15 V 5 q 0 -0.425 0.288 -0.712 T 12 4 t 0.713 0.288 T 13 5 v 7.15 l 1.875 -1.875 q 0.3 -0.3 0.713 -0.288 t 0.712 0.313 q 0.275 0.3 0.288 0.7 t -0.288 0.7 l -3.6 3.6 q -0.15 0.15 -0.325 0.213 t -0.375 0.062 t -0.375 -0.062 M 6 20 q -0.825 0 -1.412 -0.587 T 4 18 v -2 q 0 -0.425 0.288 -0.712 T 5 15 t 0.713 0.288 T 6 16 v 2 h 12 v -2 q 0 -0.425 0.288 -0.712 T 19 15 t 0.713 0.288 T 20 16 v 2 q 0 0.825 -0.587 1.413 T 18 20 z") } + val DownloadDone: ImageVector by lazy { symbol("Filled.DownloadDone", "M 9.55 13.15 L 18 4.7 q 0.3 -0.3 0.713 -0.3 t 0.712 0.3 t 0.3 0.712 t -0.3 0.713 L 10.25 15.3 q -0.3 0.3 -0.7 0.3 t -0.7 -0.3 l -4.275 -4.275 q -0.3 -0.3 -0.288 -0.712 T 4.6 9.6 t 0.713 -0.3 t 0.712 0.3 z M 6 20 q -0.425 0 -0.712 -0.288 T 5 19 t 0.288 -0.712 T 6 18 h 12 q 0.425 0 0.713 0.288 T 19 19 t -0.288 0.713 T 18 20 z") } + val DownloadForOffline: ImageVector by lazy { symbol("Filled.DownloadForOffline", "M 8 17 h 8 q 0.425 0 0.713 -0.288 T 17 16 t -0.288 -0.712 T 16 15 H 8 q -0.425 0 -0.712 0.288 T 7 16 t 0.288 0.713 T 8 17 m 3 -6.85 l -0.9 -0.875 Q 9.825 9 9.413 9 t -0.713 0.3 q -0.275 0.275 -0.275 0.7 t 0.275 0.7 l 2.6 2.6 q 0.3 0.3 0.7 0.3 t 0.7 -0.3 l 2.6 -2.6 q 0.275 -0.275 0.287 -0.687 T 15.3 9.3 q -0.275 -0.275 -0.687 -0.288 t -0.713 0.263 l -0.9 0.875 V 7 q 0 -0.425 -0.288 -0.712 T 12 6 t -0.712 0.288 T 11 7 z M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22") } + val DragHandle: ImageVector by lazy { symbol("Filled.DragHandle", "M 5 15 q -0.425 0 -0.712 -0.288 T 4 14 t 0.288 -0.712 T 5 13 h 14 q 0.425 0 0.713 0.288 T 20 14 t -0.288 0.713 T 19 15 z m 0 -4 q -0.425 0 -0.712 -0.288 T 4 10 t 0.288 -0.712 T 5 9 h 14 q 0.425 0 0.713 0.288 T 20 10 t -0.288 0.713 T 19 11 z") } + val ErrorOutline: ImageVector by lazy { symbol("Filled.ErrorOutline", "M 12.713 16.713 Q 13 16.425 13 16 t -0.288 -0.712 T 12 15 t -0.712 0.288 T 11 16 t 0.288 0.713 T 12 17 t 0.713 -0.288 m 0 -4 Q 13 12.425 13 12 V 8 q 0 -0.425 -0.288 -0.712 T 12 7 t -0.712 0.288 T 11 8 v 4 q 0 0.425 0.288 0.713 T 12 13 t 0.713 -0.288 M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22 m 0 -2 q 3.35 0 5.675 -2.325 T 20 12 t -2.325 -5.675 T 12 4 T 6.325 6.325 T 4 12 t 2.325 5.675 T 12 20 m 0 -8") } + val Favorite: ImageVector by lazy { symbol("Filled.Favorite", "M 11.288 20.2 q -0.363 -0.125 -0.638 -0.4 l -1.725 -1.575 q -2.65 -2.425 -4.787 -4.812 T 2 8.15 Q 2 5.8 3.575 4.225 T 7.5 2.65 q 1.325 0 2.5 0.562 t 2 1.538 q 0.825 -0.975 2 -1.537 t 2.5 -0.563 q 2.35 0 3.925 1.575 T 22 8.15 q 0 2.875 -2.125 5.275 T 15.05 18.25 l -1.7 1.55 q -0.275 0.275 -0.637 0.4 t -0.713 0.125 t -0.712 -0.125") } + val FavoriteBorder: ImageVector by lazy { symbol("Filled.FavoriteBorder", "M 11.288 20.2 q -0.363 -0.125 -0.638 -0.4 l -1.725 -1.575 q -2.65 -2.425 -4.787 -4.812 T 2 8.15 Q 2 5.8 3.575 4.225 T 7.5 2.65 q 1.325 0 2.5 0.562 t 2 1.538 q 0.825 -0.975 2 -1.537 t 2.5 -0.563 q 2.35 0 3.925 1.575 T 22 8.15 q 0 2.875 -2.125 5.275 T 15.05 18.25 l -1.7 1.55 q -0.275 0.275 -0.637 0.4 t -0.713 0.125 t -0.712 -0.125 M 11.05 6.75 q -0.725 -1.025 -1.55 -1.562 t -2 -0.538 q -1.5 0 -2.5 1 t -1 2.5 q 0 1.3 0.925 2.763 t 2.213 2.837 t 2.65 2.575 T 12 18.3 q 0.85 -0.775 2.213 -1.975 t 2.65 -2.575 t 2.212 -2.837 T 20 8.15 q 0 -1.5 -1 -2.5 t -2.5 -1 q -1.175 0 -2 0.538 T 12.95 6.75 q -0.175 0.25 -0.425 0.375 T 12 7.25 t -0.525 -0.125 t -0.425 -0.375 m 0.95 4.725") } + val FilterAlt: ImageVector by lazy { symbol("Filled.FilterAlt", "M 11 20 q -0.425 0 -0.712 -0.288 T 10 19 v -6 L 4.2 5.6 q -0.375 -0.5 -0.112 -1.05 T 5 4 h 14 q 0.65 0 0.913 0.55 T 19.8 5.6 L 14 13 v 6 q 0 0.425 -0.288 0.713 T 13 20 z") } + val GraphicEq: ImageVector by lazy { symbol("Filled.GraphicEq", "M 7 17 V 7 q 0 -0.425 0.288 -0.712 T 8 6 t 0.713 0.288 T 9 7 v 10 q 0 0.425 -0.288 0.713 T 8 18 t -0.712 -0.288 T 7 17 m 4 4 V 3 q 0 -0.425 0.288 -0.712 T 12 2 t 0.713 0.288 T 13 3 v 18 q 0 0.425 -0.288 0.713 T 12 22 t -0.712 -0.288 T 11 21 m -8 -8 v -2 q 0 -0.425 0.288 -0.712 T 4 10 t 0.713 0.288 T 5 11 v 2 q 0 0.425 -0.288 0.713 T 4 14 t -0.712 -0.288 T 3 13 m 12 4 V 7 q 0 -0.425 0.288 -0.712 T 16 6 t 0.713 0.288 T 17 7 v 10 q 0 0.425 -0.288 0.713 T 16 18 t -0.712 -0.288 T 15 17 m 4 -4 v -2 q 0 -0.425 0.288 -0.712 T 20 10 t 0.713 0.288 T 21 11 v 2 q 0 0.425 -0.288 0.713 T 20 14 t -0.712 -0.288 T 19 13") } + val History: ImageVector by lazy { symbol("Filled.History", "M 12 21 q -3.15 0 -5.575 -1.912 T 3.275 14.2 q -0.1 -0.375 0.15 -0.687 t 0.675 -0.363 q 0.4 -0.05 0.725 0.15 t 0.45 0.6 q 0.6 2.25 2.475 3.675 T 12 19 q 2.925 0 4.963 -2.037 T 19 12 t -2.037 -4.962 T 12 5 q -1.725 0 -3.225 0.8 T 6.25 8 H 8 q 0.425 0 0.713 0.288 T 9 9 t -0.288 0.713 T 8 10 H 4 q -0.425 0 -0.712 -0.288 T 3 9 V 5 q 0 -0.425 0.288 -0.712 T 4 4 t 0.713 0.288 T 5 5 v 1.35 q 1.275 -1.6 3.113 -2.475 T 12 3 q 1.875 0 3.513 0.713 t 2.85 1.924 t 1.925 2.85 T 21 12 t -0.712 3.513 t -1.925 2.85 t -2.85 1.925 T 12 21 m 1 -9.4 l 2.5 2.5 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 l -2.8 -2.8 q -0.15 -0.15 -0.225 -0.337 T 11 11.975 V 8 q 0 -0.425 0.288 -0.712 T 12 7 t 0.713 0.288 T 13 8 z") } + val Home: ImageVector by lazy { symbol("Filled.Home", "M 4 19 v -9 q 0 -0.475 0.213 -0.9 t 0.587 -0.7 l 6 -4.5 q 0.525 -0.4 1.2 -0.4 t 1.2 0.4 l 6 4.5 q 0.375 0.275 0.588 0.7 T 20 10 v 9 q 0 0.825 -0.588 1.413 T 18 21 h -3 q -0.425 0 -0.712 -0.288 T 14 20 v -5 q 0 -0.425 -0.288 -0.712 T 13 14 h -2 q -0.425 0 -0.712 0.288 T 10 15 v 5 q 0 0.425 -0.288 0.713 T 9 21 H 6 q -0.825 0 -1.412 -0.587 T 4 19") } + val Info: ImageVector by lazy { symbol("Filled.Info", "M 12.713 16.713 Q 13 16.425 13 16 v -4 q 0 -0.425 -0.288 -0.712 T 12 11 t -0.712 0.288 T 11 12 v 4 q 0 0.425 0.288 0.713 T 12 17 t 0.713 -0.288 m 0 -8 Q 13 8.425 13 8 t -0.288 -0.712 T 12 7 t -0.712 0.288 T 11 8 t 0.288 0.713 T 12 9 t 0.713 -0.288 M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22") } + val KeyboardArrowDown: ImageVector by lazy { symbol("Filled.KeyboardArrowDown", "M 11.625 14.913 q -0.175 -0.063 -0.325 -0.213 l -4.6 -4.6 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 t 0.7 -0.275 t 0.7 0.275 l 3.9 3.9 l 3.9 -3.9 q 0.275 -0.275 0.7 -0.275 t 0.7 0.275 t 0.275 0.7 t -0.275 0.7 l -4.6 4.6 q -0.15 0.15 -0.325 0.213 t -0.375 0.062 t -0.375 -0.062") } + val LibraryMusic: ImageVector by lazy { symbol("Filled.LibraryMusic", "M 12.5 15 q 1.05 0 1.775 -0.725 T 15 12.5 V 7 h 2 q 0.425 0 0.713 -0.288 T 18 6 t -0.288 -0.712 T 17 5 h -2 q -0.425 0 -0.712 0.288 T 14 6 v 4.5 q -0.325 -0.25 -0.7 -0.375 T 12.5 10 q -1.05 0 -1.775 0.725 T 10 12.5 t 0.725 1.775 T 12.5 15 M 8 18 q -0.825 0 -1.412 -0.587 T 6 16 V 4 q 0 -0.825 0.588 -1.412 T 8 2 h 12 q 0.825 0 1.413 0.588 T 22 4 v 12 q 0 0.825 -0.587 1.413 T 20 18 z m -4 4 q -0.825 0 -1.412 -0.587 T 2 20 V 7 q 0 -0.425 0.288 -0.712 T 3 6 t 0.713 0.288 T 4 7 v 13 h 13 q 0.425 0 0.713 0.288 T 18 21 t -0.288 0.713 T 17 22 z") } + val Lyrics: ImageVector by lazy { symbol("Filled.Lyrics", "M 7 14 h 2 q 0.425 0 0.713 -0.288 T 10 13 t -0.288 -0.712 T 9 12 H 7 q -0.425 0 -0.712 0.288 T 6 13 t 0.288 0.713 T 7 14 m 12 -2 q -1.25 0 -2.125 -0.875 T 16 9 t 0.875 -2.125 T 19 6 q 0.275 0 0.525 0.05 t 0.475 0.125 V 2 q 0 -0.425 0.288 -0.712 T 21 1 h 2 q 0.425 0 0.713 0.288 T 24 2 t -0.288 0.713 T 23 3 h -1 v 6 q 0 1.25 -0.875 2.125 T 19 12 M 7 11 h 5 q 0.425 0 0.713 -0.288 T 13 10 t -0.288 -0.712 T 12 9 H 7 q -0.425 0 -0.712 0.288 T 6 10 t 0.288 0.713 T 7 11 m 0 -3 h 5 q 0.425 0 0.713 -0.288 T 13 7 t -0.288 -0.712 T 12 6 H 7 q -0.425 0 -0.712 0.288 T 6 7 t 0.288 0.713 T 7 8 M 6 18 l -2.3 2.3 q -0.15 0.15 -0.325 0.225 T 3 20.6 q -0.4 0 -0.7 -0.288 t -0.3 -0.737 V 4 q 0 -0.825 0.588 -1.412 T 4 2 h 11 q 0.775 0 1.363 0.475 T 16.95 3.7 q 0 0.35 -0.162 0.625 t -0.438 0.45 q -1.1 0.675 -1.725 1.8 T 14 9 q 0 1.35 0.663 2.5 t 1.837 1.825 q 0.55 0.325 0.875 0.863 t 0.325 1.187 q 0 1.125 -0.788 1.875 T 15 18 z") } + val MoreVert: ImageVector by lazy { symbol("Filled.MoreVert", "M 12 20 q -0.825 0 -1.412 -0.587 T 10 18 t 0.588 -1.412 T 12 16 t 1.413 0.588 T 14 18 t -0.587 1.413 T 12 20 m 0 -6 q -0.825 0 -1.412 -0.587 T 10 12 t 0.588 -1.412 T 12 10 t 1.413 0.588 T 14 12 t -0.587 1.413 T 12 14 m 0 -6 q -0.825 0 -1.412 -0.587 T 10 6 t 0.588 -1.412 T 12 4 t 1.413 0.588 T 14 6 t -0.587 1.413 T 12 8") } + val MusicNote: ImageVector by lazy { symbol("Filled.MusicNote", "M 7.175 19.825 Q 6 18.65 6 17 t 1.175 -2.825 T 10 13 q 0.575 0 1.063 0.138 t 0.937 0.412 V 4 q 0 -0.425 0.288 -0.712 T 13 3 h 4 q 0.425 0 0.713 0.288 T 18 4 v 2 q 0 0.425 -0.288 0.713 T 17 7 h -3 v 10 q 0 1.65 -1.175 2.825 T 10 21 t -2.825 -1.175") } + val NetworkCheck: ImageVector by lazy { symbol("Filled.NetworkCheck", "M 1.05 10.05 Q 0.6 9.6 0.625 8.962 t 0.525 -1.037 q 1.125 -0.9 2.363 -1.637 T 6.125 5.05 t 2.8 -0.762 T 11.8 4 q 0.6 0 0.988 0.463 T 13.1 5.55 t -0.55 1.025 T 11.425 7 q -1.075 0.05 -2.113 0.263 t -2.037 0.562 q -1.125 0.4 -2.137 0.975 t -1.913 1.3 q -0.5 0.4 -1.125 0.388 t -1.05 -0.438 m 6.425 4.375 q -0.675 0.5 -1.287 0.35 t -0.963 -0.575 t -0.35 -1 t 0.5 -0.975 q 0.8 -0.625 1.713 -1.062 t 1.862 -0.713 q 0.7 -0.2 1.175 0.138 t 0.65 0.862 t -0.037 1.075 t -0.938 0.775 q -0.625 0.2 -1.212 0.475 t -1.113 0.65 M 11.3 19.85 q -0.8 -0.275 -1.175 -1.062 T 10.1 17.3 l 6 -12.2 q 0.1 -0.2 0.288 -0.262 t 0.412 0.012 q 0.2 0.075 0.3 0.263 t 0.05 0.387 L 13.9 18.65 q -0.2 0.8 -0.987 1.163 t -1.613 0.037 m 11.675 -9.9 q -0.375 0.425 -0.975 0.538 t -1.25 -0.413 q -0.2 -0.2 -0.488 -0.387 t -0.512 -0.338 Q 19.075 8.9 19 8.3 t 0.2 -1.075 t 0.813 -0.675 t 1.112 0.15 q 0.425 0.275 0.888 0.612 t 0.837 0.638 q 0.525 0.425 0.513 1 t -0.388 1 m -4.187 5.825 q -0.588 0.075 -1.163 -0.425 l -0.137 -0.137 q -0.088 -0.088 -0.163 -0.138 q -0.6 -0.5 -0.6 -1.1 t 0.35 -1.05 t 0.95 -0.587 t 1.175 0.337 q 0.125 0.125 0.288 0.263 t 0.287 0.262 q 0.5 0.5 0.45 1.1 t -0.45 1 t -0.987 0.475") } + val Palette: ImageVector by lazy { symbol("Filled.Palette", "M 12 22 q -2.05 0 -3.875 -0.788 t -3.187 -2.15 t -2.15 -3.187 T 2 12 q 0 -2.075 0.813 -3.9 t 2.2 -3.175 T 8.25 2.788 T 12.2 2 q 2 0 3.775 0.688 t 3.113 1.9 t 2.125 2.875 T 22 11.05 q 0 2.875 -1.75 4.413 T 16 17 h -1.85 q -0.225 0 -0.312 0.125 t -0.088 0.275 q 0 0.3 0.375 0.863 t 0.375 1.287 q 0 1.25 -0.687 1.85 T 12 22 m -4.425 -9.425 Q 8 12.15 8 11.5 t -0.425 -1.075 T 6.5 10 t -1.075 0.425 T 5 11.5 t 0.425 1.075 T 6.5 13 t 1.075 -0.425 m 3 -4 Q 11 8.15 11 7.5 t -0.425 -1.075 T 9.5 6 t -1.075 0.425 T 8 7.5 t 0.425 1.075 T 9.5 9 t 1.075 -0.425 m 5 0 Q 16 8.15 16 7.5 t -0.425 -1.075 T 14.5 6 t -1.075 0.425 T 13 7.5 t 0.425 1.075 T 14.5 9 t 1.075 -0.425 m 3 4 Q 19 12.15 19 11.5 t -0.425 -1.075 T 17.5 10 t -1.075 0.425 T 16 11.5 t 0.425 1.075 T 17.5 13 t 1.075 -0.425") } + val Pause: ImageVector by lazy { symbol("Filled.Pause", "M 16 19 q -0.825 0 -1.412 -0.587 T 14 17 V 7 q 0 -0.825 0.588 -1.412 T 16 5 t 1.413 0.588 T 18 7 v 10 q 0 0.825 -0.587 1.413 T 16 19 m -8 0 q -0.825 0 -1.412 -0.587 T 6 17 V 7 q 0 -0.825 0.588 -1.412 T 8 5 t 1.413 0.588 T 10 7 v 10 q 0 0.825 -0.587 1.413 T 8 19") } + val Person: ImageVector by lazy { symbol("Filled.Person", "M 9.175 10.825 Q 8 9.65 8 8 t 1.175 -2.825 T 12 4 t 2.825 1.175 T 16 8 t -1.175 2.825 T 12 12 t -2.825 -1.175 M 4 18 v -0.8 q 0 -0.85 0.438 -1.562 T 5.6 14.55 q 1.55 -0.775 3.15 -1.162 T 12 13 t 3.25 0.388 t 3.15 1.162 q 0.725 0.375 1.163 1.088 T 20 17.2 v 0.8 q 0 0.825 -0.587 1.413 T 18 20 H 6 q -0.825 0 -1.412 -0.587 T 4 18") } + val PhoneAndroid: ImageVector by lazy { symbol("Filled.PhoneAndroid", "M 10.5 20 h 3 q 0.2 0 0.35 -0.15 t 0.15 -0.35 t -0.15 -0.35 t -0.35 -0.15 h -3 q -0.2 0 -0.35 0.15 t -0.15 0.35 t 0.15 0.35 t 0.35 0.15 M 7 23 q -0.825 0 -1.412 -0.587 T 5 21 V 3 q 0 -0.825 0.588 -1.412 T 7 1 h 10 q 0.825 0 1.413 0.588 T 19 3 v 18 q 0 0.825 -0.587 1.413 T 17 23 z m 0 -7 h 10 V 6 H 7 z") } + val PlayArrow: ImageVector by lazy { symbol("Filled.PlayArrow", "M 8 17.175 V 6.825 q 0 -0.425 0.3 -0.713 t 0.7 -0.287 q 0.125 0 0.263 0.037 t 0.262 0.113 l 8.15 5.175 q 0.225 0.15 0.338 0.375 t 0.112 0.475 t -0.112 0.475 t -0.338 0.375 l -8.15 5.175 q -0.125 0.075 -0.262 0.113 T 9 18.175 q -0.4 0 -0.7 -0.288 t -0.3 -0.712") } + val PlaylistAdd: ImageVector by lazy { symbol("Filled.PlaylistAdd", "M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 5 q 0.425 0 0.713 0.288 T 10 15 t -0.288 0.713 T 9 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 9 q 0.425 0 0.713 0.288 T 14 11 t -0.288 0.713 T 13 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 9 q 0.425 0 0.713 0.288 T 14 7 t -0.288 0.713 T 13 8 z m 12.288 11.713 Q 16 19.425 16 19 v -3 h -3 q -0.425 0 -0.712 -0.288 T 12 15 t 0.288 -0.712 T 13 14 h 3 v -3 q 0 -0.425 0.288 -0.712 T 17 10 t 0.713 0.288 T 18 11 v 3 h 3 q 0.425 0 0.713 0.288 T 22 15 t -0.288 0.713 T 21 16 h -3 v 3 q 0 0.425 -0.288 0.713 T 17 20 t -0.712 -0.288") } + val PlaylistAddCheck: ImageVector by lazy { symbol("Filled.PlaylistAddCheck", "M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 6 q 0.425 0 0.713 0.288 T 11 15 t -0.288 0.713 T 10 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 10 q 0.425 0 0.713 0.288 T 15 11 t -0.288 0.713 T 14 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 10 q 0.425 0 0.713 0.288 T 15 7 t -0.288 0.713 T 14 8 z m 11.975 10.513 q -0.175 -0.063 -0.325 -0.213 l -2.15 -2.15 q -0.275 -0.275 -0.287 -0.687 t 0.287 -0.713 q 0.275 -0.275 0.688 -0.288 t 0.712 0.263 l 1.45 1.425 l 3.525 -3.525 q 0.3 -0.3 0.713 -0.287 t 0.712 0.312 q 0.275 0.3 0.288 0.7 t -0.288 0.7 l -4.25 4.25 q -0.15 0.15 -0.325 0.213 t -0.375 0.062 t -0.375 -0.062") } + val PlaylistPlay: ImageVector by lazy { symbol("Filled.PlaylistPlay", "M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 6 q 0.425 0 0.713 0.288 T 11 15 t -0.288 0.713 T 10 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 10 q 0.425 0 0.713 0.288 T 15 11 t -0.288 0.713 T 14 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 10 q 0.425 0 0.713 0.288 T 15 7 t -0.288 0.713 T 14 8 z m 12.775 12.475 q -0.125 0.075 -0.25 0.075 t -0.25 -0.05 t -0.2 -0.162 t -0.075 -0.263 v -6.15 q 0 -0.15 0.075 -0.262 t 0.2 -0.163 t 0.25 -0.05 t 0.25 0.075 l 4.6 3.05 q 0.125 0.075 0.175 0.188 t 0.05 0.237 t -0.05 0.238 t -0.175 0.187 z") } + val PlaylistRemove: ImageVector by lazy { symbol("Filled.PlaylistRemove", "m 17 19.4 l -1.9 1.9 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 t -0.275 -0.7 t 0.275 -0.7 l 1.9 -1.9 l -1.9 -1.9 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 t 0.7 -0.275 t 0.7 0.275 l 1.9 1.9 l 1.9 -1.9 q 0.275 -0.275 0.7 -0.275 t 0.7 0.275 t 0.275 0.7 t -0.275 0.7 L 18.4 18 l 1.9 1.9 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 z M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 5 q 0.425 0 0.713 0.288 T 10 15 t -0.288 0.713 T 9 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 9 q 0.425 0 0.713 0.288 T 14 11 t -0.288 0.713 T 13 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 9 q 0.425 0 0.713 0.288 T 14 7 t -0.288 0.713 T 13 8 z") } + val PushPin: ImageVector by lazy { symbol("Filled.PushPin", "m 16 12 l 2 2 v 2 h -5 v 6 l -1 1 l -1 -1 v -6 H 6 v -2 l 2 -2 V 5 H 7 V 3 h 10 v 2 h -1 z") } + val QueueMusic: ImageVector by lazy { symbol("Filled.QueueMusic", "M 16 20 q -1.25 0 -2.125 -0.875 T 13 17 t 0.875 -2.125 T 16 14 q 0.275 0 0.525 0.038 T 17 14.2 V 7 q 0 -0.425 0.288 -0.712 T 18 6 h 3 q 0.425 0 0.713 0.288 T 22 7 t -0.288 0.713 T 21 8 h -2 v 9 q 0 1.25 -0.875 2.125 T 16 20 M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 6 q 0.425 0 0.713 0.288 T 11 15 t -0.288 0.713 T 10 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 10 q 0.425 0 0.713 0.288 T 15 11 t -0.288 0.713 T 14 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 10 q 0.425 0 0.713 0.288 T 15 7 t -0.288 0.713 T 14 8 z") } + val Refresh: ImageVector by lazy { symbol("Filled.Refresh", "M 12 20 q -3.35 0 -5.675 -2.325 T 4 12 t 2.325 -5.675 T 12 4 q 1.725 0 3.3 0.712 T 18 6.75 V 5 q 0 -0.425 0.288 -0.712 T 19 4 t 0.713 0.288 T 20 5 v 5 q 0 0.425 -0.288 0.713 T 19 11 h -5 q -0.425 0 -0.712 -0.288 T 13 10 t 0.288 -0.712 T 14 9 h 3.2 q -0.8 -1.4 -2.187 -2.2 T 12 6 Q 9.5 6 7.75 7.75 T 6 12 t 1.75 4.25 T 12 18 q 1.7 0 3.113 -0.862 t 2.187 -2.313 q 0.2 -0.35 0.563 -0.487 t 0.737 -0.013 q 0.4 0.125 0.575 0.525 t -0.025 0.75 q -1.025 2 -2.925 3.2 T 12 20") } + val Remove: ImageVector by lazy { symbol("Filled.Remove", "M 6 13 q -0.425 0 -0.712 -0.288 T 5 12 t 0.288 -0.712 T 6 11 h 12 q 0.425 0 0.713 0.288 T 19 12 t -0.288 0.713 T 18 13 z") } + val RemoveCircleOutline: ImageVector by lazy { symbol("Filled.RemoveCircleOutline", "M 8 13 h 8 q 0.425 0 0.713 -0.288 T 17 12 t -0.288 -0.712 T 16 11 H 8 q -0.425 0 -0.712 0.288 T 7 12 t 0.288 0.713 T 8 13 m 4 9 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22 m 0 -2 q 3.35 0 5.675 -2.325 T 20 12 t -2.325 -5.675 T 12 4 T 6.325 6.325 T 4 12 t 2.325 5.675 T 12 20 m 0 -8") } + val Repeat: ImageVector by lazy { symbol("Filled.Repeat", "m 6.85 19 l 0.85 0.85 q 0.3 0.3 0.288 0.7 t -0.288 0.7 q -0.3 0.3 -0.712 0.313 t -0.713 -0.288 L 3.7 18.7 q -0.15 -0.15 -0.213 -0.325 T 3.426 18 t 0.063 -0.375 t 0.212 -0.325 l 2.575 -2.575 q 0.3 -0.3 0.713 -0.287 t 0.712 0.312 q 0.275 0.3 0.288 0.7 t -0.288 0.7 l -0.85 0.85 H 17 v -3 q 0 -0.425 0.288 -0.712 T 18 13 t 0.713 0.288 T 19 14 v 3 q 0 0.825 -0.587 1.413 T 17 19 z m 10.3 -12 H 7 v 3 q 0 0.425 -0.288 0.713 T 6 11 t -0.712 -0.288 T 5 10 V 7 q 0 -0.825 0.588 -1.412 T 7 5 h 10.15 l -0.85 -0.85 q -0.3 -0.3 -0.288 -0.7 t 0.288 -0.7 q 0.3 -0.3 0.712 -0.312 t 0.713 0.287 L 20.3 5.3 q 0.15 0.15 0.213 0.325 t 0.062 0.375 t -0.062 0.375 t -0.213 0.325 l -2.575 2.575 q -0.3 0.3 -0.712 0.288 T 16.3 9.25 q -0.275 -0.3 -0.288 -0.7 t 0.288 -0.7 z") } + val RepeatOne: ImageVector by lazy { symbol("Filled.RepeatOne", "M 11.5 10.5 h -0.75 q -0.325 0 -0.537 -0.213 T 10 9.75 t 0.213 -0.537 T 10.75 9 H 12 q 0.425 0 0.713 0.288 T 13 10 v 4.25 q 0 0.325 -0.213 0.538 T 12.25 15 t -0.537 -0.213 t -0.213 -0.537 z M 6.85 19 l 0.85 0.85 q 0.3 0.3 0.288 0.7 t -0.288 0.7 q -0.3 0.3 -0.712 0.313 t -0.713 -0.288 L 3.7 18.7 q -0.15 -0.15 -0.213 -0.325 T 3.426 18 t 0.063 -0.375 t 0.212 -0.325 l 2.575 -2.575 q 0.3 -0.3 0.713 -0.287 t 0.712 0.312 q 0.275 0.3 0.288 0.7 t -0.288 0.7 l -0.85 0.85 H 17 v -3 q 0 -0.425 0.288 -0.712 T 18 13 t 0.713 0.288 T 19 14 v 3 q 0 0.825 -0.587 1.413 T 17 19 z m 10.3 -12 H 7 v 3 q 0 0.425 -0.288 0.713 T 6 11 t -0.712 -0.288 T 5 10 V 7 q 0 -0.825 0.588 -1.412 T 7 5 h 10.15 l -0.85 -0.85 q -0.3 -0.3 -0.288 -0.7 t 0.288 -0.7 q 0.3 -0.3 0.712 -0.312 t 0.713 0.287 L 20.3 5.3 q 0.15 0.15 0.213 0.325 t 0.062 0.375 t -0.062 0.375 t -0.213 0.325 l -2.575 2.575 q -0.3 0.3 -0.712 0.288 T 16.3 9.25 q -0.275 -0.3 -0.288 -0.7 t 0.288 -0.7 z") } + val RestartAlt: ImageVector by lazy { symbol("Filled.RestartAlt", "M 9.825 20.7 q -2.575 -0.725 -4.2 -2.837 T 4 13 q 0 -1.425 0.475 -2.713 t 1.35 -2.362 q 0.275 -0.3 0.675 -0.313 t 0.725 0.313 q 0.275 0.275 0.288 0.675 t -0.263 0.75 q -0.6 0.775 -0.925 1.7 T 6 13 q 0 2.025 1.188 3.613 t 3.062 2.162 q 0.325 0.1 0.538 0.375 t 0.212 0.6 q 0 0.5 -0.35 0.788 t -0.825 0.162 m 4.35 0 q -0.475 0.125 -0.825 -0.175 t -0.35 -0.8 q 0 -0.3 0.213 -0.575 t 0.537 -0.375 q 1.875 -0.6 3.063 -2.175 T 18 13 q 0 -2.5 -1.75 -4.25 T 12 7 h -0.075 l 0.4 0.4 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 l -2.1 -2.1 q -0.15 -0.15 -0.212 -0.325 T 8.55 6 t 0.063 -0.375 t 0.212 -0.325 l 2.1 -2.1 q 0.275 -0.275 0.7 -0.275 t 0.7 0.275 t 0.275 0.7 t -0.275 0.7 l -0.4 0.4 H 12 q 3.35 0 5.675 2.325 T 20 13 q 0 2.725 -1.625 4.85 t -4.2 2.85") } + val Save: ImageVector by lazy { symbol("Filled.Save", "M 5 21 q -0.825 0 -1.412 -0.587 T 3 19 V 5 q 0 -0.825 0.588 -1.412 T 5 3 h 11.175 q 0.4 0 0.763 0.15 t 0.637 0.425 l 2.85 2.85 q 0.275 0.275 0.425 0.638 t 0.15 0.762 V 19 q 0 0.825 -0.587 1.413 T 19 21 z m 9.125 -3.875 Q 15 16.25 15 15 t -0.875 -2.125 T 12 12 t -2.125 0.875 T 9 15 t 0.875 2.125 T 12 18 t 2.125 -0.875 M 7 10 h 7 q 0.425 0 0.713 -0.288 T 15 9 V 7 q 0 -0.425 -0.288 -0.712 T 14 6 H 7 q -0.425 0 -0.712 0.288 T 6 7 v 2 q 0 0.425 0.288 0.713 T 7 10") } + val Search: ImageVector by lazy { symbol("Filled.Search", "M 9.5 16 q -2.725 0 -4.612 -1.888 T 3 9.5 t 1.888 -4.612 T 9.5 3 t 4.613 1.888 T 16 9.5 q 0 1.1 -0.35 2.075 T 14.7 13.3 l 5.6 5.6 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 l -5.6 -5.6 q -0.75 0.6 -1.725 0.95 T 9.5 16 m 0 -2 q 1.875 0 3.188 -1.312 T 14 9.5 t -1.312 -3.187 T 9.5 5 T 6.313 6.313 T 5 9.5 t 1.313 3.188 T 9.5 14") } + val SelectAll: ImageVector by lazy { symbol("Filled.SelectAll", "M 3.288 4.713 Q 3 4.425 3 4 t 0.288 -0.712 T 4 3 t 0.713 0.288 T 5 4 t -0.288 0.713 T 4 5 t -0.712 -0.288 m 4 0 Q 7 4.426 7 4 t 0.288 -0.712 T 8 3 t 0.713 0.288 T 9 4 t -0.288 0.713 T 8 5 t -0.712 -0.288 m 4 0 Q 11 4.426 11 4 t 0.288 -0.712 T 12 3 t 0.713 0.288 T 13 4 t -0.288 0.713 T 12 5 t -0.712 -0.288 m 4 0 Q 15 4.426 15 4 t 0.288 -0.712 T 16 3 t 0.713 0.288 T 17 4 t -0.288 0.713 T 16 5 t -0.712 -0.288 m 4 0 Q 19 4.426 19 4 t 0.288 -0.712 T 20 3 t 0.713 0.288 T 21 4 t -0.288 0.713 T 20 5 t -0.712 -0.288 m -16 4 Q 3 8.426 3 8 t 0.288 -0.712 T 4 7 t 0.713 0.288 T 5 8 t -0.288 0.713 T 4 9 t -0.712 -0.288 m 16 0 Q 19 8.426 19 8 t 0.288 -0.712 T 20 7 t 0.713 0.288 T 21 8 t -0.288 0.713 T 20 9 t -0.712 -0.288 m -16 4 Q 3 12.426 3 12 t 0.288 -0.712 T 4 11 t 0.713 0.288 T 5 12 t -0.288 0.713 T 4 13 t -0.712 -0.288 m 16 0 Q 19 12.426 19 12 t 0.288 -0.712 T 20 11 t 0.713 0.288 T 21 12 t -0.288 0.713 T 20 13 t -0.712 -0.288 m -16 4 Q 3 16.426 3 16 t 0.288 -0.712 T 4 15 t 0.713 0.288 T 5 16 t -0.288 0.713 T 4 17 t -0.712 -0.288 m 16 0 Q 19 16.426 19 16 t 0.288 -0.712 T 20 15 t 0.713 0.288 T 21 16 t -0.288 0.713 T 20 17 t -0.712 -0.288 m -16 4 Q 3 20.426 3 20 t 0.288 -0.712 T 4 19 t 0.713 0.288 T 5 20 t -0.288 0.713 T 4 21 t -0.712 -0.288 m 4 0 Q 7 20.426 7 20 t 0.288 -0.712 T 8 19 t 0.713 0.288 T 9 20 t -0.288 0.713 T 8 21 t -0.712 -0.288 m 4 0 Q 11 20.426 11 20 t 0.288 -0.712 T 12 19 t 0.713 0.288 T 13 20 t -0.288 0.713 T 12 21 t -0.712 -0.288 m 4 0 Q 15 20.426 15 20 t 0.288 -0.712 T 16 19 t 0.713 0.288 T 17 20 t -0.288 0.713 T 16 21 t -0.712 -0.288 m 4 0 Q 19 20.426 19 20 t 0.288 -0.712 T 20 19 t 0.713 0.288 T 21 20 t -0.288 0.713 T 20 21 t -0.712 -0.288 M 9 17 q -0.825 0 -1.412 -0.587 T 7 15 V 9 q 0 -0.825 0.588 -1.412 T 9 7 h 6 q 0.825 0 1.413 0.588 T 17 9 v 6 q 0 0.825 -0.587 1.413 T 15 17 z m 0 -2 h 6 V 9 H 9 z") } + val Settings: ImageVector by lazy { symbol("Filled.Settings", "M 10.825 22 q -0.675 0 -1.162 -0.45 t -0.588 -1.1 L 8.85 18.8 q -0.325 -0.125 -0.612 -0.3 t -0.563 -0.375 l -1.55 0.65 q -0.625 0.275 -1.25 0.05 t -0.975 -0.8 l -1.175 -2.05 q -0.35 -0.575 -0.2 -1.225 t 0.675 -1.075 l 1.325 -1 Q 4.5 12.5 4.5 12.337 v -0.675 q 0 -0.162 0.025 -0.337 l -1.325 -1 Q 2.675 9.9 2.525 9.25 t 0.2 -1.225 L 3.9 5.975 q 0.35 -0.575 0.975 -0.8 t 1.25 0.05 l 1.55 0.65 q 0.275 -0.2 0.575 -0.375 t 0.6 -0.3 l 0.225 -1.65 q 0.1 -0.65 0.588 -1.1 T 10.825 2 h 2.35 q 0.675 0 1.163 0.45 t 0.587 1.1 l 0.225 1.65 q 0.325 0.125 0.613 0.3 t 0.562 0.375 l 1.55 -0.65 q 0.625 -0.275 1.25 -0.05 t 0.975 0.8 l 1.175 2.05 q 0.35 0.575 0.2 1.225 t -0.675 1.075 l -1.325 1 q 0.025 0.175 0.025 0.338 v 0.674 q 0 0.163 -0.05 0.338 l 1.325 1 q 0.525 0.425 0.675 1.075 t -0.2 1.225 l -1.2 2.05 q -0.35 0.575 -0.975 0.8 t -1.25 -0.05 l -1.5 -0.65 q -0.275 0.2 -0.575 0.375 t -0.6 0.3 l -0.225 1.65 q -0.1 0.65 -0.587 1.1 t -1.163 0.45 z m 1.225 -6.5 q 1.45 0 2.475 -1.025 T 15.55 12 t -1.025 -2.475 T 12.05 8.5 q -1.475 0 -2.488 1.025 T 8.55 12 t 1.013 2.475 T 12.05 15.5") } + val SettingsBackupRestore: ImageVector by lazy { symbol("Filled.SettingsBackupRestore", "M 12 21 q -3.15 0 -5.575 -1.912 T 3.275 14.2 q -0.1 -0.375 0.15 -0.687 t 0.675 -0.363 q 0.4 -0.05 0.725 0.15 t 0.45 0.6 q 0.6 2.25 2.475 3.675 T 12 19 q 2.925 0 4.963 -2.037 T 19 12 t -2.037 -4.962 T 12 5 q -1.725 0 -3.225 0.8 T 6.25 8 H 8 q 0.425 0 0.713 0.288 T 9 9 t -0.288 0.713 T 8 10 H 4 q -0.425 0 -0.712 -0.288 T 3 9 V 5 q 0 -0.425 0.288 -0.712 T 4 4 t 0.713 0.288 T 5 5 v 1.35 q 1.275 -1.6 3.113 -2.475 T 12 3 q 1.875 0 3.513 0.713 t 2.85 1.924 t 1.925 2.85 T 21 12 t -0.712 3.513 t -1.925 2.85 t -2.85 1.925 T 12 21 m 0 -7 q -0.825 0 -1.412 -0.587 T 10 12 t 0.588 -1.412 T 12 10 t 1.413 0.588 T 14 12 t -0.587 1.413 T 12 14") } + val Shuffle: ImageVector by lazy { symbol("Filled.Shuffle", "M 15 20 q -0.425 0 -0.712 -0.288 T 14 19 t 0.288 -0.712 T 15 18 h 1.6 l -2.475 -2.475 q -0.3 -0.3 -0.287 -0.712 t 0.312 -0.713 t 0.713 -0.3 t 0.712 0.3 L 18 16.55 V 15 q 0 -0.425 0.288 -0.712 T 19 14 t 0.713 0.288 T 20 15 v 4 q 0 0.425 -0.288 0.713 T 19 20 z m -10.7 -0.3 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 L 16.6 6 H 15 q -0.425 0 -0.712 -0.288 T 14 5 t 0.288 -0.712 T 15 4 h 4 q 0.425 0 0.713 0.288 T 20 5 v 4 q 0 0.425 -0.288 0.713 T 19 10 t -0.712 -0.288 T 18 9 V 7.4 L 5.7 19.7 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 m -0.025 -14 Q 4 5.425 4 5 t 0.275 -0.7 t 0.687 -0.275 t 0.713 0.275 l 4.2 4.175 q 0.275 0.275 0.288 0.688 t -0.288 0.712 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 z") } + val SkipNext: ImageVector by lazy { symbol("Filled.SkipNext", "M 16.5 17 V 7 q 0 -0.425 0.288 -0.712 T 17.5 6 t 0.713 0.288 T 18.5 7 v 10 q 0 0.425 -0.288 0.713 T 17.5 18 t -0.712 -0.288 T 16.5 17 m -11 -0.875 v -8.25 q 0 -0.45 0.3 -0.725 t 0.7 -0.275 q 0.125 0 0.275 0.025 t 0.275 0.125 l 6.2 4.15 q 0.225 0.15 0.338 0.363 T 13.7 12 t -0.112 0.463 t -0.338 0.362 l -6.2 4.15 q -0.125 0.1 -0.275 0.125 t -0.275 0.025 q -0.4 0 -0.7 -0.275 t -0.3 -0.725") } + val SkipPrevious: ImageVector by lazy { symbol("Filled.SkipPrevious", "M 5.5 17 V 7 q 0 -0.425 0.288 -0.712 T 6.5 6 t 0.713 0.288 T 7.5 7 v 10 q 0 0.425 -0.288 0.713 T 6.5 18 t -0.712 -0.288 T 5.5 17 m 11.45 -0.025 l -6.2 -4.15 q -0.225 -0.15 -0.337 -0.362 T 10.3 12 t 0.113 -0.462 t 0.337 -0.363 l 6.2 -4.15 q 0.125 -0.1 0.275 -0.125 t 0.275 -0.025 q 0.4 0 0.7 0.275 t 0.3 0.725 v 8.25 q 0 0.45 -0.3 0.725 t -0.7 0.275 q -0.125 0 -0.275 -0.025 t -0.275 -0.125") } + val Sort: ImageVector by lazy { symbol("Filled.Sort", "M 4 18 q -0.425 0 -0.712 -0.288 T 3 17 t 0.288 -0.712 T 4 16 h 4 q 0.425 0 0.713 0.288 T 9 17 t -0.288 0.713 T 8 18 z m 0 -5 q -0.425 0 -0.712 -0.288 T 3 12 t 0.288 -0.712 T 4 11 h 10 q 0.425 0 0.713 0.288 T 15 12 t -0.288 0.713 T 14 13 z m 0 -5 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 16 q 0.425 0 0.713 0.288 T 21 7 t -0.288 0.713 T 20 8 z") } + val Speaker: ImageVector by lazy { symbol("Filled.Speaker", "M 17 22 H 7 q -0.825 0 -1.412 -0.587 T 5 20 V 4 q 0 -0.825 0.588 -1.412 T 7 2 h 10 q 0.825 0 1.413 0.588 T 19 4 v 16 q 0 0.825 -0.587 1.413 T 17 22 M 13.413 8.413 Q 14 7.825 14 7 t -0.587 -1.412 T 12 5 t -1.412 0.588 T 10 7 t 0.588 1.413 T 12 9 t 1.413 -0.587 m 1.412 9.412 Q 16 16.65 16 15 t -1.175 -2.825 T 12 11 t -2.825 1.175 T 8 15 t 1.175 2.825 T 12 19 t 2.825 -1.175 m -4.237 -1.412 Q 10 15.824 10 15 t 0.588 -1.412 T 12 13 t 1.413 0.588 T 14 15 t -0.587 1.413 T 12 17 t -1.412 -0.587") } + val Star: ImageVector by lazy { symbol("Filled.Star", "m 12 17.275 l -4.15 2.5 q -0.275 0.175 -0.575 0.15 t -0.525 -0.2 t -0.35 -0.437 t -0.05 -0.588 l 1.1 -4.725 L 3.775 10.8 q -0.25 -0.225 -0.312 -0.513 t 0.037 -0.562 t 0.3 -0.45 t 0.55 -0.225 l 4.85 -0.425 l 1.875 -4.45 q 0.125 -0.3 0.388 -0.45 t 0.537 -0.15 t 0.537 0.15 t 0.388 0.45 l 1.875 4.45 l 4.85 0.425 q 0.35 0.05 0.55 0.225 t 0.3 0.45 t 0.038 0.563 t -0.313 0.512 l -3.675 3.175 l 1.1 4.725 q 0.075 0.325 -0.05 0.588 t -0.35 0.437 t -0.525 0.2 t -0.575 -0.15 z") } + val StarOutline: ImageVector by lazy { symbol("Filled.StarOutline", "m 8.85 16.825 l 3.15 -1.9 l 3.15 1.925 l -0.825 -3.6 l 2.775 -2.4 l -3.65 -0.325 l -1.45 -3.4 l -1.45 3.375 l -3.65 0.325 l 2.775 2.425 z m 3.15 0.45 l -4.15 2.5 q -0.275 0.175 -0.575 0.15 t -0.525 -0.2 t -0.35 -0.437 t -0.05 -0.588 l 1.1 -4.725 L 3.775 10.8 q -0.25 -0.225 -0.312 -0.513 t 0.037 -0.562 t 0.3 -0.45 t 0.55 -0.225 l 4.85 -0.425 l 1.875 -4.45 q 0.125 -0.3 0.388 -0.45 t 0.537 -0.15 t 0.537 0.15 t 0.388 0.45 l 1.875 4.45 l 4.85 0.425 q 0.35 0.05 0.55 0.225 t 0.3 0.45 t 0.038 0.563 t -0.313 0.512 l -3.675 3.175 l 1.1 4.725 q 0.075 0.325 -0.05 0.588 t -0.35 0.437 t -0.525 0.2 t -0.575 -0.15 z m 0 -5.025") } + val Stop: ImageVector by lazy { symbol("Filled.Stop", "M 6 16 V 8 q 0 -0.825 0.588 -1.412 T 8 6 h 8 q 0.825 0 1.413 0.588 T 18 8 v 8 q 0 0.825 -0.587 1.413 T 16 18 H 8 q -0.825 0 -1.412 -0.587 T 6 16") } + val Sync: ImageVector by lazy { symbol("Filled.Sync", "M 6 12.05 q 0 1.125 0.425 2.188 T 7.75 16.2 l 0.25 0.25 V 15 q 0 -0.425 0.288 -0.712 T 9 14 t 0.713 0.288 T 10 15 v 4 q 0 0.425 -0.288 0.713 T 9 20 H 5 q -0.425 0 -0.712 -0.288 T 4 19 t 0.288 -0.712 T 5 18 h 1.75 l -0.4 -0.35 q -1.3 -1.15 -1.825 -2.625 T 4 12.05 Q 4 9.7 5.2 7.787 T 8.425 4.85 q 0.35 -0.2 0.738 -0.025 t 0.512 0.575 q 0.125 0.375 -0.012 0.75 t -0.488 0.575 q -1.45 0.8 -2.312 2.213 T 6 12.05 m 12 -0.1 q 0 -1.125 -0.425 -2.187 T 16.25 7.8 L 16 7.55 V 9 q 0 0.425 -0.288 0.713 T 15 10 t -0.712 -0.288 T 14 9 V 5 q 0 -0.425 0.288 -0.712 T 15 4 h 4 q 0.425 0 0.713 0.288 T 20 5 t -0.288 0.713 T 19 6 h -1.75 l 0.4 0.35 q 1.225 1.225 1.788 2.663 T 20 11.95 q 0 2.35 -1.2 4.263 t -3.225 2.937 q -0.35 0.2 -0.737 0.025 t -0.513 -0.575 q -0.125 -0.375 0.013 -0.75 t 0.487 -0.575 q 1.45 -0.8 2.313 -2.212 T 18 11.95") } + val Tune: ImageVector by lazy { symbol("Filled.Tune", "M 11.288 20.713 Q 11 20.425 11 20 v -4 q 0 -0.425 0.288 -0.712 T 12 15 t 0.713 0.288 T 13 16 v 1 h 7 q 0.425 0 0.713 0.288 T 21 18 t -0.288 0.713 T 20 19 h -7 v 1 q 0 0.425 -0.288 0.713 T 12 21 t -0.712 -0.288 M 4 19 q -0.425 0 -0.712 -0.288 T 3 18 t 0.288 -0.712 T 4 17 h 4 q 0.425 0 0.713 0.288 T 9 18 t -0.288 0.713 T 8 19 z m 3.288 -4.288 Q 7 14.425 7 14 v -1 H 4 q -0.425 0 -0.712 -0.288 T 3 12 t 0.288 -0.712 T 4 11 h 3 v -1 q 0 -0.425 0.288 -0.712 T 8 9 t 0.713 0.288 T 9 10 v 4 q 0 0.425 -0.288 0.713 T 8 15 t -0.712 -0.288 M 12 13 q -0.425 0 -0.712 -0.288 T 11 12 t 0.288 -0.712 T 12 11 h 8 q 0.425 0 0.713 0.288 T 21 12 t -0.288 0.713 T 20 13 z m 3.288 -4.288 Q 15 8.425 15 8 V 4 q 0 -0.425 0.288 -0.712 T 16 3 t 0.713 0.288 T 17 4 v 1 h 3 q 0.425 0 0.713 0.288 T 21 6 t -0.288 0.713 T 20 7 h -3 v 1 q 0 0.425 -0.288 0.713 T 16 9 t -0.712 -0.288 M 4 7 q -0.425 0 -0.712 -0.288 T 3 6 t 0.288 -0.712 T 4 5 h 8 q 0.425 0 0.713 0.288 T 13 6 t -0.288 0.713 T 12 7 z") } + val Visibility: ImageVector by lazy { symbol("Filled.Visibility", "M 15.188 14.688 Q 16.5 13.375 16.5 11.5 t -1.312 -3.187 T 12 7 T 8.813 8.313 T 7.5 11.5 t 1.313 3.188 T 12 16 t 3.188 -1.312 m -5.1 -1.276 Q 9.3 12.625 9.3 11.5 t 0.788 -1.912 T 12 8.8 t 1.913 0.788 t 0.787 1.912 t -0.787 1.913 T 12 14.2 t -1.912 -0.787 m -4.2 3.787 q -2.763 -1.8 -4.363 -4.75 q -0.125 -0.225 -0.187 -0.462 t -0.063 -0.488 t 0.063 -0.488 t 0.187 -0.462 q 1.6 -2.95 4.363 -4.75 T 12 4 t 6.113 1.8 t 4.362 4.75 q 0.125 0.225 0.188 0.463 t 0.062 0.487 t -0.062 0.488 t -0.188 0.462 q -1.6 2.95 -4.362 4.75 T 12 19 t -6.113 -1.8") } + val VisibilityOff: ImageVector by lazy { symbol("Filled.VisibilityOff", "m 19.1 21.9 l -3.5 -3.45 q -0.875 0.275 -1.775 0.413 T 12 19 q -3.35 0 -6.125 -1.8 t -4.35 -4.75 q -0.125 -0.225 -0.187 -0.462 t -0.063 -0.488 t 0.063 -0.488 t 0.187 -0.462 q 0.55 -0.975 1.175 -1.9 T 4.15 7 L 2.075 4.9 Q 1.8 4.625 1.8 4.213 t 0.3 -0.713 q 0.275 -0.275 0.7 -0.275 t 0.7 0.275 l 17 17 q 0.275 0.275 0.288 0.688 t -0.288 0.712 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 M 12 16 q 0.275 0 0.525 -0.025 t 0.5 -0.1 l -5.4 -5.4 q -0.075 0.25 -0.1 0.5 T 7.5 11.5 q 0 1.875 1.313 3.188 T 12 16 m 0 -12 q 3.35 0 6.138 1.813 t 4.362 4.762 q 0.125 0.2 0.188 0.438 t 0.062 0.487 t -0.05 0.488 t -0.175 0.437 q -0.475 0.925 -1.062 1.75 t -1.313 1.55 q -0.35 0.35 -0.825 0.325 t -0.825 -0.375 l -2 -2 q -0.175 -0.175 -0.225 -0.413 t 0.025 -0.487 q 0.1 -0.325 0.15 -0.625 t 0.05 -0.65 q 0 -1.875 -1.312 -3.187 T 12 7 q -0.35 0 -0.65 0.05 t -0.625 0.15 q -0.25 0.075 -0.5 0.025 T 9.8 7 l -0.825 -0.825 q -0.475 -0.475 -0.312 -1.1 t 0.787 -0.8 q 0.625 -0.125 1.263 -0.2 T 12 4 m 1.975 5.65 q 0.275 0.325 0.462 0.713 t 0.238 0.812 q 0.025 0.2 -0.15 0.275 t -0.325 -0.075 l -2.05 -2.05 Q 12 9.175 12.088 9 t 0.287 -0.175 q 0.475 0.05 0.875 0.263 t 0.725 0.562") } + val VolumeUp: ImageVector by lazy { symbol("Filled.VolumeUp", "M 19 11.975 q 0 -2.075 -1.1 -3.787 t -2.95 -2.563 q -0.375 -0.175 -0.55 -0.537 t -0.05 -0.738 q 0.15 -0.4 0.538 -0.575 t 0.787 0 Q 18.1 4.85 19.55 7.063 T 21 11.974 t -1.45 4.913 t -3.875 3.287 q -0.4 0.175 -0.788 0 t -0.537 -0.575 q -0.125 -0.375 0.05 -0.737 t 0.55 -0.538 q 1.85 -0.85 2.95 -2.562 t 1.1 -3.788 M 7 15 H 4 q -0.425 0 -0.712 -0.288 T 3 14 v -4 q 0 -0.425 0.288 -0.712 T 4 9 h 3 l 3.3 -3.3 q 0.475 -0.475 1.088 -0.213 t 0.612 0.938 v 11.15 q 0 0.675 -0.612 0.938 T 10.3 18.3 z m 9.5 -3 q 0 1.05 -0.475 1.988 t -1.25 1.537 q -0.25 0.15 -0.513 0.013 T 14 15.1 V 8.85 q 0 -0.3 0.263 -0.437 t 0.512 0.012 q 0.775 0.625 1.25 1.575 t 0.475 2") } + } + object Outlined { + val Album: ImageVector by lazy { symbol("Outlined.Album", "M 12 16.5 q 1.875 0 3.188 -1.312 T 16.5 12 t -1.312 -3.187 T 12 7.5 T 8.813 8.813 T 7.5 12 t 1.313 3.188 T 12 16.5 m -0.712 -3.787 Q 11 12.425 11 12 t 0.288 -0.712 T 12 11 t 0.713 0.288 T 13 12 t -0.288 0.713 T 12 13 t -0.712 -0.288 M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22 m 0 -2 q 3.35 0 5.675 -2.325 T 20 12 t -2.325 -5.675 T 12 4 T 6.325 6.325 T 4 12 t 2.325 5.675 T 12 20 m 0 -8") } + val BarChart: ImageVector by lazy { symbol("Outlined.BarChart", "M 17 20 q -0.425 0 -0.712 -0.288 T 16 19 v -5 q 0 -0.425 0.288 -0.712 T 17 13 h 2 q 0.425 0 0.713 0.288 T 20 14 v 5 q 0 0.425 -0.288 0.713 T 19 20 z m -6 0 q -0.425 0 -0.712 -0.288 T 10 19 V 5 q 0 -0.425 0.288 -0.712 T 11 4 h 2 q 0.425 0 0.713 0.288 T 14 5 v 14 q 0 0.425 -0.288 0.713 T 13 20 z m -6 0 q -0.425 0 -0.712 -0.288 T 4 19 v -9 q 0 -0.425 0.288 -0.712 T 5 9 h 2 q 0.425 0 0.713 0.288 T 8 10 v 9 q 0 0.425 -0.288 0.713 T 7 20 z") } + val Category: ImageVector by lazy { symbol("Outlined.Category", "M 7.425 9.475 L 11.15 3.4 q 0.15 -0.25 0.375 -0.363 T 12 2.925 t 0.475 0.113 t 0.375 0.362 l 3.725 6.075 q 0.15 0.25 0.15 0.525 t -0.125 0.5 t -0.35 0.363 t -0.525 0.137 h -7.45 q -0.3 0 -0.525 -0.137 T 7.4 10.5 t -0.125 -0.5 t 0.15 -0.525 M 17.5 22 q -1.875 0 -3.187 -1.312 T 13 17.5 t 1.313 -3.187 T 17.5 13 t 3.188 1.313 T 22 17.5 t -1.312 3.188 T 17.5 22 M 3 20.5 v -6 q 0 -0.425 0.288 -0.712 T 4 13.5 h 6 q 0.425 0 0.713 0.288 T 11 14.5 v 6 q 0 0.425 -0.288 0.713 T 10 21.5 H 4 q -0.425 0 -0.712 -0.288 T 3 20.5 m 14.5 -0.5 q 1.05 0 1.775 -0.725 T 20 17.5 t -0.725 -1.775 T 17.5 15 t -1.775 0.725 T 15 17.5 t 0.725 1.775 T 17.5 20 M 5 19.5 h 4 v -4 H 5 z M 10.05 9 h 3.9 L 12 5.85 z m 7.45 8.5") } + val CloudOff: ImageVector by lazy { symbol("Outlined.CloudOff", "M 6.5 20 q -2.3 0 -3.9 -1.6 T 1 14.5 q 0 -1.925 1.188 -3.425 T 5.25 9.15 q 0.075 -0.2 0.15 -0.387 t 0.15 -0.413 L 2.1 4.9 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 t 0.7 -0.275 t 0.7 0.275 l 17 17 q 0.275 0.275 0.288 0.688 t -0.288 0.712 q -0.275 0.275 -0.687 0.288 t -0.713 -0.263 L 17.15 20 z m 0 -2 h 8.65 L 7.1 9.95 q -0.05 0.275 -0.075 0.525 T 7 11 h -0.5 q -1.45 0 -2.475 1.025 T 3 14.5 t 1.025 2.475 T 6.5 18 m 15.1 0.75 l -1.45 -1.4 q 0.425 -0.35 0.638 -0.812 T 21 15.5 q 0 -1.05 -0.725 -1.775 T 18.5 13 H 17 v -2 q 0 -2.075 -1.463 -3.537 T 12 6 q -0.675 0 -1.3 0.163 t -1.2 0.512 l -1.45 -1.45 q 0.875 -0.6 1.863 -0.912 T 12 4 q 2.925 0 4.963 2.038 T 19 11 q 1.725 0.2 2.863 1.488 T 23 15.5 q 0 0.975 -0.375 1.813 T 21.6 18.75 m -6.775 -6.725") } + val Download: ImageVector by lazy { symbol("Outlined.Download", "M 11.625 15.513 q -0.175 -0.063 -0.325 -0.213 l -3.6 -3.6 q -0.3 -0.3 -0.288 -0.7 t 0.288 -0.7 q 0.3 -0.3 0.713 -0.312 t 0.712 0.287 L 11 12.15 V 5 q 0 -0.425 0.288 -0.712 T 12 4 t 0.713 0.288 T 13 5 v 7.15 l 1.875 -1.875 q 0.3 -0.3 0.713 -0.288 t 0.712 0.313 q 0.275 0.3 0.288 0.7 t -0.288 0.7 l -3.6 3.6 q -0.15 0.15 -0.325 0.213 t -0.375 0.062 t -0.375 -0.062 M 6 20 q -0.825 0 -1.412 -0.587 T 4 18 v -2 q 0 -0.425 0.288 -0.712 T 5 15 t 0.713 0.288 T 6 16 v 2 h 12 v -2 q 0 -0.425 0.288 -0.712 T 19 15 t 0.713 0.288 T 20 16 v 2 q 0 0.825 -0.587 1.413 T 18 20 z") } + val FilterAlt: ImageVector by lazy { symbol("Outlined.FilterAlt", "M 11 20 q -0.425 0 -0.712 -0.288 T 10 19 v -6 L 4.2 5.6 q -0.375 -0.5 -0.112 -1.05 T 5 4 h 14 q 0.65 0 0.913 0.55 T 19.8 5.6 L 14 13 v 6 q 0 0.425 -0.288 0.713 T 13 20 z m 1 -7.7 L 16.95 6 h -9.9 z m 0 0") } + val History: ImageVector by lazy { symbol("Outlined.History", "M 12 21 q -3.15 0 -5.575 -1.912 T 3.275 14.2 q -0.1 -0.375 0.15 -0.687 t 0.675 -0.363 q 0.4 -0.05 0.725 0.15 t 0.45 0.6 q 0.6 2.25 2.475 3.675 T 12 19 q 2.925 0 4.963 -2.037 T 19 12 t -2.037 -4.962 T 12 5 q -1.725 0 -3.225 0.8 T 6.25 8 H 8 q 0.425 0 0.713 0.288 T 9 9 t -0.288 0.713 T 8 10 H 4 q -0.425 0 -0.712 -0.288 T 3 9 V 5 q 0 -0.425 0.288 -0.712 T 4 4 t 0.713 0.288 T 5 5 v 1.35 q 1.275 -1.6 3.113 -2.475 T 12 3 q 1.875 0 3.513 0.713 t 2.85 1.924 t 1.925 2.85 T 21 12 t -0.712 3.513 t -1.925 2.85 t -2.85 1.925 T 12 21 m 1 -9.4 l 2.5 2.5 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 l -2.8 -2.8 q -0.15 -0.15 -0.225 -0.337 T 11 11.975 V 8 q 0 -0.425 0.288 -0.712 T 12 7 t 0.713 0.288 T 13 8 z") } + val Home: ImageVector by lazy { symbol("Outlined.Home", "M 6 19 h 3 v -5 q 0 -0.425 0.288 -0.712 T 10 13 h 4 q 0.425 0 0.713 0.288 T 15 14 v 5 h 3 v -9 l -6 -4.5 L 6 10 z m -2 0 v -9 q 0 -0.475 0.213 -0.9 t 0.587 -0.7 l 6 -4.5 q 0.525 -0.4 1.2 -0.4 t 1.2 0.4 l 6 4.5 q 0.375 0.275 0.588 0.7 T 20 10 v 9 q 0 0.825 -0.588 1.413 T 18 21 h -4 q -0.425 0 -0.712 -0.288 T 13 20 v -5 h -2 v 5 q 0 0.425 -0.288 0.713 T 10 21 H 6 q -0.825 0 -1.412 -0.587 T 4 19 m 8 -6.75") } + val Info: ImageVector by lazy { symbol("Outlined.Info", "M 12.713 16.713 Q 13 16.425 13 16 v -4 q 0 -0.425 -0.288 -0.712 T 12 11 t -0.712 0.288 T 11 12 v 4 q 0 0.425 0.288 0.713 T 12 17 t 0.713 -0.288 m 0 -8 Q 13 8.425 13 8 t -0.288 -0.712 T 12 7 t -0.712 0.288 T 11 8 t 0.288 0.713 T 12 9 t 0.713 -0.288 M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22 m 0 -2 q 3.35 0 5.675 -2.325 T 20 12 t -2.325 -5.675 T 12 4 T 6.325 6.325 T 4 12 t 2.325 5.675 T 12 20 m 0 -8") } + val LibraryMusic: ImageVector by lazy { symbol("Outlined.LibraryMusic", "M 12.5 15 q 1.05 0 1.775 -0.725 T 15 12.5 V 7 h 2 q 0.425 0 0.713 -0.288 T 18 6 t -0.288 -0.712 T 17 5 h -2 q -0.425 0 -0.712 0.288 T 14 6 v 4.5 q -0.325 -0.25 -0.7 -0.375 T 12.5 10 q -1.05 0 -1.775 0.725 T 10 12.5 t 0.725 1.775 T 12.5 15 M 8 18 q -0.825 0 -1.412 -0.587 T 6 16 V 4 q 0 -0.825 0.588 -1.412 T 8 2 h 12 q 0.825 0 1.413 0.588 T 22 4 v 12 q 0 0.825 -0.587 1.413 T 20 18 z m 0 -2 h 12 V 4 H 8 z m -4 6 q -0.825 0 -1.412 -0.587 T 2 20 V 7 q 0 -0.425 0.288 -0.712 T 3 6 t 0.713 0.288 T 4 7 v 13 h 13 q 0.425 0 0.713 0.288 T 18 21 t -0.288 0.713 T 17 22 z M 8 4 v 12 z") } + val MusicNote: ImageVector by lazy { symbol("Outlined.MusicNote", "M 7.175 19.825 Q 6 18.65 6 17 t 1.175 -2.825 T 10 13 q 0.575 0 1.063 0.138 t 0.937 0.412 V 4 q 0 -0.425 0.288 -0.712 T 13 3 h 4 q 0.425 0 0.713 0.288 T 18 4 v 2 q 0 0.425 -0.288 0.713 T 17 7 h -3 v 10 q 0 1.65 -1.175 2.825 T 10 21 t -2.825 -1.175") } + val OfflineBolt: ImageVector by lazy { symbol("Outlined.OfflineBolt", "M 12 22 q -2.075 0 -3.9 -0.788 t -3.175 -2.137 T 2.788 15.9 T 2 12 t 0.788 -3.9 t 2.137 -3.175 T 8.1 2.788 T 12 2 t 3.9 0.788 t 3.175 2.137 T 21.213 8.1 T 22 12 t -0.788 3.9 t -2.137 3.175 t -3.175 2.138 T 12 22 m 0 -2 q 3.35 0 5.675 -2.325 T 20 12 t -2.325 -5.675 T 12 4 T 6.325 6.325 T 4 12 t 2.325 5.675 T 12 20 m -0.6 -6.5 v 3.425 q 0 0.4 0.375 0.488 t 0.575 -0.263 l 3.025 -5.925 q 0.125 -0.25 -0.025 -0.487 t -0.425 -0.238 H 12.75 V 7.025 q 0 -0.4 -0.375 -0.5 t -0.575 0.25 l -3.15 6 q -0.125 0.275 0.013 0.5 t 0.412 0.225 z") } + val Person: ImageVector by lazy { symbol("Outlined.Person", "M 9.175 10.825 Q 8 9.65 8 8 t 1.175 -2.825 T 12 4 t 2.825 1.175 T 16 8 t -1.175 2.825 T 12 12 t -2.825 -1.175 M 4 18 v -0.8 q 0 -0.85 0.438 -1.562 T 5.6 14.55 q 1.55 -0.775 3.15 -1.162 T 12 13 t 3.25 0.388 t 3.15 1.162 q 0.725 0.375 1.163 1.088 T 20 17.2 v 0.8 q 0 0.825 -0.587 1.413 T 18 20 H 6 q -0.825 0 -1.412 -0.587 T 4 18 m 2 0 h 12 v -0.8 q 0 -0.275 -0.137 -0.5 t -0.363 -0.35 q -1.35 -0.675 -2.725 -1.012 T 12 15 t -2.775 0.338 T 6.5 16.35 q -0.225 0.125 -0.363 0.35 T 6 17.2 z m 7.413 -8.587 Q 14 8.825 14 8 t -0.587 -1.412 T 12 6 t -1.412 0.588 T 10 8 t 0.588 1.413 T 12 10 t 1.413 -0.587 M 12 18") } + val PushPin: ImageVector by lazy { symbol("Outlined.PushPin", "m 16 12 l 2 2 v 2 h -5 v 6 l -1 1 l -1 -1 v -6 H 6 v -2 l 2 -2 V 5 H 7 V 3 h 10 v 2 h -1 z m -7.15 2 h 6.3 L 14 12.85 V 5 h -4 v 7.85 z M 12 14") } + val Search: ImageVector by lazy { symbol("Outlined.Search", "M 9.5 16 q -2.725 0 -4.612 -1.888 T 3 9.5 t 1.888 -4.612 T 9.5 3 t 4.613 1.888 T 16 9.5 q 0 1.1 -0.35 2.075 T 14.7 13.3 l 5.6 5.6 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 l -5.6 -5.6 q -0.75 0.6 -1.725 0.95 T 9.5 16 m 0 -2 q 1.875 0 3.188 -1.312 T 14 9.5 t -1.312 -3.187 T 9.5 5 T 6.313 6.313 T 5 9.5 t 1.313 3.188 T 9.5 14") } + val StarOutline: ImageVector by lazy { symbol("Outlined.StarOutline", "m 8.85 16.825 l 3.15 -1.9 l 3.15 1.925 l -0.825 -3.6 l 2.775 -2.4 l -3.65 -0.325 l -1.45 -3.4 l -1.45 3.375 l -3.65 0.325 l 2.775 2.425 z m 3.15 0.45 l -4.15 2.5 q -0.275 0.175 -0.575 0.15 t -0.525 -0.2 t -0.35 -0.437 t -0.05 -0.588 l 1.1 -4.725 L 3.775 10.8 q -0.25 -0.225 -0.312 -0.513 t 0.037 -0.562 t 0.3 -0.45 t 0.55 -0.225 l 4.85 -0.425 l 1.875 -4.45 q 0.125 -0.3 0.388 -0.45 t 0.537 -0.15 t 0.537 0.15 t 0.388 0.45 l 1.875 4.45 l 4.85 0.425 q 0.35 0.05 0.55 0.225 t 0.3 0.45 t 0.038 0.563 t -0.313 0.512 l -3.675 3.175 l 1.1 4.725 q 0.075 0.325 -0.05 0.588 t -0.35 0.437 t -0.525 0.2 t -0.575 -0.15 z m 0 -5.025") } + } + object AutoMirrored { + object Filled { + val ArrowBack: ImageVector by lazy { symbol("AutoMirrored.Filled.ArrowBack", "m 7.825 13 l 4.9 4.9 q 0.3 0.3 0.288 0.7 t -0.313 0.7 q -0.3 0.275 -0.7 0.288 t -0.7 -0.288 l -6.6 -6.6 q -0.15 -0.15 -0.213 -0.325 T 4.426 12 t 0.063 -0.375 t 0.212 -0.325 l 6.6 -6.6 q 0.275 -0.275 0.688 -0.275 t 0.712 0.275 q 0.3 0.3 0.3 0.713 t -0.3 0.712 L 7.825 11 H 19 q 0.425 0 0.713 0.288 T 20 12 t -0.288 0.713 T 19 13 z", autoMirror = true) } + val Logout: ImageVector by lazy { symbol("AutoMirrored.Filled.Logout", "M 5 21 q -0.825 0 -1.412 -0.587 T 3 19 V 5 q 0 -0.825 0.588 -1.412 T 5 3 h 6 q 0.425 0 0.713 0.288 T 12 4 t -0.288 0.713 T 11 5 H 5 v 14 h 6 q 0.425 0 0.713 0.288 T 12 20 t -0.288 0.713 T 11 21 z m 12.175 -8 H 10 q -0.425 0 -0.712 -0.288 T 9 12 t 0.288 -0.712 T 10 11 h 7.175 L 15.3 9.125 q -0.275 -0.275 -0.275 -0.675 t 0.275 -0.7 t 0.7 -0.313 t 0.725 0.288 L 20.3 11.3 q 0.3 0.3 0.3 0.7 t -0.3 0.7 l -3.575 3.575 q -0.3 0.3 -0.712 0.288 t -0.713 -0.313 q -0.275 -0.3 -0.262 -0.712 t 0.287 -0.688 z", autoMirror = true) } + val PlaylistAdd: ImageVector by lazy { symbol("AutoMirrored.Filled.PlaylistAdd", "M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 5 q 0.425 0 0.713 0.288 T 10 15 t -0.288 0.713 T 9 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 9 q 0.425 0 0.713 0.288 T 14 11 t -0.288 0.713 T 13 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 9 q 0.425 0 0.713 0.288 T 14 7 t -0.288 0.713 T 13 8 z m 12.288 11.713 Q 16 19.425 16 19 v -3 h -3 q -0.425 0 -0.712 -0.288 T 12 15 t 0.288 -0.712 T 13 14 h 3 v -3 q 0 -0.425 0.288 -0.712 T 17 10 t 0.713 0.288 T 18 11 v 3 h 3 q 0.425 0 0.713 0.288 T 22 15 t -0.288 0.713 T 21 16 h -3 v 3 q 0 0.425 -0.288 0.713 T 17 20 t -0.712 -0.288", autoMirror = true) } + val PlaylistPlay: ImageVector by lazy { symbol("AutoMirrored.Filled.PlaylistPlay", "M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 6 q 0.425 0 0.713 0.288 T 11 15 t -0.288 0.713 T 10 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 10 q 0.425 0 0.713 0.288 T 15 11 t -0.288 0.713 T 14 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 10 q 0.425 0 0.713 0.288 T 15 7 t -0.288 0.713 T 14 8 z m 12.775 12.475 q -0.125 0.075 -0.25 0.075 t -0.25 -0.05 t -0.2 -0.162 t -0.075 -0.263 v -6.15 q 0 -0.15 0.075 -0.262 t 0.2 -0.163 t 0.25 -0.05 t 0.25 0.075 l 4.6 3.05 q 0.125 0.075 0.175 0.188 t 0.05 0.237 t -0.05 0.238 t -0.175 0.187 z", autoMirror = true) } + val QueueMusic: ImageVector by lazy { symbol("AutoMirrored.Filled.QueueMusic", "M 16 20 q -1.25 0 -2.125 -0.875 T 13 17 t 0.875 -2.125 T 16 14 q 0.275 0 0.525 0.038 T 17 14.2 V 7 q 0 -0.425 0.288 -0.712 T 18 6 h 3 q 0.425 0 0.713 0.288 T 22 7 t -0.288 0.713 T 21 8 h -2 v 9 q 0 1.25 -0.875 2.125 T 16 20 M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 6 q 0.425 0 0.713 0.288 T 11 15 t -0.288 0.713 T 10 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 10 q 0.425 0 0.713 0.288 T 15 11 t -0.288 0.713 T 14 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 10 q 0.425 0 0.713 0.288 T 15 7 t -0.288 0.713 T 14 8 z", autoMirror = true) } + val Redo: ImageVector by lazy { symbol("AutoMirrored.Filled.Redo", "M 16.2 10 H 9.9 q -1.575 0 -2.738 1 T 6 13.5 T 7.163 16 T 9.9 17 H 16 q 0.425 0 0.713 0.288 T 17 18 t -0.288 0.713 T 16 19 H 9.9 q -2.425 0 -4.163 -1.575 T 4 13.5 t 1.738 -3.925 T 9.9 8 h 6.3 l -1.9 -1.9 q -0.275 -0.275 -0.275 -0.7 t 0.275 -0.7 t 0.7 -0.275 t 0.7 0.275 l 3.6 3.6 q 0.15 0.15 0.213 0.325 t 0.062 0.375 t -0.062 0.375 t -0.213 0.325 l -3.6 3.6 q -0.275 0.275 -0.7 0.275 t -0.7 -0.275 t -0.275 -0.7 t 0.275 -0.7 z", autoMirror = true) } + val Undo: ImageVector by lazy { symbol("AutoMirrored.Filled.Undo", "M 8 19 q -0.425 0 -0.712 -0.288 T 7 18 t 0.288 -0.712 T 8 17 h 6.1 q 1.575 0 2.738 -1 T 18 13.5 T 16.838 11 T 14.1 10 H 7.8 l 1.9 1.9 q 0.275 0.275 0.275 0.7 t -0.275 0.7 t -0.7 0.275 t -0.7 -0.275 L 4.7 9.7 q -0.15 -0.15 -0.213 -0.325 T 4.426 9 t 0.063 -0.375 T 4.7 8.3 l 3.6 -3.6 q 0.275 -0.275 0.7 -0.275 t 0.7 0.275 t 0.275 0.7 t -0.275 0.7 L 7.8 8 h 6.3 q 2.425 0 4.163 1.575 T 20 13.5 t -1.737 3.925 T 14.1 19 z", autoMirror = true) } + } + object Outlined { + val QueueMusic: ImageVector by lazy { symbol("AutoMirrored.Outlined.QueueMusic", "M 16 20 q -1.25 0 -2.125 -0.875 T 13 17 t 0.875 -2.125 T 16 14 q 0.275 0 0.525 0.038 T 17 14.2 V 7 q 0 -0.425 0.288 -0.712 T 18 6 h 3 q 0.425 0 0.713 0.288 T 22 7 t -0.288 0.713 T 21 8 h -2 v 9 q 0 1.25 -0.875 2.125 T 16 20 M 4 16 q -0.425 0 -0.712 -0.288 T 3 15 t 0.288 -0.712 T 4 14 h 6 q 0.425 0 0.713 0.288 T 11 15 t -0.288 0.713 T 10 16 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 11 t 0.288 -0.712 T 4 10 h 10 q 0.425 0 0.713 0.288 T 15 11 t -0.288 0.713 T 14 12 z m 0 -4 q -0.425 0 -0.712 -0.288 T 3 7 t 0.288 -0.712 T 4 6 h 10 q 0.425 0 0.713 0.288 T 15 7 t -0.288 0.713 T 14 8 z", autoMirror = true) } + } + } + + /** Every icon, so a test can build each one. */ + internal val all: List + get() = listOf( + Filled.AccountCircle, + Filled.Add, + Filled.Album, + Filled.AllInclusive, + Filled.ArrowDropDown, + Filled.BarChart, + Filled.BatterySaver, + Filled.Bedtime, + Filled.Cancel, + Filled.Cast, + Filled.Category, + Filled.Check, + Filled.CheckCircle, + Filled.ChevronRight, + Filled.ClearAll, + Filled.Close, + Filled.Computer, + Filled.Delete, + Filled.Devices, + Filled.Download, + Filled.DownloadDone, + Filled.DownloadForOffline, + Filled.DragHandle, + Filled.ErrorOutline, + Filled.Favorite, + Filled.FavoriteBorder, + Filled.FilterAlt, + Filled.GraphicEq, + Filled.History, + Filled.Home, + Filled.Info, + Filled.KeyboardArrowDown, + Filled.LibraryMusic, + Filled.Lyrics, + Filled.MoreVert, + Filled.MusicNote, + Filled.NetworkCheck, + Filled.Palette, + Filled.Pause, + Filled.Person, + Filled.PhoneAndroid, + Filled.PlayArrow, + Filled.PlaylistAdd, + Filled.PlaylistAddCheck, + Filled.PlaylistPlay, + Filled.PlaylistRemove, + Filled.PushPin, + Filled.QueueMusic, + Filled.Refresh, + Filled.Remove, + Filled.RemoveCircleOutline, + Filled.Repeat, + Filled.RepeatOne, + Filled.RestartAlt, + Filled.Save, + Filled.Search, + Filled.SelectAll, + Filled.Settings, + Filled.SettingsBackupRestore, + Filled.Shuffle, + Filled.SkipNext, + Filled.SkipPrevious, + Filled.Sort, + Filled.Speaker, + Filled.Star, + Filled.StarOutline, + Filled.Stop, + Filled.Sync, + Filled.Tune, + Filled.Visibility, + Filled.VisibilityOff, + Filled.VolumeUp, + Outlined.Album, + Outlined.BarChart, + Outlined.Category, + Outlined.CloudOff, + Outlined.Download, + Outlined.FilterAlt, + Outlined.History, + Outlined.Home, + Outlined.Info, + Outlined.LibraryMusic, + Outlined.MusicNote, + Outlined.OfflineBolt, + Outlined.Person, + Outlined.PushPin, + Outlined.Search, + Outlined.StarOutline, + AutoMirrored.Filled.ArrowBack, + AutoMirrored.Filled.Logout, + AutoMirrored.Filled.PlaylistAdd, + AutoMirrored.Filled.PlaylistPlay, + AutoMirrored.Filled.QueueMusic, + AutoMirrored.Filled.Redo, + AutoMirrored.Filled.Undo, + AutoMirrored.Outlined.QueueMusic, + ) +} + +private fun symbol(name: String, vararg paths: String, autoMirror: Boolean = false): ImageVector = + ImageVector.Builder(name, 24.dp, 24.dp, 24f, 24f, autoMirror = autoMirror) + .apply { for (d in paths) addPath(addPathNodes(d), fill = SolidColor(Color.Black)) } + .build() diff --git a/android/app/src/main/java/app/hocket/ui/lyrics/LyricsPage.kt b/android/app/src/main/java/app/hocket/ui/lyrics/LyricsPage.kt index fd9bcf39..b45e73a3 100644 --- a/android/app/src/main/java/app/hocket/ui/lyrics/LyricsPage.kt +++ b/android/app/src/main/java/app/hocket/ui/lyrics/LyricsPage.kt @@ -38,10 +38,6 @@ import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.SemanticsPropertyKey import androidx.compose.ui.semantics.SemanticsPropertyReceiver -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Remove -import androidx.compose.material.icons.filled.RestartAlt import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.ui.semantics.clearAndSetSemantics @@ -69,6 +65,7 @@ import app.hocket.core.api.QueryResult import app.hocket.core.lyrics.LyricsCursor import app.hocket.ui.LocalCoreClient import app.hocket.ui.components.EmptyState +import app.hocket.ui.icons.HocketIcons /** * Native lyrics renderer. Syllable tier: continuous per-syllable gradient sweep from each line's own @@ -153,9 +150,9 @@ private fun OffsetControl(doc: Lyrics, onOffset: (Int) -> Unit, systemBarPadding if (doc.tier != LyricsTier.Unsynced) { Text(stringResource(R.string.lyrics_offset_value, doc.offsetMs), style = MaterialTheme.typography.labelMedium, color = LyricsContrast.active, modifier = Modifier.padding(end = 4.dp)) // Labelled 48 dp buttons (a "−" glyph alone reads as "minus"). - IconButton(onClick = { onOffset(doc.offsetMs - 100) }, modifier = Modifier.testTag("lyrics.offset.earlier")) { Icon(Icons.Filled.Remove, stringResource(R.string.lyrics_offset_earlier_a11y), tint = LyricsContrast.active) } - IconButton(onClick = { onOffset(0) }, modifier = Modifier.testTag("lyrics.offset.reset")) { Icon(Icons.Filled.RestartAlt, stringResource(R.string.lyrics_offset_reset_a11y), tint = LyricsContrast.active) } - IconButton(onClick = { onOffset(doc.offsetMs + 100) }, modifier = Modifier.testTag("lyrics.offset.later")) { Icon(Icons.Filled.Add, stringResource(R.string.lyrics_offset_later_a11y), tint = LyricsContrast.active) } + IconButton(onClick = { onOffset(doc.offsetMs - 100) }, modifier = Modifier.testTag("lyrics.offset.earlier")) { Icon(HocketIcons.Filled.Remove, stringResource(R.string.lyrics_offset_earlier_a11y), tint = LyricsContrast.active) } + IconButton(onClick = { onOffset(0) }, modifier = Modifier.testTag("lyrics.offset.reset")) { Icon(HocketIcons.Filled.RestartAlt, stringResource(R.string.lyrics_offset_reset_a11y), tint = LyricsContrast.active) } + IconButton(onClick = { onOffset(doc.offsetMs + 100) }, modifier = Modifier.testTag("lyrics.offset.later")) { Icon(HocketIcons.Filled.Add, stringResource(R.string.lyrics_offset_later_a11y), tint = LyricsContrast.active) } } } } diff --git a/android/app/src/main/java/app/hocket/ui/nav/AccountSheet.kt b/android/app/src/main/java/app/hocket/ui/nav/AccountSheet.kt index 9ada085b..a911bb27 100644 --- a/android/app/src/main/java/app/hocket/ui/nav/AccountSheet.kt +++ b/android/app/src/main/java/app/hocket/ui/nav/AccountSheet.kt @@ -15,12 +15,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.material.icons.filled.Devices -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.filled.Tune -import androidx.compose.material.icons.outlined.CloudOff import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon @@ -50,6 +44,7 @@ import app.hocket.R import app.hocket.core.api.NetworkKind import app.hocket.core.api.ServerInfo import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons /** The user's (or else the server's) initial, for the avatar. */ private fun initialOf(server: ServerInfo?): String = @@ -94,7 +89,7 @@ fun OfflineIndicator(modifier: Modifier = Modifier) { if (network?.kind != NetworkKind.Offline) return val description = stringResource(R.string.offline_indicator) IconButton(onClick = shell.openAvailableOffline, modifier = modifier.semantics { contentDescription = description }.testTag("offline.indicator")) { - Icon(Icons.Outlined.CloudOff, contentDescription = null) + Icon(HocketIcons.Outlined.CloudOff, contentDescription = null) } } @@ -144,9 +139,9 @@ fun AccountSheetContent( colors = ListItemDefaults.colors(containerColor = MaterialTheme.colorScheme.surfaceContainerLow), ) HorizontalDivider(Modifier.padding(vertical = 4.dp)) - SheetRow(Icons.Filled.Settings, stringResource(R.string.account_settings), "account.settings", onSettings) - SheetRow(Icons.Filled.BarChart, stringResource(R.string.account_stats), "account.stats", onStats) - SheetRow(Icons.Filled.Devices, stringResource(R.string.account_devices), "account.devices", onDevices) + SheetRow(HocketIcons.Filled.Settings, stringResource(R.string.account_settings), "account.settings", onSettings) + SheetRow(HocketIcons.Filled.BarChart, stringResource(R.string.account_stats), "account.stats", onStats) + SheetRow(HocketIcons.Filled.Devices, stringResource(R.string.account_devices), "account.devices", onDevices) val others = NavItem.entries.filter { it !in barItems && it != NavItem.Stats } if (others.isNotEmpty()) { HorizontalDivider(Modifier.padding(vertical = 4.dp)) @@ -155,7 +150,7 @@ fun AccountSheetContent( others.forEach { item -> SheetRow(item.icon(false), item.label(), "account.place.${item.id}") { onPlace(item) } } } HorizontalDivider(Modifier.padding(vertical = 4.dp)) - SheetRow(Icons.Filled.Tune, stringResource(R.string.bottom_bar_edit), "account.editBar", onEditBar) + SheetRow(HocketIcons.Filled.Tune, stringResource(R.string.bottom_bar_edit), "account.editBar", onEditBar) Spacer(Modifier.height(8.dp)) } } diff --git a/android/app/src/main/java/app/hocket/ui/nav/AppRoot.kt b/android/app/src/main/java/app/hocket/ui/nav/AppRoot.kt index bd2451a8..1fe8bc50 100644 --- a/android/app/src/main/java/app/hocket/ui/nav/AppRoot.kt +++ b/android/app/src/main/java/app/hocket/ui/nav/AppRoot.kt @@ -17,21 +17,6 @@ import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.LibraryMusic -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.filled.Settings -import androidx.compose.material.icons.outlined.BarChart -import androidx.compose.material.icons.outlined.Download -import androidx.compose.material.icons.outlined.FilterAlt -import androidx.compose.material.icons.outlined.Home -import androidx.compose.material.icons.outlined.LibraryMusic -import androidx.compose.material.icons.outlined.Search -import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.LoadingIndicator diff --git a/android/app/src/main/java/app/hocket/ui/nav/BottomBar.kt b/android/app/src/main/java/app/hocket/ui/nav/BottomBar.kt index 5b0f9121..c41143f0 100644 --- a/android/app/src/main/java/app/hocket/ui/nav/BottomBar.kt +++ b/android/app/src/main/java/app/hocket/ui/nav/BottomBar.kt @@ -10,34 +10,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.QueueMusic -import androidx.compose.material.icons.automirrored.outlined.QueueMusic -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Album -import androidx.compose.material.icons.filled.BarChart -import androidx.compose.material.icons.filled.Category -import androidx.compose.material.icons.filled.Download -import androidx.compose.material.icons.filled.DragHandle -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.History -import androidx.compose.material.icons.filled.Home -import androidx.compose.material.icons.filled.LibraryMusic -import androidx.compose.material.icons.filled.MusicNote -import androidx.compose.material.icons.filled.Person -import androidx.compose.material.icons.filled.RemoveCircleOutline -import androidx.compose.material.icons.filled.Search -import androidx.compose.material.icons.outlined.Album -import androidx.compose.material.icons.outlined.BarChart -import androidx.compose.material.icons.outlined.Category -import androidx.compose.material.icons.outlined.Download -import androidx.compose.material.icons.outlined.FilterAlt -import androidx.compose.material.icons.outlined.History -import androidx.compose.material.icons.outlined.Home -import androidx.compose.material.icons.outlined.LibraryMusic -import androidx.compose.material.icons.outlined.MusicNote -import androidx.compose.material.icons.outlined.Person -import androidx.compose.material.icons.outlined.Search import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -86,6 +58,7 @@ import app.hocket.NavBarPrefs import app.hocket.R import app.hocket.SyncedNavBarPrefs import app.hocket.core.client.CoreClient +import app.hocket.ui.icons.HocketIcons import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.launch @@ -263,18 +236,18 @@ fun NavDestination.isSettings(): Boolean = listOf( ).any { hasRoute(it) } fun NavItem.icon(selected: Boolean): ImageVector = when (this) { - NavItem.Home -> if (selected) Icons.Filled.Home else Icons.Outlined.Home - NavItem.Search -> if (selected) Icons.Filled.Search else Icons.Outlined.Search - NavItem.Library -> if (selected) Icons.Filled.LibraryMusic else Icons.Outlined.LibraryMusic - NavItem.Playlists -> if (selected) Icons.AutoMirrored.Filled.QueueMusic else Icons.AutoMirrored.Outlined.QueueMusic - NavItem.Artists -> if (selected) Icons.Filled.Person else Icons.Outlined.Person - NavItem.Albums -> if (selected) Icons.Filled.Album else Icons.Outlined.Album - NavItem.Songs -> if (selected) Icons.Filled.MusicNote else Icons.Outlined.MusicNote - NavItem.Genres -> if (selected) Icons.Filled.Category else Icons.Outlined.Category - NavItem.RecentQueues -> if (selected) Icons.Filled.History else Icons.Outlined.History - NavItem.Stats -> if (selected) Icons.Filled.BarChart else Icons.Outlined.BarChart - NavItem.Downloads -> if (selected) Icons.Filled.Download else Icons.Outlined.Download - NavItem.Filters -> if (selected) Icons.Filled.FilterAlt else Icons.Outlined.FilterAlt + NavItem.Home -> if (selected) HocketIcons.Filled.Home else HocketIcons.Outlined.Home + NavItem.Search -> if (selected) HocketIcons.Filled.Search else HocketIcons.Outlined.Search + NavItem.Library -> if (selected) HocketIcons.Filled.LibraryMusic else HocketIcons.Outlined.LibraryMusic + NavItem.Playlists -> if (selected) HocketIcons.AutoMirrored.Filled.QueueMusic else HocketIcons.AutoMirrored.Outlined.QueueMusic + NavItem.Artists -> if (selected) HocketIcons.Filled.Person else HocketIcons.Outlined.Person + NavItem.Albums -> if (selected) HocketIcons.Filled.Album else HocketIcons.Outlined.Album + NavItem.Songs -> if (selected) HocketIcons.Filled.MusicNote else HocketIcons.Outlined.MusicNote + NavItem.Genres -> if (selected) HocketIcons.Filled.Category else HocketIcons.Outlined.Category + NavItem.RecentQueues -> if (selected) HocketIcons.Filled.History else HocketIcons.Outlined.History + NavItem.Stats -> if (selected) HocketIcons.Filled.BarChart else HocketIcons.Outlined.BarChart + NavItem.Downloads -> if (selected) HocketIcons.Filled.Download else HocketIcons.Outlined.Download + NavItem.Filters -> if (selected) HocketIcons.Filled.FilterAlt else HocketIcons.Outlined.FilterAlt } @Composable @@ -361,9 +334,9 @@ fun BottomBarEditor(modifier: Modifier = Modifier) { IconButton( onClick = { if (items.size <= NavItem.MIN) message = minimumMessage else update(items - item) }, modifier = Modifier.testTag("bottomBar.remove.${item.id}"), - ) { Icon(Icons.Filled.RemoveCircleOutline, stringResource(R.string.bottom_bar_remove, label)) } + ) { Icon(HocketIcons.Filled.RemoveCircleOutline, stringResource(R.string.bottom_bar_remove, label)) } Box(Modifier.padding(12.dp).draggableHandle(onDragStarted = { haptics.performHapticFeedback(HapticFeedbackType.GestureThresholdActivate) })) { - Icon(Icons.Filled.DragHandle, null) + Icon(HocketIcons.Filled.DragHandle, null) } } } @@ -376,7 +349,7 @@ fun BottomBarEditor(modifier: Modifier = Modifier) { FlowRow(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), itemVerticalAlignment = Alignment.CenterVertically) { Box { OutlinedButton(onClick = { if (items.size >= NavItem.MAX) message = fullMessage else { message = null; adding = true } }, modifier = Modifier.testTag("bottomBar.add")) { - Icon(Icons.Filled.Add, null) + Icon(HocketIcons.Filled.Add, null) Spacer(Modifier.width(8.dp)) Text(stringResource(R.string.bottom_bar_add)) } diff --git a/android/app/src/main/java/app/hocket/ui/player/HandoffSheet.kt b/android/app/src/main/java/app/hocket/ui/player/HandoffSheet.kt index c9c556d0..f141c8e2 100644 --- a/android/app/src/main/java/app/hocket/ui/player/HandoffSheet.kt +++ b/android/app/src/main/java/app/hocket/ui/player/HandoffSheet.kt @@ -9,11 +9,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Computer -import androidx.compose.material.icons.filled.PhoneAndroid -import androidx.compose.material.icons.filled.Speaker -import androidx.compose.material.icons.filled.VolumeUp import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults @@ -39,6 +34,7 @@ import app.hocket.core.api.Platform import app.hocket.ui.LocalCoreClient import app.hocket.ui.components.EmptyState import app.hocket.ui.components.formatAgo +import app.hocket.ui.icons.HocketIcons /** * The Connect picker. Opening it makes targets pre-buffer (the "ready" state); playback keeps going @@ -67,7 +63,7 @@ fun HandoffSheet(onDismiss: () -> Unit) { Spacer(Modifier.height(8.dp)) devices.firstOrNull { it.isSelf }?.let { self -> ListItem(headlineContent = { Text(stringResource(R.string.handoff_this_device)) }, supportingContent = { if (self.playing) Text(stringResource(R.string.handoff_playing)) }, - leadingContent = { Icon(Icons.Filled.PhoneAndroid, null) }, trailingContent = { if (self.playing) Icon(Icons.Filled.VolumeUp, null, tint = MaterialTheme.colorScheme.primary) }, + leadingContent = { Icon(HocketIcons.Filled.PhoneAndroid, null) }, trailingContent = { if (self.playing) Icon(HocketIcons.Filled.VolumeUp, null, tint = MaterialTheme.colorScheme.primary) }, colors = ListItemDefaults.colors(containerColor = Color.Transparent), modifier = Modifier.clickable { if (!self.playing) client.dispatch(Commands.handoffTo(self.id)); onDismiss() }) } if (targets.isEmpty()) { @@ -77,8 +73,8 @@ fun HandoffSheet(onDismiss: () -> Unit) { ListItem( headlineContent = { Text(d.name) }, supportingContent = { Text(when { d.playing -> stringResource(R.string.handoff_playing); d.ready -> stringResource(R.string.handoff_ready); else -> stringResource(R.string.handoff_preparing) }) }, - leadingContent = { Icon(when (d.platform) { Platform.Android -> Icons.Filled.PhoneAndroid; Platform.Coordinator -> Icons.Filled.Speaker; else -> Icons.Filled.Computer }, null) }, - trailingContent = { if (d.playing) Icon(Icons.Filled.VolumeUp, null, tint = MaterialTheme.colorScheme.primary) else if (!d.ready) LoadingIndicator(Modifier.size(28.dp)) else Text(stringResource(R.string.handoff_last_seen, formatAgo(d.lastSeen)), style = MaterialTheme.typography.labelSmall) }, + leadingContent = { Icon(when (d.platform) { Platform.Android -> HocketIcons.Filled.PhoneAndroid; Platform.Coordinator -> HocketIcons.Filled.Speaker; else -> HocketIcons.Filled.Computer }, null) }, + trailingContent = { if (d.playing) Icon(HocketIcons.Filled.VolumeUp, null, tint = MaterialTheme.colorScheme.primary) else if (!d.ready) LoadingIndicator(Modifier.size(28.dp)) else Text(stringResource(R.string.handoff_last_seen, formatAgo(d.lastSeen)), style = MaterialTheme.typography.labelSmall) }, colors = ListItemDefaults.colors(containerColor = Color.Transparent), modifier = Modifier.clickable { client.dispatch(Commands.handoffTo(d.id)); onDismiss() }.testTag("handoff.device.${d.id}"), ) diff --git a/android/app/src/main/java/app/hocket/ui/player/MiniPlayerBar.kt b/android/app/src/main/java/app/hocket/ui/player/MiniPlayerBar.kt index 2a00bf78..310f0b8e 100644 --- a/android/app/src/main/java/app/hocket/ui/player/MiniPlayerBar.kt +++ b/android/app/src/main/java/app/hocket/ui/player/MiniPlayerBar.kt @@ -20,10 +20,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -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.SkipNext import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LoadingIndicator @@ -64,6 +60,7 @@ import app.hocket.core.api.Command import app.hocket.ui.LocalCoreClient import app.hocket.ui.a11y.LocalReducedMotion import app.hocket.ui.components.Artwork +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.theme.Motion import kotlinx.coroutines.launch import kotlin.math.abs @@ -215,9 +212,9 @@ internal fun MiniPlayerBar(onExpand: () -> Unit, position: () -> Long, hero: Her modifier = Modifier.testTag("miniPlayer.playPause").semantics { contentDescription = playLabel; if (transport.buffering) stateDescription = bufferingLabel }, ) { if (transport.buffering) LoadingIndicator(Modifier.size(28.dp), color = androidx.compose.material3.LocalContentColor.current) - else Icon(if (playing) Icons.Filled.Pause else Icons.Filled.PlayArrow, null) + else Icon(if (playing) HocketIcons.Filled.Pause else HocketIcons.Filled.PlayArrow, null) } - IconButton(onClick = { client.dispatch(Command.Next) }, colors = colors) { Icon(Icons.Filled.SkipNext, stringResource(R.string.action_next)) } + IconButton(onClick = { client.dispatch(Command.Next) }, colors = colors) { Icon(HocketIcons.Filled.SkipNext, stringResource(R.string.action_next)) } } } } diff --git a/android/app/src/main/java/app/hocket/ui/player/NowPlayingPage.kt b/android/app/src/main/java/app/hocket/ui/player/NowPlayingPage.kt index b4173e57..2c7fa244 100644 --- a/android/app/src/main/java/app/hocket/ui/player/NowPlayingPage.kt +++ b/android/app/src/main/java/app/hocket/ui/player/NowPlayingPage.kt @@ -38,19 +38,6 @@ import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.PlaylistAdd -import androidx.compose.material.icons.automirrored.filled.QueueMusic -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.Bedtime -import androidx.compose.material.icons.filled.Cast -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.Lyrics -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Star -import androidx.compose.material.icons.filled.StarBorder -import androidx.compose.material.icons.outlined.Info import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconToggleButton @@ -116,6 +103,7 @@ import app.hocket.ui.a11y.LocalReducedMotion import app.hocket.ui.components.Artwork import app.hocket.ui.components.PlaylistPicker import app.hocket.ui.components.formatClock +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.lyrics.LyricsPage import app.hocket.ui.queue.QueueList import app.hocket.ui.theme.ArtworkColors @@ -189,7 +177,7 @@ internal fun FullPlayer( // the player adds the sleep timer as its extra. val sleep = client.sleepTimer.value val sleepState = sleep?.let { t -> t.endsAt?.let { resources.getString(R.string.sleep_active, formatClock((it - System.currentTimeMillis()).toLong().coerceAtLeast(0))) } ?: resources.getString(R.string.sleep_active_end_of_track) } - track?.let { songMenu.open(it, extras = listOf(app.hocket.ui.components.SongMenuExtra(sleepLabel, Icons.Filled.Bedtime, + track?.let { songMenu.open(it, extras = listOf(app.hocket.ui.components.SongMenuExtra(sleepLabel, HocketIcons.Filled.Bedtime, onClick = { sleepSheet = true }, supporting = sleepState, highlighted = sleep != null, testTag = "player.sleep"))) } }, onAddTo = { addTo = true }) }, items = listOf( @@ -329,7 +317,7 @@ private fun PlayerHeader(onCollapse: () -> Unit, onConnect: () -> Unit, onSwitch Text(stringResource(R.string.player_playing_from), style = MaterialTheme.typography.labelLarge, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1) Text(source, style = MaterialTheme.typography.titleLargeEmphasized, fontWeight = FontWeight.Bold, maxLines = 1, overflow = TextOverflow.Ellipsis) } - Icon(Icons.Filled.ArrowDropDown, null, Modifier.padding(bottom = 2.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) + Icon(HocketIcons.Filled.ArrowDropDown, null, Modifier.padding(bottom = 2.dp), tint = MaterialTheme.colorScheme.onSurfaceVariant) } // Connect, highlighted while another device plays. Tap opens the picker; long-press pulls // playback straight to this device. @@ -353,10 +341,10 @@ private fun PlayerHeader(onCollapse: () -> Unit, onConnect: () -> Unit, onSwitch .testTag("player.connect"), contentAlignment = Alignment.Center, ) { - Icon(Icons.Filled.Cast, stringResource(R.string.player_connect), tint = if (!owns) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) + Icon(HocketIcons.Filled.Cast, stringResource(R.string.player_connect), tint = if (!owns) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) } IconButton(onClick = onCollapse, modifier = Modifier.testTag("player.collapse")) { - Icon(Icons.Filled.KeyboardArrowDown, stringResource(R.string.player_collapse), Modifier.size(28.dp)) + Icon(HocketIcons.Filled.KeyboardArrowDown, stringResource(R.string.player_collapse), Modifier.size(28.dp)) } } } @@ -448,7 +436,7 @@ private fun PlayerNoticeLines() { Row(Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Text(stringResource(R.string.player_resume_offer, offer.deviceName, offer.track.title), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis) TextButton(onClick = { client.dispatch(Command.ResumeHere) }, modifier = Modifier.testTag("player.resumeHere")) { Text(stringResource(R.string.player_resume_here)) } - IconButton(onClick = { client.dispatch(Command.DismissResumeOffer) }) { Icon(Icons.Filled.Close, stringResource(R.string.action_dismiss)) } + IconButton(onClick = { client.dispatch(Command.DismissResumeOffer) }) { Icon(HocketIcons.Filled.Close, stringResource(R.string.action_dismiss)) } } } (entry?.item?.source as? QueueSource.Autoplay)?.let { NoticeLine(stringResource(R.string.player_autoplay_reason, it.data.reason), MaterialTheme.colorScheme.tertiary) } @@ -527,8 +515,8 @@ private fun TitleRow(track: app.hocket.core.api.TrackSummary, compact: Boolean, } } } - IconButton(onClick = onAddTo, modifier = Modifier.testTag("player.addToPlaylist")) { Icon(Icons.AutoMirrored.Filled.PlaylistAdd, stringResource(R.string.player_add_to_playlist)) } - IconButton(onClick = onMore, modifier = Modifier.testTag("player.more")) { Icon(Icons.Filled.MoreVert, stringResource(R.string.action_more)) } + IconButton(onClick = onAddTo, modifier = Modifier.testTag("player.addToPlaylist")) { Icon(HocketIcons.AutoMirrored.Filled.PlaylistAdd, stringResource(R.string.player_add_to_playlist)) } + IconButton(onClick = onMore, modifier = Modifier.testTag("player.more")) { Icon(HocketIcons.Filled.MoreVert, stringResource(R.string.action_more)) } } } } @@ -552,7 +540,7 @@ private fun ModeBar(mode: PlayerMode, onMode: (PlayerMode) -> Unit, pageWidth: D Row(Modifier.fillMaxWidth().padding(horizontal = 12.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.weight(1f), horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { labels.forEach { (m, label) -> - val icon = when (m) { PlayerMode.Lyrics -> Icons.Filled.Lyrics; PlayerMode.Queue -> Icons.AutoMirrored.Filled.QueueMusic; else -> Icons.Outlined.Info } + val icon = when (m) { PlayerMode.Lyrics -> HocketIcons.Filled.Lyrics; PlayerMode.Queue -> HocketIcons.AutoMirrored.Filled.QueueMusic; else -> HocketIcons.Outlined.Info } ModePill(label, icon, selected = mode == m, showLabel = !compact || mode == m, tag = "player.mode." + m.name.lowercase(), onClick = { onMode(if (mode == m) PlayerMode.Artwork else m) }) } } diff --git a/android/app/src/main/java/app/hocket/ui/player/Transport.kt b/android/app/src/main/java/app/hocket/ui/player/Transport.kt index 3857d267..6d404ecc 100644 --- a/android/app/src/main/java/app/hocket/ui/player/Transport.kt +++ b/android/app/src/main/java/app/hocket/ui/player/Transport.kt @@ -40,11 +40,6 @@ 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.SkipNext -import androidx.compose.material.icons.filled.SkipPrevious import androidx.compose.material3.Icon import androidx.compose.material3.LinearWavyProgressIndicator import androidx.compose.material3.LoadingIndicator @@ -93,6 +88,7 @@ import app.hocket.R import app.hocket.ui.a11y.LocalReducedMotion import app.hocket.ui.a11y.Spoken import app.hocket.ui.components.formatClock +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.theme.Motion import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.first @@ -199,7 +195,7 @@ fun PlayPauseButton( label = "playPauseIcon", ) { p -> if (p == null) LoadingIndicator(Modifier.size(iconSize * 1.2f), color = androidx.compose.material3.LocalContentColor.current) - else Icon(if (p) Icons.Filled.Pause else Icons.Filled.PlayArrow, null, Modifier.size(iconSize)) + else Icon(if (p) HocketIcons.Filled.Pause else HocketIcons.Filled.PlayArrow, null, Modifier.size(iconSize)) } } } @@ -239,9 +235,9 @@ fun TransportRow(playing: Boolean, onPrevious: () -> Unit, onToggle: () -> Unit, val tonal = MaterialTheme.colorScheme.secondaryContainer val onTonal = MaterialTheme.colorScheme.onSecondaryContainer Row(modifier.fillMaxWidth().height(height), horizontalArrangement = Arrangement.spacedBy(20.dp, Alignment.CenterHorizontally), verticalAlignment = Alignment.CenterVertically) { - SkipButton(onPrevious, stringResource(R.string.action_previous), Icons.Filled.SkipPrevious, tonal, onTonal, prevSource, prevPressed, Modifier.size(prevWidth, skip).testTag("player.previous")) + SkipButton(onPrevious, stringResource(R.string.action_previous), HocketIcons.Filled.SkipPrevious, tonal, onTonal, prevSource, prevPressed, Modifier.size(prevWidth, skip).testTag("player.previous")) PlayPauseButton(playing, onToggle, Modifier.size(height), playSource, iconSize = height * 0.45f, buffering = buffering) - SkipButton(onNext, stringResource(R.string.action_next), Icons.Filled.SkipNext, tonal, onTonal, nextSource, nextPressed, Modifier.size(nextWidth, skip).testTag("player.next")) + SkipButton(onNext, stringResource(R.string.action_next), HocketIcons.Filled.SkipNext, tonal, onTonal, nextSource, nextPressed, Modifier.size(nextWidth, skip).testTag("player.next")) } } diff --git a/android/app/src/main/java/app/hocket/ui/queue/QueuePanel.kt b/android/app/src/main/java/app/hocket/ui/queue/QueuePanel.kt index cc3cc749..e068f96f 100644 --- a/android/app/src/main/java/app/hocket/ui/queue/QueuePanel.kt +++ b/android/app/src/main/java/app/hocket/ui/queue/QueuePanel.kt @@ -25,14 +25,6 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.AllInclusive -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.DragHandle -import androidx.compose.material.icons.filled.GraphicEq -import androidx.compose.material.icons.filled.Repeat -import androidx.compose.material.icons.filled.RepeatOne -import androidx.compose.material.icons.filled.Shuffle import androidx.compose.material3.FilledTonalToggleButton import androidx.compose.material3.Icon import androidx.compose.material3.LocalContentColor @@ -100,6 +92,7 @@ import app.hocket.ui.components.offlineStateText import app.hocket.ui.components.trackLabel import app.hocket.ui.components.trackRowActions import app.hocket.core.ActionIds +import app.hocket.ui.icons.HocketIcons import kotlinx.coroutines.delay import sh.calvin.reorderable.ReorderableItem import sh.calvin.reorderable.rememberReorderableLazyListState @@ -144,19 +137,19 @@ private fun QueueModeHeader(modifier: Modifier = Modifier) { Row(modifier, horizontalArrangement = Arrangement.spacedBy(8.dp)) { ModeToggle( checked = queue.shuffle, onCheckedChange = { client.dispatch(Commands.setShuffle(it)) }, - icon = Icons.Filled.Shuffle, label = stringResource(R.string.action_shuffle), tag = "queue.shuffle", modifier = Modifier.weight(1f), + icon = HocketIcons.Filled.Shuffle, label = stringResource(R.string.action_shuffle), tag = "queue.shuffle", modifier = Modifier.weight(1f), ) ModeToggle( checked = queue.repeat != RepeatMode.Off, onCheckedChange = { client.dispatch(Commands.setRepeat(when (queue.repeat) { RepeatMode.Off -> RepeatMode.All; RepeatMode.All -> RepeatMode.One; RepeatMode.One -> RepeatMode.Off })) }, - icon = if (queue.repeat == RepeatMode.One) Icons.Filled.RepeatOne else Icons.Filled.Repeat, + icon = if (queue.repeat == RepeatMode.One) HocketIcons.Filled.RepeatOne else HocketIcons.Filled.Repeat, label = stringResource(R.string.action_label_repeat), state = stringResource(when (queue.repeat) { RepeatMode.Off -> R.string.player_repeat_off; RepeatMode.All -> R.string.player_repeat_all; RepeatMode.One -> R.string.player_repeat_one }), tag = "queue.repeat", modifier = Modifier.weight(1f), ) ModeToggle( checked = queue.autoplay, onCheckedChange = { client.dispatch(Commands.setAutoplay(it)) }, - icon = Icons.Filled.AllInclusive, label = stringResource(R.string.player_autoplay), tag = "queue.infinite", modifier = Modifier.weight(1f), + icon = HocketIcons.Filled.AllInclusive, label = stringResource(R.string.player_autoplay), tag = "queue.infinite", modifier = Modifier.weight(1f), ) } } @@ -403,7 +396,7 @@ private fun QueueTimeline(modifier: Modifier, contentPadding: PaddingValues) { onDragStopped = { drop() }, ), contentAlignment = Alignment.Center, - ) { Icon(Icons.Filled.DragHandle, null, tint = MaterialTheme.colorScheme.onSurfaceVariant) } + ) { Icon(HocketIcons.Filled.DragHandle, null, tint = MaterialTheme.colorScheme.onSurfaceVariant) } }) else null, ) } @@ -490,13 +483,13 @@ private fun QueueRow( Row(Modifier.fillMaxWidth().heightIn(min = 64.dp).padding(start = 16.dp, end = if (handle != null) 4.dp else 16.dp, top = 8.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically) { Box(Modifier.size(48.dp), contentAlignment = Alignment.Center) { Artwork(track.coverArt, ArtworkSizes.THUMB, null, Modifier.size(48.dp), RoundedCornerShape(ListArtCorner)) - if (selected) Surface(shape = CircleShape, color = MaterialTheme.colorScheme.primary) { Icon(Icons.Filled.Check, null, Modifier.padding(4.dp), tint = MaterialTheme.colorScheme.onPrimary) } + if (selected) Surface(shape = CircleShape, color = MaterialTheme.colorScheme.primary) { Icon(HocketIcons.Filled.Check, null, Modifier.padding(4.dp), tint = MaterialTheme.colorScheme.onPrimary) } } Spacer(Modifier.width(14.dp)) Column(Modifier.weight(1f)) { Row(verticalAlignment = Alignment.CenterVertically) { if (current) { - Icon(Icons.Filled.GraphicEq, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) + Icon(HocketIcons.Filled.GraphicEq, null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(16.dp)) Spacer(Modifier.width(6.dp)) } Text(track.title, style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis, color = content.copy(alpha = content.alpha * dim)) diff --git a/android/app/src/main/java/app/hocket/ui/queue/RecentQueues.kt b/android/app/src/main/java/app/hocket/ui/queue/RecentQueues.kt index 09d10328..d532d0f4 100644 --- a/android/app/src/main/java/app/hocket/ui/queue/RecentQueues.kt +++ b/android/app/src/main/java/app/hocket/ui/queue/RecentQueues.kt @@ -15,13 +15,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.History -import androidx.compose.material.icons.filled.PlaylistAdd -import androidx.compose.material.icons.filled.PushPin -import androidx.compose.material.icons.outlined.PushPin import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -50,6 +43,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import app.hocket.R +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.AccountButton import app.hocket.core.ArtworkSizes import app.hocket.core.Commands @@ -104,10 +98,10 @@ private fun SavedQueueRow(sq: SavedQueue, onDelete: () -> Unit, onSaveAs: () -> Text(stringResource(R.string.saved_tracks, sq.trackCount.toInt()) + stringResource(R.string.dot_separator) + stringResource(R.string.saved_played, formatAgo(sq.lastInteractedAt)), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onSurfaceVariant) } IconToggleButton(checked = sq.pinned, onCheckedChange = { client.dispatch(Commands.pinSavedQueue(sq.id, it)) }) { - Icon(if (sq.pinned) Icons.Filled.PushPin else Icons.Outlined.PushPin, stringResource(if (sq.pinned) R.string.saved_unpin else R.string.saved_pin), tint = if (sq.pinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) + Icon(if (sq.pinned) HocketIcons.Filled.PushPin else HocketIcons.Outlined.PushPin, stringResource(if (sq.pinned) R.string.saved_unpin else R.string.saved_pin), tint = if (sq.pinned) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant) } - IconButton(onClick = onSaveAs) { Icon(Icons.Filled.PlaylistAdd, stringResource(R.string.queue_save_as_playlist)) } - IconButton(onClick = onDelete) { Icon(Icons.Filled.Delete, stringResource(R.string.action_delete)) } + IconButton(onClick = onSaveAs) { Icon(HocketIcons.Filled.PlaylistAdd, stringResource(R.string.queue_save_as_playlist)) } + IconButton(onClick = onDelete) { Icon(HocketIcons.Filled.Delete, stringResource(R.string.action_delete)) } } } @@ -126,7 +120,7 @@ fun UndoHistoryPanel(modifier: Modifier = Modifier) { LazyColumn(contentPadding = PaddingValues(bottom = app.hocket.ui.nav.BottomContentInset)) { items(undo.history, key = { it.id }) { e -> Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 6.dp), verticalAlignment = Alignment.CenterVertically) { - Icon(Icons.Filled.History, null, tint = MaterialTheme.colorScheme.onSurfaceVariant) + Icon(HocketIcons.Filled.History, null, tint = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(Modifier.width(12.dp)) Column(Modifier.weight(1f)) { Text(e.label, style = MaterialTheme.typography.bodyLarge) @@ -151,10 +145,10 @@ fun SavedQueuesScreen(nav: NavHostController) { TopAppBar( title = { Text(stringResource(R.string.home_saved_queues)) }, actions = { - IconButton(onClick = { undoHistory = true }, modifier = Modifier.testTag("savedQueues.undoHistory")) { Icon(Icons.Filled.History, stringResource(R.string.queue_undo_history)) } + IconButton(onClick = { undoHistory = true }, modifier = Modifier.testTag("savedQueues.undoHistory")) { Icon(HocketIcons.Filled.History, stringResource(R.string.queue_undo_history)) } AccountButton() }, - navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, + navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, ) }) { padding -> RecentQueuesList(Modifier.fillMaxSize().padding(padding)) diff --git a/android/app/src/main/java/app/hocket/ui/screens/detail/DetailScreens.kt b/android/app/src/main/java/app/hocket/ui/screens/detail/DetailScreens.kt index afef2cd8..e6ccabde 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/detail/DetailScreens.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/detail/DetailScreens.kt @@ -17,15 +17,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.PlaylistRemove -import androidx.compose.material.icons.filled.DragHandle -import androidx.compose.material.icons.filled.Favorite -import androidx.compose.material.icons.filled.FavoriteBorder -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Shuffle import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -63,6 +54,7 @@ import app.hocket.R import androidx.lifecycle.Lifecycle import androidx.lifecycle.compose.LocalLifecycleOwner import androidx.lifecycle.repeatOnLifecycle +import app.hocket.ui.icons.HocketIcons import kotlinx.coroutines.delay import app.hocket.ui.nav.BottomContentInset import app.hocket.core.ArtworkSizes @@ -137,7 +129,7 @@ private fun DetailScaffold( else Text(title, maxLines = 2, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, modifier = Modifier.semantics { heading() }) }, subtitle = subtitle?.let { { Text(it, maxLines = 1, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis) } }, - navigationIcon = { if (!embedded) IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, + navigationIcon = { if (!embedded) IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, actions = actions, scrollBehavior = scroll, ) }, @@ -247,14 +239,14 @@ fun AlbumDetailScreen(nav: NavHostController, id: String, embedded: Boolean = fa val songMenu = app.hocket.ui.components.rememberSongMenu() var albumSheet by remember { mutableStateOf(false) } val context = Commands.albumContext(a.serverId, a.id, a.name) - DetailScaffold(nav, embedded, a.name, a.artist, a.coverArt, actions = { IconButton(onClick = { albumSheet = true }) { Icon(Icons.Filled.MoreVert, stringResource(R.string.action_more)) } }, header = { + DetailScaffold(nav, embedded, a.name, a.artist, a.coverArt, actions = { IconButton(onClick = { albumSheet = true }) { Icon(HocketIcons.Filled.MoreVert, stringResource(R.string.action_more)) } }, header = { Text(listOfNotNull(a.year?.toString(), a.genre).joinToString(stringResource(R.string.dot_separator)), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Text(stringResource(R.string.album_tracks_count, a.songCount.toInt(), formatDurationWords(a.durationMs.toLong())), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant) Spacer(Modifier.height(8.dp)) FlowRow(itemVerticalAlignment = Alignment.CenterVertically) { RatingStars(a.rating.toInt(), onRate = { client.dispatch(Commands.setRating(listOf(app.hocket.core.api.RatingTarget.Album(app.hocket.core.api.RatingTargetAlbumInner(a.id))), it)) }, starSize = 20.dp) IconToggleButton(checked = a.loved, onCheckedChange = { client.dispatch(Commands.loveAlbum(a.id, it)) }) { - Icon(if (a.loved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, stringResource(if (a.loved) R.string.action_unlove else R.string.action_love), tint = if (a.loved) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant) + Icon(if (a.loved) HocketIcons.Filled.Favorite else HocketIcons.Filled.FavoriteBorder, stringResource(if (a.loved) R.string.action_unlove else R.string.action_love), tint = if (a.loved) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant) } } Spacer(Modifier.height(8.dp)) @@ -274,8 +266,8 @@ fun AlbumDetailScreen(nav: NavHostController, id: String, embedded: Boolean = fa @Composable fun PlayShuffleRow(onPlay: () -> Unit, onShuffle: () -> Unit) { FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - Button(onClick = onPlay, shapes = ButtonDefaults.shapes()) { Icon(Icons.Filled.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text(stringResource(R.string.action_play)) } - FilledTonalButton(onClick = onShuffle, shapes = ButtonDefaults.shapes()) { Icon(Icons.Filled.Shuffle, null); Spacer(Modifier.width(6.dp)); Text(stringResource(R.string.action_shuffle)) } + Button(onClick = onPlay, shapes = ButtonDefaults.shapes()) { Icon(HocketIcons.Filled.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text(stringResource(R.string.action_play)) } + FilledTonalButton(onClick = onShuffle, shapes = ButtonDefaults.shapes()) { Icon(HocketIcons.Filled.Shuffle, null); Spacer(Modifier.width(6.dp)); Text(stringResource(R.string.action_shuffle)) } } } @@ -305,7 +297,7 @@ fun ArtistDetailScreen(nav: NavHostController, id: String, embedded: Boolean = f val songMenu = app.hocket.ui.components.rememberSongMenu() DetailScaffold(nav, embedded, ar.name, stringResource(R.string.library_count_albums, ar.albumCount.toInt()), ar.coverArt, roundArtwork = true, header = { IconToggleButton(checked = ar.loved, onCheckedChange = { client.dispatch(Commands.setArtistLoved(ar.id, it)) }) { - Icon(if (ar.loved) Icons.Filled.Favorite else Icons.Filled.FavoriteBorder, stringResource(if (ar.loved) R.string.action_unlove else R.string.action_love), tint = if (ar.loved) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant) + Icon(if (ar.loved) HocketIcons.Filled.Favorite else HocketIcons.Filled.FavoriteBorder, stringResource(if (ar.loved) R.string.action_unlove else R.string.action_love), tint = if (ar.loved) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.onSurfaceVariant) } PlayShuffleRow(onPlay = { client.dispatch(Commands.playContext(context)) }, onShuffle = { client.dispatch(Commands.playContext(context, shuffle = true)) }) }) { @@ -393,8 +385,8 @@ fun PlaylistDetailScreen(nav: NavHostController, id: String, embedded: Boolean = LargeFlexibleTopAppBar( title = { if (p == null) SkeletonLine(Modifier.width(200.dp), 24.dp) else Text(p.name, maxLines = 2, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, modifier = Modifier.semantics { heading() }) }, subtitle = { Text(p?.owner?.let { stringResource(R.string.playlist_by, it) } ?: "") }, - navigationIcon = { if (!embedded) IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, - actions = { if (p != null) IconButton(onClick = { playlistSheet = true }) { Icon(Icons.Filled.MoreVert, stringResource(R.string.action_more)) } }, scrollBehavior = scroll) + navigationIcon = { if (!embedded) IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, + actions = { if (p != null) IconButton(onClick = { playlistSheet = true }) { Icon(HocketIcons.Filled.MoreVert, stringResource(R.string.action_more)) } }, scrollBehavior = scroll) }, ) { padding -> Box(Modifier.fillMaxSize()) { @@ -431,14 +423,14 @@ fun PlaylistDetailScreen(nav: NavHostController, id: String, embedded: Boolean = ReorderableItem(reorderable, key = e.key, enabled = editable) { dragging -> TrackRow(t.toSummary(), onClick = { client.dispatch(Commands.playContext(context, startIndex = i)) }, onMore = { // The playlist's own extra: remove this entry (by its position). - val extras = if (editable) listOf(app.hocket.ui.components.SongMenuExtra(removeFromPlaylist, Icons.Filled.PlaylistRemove, + val extras = if (editable) listOf(app.hocket.ui.components.SongMenuExtra(removeFromPlaylist, HocketIcons.Filled.PlaylistRemove, onClick = { client.dispatch(Commands.playlistRemove(p.id, listOf(i))) }, destructive = true, testTag = "songMenu.removeFromPlaylist")) else emptyList() songMenu.open(t.toSummary(), extras = extras) }, selected = selecting && selection.contains(t.id), selectionActive = selecting, onToggleSelect = { client.toggleSelected(SelectionKind.Tracks, t.id) }, nowPlaying = nowPlaying?.track?.id == t.id, trailing = if (editable) ({ - Icon(Icons.Filled.DragHandle, stringResource(R.string.playlist_reorder_handle), tint = MaterialTheme.colorScheme.onSurfaceVariant, + Icon(HocketIcons.Filled.DragHandle, stringResource(R.string.playlist_reorder_handle), tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp).draggableHandle( onDragStarted = { dragKey = e.key diff --git a/android/app/src/main/java/app/hocket/ui/screens/downloads/DownloadsScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/downloads/DownloadsScreen.kt index 8402399e..baa65d7a 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/downloads/DownloadsScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/downloads/DownloadsScreen.kt @@ -12,9 +12,6 @@ import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -38,10 +35,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import app.hocket.R +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.AccountButton import androidx.compose.foundation.clickable -import androidx.compose.material.icons.filled.ChevronRight -import androidx.compose.material.icons.outlined.OfflineBolt import androidx.compose.material3.ListItem import androidx.compose.ui.platform.testTag import app.hocket.ui.nav.Route @@ -73,7 +69,7 @@ fun DownloadsScreen(nav: NavHostController) { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.downloads_title)) }, subtitle = { Text(stringResource(R.string.downloads_storage, formatBytes(storage.downloadsBytes), formatBytes(storage.cacheBytes), storage.freeBytes?.let { formatBytes(it) } ?: "?")) }, - navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, + navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, actions = { AccountButton() }, scrollBehavior = scroll, ) @@ -85,8 +81,8 @@ fun DownloadsScreen(nav: NavHostController) { ListItem( headlineContent = { Text(stringResource(R.string.available_offline)) }, supportingContent = { Text(stringResource(R.string.settings_available_offline_body)) }, - leadingContent = { Icon(Icons.Outlined.OfflineBolt, null) }, - trailingContent = { Icon(Icons.Filled.ChevronRight, null) }, + leadingContent = { Icon(HocketIcons.Outlined.OfflineBolt, null) }, + trailingContent = { Icon(HocketIcons.Filled.ChevronRight, null) }, modifier = Modifier.clickable { nav.navigate(Route.AvailableOffline) }.testTag("downloads.availableOffline"), ) } @@ -106,7 +102,7 @@ fun DownloadsScreen(nav: NavHostController) { Badge(stringResource(if (pin.transcoded) R.string.downloads_transcoded else R.string.downloads_original)) } } - IconButton(onClick = { removing = pin }) { Icon(Icons.Filled.Delete, stringResource(R.string.action_remove_download)) } + IconButton(onClick = { removing = pin }) { Icon(HocketIcons.Filled.Delete, stringResource(R.string.action_remove_download)) } } if (pin.downloadedCount < pin.trackCount) { LinearWavyProgressIndicator(progress = { pin.downloadedCount.toFloat() / pin.trackCount.toFloat().coerceAtLeast(1f) }, modifier = Modifier.fillMaxWidth().padding(top = 6.dp)) diff --git a/android/app/src/main/java/app/hocket/ui/screens/filters/FilterBuilderScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/filters/FilterBuilderScreen.kt index 23d22ca3..3d0acb0e 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/filters/FilterBuilderScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/filters/FilterBuilderScreen.kt @@ -13,12 +13,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.PlayArrow -import androidx.compose.material.icons.filled.Save import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox @@ -53,6 +47,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import app.hocket.R +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.BottomContentInset import app.hocket.core.Commands import app.hocket.core.Queries @@ -140,8 +135,8 @@ fun FilterBuilderScreen(nav: NavHostController, id: String?) { } } Scaffold(topBar = { - TopAppBar(title = { Text(filter.name) }, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, - actions = { IconButton(onClick = { client.dispatch(Commands.saveFilter(filter)) }) { Icon(Icons.Filled.Save, stringResource(R.string.filter_save)) } }) + TopAppBar(title = { Text(filter.name) }, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, + actions = { IconButton(onClick = { client.dispatch(Commands.saveFilter(filter)) }) { Icon(HocketIcons.Filled.Save, stringResource(R.string.filter_save)) } }) }) { padding -> LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = padding.calculateTopPadding(), bottom = BottomContentInset, start = 16.dp, end = 16.dp)) { item { OutlinedTextField(value = filter.name, onValueChange = { filter = filter.copy(name = it) }, label = { Text(stringResource(R.string.filter_name)) }, singleLine = true, modifier = Modifier.fillMaxWidth()) } @@ -177,9 +172,9 @@ fun FilterBuilderScreen(nav: NavHostController, id: String?) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) { var menu by remember { mutableStateOf(false) } SplitButtonLayout( - leadingButton = { SplitButtonDefaults.LeadingButton(onClick = { sid?.let { client.dispatch(Commands.playContext(Commands.filterContext(it, filter))) } }) { Icon(Icons.Filled.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text(stringResource(R.string.filter_play)) } }, + leadingButton = { SplitButtonDefaults.LeadingButton(onClick = { sid?.let { client.dispatch(Commands.playContext(Commands.filterContext(it, filter))) } }) { Icon(HocketIcons.Filled.PlayArrow, null); Spacer(Modifier.width(6.dp)); Text(stringResource(R.string.filter_play)) } }, trailingButton = { - SplitButtonDefaults.TrailingButton(onClick = { menu = true }) { Icon(Icons.Filled.ArrowDropDown, stringResource(R.string.action_more)) } + SplitButtonDefaults.TrailingButton(onClick = { menu = true }) { Icon(HocketIcons.Filled.ArrowDropDown, stringResource(R.string.action_more)) } DropdownMenu(expanded = menu, onDismissRequest = { menu = false }) { DropdownMenuItem(text = { Text(stringResource(R.string.filter_save)) }, onClick = { client.dispatch(Commands.saveFilter(filter)); menu = false }) DropdownMenuItem(text = { Text(stringResource(R.string.filter_create_smart)) }, enabled = smartOk, onClick = { namingFor = "smart"; menu = false }) @@ -227,7 +222,7 @@ private fun GroupEditor(node: FilterNode, depth: Int, onChange: (FilterNode) -> val group: FilterNode = FilterNode.Any(listOf(FilterNode.Rule(FilterRule(FilterField.Artist, FilterOp.Is, FilterValue.Text(""))))) onChange(rebuild(children + group)) }) { Text(stringResource(R.string.filter_add_group)) } - if (onRemove != null) IconButton(onClick = onRemove) { Icon(Icons.Filled.Close, stringResource(R.string.filter_remove_rule)) } + if (onRemove != null) IconButton(onClick = onRemove) { Icon(HocketIcons.Filled.Close, stringResource(R.string.filter_remove_rule)) } } children.forEachIndexed { i, child -> Spacer(Modifier.height(6.dp)) @@ -248,7 +243,7 @@ private fun RuleEditor(rule: FilterRule, onChange: (FilterRule) -> Unit, onRemov Spacer(Modifier.width(6.dp)) EnumPicker(stringResource(R.string.filter_op), rule.op.string, FilterText.opsFor(rule.field).map { it to it.string }) { op -> onChange(rule.copy(op = op, value = FilterText.defaultValue(rule.field, op))) } Spacer(Modifier.weight(1f)) - IconButton(onClick = onRemove) { Icon(Icons.Filled.Close, stringResource(R.string.filter_remove_rule)) } + IconButton(onClick = onRemove) { Icon(HocketIcons.Filled.Close, stringResource(R.string.filter_remove_rule)) } } ValueEditor(rule.value, onChange = { onChange(rule.copy(value = it)) }) } @@ -276,7 +271,7 @@ private fun ValueEditor(value: FilterValue, onChange: (FilterValue) -> Unit) { fun EnumPicker(label: String?, current: String, options: List>, onPick: (T) -> Unit) { var open by remember { mutableStateOf(false) } androidx.compose.foundation.layout.Box { - OutlinedButton(onClick = { open = true }, shapes = ButtonDefaults.shapes()) { Text(if (label != null) "$label: $current" else current); Icon(Icons.Filled.ArrowDropDown, null) } + OutlinedButton(onClick = { open = true }, shapes = ButtonDefaults.shapes()) { Text(if (label != null) "$label: $current" else current); Icon(HocketIcons.Filled.ArrowDropDown, null) } DropdownMenu(expanded = open, onDismissRequest = { open = false }) { options.forEach { (v, text) -> DropdownMenuItem(text = { Text(text) }, onClick = { onPick(v); open = false }) } } diff --git a/android/app/src/main/java/app/hocket/ui/screens/filters/FiltersScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/filters/FiltersScreen.kt index a128bb53..b6254ff5 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/filters/FiltersScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/filters/FiltersScreen.kt @@ -6,12 +6,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.Add -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.FilterAlt -import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon @@ -33,6 +27,7 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavHostController import app.hocket.R +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.AccountButton import app.hocket.ui.nav.BottomContentInset import app.hocket.core.Commands @@ -52,8 +47,8 @@ fun FiltersScreen(nav: NavHostController) { val scroll = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( modifier = Modifier.nestedScroll(scroll.nestedScrollConnection), - topBar = { MediumFlexibleTopAppBar(title = { Text(stringResource(R.string.filters_title)) }, actions = { AccountButton() }, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, scrollBehavior = scroll) }, - floatingActionButton = { FloatingActionButton(onClick = { nav.navigate(Route.FilterBuilder()) }, modifier = Modifier.padding(bottom = app.hocket.ui.nav.BottomOverlayInset - 16.dp)) { Icon(Icons.Filled.Add, stringResource(R.string.filters_new)) } }, + topBar = { MediumFlexibleTopAppBar(title = { Text(stringResource(R.string.filters_title)) }, actions = { AccountButton() }, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, scrollBehavior = scroll) }, + floatingActionButton = { FloatingActionButton(onClick = { nav.navigate(Route.FilterBuilder()) }, modifier = Modifier.padding(bottom = app.hocket.ui.nav.BottomOverlayInset - 16.dp)) { Icon(HocketIcons.Filled.Add, stringResource(R.string.filters_new)) } }, ) { padding -> if (filters.isEmpty()) { EmptyState(stringResource(R.string.empty_filters_title), stringResource(R.string.empty_filters_body), Modifier.padding(top = padding.calculateTopPadding()), stringResource(R.string.filters_new)) { nav.navigate(Route.FilterBuilder()) } @@ -64,11 +59,11 @@ fun FiltersScreen(nav: NavHostController) { ListItem( headlineContent = { Text(f.name) }, supportingContent = { Text(describe(f)) }, - leadingContent = { Icon(Icons.Filled.FilterAlt, null) }, + leadingContent = { Icon(HocketIcons.Filled.FilterAlt, null) }, trailingContent = { androidx.compose.foundation.layout.Row { - IconButton(onClick = { server?.let { client.dispatch(Commands.playContext(Commands.filterContext(it.id, f))) } }) { Icon(Icons.Filled.PlayArrow, stringResource(R.string.filter_play)) } - IconButton(onClick = { deleting = f }) { Icon(Icons.Filled.Delete, stringResource(R.string.action_delete)) } + IconButton(onClick = { server?.let { client.dispatch(Commands.playContext(Commands.filterContext(it.id, f))) } }) { Icon(HocketIcons.Filled.PlayArrow, stringResource(R.string.filter_play)) } + IconButton(onClick = { deleting = f }) { Icon(HocketIcons.Filled.Delete, stringResource(R.string.action_delete)) } } }, modifier = Modifier.clickable { nav.navigate(Route.FilterBuilder(f.id)) }, diff --git a/android/app/src/main/java/app/hocket/ui/screens/library/AvailableOfflineScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/library/AvailableOfflineScreen.kt index f8b1a88d..f5f69ef5 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/library/AvailableOfflineScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/library/AvailableOfflineScreen.kt @@ -3,8 +3,6 @@ package app.hocket.ui.screens.library import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon @@ -28,6 +26,7 @@ import app.hocket.R import app.hocket.core.Commands import app.hocket.core.api.SortOrder import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.screens.detail.PlayShuffleRow /** @@ -50,7 +49,7 @@ fun AvailableOfflineScreen(nav: NavHostController) { MediumFlexibleTopAppBar( title = { Text(stringResource(R.string.available_offline), modifier = Modifier.semantics { heading() }) }, subtitle = { Text(stringResource(R.string.settings_available_offline_body)) }, - navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, + navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, scrollBehavior = scroll, ) }, diff --git a/android/app/src/main/java/app/hocket/ui/screens/library/LibraryScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/library/LibraryScreen.kt index cef5a4e2..e6580b0c 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/library/LibraryScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/library/LibraryScreen.kt @@ -1,5 +1,6 @@ package app.hocket.ui.screens.library +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.ScrollToTopOnReselect import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.foundation.background @@ -20,10 +21,6 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Sort -import androidx.compose.material.icons.outlined.OfflineBolt import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -168,16 +165,16 @@ fun LibraryScreen(nav: NavHostController, initialTab: Int = 0) { title = { Text(stringResource(R.string.nav_library), modifier = Modifier.semantics { heading() }) }, actions = { Box { - IconButton(onClick = { sortMenu = true }) { Icon(Icons.Filled.Sort, stringResource(R.string.action_sort)) } + IconButton(onClick = { sortMenu = true }) { Icon(HocketIcons.Filled.Sort, stringResource(R.string.action_sort)) } DropdownMenu(expanded = sortMenu, onDismissRequest = { sortMenu = false }) { val options = if (pager.currentPage == 3) listOf(SortOrder.Title, SortOrder.Artist, SortOrder.Album, SortOrder.Year, SortOrder.DateAdded, SortOrder.Rating, SortOrder.PlayCount, SortOrder.Duration, SortOrder.Bpm, SortOrder.Energy, SortOrder.Random) else listOf(SortOrder.Default, SortOrder.Artist, SortOrder.Year, SortOrder.DateAdded, SortOrder.Rating, SortOrder.PlayCount, SortOrder.Random) options.forEach { o -> // The check mark is drawn only; the chosen order is announced as "selected". - DropdownMenuItem(text = { Text(sortLabel(o)) }, trailingIcon = { if (sort == o) Icon(Icons.Filled.Check, null) }, onClick = { sort = o; sortMenu = false }, + DropdownMenuItem(text = { Text(sortLabel(o)) }, trailingIcon = { if (sort == o) Icon(HocketIcons.Filled.Check, null) }, onClick = { sort = o; sortMenu = false }, modifier = Modifier.semantics { selected = sort == o }) } - DropdownMenuItem(text = { Text(stringResource(R.string.sort_descending)) }, trailingIcon = { if (descending) Icon(Icons.Filled.Check, null) }, onClick = { descending = !descending; sortMenu = false }, + DropdownMenuItem(text = { Text(stringResource(R.string.sort_descending)) }, trailingIcon = { if (descending) Icon(HocketIcons.Filled.Check, null) }, onClick = { descending = !descending; sortMenu = false }, modifier = Modifier.semantics { stateDescription = if (descending) onLabel else offLabel }) } } @@ -373,7 +370,7 @@ internal fun SongsTab(serverId: String, sort: SortOrder, descending: Boolean, na FilterChip( selected = offlineOnly, onClick = { onOfflineOnlyChange(!offlineOnly) }, label = { Text(stringResource(R.string.available_offline)) }, - leadingIcon = { Icon(if (offlineOnly) Icons.Filled.Check else Icons.Outlined.OfflineBolt, null, Modifier.size(FilterChipDefaults.IconSize)) }, + leadingIcon = { Icon(if (offlineOnly) HocketIcons.Filled.Check else HocketIcons.Outlined.OfflineBolt, null, Modifier.size(FilterChipDefaults.IconSize)) }, modifier = Modifier.testTag("library.availableOffline"), ) } diff --git a/android/app/src/main/java/app/hocket/ui/screens/search/SearchScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/search/SearchScreen.kt index a1fa3030..f6e10e02 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/search/SearchScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/search/SearchScreen.kt @@ -1,5 +1,6 @@ package app.hocket.ui.screens.search +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.ScrollToTopOnReselect import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.layout.Box @@ -17,9 +18,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Search import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -116,8 +114,8 @@ fun SearchScreen(nav: NavHostController) { Row(verticalAlignment = androidx.compose.ui.Alignment.CenterVertically) { OutlinedTextField( value = query, onValueChange = { query = it }, singleLine = true, placeholder = { Text(stringResource(R.string.search_hint)) }, - leadingIcon = { Icon(Icons.Filled.Search, null) }, - trailingIcon = { if (query.isNotEmpty()) IconButton(onClick = { query = "" }) { Icon(Icons.Filled.Close, stringResource(R.string.search_clear)) } }, + leadingIcon = { Icon(HocketIcons.Filled.Search, null) }, + trailingIcon = { if (query.isNotEmpty()) IconButton(onClick = { query = "" }) { Icon(HocketIcons.Filled.Close, stringResource(R.string.search_clear)) } }, modifier = Modifier.weight(1f).padding(start = 16.dp, end = 4.dp, top = 8.dp, bottom = 8.dp).focusRequester(focus).testTag("search.input"), ) AccountButton(Modifier.padding(end = 4.dp)) diff --git a/android/app/src/main/java/app/hocket/ui/screens/settings/SettingsCategoryScreens.kt b/android/app/src/main/java/app/hocket/ui/screens/settings/SettingsCategoryScreens.kt index a57c619e..7be4b006 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/settings/SettingsCategoryScreens.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/settings/SettingsCategoryScreens.kt @@ -16,8 +16,6 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.selection.selectable import androidx.compose.foundation.selection.selectableGroup import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.icons.automirrored.filled.Logout -import androidx.compose.material.icons.filled.Check import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -28,8 +26,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import app.hocket.core.api.SettingScope -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ChevronRight import androidx.compose.material3.Icon import androidx.compose.material3.Slider import androidx.compose.runtime.Composable @@ -65,6 +61,7 @@ import app.hocket.ui.LocalCoreClient import app.hocket.ui.components.ConfirmDialog import app.hocket.ui.components.formatAgo import app.hocket.ui.components.formatBytes +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.Route import kotlinx.coroutines.launch import kotlin.math.roundToInt @@ -77,7 +74,7 @@ import kotlin.math.roundToInt */ @Composable -private fun Chevron() = Icon(Icons.Filled.ChevronRight, null) +private fun Chevron() = Icon(HocketIcons.Filled.ChevronRight, null) /** Account & server: the settings-sync master switch, the server, sync now / full sync, sign out. */ @Composable @@ -95,7 +92,7 @@ fun AccountSettingsScreen(nav: NavHostController) { } SwitchRow(stringResource(R.string.settings_sync_master), sync.bool ?: true, { client.dispatch(Commands.setSettingsSync(it)) }, stringResource(R.string.settings_sync_master_body), scope = sync.scope ?: SettingScope.DeviceLocal, tag = "sync.master") // Destructive, and last: nothing below it to hit by mistake. - server?.let { SettingRow(stringResource(R.string.settings_remove_server), onClick = { signOut = true }, tag = "server.remove", destructive = true) { Icon(Icons.AutoMirrored.Filled.Logout, null, tint = MaterialTheme.colorScheme.error) } } + server?.let { SettingRow(stringResource(R.string.settings_remove_server), onClick = { signOut = true }, tag = "server.remove", destructive = true) { Icon(HocketIcons.AutoMirrored.Filled.Logout, null, tint = MaterialTheme.colorScheme.error) } } } // Sign-out goes through CoreHost.removeServer: the stored login is removed before RemoveServer, // so nothing can replay it and resurrect the server. @@ -160,7 +157,7 @@ private fun AccentChoices(dynamicLabel: String, swatches: List Icons.Filled.AccountCircle - SettingsCategory.Appearance -> Icons.Filled.Palette - SettingsCategory.Playback -> Icons.AutoMirrored.Filled.QueueMusic - SettingsCategory.Audio -> Icons.Filled.GraphicEq - SettingsCategory.Streaming -> Icons.Filled.NetworkCheck - SettingsCategory.Downloads -> Icons.Filled.Download - SettingsCategory.Lyrics -> Icons.Filled.Lyrics - SettingsCategory.Library -> Icons.Filled.LibraryMusic - SettingsCategory.Battery -> Icons.Filled.BatterySaver - SettingsCategory.Connect -> Icons.Filled.Devices - SettingsCategory.Customise -> Icons.Filled.Tune - SettingsCategory.Backup -> Icons.Filled.SettingsBackupRestore - SettingsCategory.About -> Icons.Filled.Info + SettingsCategory.Account -> HocketIcons.Filled.AccountCircle + SettingsCategory.Appearance -> HocketIcons.Filled.Palette + SettingsCategory.Playback -> HocketIcons.AutoMirrored.Filled.QueueMusic + SettingsCategory.Audio -> HocketIcons.Filled.GraphicEq + SettingsCategory.Streaming -> HocketIcons.Filled.NetworkCheck + SettingsCategory.Downloads -> HocketIcons.Filled.Download + SettingsCategory.Lyrics -> HocketIcons.Filled.Lyrics + SettingsCategory.Library -> HocketIcons.Filled.LibraryMusic + SettingsCategory.Battery -> HocketIcons.Filled.BatterySaver + SettingsCategory.Connect -> HocketIcons.Filled.Devices + SettingsCategory.Customise -> HocketIcons.Filled.Tune + SettingsCategory.Backup -> HocketIcons.Filled.SettingsBackupRestore + SettingsCategory.About -> HocketIcons.Filled.Info } @Composable diff --git a/android/app/src/main/java/app/hocket/ui/screens/settings/SubSettingsScreens.kt b/android/app/src/main/java/app/hocket/ui/screens/settings/SubSettingsScreens.kt index 69413cc5..7d2f17d6 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/settings/SubSettingsScreens.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/settings/SubSettingsScreens.kt @@ -16,8 +16,6 @@ import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.DragHandle import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox @@ -51,6 +49,7 @@ import app.hocket.core.ActionIds import app.hocket.core.Commands import app.hocket.core.SettingKeys import androidx.compose.runtime.rememberCoroutineScope +import app.hocket.ui.icons.HocketIcons import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.launch import kotlinx.serialization.builtins.MapSerializer @@ -335,7 +334,7 @@ internal fun ChooseAndOrder(all: List, enabled: List, minEnabled Checkbox(checked = checked, onCheckedChange = null) Spacer(Modifier.width(12.dp)) Text(label(id), modifier = Modifier.weight(1f)) - Icon(Icons.Filled.DragHandle, null, modifier = Modifier.draggableHandle(onDragStopped = { onChange(order.filter { it in enabledSet }) })) + Icon(HocketIcons.Filled.DragHandle, null, modifier = Modifier.draggableHandle(onDragStopped = { onChange(order.filter { it in enabledSet }) })) } } } diff --git a/android/app/src/main/java/app/hocket/ui/screens/setup/ServerSetupScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/setup/ServerSetupScreen.kt index a991f4d7..b9477074 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/setup/ServerSetupScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/setup/ServerSetupScreen.kt @@ -16,11 +16,6 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Visibility -import androidx.compose.material.icons.filled.VisibilityOff import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon @@ -54,6 +49,7 @@ import app.hocket.core.api.ServerInfo import app.hocket.playback.CoreHost import app.hocket.playback.ServerCredential import app.hocket.ui.LocalCoreClient +import app.hocket.ui.icons.HocketIcons /** * Server setup is the entire first screen (design: empty states). URL, username, password, connect. @@ -134,7 +130,7 @@ fun ServerSetupScreen(existing: ServerInfo?, needsRelogin: Boolean = false) { keyboardActions = KeyboardActions(onGo = { connect() }), trailingIcon = { IconButton(onClick = { showPassword = !showPassword }) { - Icon(if (showPassword) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, stringResource(if (showPassword) R.string.setup_hide_password else R.string.setup_show_password)) + Icon(if (showPassword) HocketIcons.Filled.VisibilityOff else HocketIcons.Filled.Visibility, stringResource(if (showPassword) R.string.setup_hide_password else R.string.setup_show_password)) } }, modifier = Modifier.fillMaxWidth().testTag("setup.password"), enabled = !connecting, @@ -192,7 +188,7 @@ fun CapabilitySummary(server: ServerInfo) { @Composable private fun CapRow(label: String, ok: Boolean) { Row(verticalAlignment = Alignment.CenterVertically) { - Icon(if (ok) Icons.Filled.Check else Icons.Filled.Close, stringResource(if (ok) R.string.cap_yes else R.string.cap_no), tint = if (ok) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, modifier = Modifier.size(18.dp)) + Icon(if (ok) HocketIcons.Filled.Check else HocketIcons.Filled.Close, stringResource(if (ok) R.string.cap_yes else R.string.cap_no), tint = if (ok) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.outline, modifier = Modifier.size(18.dp)) Spacer(Modifier.size(8.dp)) Text(label, style = MaterialTheme.typography.bodyMedium) } diff --git a/android/app/src/main/java/app/hocket/ui/screens/stats/StatsScreen.kt b/android/app/src/main/java/app/hocket/ui/screens/stats/StatsScreen.kt index 1301a7ba..9621b99d 100644 --- a/android/app/src/main/java/app/hocket/ui/screens/stats/StatsScreen.kt +++ b/android/app/src/main/java/app/hocket/ui/screens/stats/StatsScreen.kt @@ -12,8 +12,6 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -40,6 +38,7 @@ import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController import app.hocket.R +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.AccountButton import app.hocket.ui.nav.BottomContentInset import app.hocket.core.Queries @@ -68,7 +67,7 @@ fun StatsScreen(nav: NavHostController) { val scroll = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() Scaffold( modifier = Modifier.nestedScroll(scroll.nestedScrollConnection), - topBar = { MediumFlexibleTopAppBar(title = { Text(stringResource(R.string.stats_title)) }, actions = { AccountButton() }, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(Icons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, scrollBehavior = scroll) }, + topBar = { MediumFlexibleTopAppBar(title = { Text(stringResource(R.string.stats_title)) }, actions = { AccountButton() }, navigationIcon = { IconButton(onClick = { nav.popBackStack() }) { Icon(HocketIcons.AutoMirrored.Filled.ArrowBack, stringResource(R.string.action_back)) } }, scrollBehavior = scroll) }, ) { padding -> val s = stats LazyColumn(Modifier.fillMaxSize(), contentPadding = PaddingValues(top = padding.calculateTopPadding(), bottom = BottomContentInset)) { diff --git a/android/app/src/test/java/app/hocket/ui/a11y/AccessibilityChecksTest.kt b/android/app/src/test/java/app/hocket/ui/a11y/AccessibilityChecksTest.kt index 6351df47..34fe71d4 100644 --- a/android/app/src/test/java/app/hocket/ui/a11y/AccessibilityChecksTest.kt +++ b/android/app/src/test/java/app/hocket/ui/a11y/AccessibilityChecksTest.kt @@ -5,8 +5,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.ui.Modifier @@ -25,6 +23,7 @@ import androidx.compose.ui.semantics.SemanticsActions import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 import app.hocket.ui.TestCore +import app.hocket.ui.icons.HocketIcons import app.hocket.ui.nav.AppRoot import app.hocket.ui.screens.settings.SettingsCategory import app.hocket.ui.setThemedContent @@ -71,7 +70,7 @@ class AccessibilityChecksTest { Column { Box(Modifier.size(20.dp).clickable { }) // no label (Compose extends its touch area to 48 dp) Box(Modifier.size(20.dp).semantics { contentDescription = "tiny"; onClick { true } }) // too small: no pointer input to extend - IconButton(onClick = {}) { Icon(Icons.Filled.Add, "Add button") } // redundant "button" + IconButton(onClick = {}) { Icon(HocketIcons.Filled.Add, "Add button") } // redundant "button" Box(Modifier.size(48.dp)) { Box(Modifier.size(48.dp).semantics { contentDescription = "one" }.clickable { }) Box(Modifier.size(48.dp).semantics { contentDescription = "two" }.clickable { }) diff --git a/android/app/src/test/java/app/hocket/ui/icons/HocketIconsTest.kt b/android/app/src/test/java/app/hocket/ui/icons/HocketIconsTest.kt new file mode 100644 index 00000000..38bf1094 --- /dev/null +++ b/android/app/src/test/java/app/hocket/ui/icons/HocketIconsTest.kt @@ -0,0 +1,40 @@ +package app.hocket.ui.icons + +import androidx.compose.ui.graphics.vector.VectorPath +import androidx.compose.ui.unit.dp +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.annotation.Config + +/** + * Builds every generated Material Symbol, so a path the parser can't read fails here rather than on + * the first screen that draws it, and checks each is a mirrored-where-asked 24dp glyph. + */ +@RunWith(AndroidJUnit4::class) +@Config(sdk = [35], application = android.app.Application::class) +class HocketIconsTest { + @Test + fun everyIconBuilds() { + val icons = HocketIcons.all + assertEquals("names are unique", icons.size, icons.map { it.name }.toSet().size) + for (icon in icons) { + assertEquals(icon.name, 24.dp, icon.defaultWidth) + assertEquals(icon.name, 24f, icon.viewportWidth) + assertEquals(icon.name, icon.name.startsWith("AutoMirrored."), icon.autoMirror) + val paths = icon.root.filterIsInstance() + assertTrue("${icon.name} has paths", paths.isNotEmpty()) + assertTrue("${icon.name} has path data", paths.all { it.pathData.isNotEmpty() }) + } + } + + @Test + fun filledAndOutlinedDiffer() { + // The bottom bar marks the selected place by fill alone. + val filled = HocketIcons.Filled.Home.root[0] as VectorPath + val outlined = HocketIcons.Outlined.Home.root[0] as VectorPath + assertTrue(filled.pathData != outlined.pathData) + } +} diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml index 0afdfbec..6fac6cbf 100644 --- a/android/gradle/libs.versions.toml +++ b/android/gradle/libs.versions.toml @@ -5,7 +5,6 @@ coroutines = "1.10.2" serialization = "1.9.0" compose = "1.13.0-alpha03" material3 = "1.5.0-alpha28" -materialIcons = "1.7.8" activity = "1.13.0" lifecycle = "2.11.0" navigation = "2.10.1" @@ -50,7 +49,6 @@ compose-foundation = { module = "androidx.compose.foundation:foundation", versio compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } compose-material3-window-size = { module = "androidx.compose.material3:material3-window-size-class", version.ref = "material3" } compose-material3-adaptive = { module = "androidx.compose.material3.adaptive:adaptive", version.ref = "adaptive" } -compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIcons" } compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose" } compose-animation = { module = "androidx.compose.animation:animation", version.ref = "compose" } media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3" } diff --git a/scripts/gen-android-icons.mjs b/scripts/gen-android-icons.mjs new file mode 100644 index 00000000..78ae3f10 --- /dev/null +++ b/scripts/gen-android-icons.mjs @@ -0,0 +1,186 @@ +// Generates android/app/src/main/java/app/hocket/ui/icons/HocketIcons.kt: the +// Material Symbols (rounded) glyphs the Android app uses, as Compose +// ImageVectors, from the same Iconify dataset desktop's icons come from +// (@iconify-json/material-symbols, Apache-2.0; a desktop dev dependency, so run +// `pnpm install` in desktop/ first). Only the icons listed here are emitted, so +// the app doesn't ship the whole set. +// +// node scripts/gen-android-icons.mjs +// +// The object mirrors androidx's `Icons` layout (HocketIcons.Filled.PlayArrow, +// HocketIcons.Outlined.Home, HocketIcons.AutoMirrored.Filled.ArrowBack), keyed +// by the Compose icon names the app used before, so call sites read the same. +// Filled picks the symbol's filled rounded variant, Outlined its outlined one +// (the same glyph when the symbol has only one). Re-run after changing the +// lists; the output is committed. +import { writeFileSync } from "node:fs"; +import { createRequire } from "node:module"; +import { fileURLToPath } from "node:url"; + +const require = createRequire(new URL("../desktop/package.json", import.meta.url)); +const set = require("@iconify-json/material-symbols/icons.json"); + +/** + * Compose name → Material Symbols name. A ":outline" suffix forces the outlined + * variant where the Compose name is itself an outlined glyph (FavoriteBorder). + */ +const SYMBOLS = { + AccountCircle: "account_circle", Add: "add", Album: "album", AllInclusive: "all_inclusive", ArrowBack: "arrow_back", + ArrowDropDown: "arrow_drop_down", BarChart: "bar_chart", BatterySaver: "battery_saver", Bedtime: "bedtime", Cancel: "cancel", + Cast: "cast", Category: "category", Check: "check", CheckCircle: "check_circle", ChevronRight: "chevron_right", + ClearAll: "clear_all", Close: "close", CloudOff: "cloud_off", Computer: "computer", Delete: "delete", Devices: "devices", + Download: "download", DownloadDone: "download_done", DownloadForOffline: "download_for_offline", DragHandle: "drag_handle", + ErrorOutline: "error:outline", Favorite: "favorite", FavoriteBorder: "favorite:outline", FilterAlt: "filter_alt", + GraphicEq: "graphic_eq", History: "history", Home: "home", Info: "info", KeyboardArrowDown: "keyboard_arrow_down", + LibraryMusic: "library_music", Logout: "logout", Lyrics: "lyrics", MoreVert: "more_vert", MusicNote: "music_note", + NetworkCheck: "network_check", OfflineBolt: "offline_bolt", Palette: "palette", Pause: "pause", Person: "person", + PhoneAndroid: "phone_android", PlayArrow: "play_arrow", PlaylistAdd: "playlist_add", PlaylistAddCheck: "playlist_add_check", + PlaylistPlay: "playlist_play", PlaylistRemove: "playlist_remove", PushPin: "push_pin", QueueMusic: "queue_music", + Redo: "redo", Refresh: "refresh", Remove: "remove", RemoveCircleOutline: "do_not_disturb_on:outline", Repeat: "repeat", + RepeatOne: "repeat_one", RestartAlt: "restart_alt", Save: "save", Search: "search", SelectAll: "select_all", + Settings: "settings", SettingsBackupRestore: "settings_backup_restore", Shuffle: "shuffle", SkipNext: "skip_next", + SkipPrevious: "skip_previous", Sort: "sort", Speaker: "speaker", Star: "star", StarOutline: "star:outline", Stop: "stop", + Sync: "sync", Tune: "tune", Undo: "undo", Visibility: "visibility", VisibilityOff: "visibility_off", VolumeUp: "volume_up", +}; + +/** Which glyphs each group emits. AutoMirrored ones flip in right-to-left layouts. */ +const GROUPS = [ + { path: ["Filled"], fill: true, mirror: false, names: [ + "AccountCircle", "Add", "Album", "AllInclusive", "ArrowDropDown", "BarChart", "BatterySaver", "Bedtime", "Cancel", "Cast", + "Category", "Check", "CheckCircle", "ChevronRight", "ClearAll", "Close", "Computer", "Delete", "Devices", "Download", + "DownloadDone", "DownloadForOffline", "DragHandle", "ErrorOutline", "Favorite", "FavoriteBorder", "FilterAlt", "GraphicEq", + "History", "Home", "Info", "KeyboardArrowDown", "LibraryMusic", "Lyrics", "MoreVert", "MusicNote", "NetworkCheck", "Palette", + "Pause", "Person", "PhoneAndroid", "PlayArrow", "PlaylistAdd", "PlaylistAddCheck", "PlaylistPlay", "PlaylistRemove", + "PushPin", "QueueMusic", "Refresh", "Remove", "RemoveCircleOutline", "Repeat", "RepeatOne", "RestartAlt", "Save", "Search", + "SelectAll", "Settings", "SettingsBackupRestore", "Shuffle", "SkipNext", "SkipPrevious", "Sort", "Speaker", "Star", + "StarOutline", "Stop", "Sync", "Tune", "Visibility", "VisibilityOff", "VolumeUp", + ] }, + { path: ["Outlined"], fill: false, mirror: false, names: [ + "Album", "BarChart", "Category", "CloudOff", "Download", "FilterAlt", "History", "Home", "Info", "LibraryMusic", "MusicNote", + "OfflineBolt", "Person", "PushPin", "Search", "StarOutline", + ] }, + { path: ["AutoMirrored", "Filled"], fill: true, mirror: true, names: [ + "ArrowBack", "Logout", "PlaylistAdd", "PlaylistPlay", "QueueMusic", "Redo", "Undo", + ] }, + { path: ["AutoMirrored", "Outlined"], fill: false, mirror: true, names: ["QueueMusic"] }, +]; + +function body(name) { + let n = name; + for (let i = 0; i < 8; i++) { + const icon = set.icons[n]; + if (icon) return icon.body; + const alias = set.aliases?.[n]; + if (!alias) return undefined; + n = alias.parent; + } + return undefined; +} + +const first = (...names) => { + for (const n of names) { + const b = body(n); + if (b) return b; + } + return undefined; +}; + +/** + * Re-spells minified SVG path data with explicit separators ("M4 19v-9q0-.475.213-.9" + * → "M 4 19 v -9 q 0 -0.475 0.213 -0.9"), so Compose's parser never has to split + * run-together numbers. Arc flags are single digits that may run into the next number. + */ +function normalize(d) { + const out = []; + let cmd = ""; + let arg = 0; + let i = 0; + const num = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?/; + while (i < d.length) { + const c = d[i]; + if (/[\s,]/.test(c)) { i++; continue; } + if (/[MmLlHhVvCcSsQqTtAaZz]/.test(c)) { out.push(c); cmd = c; arg = 0; i++; continue; } + const flag = /[Aa]/.test(cmd) && (arg % 7 === 3 || arg % 7 === 4); + const m = flag ? d.slice(i).match(/^[01]/) : d.slice(i).match(num); + if (!m) throw new Error(`unparseable path data at ${i}: ${d.slice(i, i + 20)}`); + out.push(flag ? m[0] : String(Number(m[0]))); + arg++; + i += m[0].length; + } + return out.join(" "); +} + +function paths(svg) { + const ds = [...svg.matchAll(/]*)\/?>/g)].map(([, attrs]) => { + const fill = attrs.match(/\bfill="([^"]*)"/)?.[1]; + if (fill && fill !== "currentColor") throw new Error(`unexpected fill ${fill}`); + const d = attrs.match(/\bd="([^"]*)"/)?.[1]; + if (!d) throw new Error(`path without d: ${attrs}`); + return normalize(d); + }); + if (!ds.length || svg.replace(/]*\/?>(<\/path>)?/g, "").trim()) throw new Error(`unexpected SVG body: ${svg.slice(0, 80)}`); + return ds; +} + +function glyph(compose, fill) { + const spec = SYMBOLS[compose]; + if (!spec) throw new Error(`no Material Symbol mapped for ${compose}`); + const [symbol, variant] = spec.split(":"); + const m = symbol.replace(/_/g, "-"); + const outline = variant === "outline" || !fill; + const svg = outline ? first(`${m}-outline-rounded`, `${m}-rounded`, `${m}-outline`, m) : first(`${m}-rounded`, m); + if (!svg) throw new Error(`missing Material Symbol ${symbol} (${compose})`); + return paths(svg); +} + +const ind = (n) => " ".repeat(n); +const all = []; +const tree = {}; +for (const g of GROUPS) { + let node = tree; + for (const p of g.path) node = node[p] ??= {}; + for (const name of g.names) { + const ref = [...g.path, name].join("."); + const args = glyph(name, g.fill).map((d) => `"${d}"`).join(", "); + (node[""] ??= []).push(`val ${name}: ImageVector by lazy { symbol("${ref}", ${args}${g.mirror ? ", autoMirror = true" : ""}) }`); + all.push(ref); + } +} + +function emit(node, depth) { + const lines = []; + for (const [key, child] of Object.entries(node)) { + if (key === "") lines.push(...child.map((l) => ind(depth) + l)); + else lines.push(`${ind(depth)}object ${key} {`, ...emit(child, depth + 1), `${ind(depth)}}`); + } + return lines; +} + +const target = fileURLToPath(new URL("../android/app/src/main/java/app/hocket/ui/icons/HocketIcons.kt", import.meta.url)); +writeFileSync(target, `// Generated by scripts/gen-android-icons.mjs from @iconify-json/material-symbols +// (Material Symbols, Apache-2.0, rounded). Do not edit; edit the script's lists. +package app.hocket.ui.icons + +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.addPathNodes +import androidx.compose.ui.unit.dp + +/** The app's icons: Material Symbols, rounded, as 24dp vectors (tinted by \`Icon\` like any other). */ +object HocketIcons { +${emit(tree, 1).join("\n")} + + /** Every icon, so a test can build each one. */ + internal val all: List + get() = listOf( +${all.map((r) => `${ind(3)}${r},`).join("\n")} + ) +} + +private fun symbol(name: String, vararg paths: String, autoMirror: Boolean = false): ImageVector = + ImageVector.Builder(name, 24.dp, 24.dp, 24f, 24f, autoMirror = autoMirror) + .apply { for (d in paths) addPath(addPathNodes(d), fill = SolidColor(Color.Black)) } + .build() +`); +console.log(`[gen-android-icons] ${all.length} icons → ${target}`);