diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index 8aee91ce..32787564 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -21,6 +21,12 @@
Filter queue...
No queue entries
+ Search songs, artists, albums...
+ Clear all history
+ Search...
+
+ Selected
+
Settings
Plugins
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
index e8646ed2..5c7ec032 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/AdaptiveDropdownBottomSheet.kt
@@ -19,23 +19,17 @@ package dev.krtirtho.spotube.core.ui.component
import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.collectIsHoveredAsState
-import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.ui.draw.clip
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -45,19 +39,13 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import com.composeunstyled.currentWindowWidthBreakpoint
import org.spotube.millennium.adaptive.WindowBreakpoints
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import com.composeunstyled.DropdownMenuPanel
import com.composeunstyled.SheetDetent
-import com.composeunstyled.Text
-import com.composeunstyled.UnstyledDropdownMenu
-import com.composeunstyled.UnstyledDropdownMenuItem
import com.composeunstyled.rememberModalBottomSheetState
import compose.icons.FeatherIcons
import compose.icons.feathericons.Check
@@ -67,16 +55,19 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax3DotsMore
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart
import dev.krtirtho.spotube.resources.iconsax.IconsaxShare
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
+import org.jetbrains.compose.resources.stringResource
+import org.spotube.millennium.composables.MillenniumDropdownMenu
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
+import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumModalBottomSheet
import org.spotube.millennium.composables.MillenniumSeparator
+import org.spotube.millennium.composables.MillenniumText
import org.spotube.millennium.composables.MillenniumTextField
-import org.spotube.millennium.theme.MillenniumTheme
-import org.spotube.millennium.theme.gelBrush
-import org.spotube.millennium.theme.millenniumPanelBrush
-import org.spotube.millennium.theme.millenniumSurface
+import spotube.composeapp.generated.resources.Res
+import spotube.composeapp.generated.resources.accessibility_selected
+import spotube.composeapp.generated.resources.search_filter_placeholder
data class AdaptiveMenuItem(
val icon: ImageVector? = null,
@@ -108,118 +99,17 @@ fun AdaptiveDropdownBottomSheet(
Box(modifier = modifier) {
if (isLargeScreen) {
- UnstyledDropdownMenu(
+ MillenniumDropdownMenu(
expanded = expanded,
onExpandedChange = { expanded = it },
- panel = {
- DropdownMenuPanel(
- Modifier
- .width(menuMinWidth)
- .millenniumSurface(
- background = millenniumPanelBrush(MillenniumTheme.colors),
- shape = AppTheme.shapes.medium,
- borderColor = AppTheme.colors.outlineVariant,
- borderWidth = 1.dp)
- .padding(4.dp)) {
- if (header != null && (headerDisplayMode == HeaderDisplayMode.Always || headerDisplayMode == HeaderDisplayMode.OnlyInDropdown)) {
- header()
- }
-
- val dropdownQueryState = rememberTextFieldState()
- val query = dropdownQueryState.text.toString()
- if (filter != null) {
- MillenniumTextField(
- state = dropdownQueryState,
- label = "",
- placeholder = "Search...",
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 12.dp, vertical = 8.dp))
- }
-
- items.forEach { item ->
- if (query.isNotBlank() && filter != null && !filter(item, query)) {
- return@forEach
- }
- if (item.dividerBefore) {
- MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp))
- }
- val itemInteraction = remember { MutableInteractionSource() }
- val itemHovered by itemInteraction.collectIsHoveredAsState()
- val itemPressed by itemInteraction.collectIsPressedAsState()
- val itemEnabled = item.enabled
- val itemHighlighted = itemEnabled && (itemHovered || itemPressed || item.selected)
- val itemForeground = when {
- !itemEnabled -> AppTheme.colors.onSurface.copy(alpha = 0.38f)
- itemHighlighted -> MillenniumTheme.colors.onAccent
- else -> AppTheme.colors.onSurface
- }
- UnstyledDropdownMenuItem(
- onClick = { item.onClick() },
- enabled = itemEnabled,
- indication = null,
- interactionSource = itemInteraction) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .then(
- if (itemHighlighted) {
- Modifier.millenniumSurface(
- background = gelBrush(
- MillenniumTheme.colors.accentTop,
- MillenniumTheme.colors.accentBottom),
- shape = RoundedCornerShape(8.dp))
- } else {
- Modifier
- })
- .padding(horizontal = 12.dp, vertical = 8.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- val itemIcon = item.icon
- when {
- item.leadingContent != null -> item.leadingContent.invoke()
- itemIcon != null -> MillenniumIcon(
- imageVector = itemIcon,
- contentDescription = null,
- tint = itemForeground,
- modifier = Modifier.size(20.dp))
-
- item.selected -> MillenniumIcon(
- imageVector = FeatherIcons.Check,
- contentDescription = "Selected",
- tint = itemForeground,
- modifier = Modifier.size(20.dp))
- }
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = item.label,
- style = AppTheme.typography.bodyMedium,
- color = itemForeground)
- item.subtitle?.let { subtitle ->
- Text(
- text = subtitle,
- style = AppTheme.typography.bodySmall,
- color = if (itemHighlighted) {
- itemForeground.copy(alpha = 0.85f)
- } else {
- AppTheme.colors.onSurfaceVariant
- })
- }
- }
- if (item.selected && (item.icon != null || item.leadingContent != null)) {
- MillenniumIcon(
- imageVector = FeatherIcons.Check,
- contentDescription = "Selected",
- tint = itemForeground,
- modifier = Modifier.size(20.dp))
- }
- }
- }
- }
- }
- },
- anchor = { trigger { expanded = true } },
- )
+ items = items.map { it.label },
+ onItemClick = { label ->
+ items.firstOrNull { it.label == label && it.enabled }?.onClick?.invoke()
+ expanded = false
+ },
+ minimumWidth = menuMinWidth,
+ anchor = { trigger { expanded = true } }
+ )
} else {
trigger { expanded = true }
if (expanded) {
@@ -267,7 +157,7 @@ private fun AdaptiveBottomSheetContent(
MillenniumTextField(
state = queryState,
label = "",
- placeholder = "Search...",
+ placeholder = stringResource(Res.string.search_filter_placeholder),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp))
@@ -285,72 +175,69 @@ private fun AdaptiveBottomSheetContent(
if (item.dividerBefore) {
MillenniumSeparator(modifier = Modifier.padding(vertical = 4.dp))
}
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .clip(AppTheme.shapes.medium)
- .clickable(
- enabled = item.enabled,
- onClick = {
- item.onClick()
- onDismiss()
- })
- .background(
- color = if (item.selected) {
- AppTheme.colors.secondary.copy(alpha = 0.12f)
- } else {
- Color.Transparent
- },
- shape = AppTheme.shapes.medium)
- .padding(horizontal = 12.dp, vertical = 14.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(12.dp)) {
- when {
- item.leadingContent != null -> item.leadingContent.invoke()
- item.icon != null -> MillenniumIcon(
- imageVector = item.icon,
- contentDescription = null,
- modifier = Modifier.size(22.dp),
- tint = if (item.enabled) {
- AppTheme.colors.onSurface
- } else {
- AppTheme.colors.onSurface.copy(alpha = 0.38f)
- })
-
- item.selected -> MillenniumIcon(
- imageVector = FeatherIcons.Check,
- contentDescription = "Selected",
- modifier = Modifier.size(22.dp),
- tint = AppTheme.colors.onSurfaceVariant)
-
- hasSelection -> Spacer(modifier = Modifier.size(22.dp))
- }
-
- Column(modifier = Modifier.weight(1f)) {
- Text(
- text = item.label,
+ MillenniumListTile(
+ title = {
+ MillenniumText(
+ item.label,
style = AppTheme.typography.bodyLarge,
color = if (item.enabled) {
AppTheme.colors.onSurface
} else {
AppTheme.colors.onSurface.copy(alpha = 0.38f)
- })
- item.subtitle?.let { subtitle ->
- Text(
- text = subtitle,
+ }
+ )
+ },
+ subtitle = item.subtitle?.let { subtitle ->
+ {
+ MillenniumText(
+ subtitle,
style = AppTheme.typography.bodySmall,
- color = AppTheme.colors.onSurfaceVariant)
+ color = AppTheme.colors.onSurfaceVariant
+ )
}
- }
-
- if (item.selected && (item.icon != null || item.leadingContent != null)) {
- MillenniumIcon(
- imageVector = FeatherIcons.Check,
- contentDescription = "Selected",
- modifier = Modifier.size(22.dp),
- tint = AppTheme.colors.onSurfaceVariant)
- }
- }
+ },
+ leading = when {
+ item.leadingContent != null -> item.leadingContent
+ item.icon != null -> {
+ {
+ MillenniumIcon(
+ imageVector = item.icon,
+ contentDescription = null,
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ }
+ item.selected -> {
+ {
+ MillenniumIcon(
+ imageVector = FeatherIcons.Check,
+ contentDescription = stringResource(Res.string.accessibility_selected),
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ }
+ hasSelection -> {{ Spacer(modifier = Modifier.size(22.dp)) }}
+ else -> null
+ },
+ trailing = if (item.selected && (item.icon != null || item.leadingContent != null)) {
+ {
+ MillenniumIcon(
+ imageVector = FeatherIcons.Check,
+ contentDescription = stringResource(Res.string.accessibility_selected),
+ modifier = Modifier.size(22.dp)
+ )
+ }
+ } else {
+ null
+ },
+ selected = item.selected,
+ enabled = item.enabled,
+ onClick = {
+ item.onClick()
+ onDismiss()
+ },
+ minimumHeight = 56.dp
+ )
}
}
}
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/theming/AppTheme.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/theming/AppTheme.kt
index 11584429..b20a2ede 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/theming/AppTheme.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/theming/AppTheme.kt
@@ -36,7 +36,12 @@ import androidx.compose.ui.unit.sp
import dev.krtirtho.spotube.modules.settings.Theme
import dev.krtirtho.spotube.modules.settings.UserSettings
import org.spotube.millennium.composables.MillenniumTooltipHost
+import org.spotube.millennium.composables.MillenniumTopAppBarTheme
+import org.spotube.millennium.composables.MillenniumTopAppBarThemeProperties
+import org.spotube.millennium.composables.defaultMillenniumTopAppBarTheme
import org.spotube.millennium.theme.MillenniumColors
+import org.spotube.millennium.theme.MillenniumDarkThemeComponents
+import org.spotube.millennium.theme.MillenniumLightThemeComponents
import org.spotube.millennium.theme.MillenniumPalette
import org.spotube.millennium.theme.MillenniumTheme
import org.spotube.millennium.theme.MillenniumThemeConfig
@@ -201,7 +206,29 @@ fun SpotubeTheme(
}
val palette: MillenniumPalette = settings.palette
- MillenniumTheme(dark = isDarkTheme, config = MillenniumThemeConfig(palette = palette)) {
+ val darkTheme = MillenniumDarkThemeComponents
+ val lightTheme = MillenniumLightThemeComponents
+
+ MillenniumTheme(
+ dark = isDarkTheme,
+ config = MillenniumThemeConfig(
+ palette = palette,
+ dark = darkTheme.copy(
+ topAppBarTheme = MillenniumTopAppBarTheme(
+ properties = darkTheme.topAppBarTheme.properties.copy(
+ separatorColor = Color.Transparent
+ )
+ )
+ ),
+ light = lightTheme.copy(
+ topAppBarTheme = MillenniumTopAppBarTheme(
+ properties = lightTheme.topAppBarTheme.properties.copy(
+ separatorColor = Color.Transparent
+ )
+ )
+ )
+ )
+ ) {
val colors = appColorsFrom(MillenniumTheme.colors)
MillenniumTooltipHost {
CompositionLocalProvider(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
index f4709cb0..ff720d6c 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreen.kt
@@ -19,13 +19,12 @@ package dev.krtirtho.spotube.modules.search
import org.spotube.millennium.composables.MillenniumIcon
-import org.spotube.millennium.composables.MillenniumIconButton
-import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumButton
import org.spotube.millennium.composables.MillenniumButtonStyle
import org.spotube.millennium.composables.MillenniumIndeterminateProgress
import org.spotube.millennium.composables.MillenniumListTile
import org.spotube.millennium.composables.MillenniumAutocomplete
+import org.spotube.millennium.composables.MillenniumText
import org.spotube.millennium.composables.MillenniumTextField
import org.spotube.millennium.theme.MillenniumTheme
import androidx.compose.animation.AnimatedContent
@@ -78,8 +77,6 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import compose.icons.FeatherIcons
-import compose.icons.feathericons.X
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
@@ -115,6 +112,10 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
import dev.krtirtho.spotube.resources.iconsax.InconsaxClock
+import org.jetbrains.compose.resources.stringResource
+import spotube.composeapp.generated.resources.Res
+import spotube.composeapp.generated.resources.search_clear_history
+import spotube.composeapp.generated.resources.search_placeholder
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
@@ -145,8 +146,12 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
var tracksToAddToPlaylist by remember { mutableStateOf>(emptyList()) }
var currentUserId by remember { mutableStateOf(null) }
- val blacklistedTracks by blacklistRepository.blacklistedTracks.collectAsStateWithLifecycle(emptyList())
- val blacklistedArtists by blacklistRepository.blacklistedArtists.collectAsStateWithLifecycle(emptyList())
+ val blacklistedTracks by blacklistRepository.blacklistedTracks.collectAsStateWithLifecycle(
+ emptyList()
+ )
+ val blacklistedArtists by blacklistRepository.blacklistedArtists.collectAsStateWithLifecycle(
+ emptyList()
+ )
fun isTrackBlacklisted(track: MetadataTrack): Boolean {
val trackIds = blacklistedTracks.map { it.id }.toSet()
@@ -172,7 +177,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
entries = listOf(QueueEntry.StreamingTrack(track = track, url = "")),
autoPlay = true,
startPosition = 0,
- collectionEntry = null)
+ collectionEntry = null
+ )
}
}
@@ -188,7 +194,14 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
if (existing != null) {
// Already in the local queue: move it to the next position
audioPlayerQueue.removeFromQueue(existing)
- audioPlayerQueue.addAllAfterCurrent(listOf(QueueEntry.StreamingTrack(track = track, url = "")))
+ audioPlayerQueue.addAllAfterCurrent(
+ listOf(
+ QueueEntry.StreamingTrack(
+ track = track,
+ url = ""
+ )
+ )
+ )
} else {
remotePlaybackController.requestTrackPlayNext(track)
}
@@ -197,6 +210,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
is TrackOptionsAction.AddToQueue -> {
remotePlaybackController.requestTrackAddToQueue(track)
}
+
is TrackOptionsAction.AddToJam -> {
remotePlaybackController.addTrackToJam(track)
}
@@ -217,6 +231,7 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
shareService.share(uri, track.title)
}
}
+
is TrackOptionsAction.AddToPlaylist -> {
tracksToAddToPlaylist = listOf(track)
showAddToPlaylistPicker = true
@@ -236,7 +251,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
isInQueue = queueTrackIds.contains(track.id),
isCurrentlyPlaying = track.id == currentTrackId,
isFavorite = savedTrackIds.contains(track.id),
- isBlacklisted = isTrackBlacklisted(track))
+ isBlacklisted = isTrackBlacklisted(track)
+ )
}
val queue by audioPlayerQueue.queueFlow.collectAsStateWithLifecycle(emptyList())
@@ -251,7 +267,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
isInQueue = queueTrackIds.contains(track.id),
isCurrentlyPlaying = track.id == currentTrackId,
isFavorite = savedTrackIds.contains(track.id),
- isBlacklisted = isTrackBlacklisted(track))
+ isBlacklisted = isTrackBlacklisted(track)
+ )
}
fun bulkAddToQueue(tracks: List) {
@@ -269,7 +286,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
Column(
modifier = Modifier
.fillMaxSize()
- .background(MillenniumTheme.colors.background)) {
+ .background(MillenniumTheme.colors.background)
+ ) {
ApplicationMainBar(backButton = false)
Column(
modifier = Modifier
@@ -282,12 +300,11 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
SearchBar(
query = state.query,
onQueryChange = viewModel::onQueryChange,
- recentSearches = state.recentSearches,
+ recentSearchSuggestions = state.recentSearchSuggestions,
onRecentSearchClick = { search ->
viewModel.applyRecentSearch(search)
focusManager.clearFocus()
},
- onRecentSearchRemove = viewModel::removeRecentSearch,
onClearAllRecentSearches = {
viewModel.clearAllRecentSearches()
focusManager.clearFocus()
@@ -297,7 +314,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
SearchTabs(
types = state.supportedSearchTypes,
selectedType = selectedType,
- onTabSelected = viewModel::onTabSelected)
+ onTabSelected = viewModel::onTabSelected
+ )
}
}
@@ -305,14 +323,16 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
state.isLoadingSearchTypes -> {
SearchLoadingIndicator(
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == null -> {
SearchMessage(
message = "No search types available",
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == MetadataSupportedSearchType.ALL -> {
@@ -348,7 +368,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
// No-op for now, could show a dialog with all artists
},
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == MetadataSupportedSearchType.TRACK -> {
@@ -379,7 +400,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
// No-op for now, could show a dialog with all artists
},
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == MetadataSupportedSearchType.PLAYLIST -> {
@@ -388,7 +410,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
playlists = state.playlists,
onLoadNextPage = viewModel::loadNextPlaylists,
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == MetadataSupportedSearchType.ALBUM -> {
@@ -397,7 +420,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
albums = state.albums,
onLoadNextPage = viewModel::loadNextAlbums,
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == MetadataSupportedSearchType.ARTIST -> {
@@ -406,7 +430,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
artists = state.artists,
onLoadNextPage = viewModel::loadNextArtists,
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
selectedType == MetadataSupportedSearchType.USER -> {
@@ -415,7 +440,8 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
users = state.users,
onLoadNextPage = viewModel::loadNextUsers,
modifier = Modifier
- .fillMaxSize())
+ .fillMaxSize()
+ )
}
}
}
@@ -439,11 +465,11 @@ fun SearchScreen(viewModel: SearchScreenViewModel = koinViewModel()) {
private fun SearchBar(
query: String,
onQueryChange: (String) -> Unit,
- recentSearches: List,
+ recentSearchSuggestions: List,
onRecentSearchClick: (String) -> Unit,
- onRecentSearchRemove: (String) -> Unit,
- onClearAllRecentSearches: () -> Unit) {
- val showRecentSearches = query.isBlank() && recentSearches.isNotEmpty()
+ onClearAllRecentSearches: () -> Unit
+) {
+ val hasRecentSearchSuggestions = recentSearchSuggestions.isNotEmpty()
val searchState = rememberTextFieldState(query)
LaunchedEffect(searchState) {
@@ -455,106 +481,61 @@ private fun SearchBar(
}
}
- val suggestions = remember(showRecentSearches, recentSearches) {
- buildList {
- if (showRecentSearches) {
- add(SearchDropdownEntry.ClearAll(onClick = onClearAllRecentSearches))
- recentSearches.forEach { search ->
- add(
- SearchDropdownEntry.RecentSearch(
- query = search,
- onClick = { onRecentSearchClick(search) },
- onRemove = { onRecentSearchRemove(search) })
- )
- }
- }
- }
- }
-
- Box(
+ MillenniumAutocomplete(
+ state = searchState,
+ suggestions = recentSearchSuggestions,
+ onSuggestionSelected = onRecentSearchClick,
+ suggestionLabel = { it },
+ leadingContent = {
+ MillenniumIcon(
+ imageVector = Iconsax.InconsaxClock,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ tint = AppTheme.colors.onSurfaceVariant
+ )
+ },
+ placeholder = stringResource(Res.string.search_placeholder),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
- .padding(bottom = 8.dp)) {
- MillenniumAutocomplete(
- state = searchState,
-
- suggestions = suggestions,
- onSuggestionSelected = { entry -> entry.onClick() },
- suggestionLabel = { entry ->
- when (entry) {
- is SearchDropdownEntry.ClearAll -> "Clear all history"
- is SearchDropdownEntry.RecentSearch -> entry.query
- }
- },
- placeholder = "Search songs, artists, albums...",
- modifier = Modifier.fillMaxWidth(),
- suggestionContent = { entry, _ -> SearchSuggestionRow(entry) })
- }
-}
-
-private sealed interface SearchDropdownEntry {
- val onClick: () -> Unit
-
- data class ClearAll(override val onClick: () -> Unit) : SearchDropdownEntry
- data class RecentSearch(
- val query: String,
- override val onClick: () -> Unit,
- val onRemove: () -> Unit) : SearchDropdownEntry
-}
-
-@Composable
-private fun SearchSuggestionRow(entry: SearchDropdownEntry) {
- Row(
- modifier = Modifier
- .fillMaxWidth()
- .padding(horizontal = 12.dp, vertical = 10.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(10.dp)) {
- when (entry) {
- is SearchDropdownEntry.ClearAll -> {
- MillenniumIcon(
- imageVector = Iconsax.IconsaxTrash,
- contentDescription = null,
- tint = AppTheme.colors.error,
- modifier = Modifier.size(16.dp))
- Text(
- "Clear all history",
- color = AppTheme.colors.error,
- style = AppTheme.typography.bodyMedium)
- }
-
- is SearchDropdownEntry.RecentSearch -> {
- MillenniumIcon(
- imageVector = Iconsax.InconsaxClock,
- contentDescription = null,
- modifier = Modifier.size(16.dp),
- tint = AppTheme.colors.onSurfaceVariant)
- Text(
- entry.query,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- style = AppTheme.typography.bodyMedium,
- modifier = Modifier.weight(1f))
- MillenniumIconButton(
- onClick = entry.onRemove,
- style = MillenniumIconButtonStyle.Quiet) {
- MillenniumIcon(
- imageVector = FeatherIcons.X,
- contentDescription = "Remove",
- modifier = Modifier.size(14.dp),
- tint = AppTheme.colors.onSurfaceVariant.copy(alpha = 0.6f))
- }
+ .padding(bottom = 8.dp),
+ menuHeader = if (hasRecentSearchSuggestions) {
+ {
+ MillenniumListTile(
+ title = {
+ MillenniumText(
+ text = stringResource(Res.string.search_clear_history),
+ color = AppTheme.colors.error,
+ style = AppTheme.typography.bodyMedium
+ )
+ },
+ leading = {
+ MillenniumIcon(
+ imageVector = Iconsax.IconsaxTrash,
+ contentDescription = null,
+ modifier = Modifier.size(16.dp),
+ tint = AppTheme.colors.error
+ )
+ },
+ onClick = onClearAllRecentSearches,
+ minimumHeight = 36.dp,
+ contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp),
+ leadingSpacing = 8.dp,
+ shape = AppTheme.shapes.small
+ )
}
+ } else {
+ null
}
- }
+ )
}
@Composable
private fun SearchTabs(
types: List,
selectedType: MetadataSupportedSearchType?,
- onTabSelected: (MetadataSupportedSearchType) -> Unit) {
+ onTabSelected: (MetadataSupportedSearchType) -> Unit
+) {
val listState = rememberLazyListState()
LazyRow(
state = listState,
@@ -570,7 +551,8 @@ private fun SearchTabs(
MillenniumButtonStyle.Primary
} else {
MillenniumButtonStyle.Secondary
- }) {
+ }
+ ) {
Text(type.tabTitle())
}
}
@@ -600,7 +582,8 @@ private fun SearchAllTab(
onArtistClick: (MetadataArtist.Basic) -> Unit,
onAlbumClick: (MetadataAlbum.Detailed) -> Unit,
onArtistsOverflowClick: (MetadataTrack) -> Unit,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
if (query.isBlank()) {
SearchMessage(message = "Start typing to search", modifier = modifier)
return
@@ -624,7 +607,8 @@ private fun SearchAllTab(
ErrorDisplay(
errorMessage = hasAnyError,
onRetry = { },
- modifier = modifier)
+ modifier = modifier
+ )
return
}
@@ -665,7 +649,8 @@ private fun SearchAllTab(
text = "Tracks",
style = AppTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
- modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 12.dp))
+ modifier = Modifier.padding(horizontal = 16.dp).padding(bottom = 12.dp)
+ )
}
if (MetadataSupportedSearchType.TRACK in supportedSearchTypes) {
if (tracks.isLoading && tracks.items.isEmpty()) {
@@ -743,7 +728,8 @@ private fun SearchTracksTab(
onArtistClick: (MetadataArtist.Basic) -> Unit,
onAlbumClick: (MetadataAlbum.Detailed) -> Unit,
onArtistsOverflowClick: (MetadataTrack) -> Unit,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
if (query.isBlank()) {
SearchMessage(message = "Start typing to search tracks", modifier = modifier)
return
@@ -775,7 +761,8 @@ private fun SearchTracksTab(
onAlbumClick = onAlbumClick,
onArtistsOverflowClick = onArtistsOverflowClick,
simplified = true,
- modifier = modifier)
+ modifier = modifier
+ )
}
@Composable
@@ -783,7 +770,8 @@ private fun SearchAlbumsTab(
query: String,
albums: SearchPagedState,
onLoadNextPage: () -> Unit,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
SearchGridTab(
query = query,
items = albums.items,
@@ -793,7 +781,8 @@ private fun SearchAlbumsTab(
onLoadNextPage = onLoadNextPage,
emptyMessage = "No albums found",
loadingMessage = "Loading albums...",
- modifier = modifier) { album ->
+ modifier = modifier
+ ) { album ->
AlbumCard(album = album, modifier = Modifier.fillMaxWidth())
}
}
@@ -803,7 +792,8 @@ private fun SearchArtistsTab(
query: String,
artists: SearchPagedState,
onLoadNextPage: () -> Unit,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
SearchGridTab(
query = query,
items = artists.items,
@@ -813,7 +803,8 @@ private fun SearchArtistsTab(
onLoadNextPage = onLoadNextPage,
emptyMessage = "No artists found",
loadingMessage = "Loading artists...",
- modifier = modifier) { artist ->
+ modifier = modifier
+ ) { artist ->
ArtistCard(artist = artist, modifier = Modifier.fillMaxWidth())
}
}
@@ -823,7 +814,8 @@ private fun SearchPlaylistsTab(
query: String,
playlists: SearchPagedState,
onLoadNextPage: () -> Unit,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
SearchGridTab(
query = query,
items = playlists.items,
@@ -833,7 +825,8 @@ private fun SearchPlaylistsTab(
onLoadNextPage = onLoadNextPage,
emptyMessage = "No playlists found",
loadingMessage = "Loading playlists...",
- modifier = modifier) { playlist ->
+ modifier = modifier
+ ) { playlist ->
PlaylistCard(playlist = playlist, modifier = Modifier.fillMaxWidth())
}
}
@@ -843,7 +836,8 @@ private fun SearchUsersTab(
query: String,
users: SearchPagedState,
onLoadNextPage: () -> Unit,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
SearchGridTab(
query = query,
items = users.items,
@@ -853,7 +847,8 @@ private fun SearchUsersTab(
onLoadNextPage = onLoadNextPage,
emptyMessage = "No users found",
loadingMessage = "Loading users...",
- modifier = modifier) { user ->
+ modifier = modifier
+ ) { user ->
UserCard(user = user, modifier = Modifier.fillMaxWidth())
}
}
@@ -870,7 +865,8 @@ private fun SearchGridTab(
emptyMessage: String,
loadingMessage: String,
modifier: Modifier = Modifier,
- itemContent: @Composable (T) -> Unit) {
+ itemContent: @Composable (T) -> Unit
+) {
if (query.isBlank()) {
SearchMessage(message = "Start typing to search", modifier = modifier)
return
@@ -897,14 +893,16 @@ private fun SearchGridTab(
isLoading && items.isEmpty() -> {
SearchLoadingIndicator(
message = loadingMessage,
- modifier = modifier)
+ modifier = modifier
+ )
}
error != null && items.isEmpty() -> {
ErrorDisplay(
errorMessage = error,
onRetry = onLoadNextPage,
- modifier = modifier)
+ modifier = modifier
+ )
}
!isLoading && items.isEmpty() -> {
@@ -918,7 +916,8 @@ private fun SearchGridTab(
modifier = modifier.padding(horizontal = 12.dp),
contentPadding = PaddingValues(top = 12.dp, bottom = 16.dp + bottomInset),
horizontalArrangement = Arrangement.spacedBy(12.dp),
- verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
items(items) { item ->
itemContent(item)
}
@@ -929,7 +928,8 @@ private fun SearchGridTab(
PlayableCard(
title = "Sample Title",
subtitle = "Sample Subtitle",
- imageURL = "https://placehold.co/600x400")
+ imageURL = "https://placehold.co/600x400"
+ )
}
}
}
@@ -942,32 +942,38 @@ private fun SearchGridTab(
private fun SearchHorizontalSection(
title: String,
onSeeAll: () -> Unit,
- content: androidx.compose.foundation.lazy.LazyListScope.() -> Unit) {
+ content: androidx.compose.foundation.lazy.LazyListScope.() -> Unit
+) {
Column(
modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(12.dp)) {
+ verticalArrangement = Arrangement.spacedBy(12.dp)
+ ) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween) {
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
Text(
text = title,
style = AppTheme.typography.titleLarge,
- fontWeight = FontWeight.SemiBold)
+ fontWeight = FontWeight.SemiBold
+ )
Text(
text = "See all",
style = AppTheme.typography.labelLarge,
color = AppTheme.colors.primary,
- modifier = Modifier.clickable(onClick = onSeeAll))
+ modifier = Modifier.clickable(onClick = onSeeAll)
+ )
}
LazyRow(
contentPadding = PaddingValues(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
- content = content)
+ content = content
+ )
}
}
@@ -975,31 +981,37 @@ private fun SearchHorizontalSection(
private fun SearchTrackRow(
index: Int,
track: MetadataTrack,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
Row(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(10.dp)) {
+ horizontalArrangement = Arrangement.spacedBy(10.dp)
+ ) {
Text(
text = (index + 1).toString(),
style = AppTheme.typography.labelMedium,
color = AppTheme.colors.onSurfaceVariant,
- textAlign = TextAlign.End)
+ textAlign = TextAlign.End
+ )
Column(
modifier = Modifier.weight(1f),
- verticalArrangement = Arrangement.spacedBy(2.dp)) {
+ verticalArrangement = Arrangement.spacedBy(2.dp)
+ ) {
Text(
text = track.title,
style = AppTheme.typography.bodyLarge,
- maxLines = 1)
+ maxLines = 1
+ )
Text(
text = track.artists.joinToString { it.name },
style = AppTheme.typography.bodySmall,
color = AppTheme.colors.onSurfaceVariant,
- maxLines = 1)
+ maxLines = 1
+ )
}
if (track.album != null) Text(
@@ -1007,7 +1019,8 @@ private fun SearchTrackRow(
style = AppTheme.typography.bodyMedium,
color = AppTheme.colors.onSurfaceVariant,
maxLines = 1,
- modifier = Modifier.weight(1f))
+ modifier = Modifier.weight(1f)
+ )
}
}
@@ -1018,27 +1031,33 @@ private val WarmSearchMessages = listOf(
"Dusting off the vinyl...",
"Consulting the music gods...",
"Tuning the antennas...",
- "Flipping through the records...")
+ "Flipping through the records..."
+)
@Composable
private fun SearchLoadingIndicator(
message: String? = null,
- modifier: Modifier = Modifier) {
+ modifier: Modifier = Modifier
+) {
val infiniteTransition = rememberInfiniteTransition(label = "search-loading")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 360f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 1500, easing = LinearEasing),
- repeatMode = RepeatMode.Restart),
- label = "icon-rotation")
+ repeatMode = RepeatMode.Restart
+ ),
+ label = "icon-rotation"
+ )
val bounce by infiniteTransition.animateFloat(
initialValue = -8f,
targetValue = 8f,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 800, easing = LinearEasing),
- repeatMode = RepeatMode.Reverse),
- label = "icon-bounce")
+ repeatMode = RepeatMode.Reverse
+ ),
+ label = "icon-bounce"
+ )
val messageIndex by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = WarmSearchMessages.size.toFloat(),
@@ -1047,18 +1066,22 @@ private fun SearchLoadingIndicator(
durationMillis = WarmSearchMessages.size * 2000,
easing = LinearEasing
),
- repeatMode = RepeatMode.Restart),
- label = "message-cycle")
+ repeatMode = RepeatMode.Restart
+ ),
+ label = "message-cycle"
+ )
val displayMessage =
message ?: WarmSearchMessages[messageIndex.toInt() % WarmSearchMessages.size]
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.Center) {
+ verticalArrangement = Arrangement.Center
+ ) {
Box(
modifier = Modifier.size(64.dp),
- contentAlignment = Alignment.Center) {
+ contentAlignment = Alignment.Center
+ ) {
MillenniumIcon(
imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = "Searching",
@@ -1073,18 +1096,21 @@ private fun SearchLoadingIndicator(
MillenniumIndeterminateProgress(
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 32.dp, vertical = 16.dp))
+ .padding(horizontal = 32.dp, vertical = 16.dp)
+ )
AnimatedContent(
targetState = displayMessage,
transitionSpec = {
fadeIn(tween(300)) togetherWith fadeOut(tween(300))
},
- label = "message-transition") { msg ->
+ label = "message-transition"
+ ) { msg ->
Text(
text = msg,
color = AppTheme.colors.onSurfaceVariant,
style = AppTheme.typography.bodyMedium,
- textAlign = TextAlign.Center)
+ textAlign = TextAlign.Center
+ )
}
}
}
@@ -1093,30 +1119,35 @@ private fun SearchLoadingIndicator(
private fun SearchMessage(
message: String,
modifier: Modifier = Modifier,
- isError: Boolean = false) {
+ isError: Boolean = false
+) {
Box(
modifier = modifier,
- contentAlignment = Alignment.Center) {
+ contentAlignment = Alignment.Center
+ ) {
Text(
text = message,
color = if (isError) AppTheme.colors.error else AppTheme.colors.onSurfaceVariant,
style = AppTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
- modifier = Modifier.padding(16.dp))
+ modifier = Modifier.padding(16.dp)
+ )
}
}
@Composable
private fun SearchMessageInline(
message: String,
- isError: Boolean = false) {
+ isError: Boolean = false
+) {
Text(
text = message,
color = if (isError) AppTheme.colors.error else AppTheme.colors.onSurfaceVariant,
style = AppTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
- .padding(horizontal = 16.dp))
+ .padding(horizontal = 16.dp)
+ )
}
@Composable
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt
index 5134106a..44b0b425 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/search/SearchScreenViewModel.kt
@@ -59,7 +59,14 @@ data class SearchScreenState(
val albums: SearchPagedState = SearchPagedState(),
val artists: SearchPagedState = SearchPagedState(),
val playlists: SearchPagedState = SearchPagedState(),
- val users: SearchPagedState = SearchPagedState())
+ val users: SearchPagedState = SearchPagedState()) {
+ val recentSearchSuggestions: List
+ get() = if (query.isBlank()) {
+ recentSearches
+ } else {
+ recentSearches.filter { it.contains(query, ignoreCase = true) }
+ }
+}
@OptIn(ExperimentalCoroutinesApi::class)
class SearchScreenViewModel(
diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt
index 2a5d0ed8..38428743 100644
--- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt
+++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt
@@ -50,10 +50,14 @@ import dev.krtirtho.spotube.modules.support.LocalSupportPrompt
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxHeart2
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2
+import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarLeftBroken
+import dev.krtirtho.spotube.resources.iconsax.IconsaxSidebarRightBroken
import dev.krtirtho.spotube.sidebarTabs
import org.jetbrains.compose.resources.Font
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.koinInject
+import org.spotube.millennium.composables.MillenniumIconButton
+import org.spotube.millennium.composables.MillenniumIconButtonStyle
import org.spotube.millennium.composables.MillenniumSidebar
import org.spotube.millennium.composables.MillenniumSidebarContent
import org.spotube.millennium.composables.MillenniumSidebarFooter
@@ -89,7 +93,7 @@ fun AppSidebar(
) {
MillenniumSidebarHeader {
Row(
- modifier = Modifier.fillMaxWidth().padding(horizontal = 10.dp, vertical = 8.dp),
+ modifier = Modifier.fillMaxWidth(),
horizontalArrangement = if (expanded) Arrangement.SpaceBetween else Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
@@ -104,7 +108,14 @@ fun AppSidebar(
color = AppTheme.colors.onSurface
)
}
- MillenniumSidebarTrigger()
+ MillenniumSidebarTrigger {
+ MillenniumIconButton(
+ if (it.collapsed) Iconsax.IconsaxSidebarRightBroken else Iconsax.IconsaxSidebarLeftBroken,
+ contentDescription = "Toggle Sidebar",
+ style = MillenniumIconButtonStyle.Quiet,
+ onClick = { it.toggle() }
+ )
+ }
}
}
MillenniumSidebarContent {
@@ -120,8 +131,9 @@ fun AppSidebar(
MillenniumSidebarGroupContent {
MillenniumSidebarMenu {
entry.items.forEach { item ->
- val selected = navigationState.topLevelRoute == item.route &&
- (item.route != Routes.Library || currentLibraryTab == item.data)
+ val selected =
+ navigationState.topLevelRoute == item.route &&
+ (item.route != Routes.Library || currentLibraryTab == item.data)
SidebarNavItem(
item = item,
selected = selected,
@@ -154,13 +166,15 @@ fun AppSidebar(
label = supportLabel,
onClick = { openSupport() },
contentDescription = supportLabel,
- icon = Iconsax.IconsaxHeart2)
+ icon = Iconsax.IconsaxHeart2
+ )
MillenniumSidebarItem(
label = stringResource(Res.string.settings_screen_title),
onClick = { navigator.navigate(Routes.Settings) },
selected = navigationState.topLevelRoute == Routes.Settings,
contentDescription = stringResource(Res.string.settings_screen_title),
- icon = Iconsax.IconsaxSetting2)
+ icon = Iconsax.IconsaxSetting2
+ )
}
}
}
@@ -169,11 +183,13 @@ fun AppSidebar(
private fun SidebarNavItem(
item: NavigationItem.Tab<*>,
selected: Boolean,
- onClick: () -> Unit) {
+ onClick: () -> Unit
+) {
MillenniumSidebarItem(
label = item.title,
onClick = onClick,
selected = selected,
contentDescription = item.title,
- icon = item.icon)
+ icon = item.icon
+ )
}