mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
search: add a type filter bar and share the search field
Add a filter chip bar above the keyboard to toggle songs, albums, artists and playlists independently, all on by default and scrolling when it overflows. Extract one compact 48dp search field and use it in both the cross-library screen and the library tabs.
This commit is contained in:
@@ -0,0 +1,95 @@
|
|||||||
|
package com.subtracks.ui.components
|
||||||
|
|
||||||
|
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.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Close
|
||||||
|
import androidx.compose.material.icons.rounded.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.subtracks.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchField(
|
||||||
|
value: String,
|
||||||
|
onValueChange: (String) -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
focusRequester: FocusRequester,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
autoFocus: Boolean = true,
|
||||||
|
) {
|
||||||
|
val hint = stringResource(R.string.search_input_placeholder)
|
||||||
|
val closeLabel = stringResource(R.string.close_search)
|
||||||
|
LaunchedEffect(autoFocus) { if (autoFocus) focusRequester.requestFocus() }
|
||||||
|
Surface(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
modifier = modifier.height(48.dp),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(start = 12.dp, end = 4.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
BasicTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.primary),
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.semantics { contentDescription = hint },
|
||||||
|
decorationBox = { inner ->
|
||||||
|
if (value.isEmpty()) {
|
||||||
|
Text(
|
||||||
|
text = hint,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
inner()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Close,
|
||||||
|
contentDescription = closeLabel,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,7 +32,6 @@ import androidx.compose.material.icons.rounded.Add
|
|||||||
import androidx.compose.material.icons.rounded.Album
|
import androidx.compose.material.icons.rounded.Album
|
||||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
import androidx.compose.material.icons.rounded.ArrowUpward
|
||||||
import androidx.compose.material.icons.rounded.Close
|
|
||||||
import androidx.compose.material.icons.rounded.CloudOff
|
import androidx.compose.material.icons.rounded.CloudOff
|
||||||
import androidx.compose.material.icons.rounded.DownloadDone
|
import androidx.compose.material.icons.rounded.DownloadDone
|
||||||
import androidx.compose.material.icons.rounded.FilterAltOff
|
import androidx.compose.material.icons.rounded.FilterAltOff
|
||||||
@@ -53,8 +52,6 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.ModalBottomSheet
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults
|
||||||
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState
|
||||||
@@ -77,7 +74,6 @@ import androidx.compose.ui.draw.clipToBounds
|
|||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Rect
|
import androidx.compose.ui.geometry.Rect
|
||||||
@@ -126,6 +122,7 @@ import com.subtracks.ui.components.DeleteDownloadsDialog
|
|||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
import com.subtracks.ui.components.PendingDownloadDelete
|
import com.subtracks.ui.components.PendingDownloadDelete
|
||||||
|
import com.subtracks.ui.components.SearchField
|
||||||
import com.subtracks.ui.components.bulkRef
|
import com.subtracks.ui.components.bulkRef
|
||||||
import com.subtracks.ui.components.statusBarScrim
|
import com.subtracks.ui.components.statusBarScrim
|
||||||
import com.subtracks.ui.home.HomeRoute
|
import com.subtracks.ui.home.HomeRoute
|
||||||
@@ -807,38 +804,6 @@ private val TOUCH_TARGET = 48.dp
|
|||||||
private const val LIST_OPTIONS_WEIGHT = 0.11f
|
private const val LIST_OPTIONS_WEIGHT = 0.11f
|
||||||
private val LIST_OPTIONS_BAR_WIDTHS = listOf(1f, 0.62f, 0.34f)
|
private val LIST_OPTIONS_BAR_WIDTHS = listOf(1f, 0.62f, 0.34f)
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SearchField(
|
|
||||||
value: String,
|
|
||||||
onValueChange: (String) -> Unit,
|
|
||||||
onClose: () -> Unit,
|
|
||||||
focusRequester: FocusRequester,
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
|
||||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
|
||||||
TextField(
|
|
||||||
value = value,
|
|
||||||
onValueChange = onValueChange,
|
|
||||||
singleLine = true,
|
|
||||||
placeholder = { Text(stringResource(R.string.search_input_placeholder)) },
|
|
||||||
leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) },
|
|
||||||
trailingIcon = {
|
|
||||||
IconButton(onClick = onClose) {
|
|
||||||
Icon(Icons.Rounded.Close, contentDescription = stringResource(R.string.close_search))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(4.dp),
|
|
||||||
colors =
|
|
||||||
TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
modifier = modifier.focusRequester(focusRequester),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ListOptionsGlyph(modifier: Modifier = Modifier) {
|
private fun ListOptionsGlyph(modifier: Modifier = Modifier) {
|
||||||
val color = LocalContentColor.current
|
val color = LocalContentColor.current
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
package com.subtracks.ui.search
|
package com.subtracks.ui.search
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListScope
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
import androidx.compose.material.icons.rounded.Close
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextField
|
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.focus.FocusRequester
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
import androidx.compose.ui.focus.focusRequester
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
|
||||||
import androidx.compose.ui.semantics.semantics
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.subtracks.R
|
import com.subtracks.R
|
||||||
@@ -49,6 +49,7 @@ import com.subtracks.ui.components.EmptyState
|
|||||||
import com.subtracks.ui.components.ItemActions
|
import com.subtracks.ui.components.ItemActions
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.components.MenuTarget
|
import com.subtracks.ui.components.MenuTarget
|
||||||
|
import com.subtracks.ui.components.SearchField
|
||||||
import com.subtracks.ui.home.AlbumListRow
|
import com.subtracks.ui.home.AlbumListRow
|
||||||
import com.subtracks.ui.home.ArtistListRow
|
import com.subtracks.ui.home.ArtistListRow
|
||||||
import com.subtracks.ui.library.PlaylistRow
|
import com.subtracks.ui.library.PlaylistRow
|
||||||
@@ -56,6 +57,17 @@ import com.subtracks.ui.library.SongRow
|
|||||||
import org.koin.compose.koinInject
|
import org.koin.compose.koinInject
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
|
enum class SearchFilter(
|
||||||
|
@StringRes val label: Int,
|
||||||
|
) {
|
||||||
|
Songs(R.string.search_songs),
|
||||||
|
Albums(R.string.resources_album_name),
|
||||||
|
Artists(R.string.resources_artist_name),
|
||||||
|
Playlists(R.string.resources_playlist_name),
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ALL_FILTERS = SearchFilter.entries.toSet()
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchRoute(
|
fun SearchRoute(
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
@@ -72,6 +84,7 @@ fun SearchRoute(
|
|||||||
val query by viewModel.query.collectAsStateWithLifecycle()
|
val query by viewModel.query.collectAsStateWithLifecycle()
|
||||||
val results by viewModel.results.collectAsStateWithLifecycle()
|
val results by viewModel.results.collectAsStateWithLifecycle()
|
||||||
val playback by playbackController.state.collectAsStateWithLifecycle()
|
val playback by playbackController.state.collectAsStateWithLifecycle()
|
||||||
|
var filterMask by rememberSaveable { mutableIntStateOf(allFilterMask()) }
|
||||||
val actions =
|
val actions =
|
||||||
ItemActions(
|
ItemActions(
|
||||||
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
|
playNext = { playbackController.playNext(it.sourceId, it.kind, it.refId) },
|
||||||
@@ -84,6 +97,8 @@ fun SearchRoute(
|
|||||||
query = query,
|
query = query,
|
||||||
onQueryChange = viewModel::setQuery,
|
onQueryChange = viewModel::setQuery,
|
||||||
results = results,
|
results = results,
|
||||||
|
filters = maskToFilters(filterMask),
|
||||||
|
onToggleFilter = { filterMask = filterMask xor (1 shl it.ordinal) },
|
||||||
coverArt = viewModel::coverArt,
|
coverArt = viewModel::coverArt,
|
||||||
playingSongId = playback.item?.id,
|
playingSongId = playback.item?.id,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
@@ -95,12 +110,18 @@ fun SearchRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun allFilterMask(): Int = SearchFilter.entries.fold(0) { mask, filter -> mask or (1 shl filter.ordinal) }
|
||||||
|
|
||||||
|
private fun maskToFilters(mask: Int): Set<SearchFilter> = SearchFilter.entries.filterTo(mutableSetOf()) { mask and (1 shl it.ordinal) != 0 }
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchScreen(
|
fun SearchScreen(
|
||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
results: SearchResults,
|
results: SearchResults,
|
||||||
|
filters: Set<SearchFilter> = ALL_FILTERS,
|
||||||
|
onToggleFilter: (SearchFilter) -> Unit = {},
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
|
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
|
||||||
playingSongId: String? = null,
|
playingSongId: String? = null,
|
||||||
onBack: () -> Unit = {},
|
onBack: () -> Unit = {},
|
||||||
@@ -112,15 +133,16 @@ fun SearchScreen(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
|
||||||
val backLabel = stringResource(R.string.navigation_back)
|
val backLabel = stringResource(R.string.navigation_back)
|
||||||
val clearLabel = stringResource(R.string.close_search)
|
|
||||||
val hint = stringResource(R.string.search_input_placeholder)
|
|
||||||
val minLength = stringResource(R.string.search_min_length, SEARCH_MIN_LENGTH)
|
val minLength = stringResource(R.string.search_min_length, SEARCH_MIN_LENGTH)
|
||||||
val songsLabel = stringResource(R.string.search_songs)
|
val songsLabel = stringResource(R.string.search_songs)
|
||||||
val albumsLabel = stringResource(R.string.resources_album_name)
|
val albumsLabel = stringResource(R.string.resources_album_name)
|
||||||
val artistsLabel = stringResource(R.string.resources_artist_name)
|
val artistsLabel = stringResource(R.string.resources_artist_name)
|
||||||
val playlistsLabel = stringResource(R.string.resources_playlist_name)
|
val playlistsLabel = stringResource(R.string.resources_playlist_name)
|
||||||
|
val showSongs = SearchFilter.Songs in filters && results.songs.isNotEmpty()
|
||||||
|
val showAlbums = SearchFilter.Albums in filters && results.albums.isNotEmpty()
|
||||||
|
val showArtists = SearchFilter.Artists in filters && results.artists.isNotEmpty()
|
||||||
|
val showPlaylists = SearchFilter.Playlists in filters && results.playlists.isNotEmpty()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -131,35 +153,19 @@ fun SearchScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
title = {
|
title = {
|
||||||
TextField(
|
SearchField(
|
||||||
value = query,
|
value = query,
|
||||||
onValueChange = onQueryChange,
|
onValueChange = onQueryChange,
|
||||||
singleLine = true,
|
onClose = { onQueryChange("") },
|
||||||
placeholder = { Text(hint) },
|
focusRequester = focusRequester,
|
||||||
trailingIcon = {
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
if (query.isNotEmpty()) {
|
|
||||||
IconButton(onClick = { onQueryChange("") }) {
|
|
||||||
Icon(Icons.Rounded.Close, contentDescription = clearLabel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
shape = RoundedCornerShape(8.dp),
|
|
||||||
colors =
|
|
||||||
TextFieldDefaults.colors(
|
|
||||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
focusedIndicatorColor = Color.Transparent,
|
|
||||||
unfocusedIndicatorColor = Color.Transparent,
|
|
||||||
),
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.focusRequester(focusRequester)
|
|
||||||
.semantics { contentDescription = hint },
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
bottomBar = {
|
||||||
|
SearchFilterBar(filters = filters, onToggle = onToggleFilter)
|
||||||
|
},
|
||||||
) { padding ->
|
) { padding ->
|
||||||
when {
|
when {
|
||||||
!searchReady(query) -> {
|
!searchReady(query) -> {
|
||||||
@@ -173,7 +179,7 @@ fun SearchScreen(
|
|||||||
LoadingState(Modifier.padding(padding))
|
LoadingState(Modifier.padding(padding))
|
||||||
}
|
}
|
||||||
|
|
||||||
results.isEmpty -> {
|
!showSongs && !showAlbums && !showArtists && !showPlaylists -> {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
text = stringResource(R.string.search_no_results, query),
|
text = stringResource(R.string.search_no_results, query),
|
||||||
modifier = Modifier.padding(padding),
|
modifier = Modifier.padding(padding),
|
||||||
@@ -185,16 +191,40 @@ fun SearchScreen(
|
|||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
contentPadding = PaddingValues(bottom = 24.dp),
|
contentPadding = PaddingValues(bottom = 24.dp),
|
||||||
) {
|
) {
|
||||||
songResults(results.songs, songsLabel, coverArt, playingSongId, onSongClick, onLongClick)
|
if (showSongs) songResults(results.songs, songsLabel, coverArt, playingSongId, onSongClick, onLongClick)
|
||||||
albumResults(results.albums, albumsLabel, coverArt, onAlbumClick, onLongClick)
|
if (showAlbums) albumResults(results.albums, albumsLabel, coverArt, onAlbumClick, onLongClick)
|
||||||
artistResults(results.artists, artistsLabel, coverArt, onArtistClick, onLongClick)
|
if (showArtists) artistResults(results.artists, artistsLabel, coverArt, onArtistClick, onLongClick)
|
||||||
playlistResults(results.playlists, playlistsLabel, coverArt, onPlaylistClick, onLongClick)
|
if (showPlaylists) playlistResults(results.playlists, playlistsLabel, coverArt, onPlaylistClick, onLongClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchFilterBar(
|
||||||
|
filters: Set<SearchFilter>,
|
||||||
|
onToggle: (SearchFilter) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
SearchFilter.entries.forEach { filter ->
|
||||||
|
FilterChip(
|
||||||
|
selected = filter in filters,
|
||||||
|
onClick = { onToggle(filter) },
|
||||||
|
label = { Text(stringResource(filter.label)) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun LazyListScope.resultHeader(title: String) {
|
private fun LazyListScope.resultHeader(title: String) {
|
||||||
item(key = "search-header-$title") {
|
item(key = "search-header-$title") {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -49,10 +49,10 @@ class SearchScreenTest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
composeRule.onNodeWithText("Songs").assertIsDisplayed()
|
composeRule.onNodeWithText("Road Song").assertIsDisplayed()
|
||||||
composeRule.onNodeWithText("Albums").assertIsDisplayed()
|
composeRule.onNodeWithText("Road Album").assertIsDisplayed()
|
||||||
composeRule.onNodeWithText("Artists").assertIsDisplayed()
|
composeRule.onNodeWithText("Road Artist").assertIsDisplayed()
|
||||||
composeRule.onNodeWithText("Playlists").assertIsDisplayed()
|
composeRule.onNodeWithText("Road Trip").assertIsDisplayed()
|
||||||
|
|
||||||
composeRule.onNodeWithText("Road Song").performClick()
|
composeRule.onNodeWithText("Road Song").performClick()
|
||||||
composeRule.onNodeWithText("Road Album").performClick()
|
composeRule.onNodeWithText("Road Album").performClick()
|
||||||
@@ -107,6 +107,46 @@ class SearchScreenTest {
|
|||||||
composeRule.onNodeWithText("Road Trip").assertIsDisplayed()
|
composeRule.onNodeWithText("Road Trip").assertIsDisplayed()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun disablingATypeHidesItsSection() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
SearchScreen(
|
||||||
|
query = "road",
|
||||||
|
onQueryChange = {},
|
||||||
|
results =
|
||||||
|
SearchResults(
|
||||||
|
albums = listOf(album("al-1", "Road Album")),
|
||||||
|
songs = listOf(AlbumSongItem(song("s1", "Road Song"), coverArt = null)),
|
||||||
|
),
|
||||||
|
filters = setOf(SearchFilter.Albums),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRule.onNodeWithText("Road Album").assertIsDisplayed()
|
||||||
|
composeRule.onNodeWithText("Road Song").assertDoesNotExist()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun tappingAFilterChipTogglesIt() {
|
||||||
|
var toggled: SearchFilter? = null
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
SearchScreen(
|
||||||
|
query = "ro",
|
||||||
|
onQueryChange = {},
|
||||||
|
results = SearchResults(),
|
||||||
|
onToggleFilter = { toggled = it },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
composeRule.onNodeWithText("Playlists").performClick()
|
||||||
|
|
||||||
|
assertEquals(SearchFilter.Playlists, toggled)
|
||||||
|
}
|
||||||
|
|
||||||
private fun album(
|
private fun album(
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user