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}`);