mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
search: list playlists before artists, albums and songs
Order the result sections smallest-first and apply the review fixes: pad the filter bar for the navigation bar, keep at least one filter enabled, hide the clear button on an empty field, drop the unused autoFocus and bar-modifier parameters, and test the filter mask and section order.
This commit is contained in:
@@ -39,11 +39,11 @@ fun SearchField(
|
|||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
focusRequester: FocusRequester,
|
focusRequester: FocusRequester,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
autoFocus: Boolean = true,
|
showClose: Boolean = true,
|
||||||
) {
|
) {
|
||||||
val hint = stringResource(R.string.search_input_placeholder)
|
val hint = stringResource(R.string.search_input_placeholder)
|
||||||
val closeLabel = stringResource(R.string.close_search)
|
val closeLabel = stringResource(R.string.close_search)
|
||||||
LaunchedEffect(autoFocus) { if (autoFocus) focusRequester.requestFocus() }
|
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||||
Surface(
|
Surface(
|
||||||
shape = RoundedCornerShape(8.dp),
|
shape = RoundedCornerShape(8.dp),
|
||||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
@@ -85,6 +85,7 @@ fun SearchField(
|
|||||||
inner()
|
inner()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (showClose) {
|
||||||
IconButton(onClick = onClose) {
|
IconButton(onClick = onClose) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Rounded.Close,
|
imageVector = Icons.Rounded.Close,
|
||||||
@@ -95,3 +96,4 @@ fun SearchField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||||||
import androidx.compose.foundation.layout.Row
|
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.navigationBarsPadding
|
||||||
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
|
||||||
@@ -98,7 +99,10 @@ fun SearchRoute(
|
|||||||
onQueryChange = viewModel::setQuery,
|
onQueryChange = viewModel::setQuery,
|
||||||
results = results,
|
results = results,
|
||||||
filters = maskToFilters(filterMask),
|
filters = maskToFilters(filterMask),
|
||||||
onToggleFilter = { filterMask = filterMask xor (1 shl it.ordinal) },
|
onToggleFilter = { filter ->
|
||||||
|
val next = filterMask xor (1 shl filter.ordinal)
|
||||||
|
if (next != 0) filterMask = next
|
||||||
|
},
|
||||||
coverArt = viewModel::coverArt,
|
coverArt = viewModel::coverArt,
|
||||||
playingSongId = playback.item?.id,
|
playingSongId = playback.item?.id,
|
||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
@@ -110,9 +114,10 @@ fun SearchRoute(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun allFilterMask(): Int = SearchFilter.entries.fold(0) { mask, filter -> mask or (1 shl filter.ordinal) }
|
internal 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 }
|
internal 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
|
||||||
@@ -158,6 +163,7 @@ fun SearchScreen(
|
|||||||
onValueChange = onQueryChange,
|
onValueChange = onQueryChange,
|
||||||
onClose = { onQueryChange("") },
|
onClose = { onQueryChange("") },
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
|
showClose = query.isNotEmpty(),
|
||||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -191,10 +197,10 @@ fun SearchScreen(
|
|||||||
modifier = Modifier.fillMaxSize().padding(padding),
|
modifier = Modifier.fillMaxSize().padding(padding),
|
||||||
contentPadding = PaddingValues(bottom = 24.dp),
|
contentPadding = PaddingValues(bottom = 24.dp),
|
||||||
) {
|
) {
|
||||||
if (showSongs) songResults(results.songs, songsLabel, coverArt, playingSongId, onSongClick, onLongClick)
|
|
||||||
if (showAlbums) albumResults(results.albums, albumsLabel, coverArt, onAlbumClick, onLongClick)
|
|
||||||
if (showArtists) artistResults(results.artists, artistsLabel, coverArt, onArtistClick, onLongClick)
|
|
||||||
if (showPlaylists) playlistResults(results.playlists, playlistsLabel, coverArt, onPlaylistClick, onLongClick)
|
if (showPlaylists) playlistResults(results.playlists, playlistsLabel, coverArt, onPlaylistClick, onLongClick)
|
||||||
|
if (showArtists) artistResults(results.artists, artistsLabel, coverArt, onArtistClick, onLongClick)
|
||||||
|
if (showAlbums) albumResults(results.albums, albumsLabel, coverArt, onAlbumClick, onLongClick)
|
||||||
|
if (showSongs) songResults(results.songs, songsLabel, coverArt, playingSongId, onSongClick, onLongClick)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,12 +211,12 @@ fun SearchScreen(
|
|||||||
private fun SearchFilterBar(
|
private fun SearchFilterBar(
|
||||||
filters: Set<SearchFilter>,
|
filters: Set<SearchFilter>,
|
||||||
onToggle: (SearchFilter) -> Unit,
|
onToggle: (SearchFilter) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.navigationBarsPadding()
|
||||||
.horizontalScroll(rememberScrollState())
|
.horizontalScroll(rememberScrollState())
|
||||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.subtracks.data.model.Playlist
|
|||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
import com.subtracks.ui.theme.SubtracksTheme
|
import com.subtracks.ui.theme.SubtracksTheme
|
||||||
import org.junit.Assert.assertEquals
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Assert.assertTrue
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
@@ -147,6 +148,59 @@ class SearchScreenTest {
|
|||||||
assertEquals(SearchFilter.Playlists, toggled)
|
assertEquals(SearchFilter.Playlists, toggled)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun smallerGroupsAreListedFirst() {
|
||||||
|
composeRule.setContent {
|
||||||
|
SubtracksTheme {
|
||||||
|
SearchScreen(
|
||||||
|
query = "road",
|
||||||
|
onQueryChange = {},
|
||||||
|
results =
|
||||||
|
SearchResults(
|
||||||
|
albums = listOf(album("al-1", "Road Album")),
|
||||||
|
artists = listOf(artist("ar-1", "Road Artist")),
|
||||||
|
playlists = listOf(playlist("pl-1", "Road Trip")),
|
||||||
|
songs = listOf(AlbumSongItem(song("s1", "Road Song"), coverArt = null)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val playlistTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithText("Road Trip")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
val artistTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithText("Road Artist")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
val albumTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithText("Road Album")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
val songTop =
|
||||||
|
composeRule
|
||||||
|
.onNodeWithText("Road Song")
|
||||||
|
.fetchSemanticsNode()
|
||||||
|
.boundsInRoot.top
|
||||||
|
|
||||||
|
assertTrue("playlists should come first", playlistTop < artistTop)
|
||||||
|
assertTrue("artists should come before albums", artistTop < albumTop)
|
||||||
|
assertTrue("albums should come before songs", albumTop < songTop)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun filterMaskRoundTrips() {
|
||||||
|
assertEquals(SearchFilter.entries.toSet(), maskToFilters(allFilterMask()))
|
||||||
|
|
||||||
|
val withoutSongs = allFilterMask() and (1 shl SearchFilter.Songs.ordinal).inv()
|
||||||
|
|
||||||
|
assertEquals(SearchFilter.entries.toSet() - SearchFilter.Songs, maskToFilters(withoutSongs))
|
||||||
|
}
|
||||||
|
|
||||||
private fun album(
|
private fun album(
|
||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
Reference in New Issue
Block a user