polish library controls and song ordering

This commit is contained in:
austinried
2026-10-04 00:29:56 +00:00
parent f1c030f0e1
commit 448a211d88
3 changed files with 39 additions and 24 deletions
@@ -26,7 +26,7 @@ internal const val SONGS_SQL =
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId " + "WHERE songs.sourceId = :sourceId " +
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " + "ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
"songs.title COLLATE NOCASE, songs.id" "songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id"
@Dao @Dao
@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class) @DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class)
@@ -350,7 +350,7 @@ interface LibraryDao {
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " + "ORDER BY albums.albumArtist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, " +
"songs.title COLLATE NOCASE, songs.id", "songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songs( fun songs(
sourceId: Long, sourceId: Long,
@@ -367,7 +367,7 @@ interface LibraryDao {
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY albums.albumArtist COLLATE NOCASE DESC, songs.album COLLATE NOCASE DESC, songs.disc DESC, " + "ORDER BY albums.albumArtist COLLATE NOCASE DESC, songs.album COLLATE NOCASE DESC, songs.disc DESC, " +
"songs.track DESC, songs.title COLLATE NOCASE DESC, songs.id DESC", "songs.track DESC, songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id DESC",
) )
fun songsByAlbumReversed( fun songsByAlbumReversed(
sourceId: Long, sourceId: Long,
@@ -383,7 +383,7 @@ interface LibraryDao {
"(:starredFilter = 2 AND songs.starred IS NULL)) " + "(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.title COLLATE NOCASE, songs.id", "ORDER BY songs.title COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songsByTitle( fun songsByTitle(
sourceId: Long, sourceId: Long,
@@ -399,7 +399,7 @@ interface LibraryDao {
"(:starredFilter = 2 AND songs.starred IS NULL)) " + "(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.title COLLATE NOCASE DESC, songs.id", "ORDER BY songs.title COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songsByTitleReversed( fun songsByTitleReversed(
sourceId: Long, sourceId: Long,
@@ -447,7 +447,7 @@ interface LibraryDao {
"(:starredFilter = 2 AND songs.starred IS NULL)) " + "(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.starred IS NULL, songs.starred DESC, songs.title COLLATE NOCASE, songs.id", "ORDER BY songs.starred IS NULL, songs.starred DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songsByStarred( fun songsByStarred(
sourceId: Long, sourceId: Long,
@@ -463,7 +463,7 @@ interface LibraryDao {
"(:starredFilter = 2 AND songs.starred IS NULL)) " + "(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.starred IS NULL, songs.starred ASC, songs.title COLLATE NOCASE, songs.id", "ORDER BY songs.starred IS NULL, songs.starred ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songsByStarredReversed( fun songsByStarredReversed(
sourceId: Long, sourceId: Long,
@@ -479,7 +479,7 @@ interface LibraryDao {
"(:starredFilter = 2 AND songs.starred IS NULL)) " + "(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.created DESC, songs.title COLLATE NOCASE, songs.id", "ORDER BY songs.created DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songsByAdded( fun songsByAdded(
sourceId: Long, sourceId: Long,
@@ -495,7 +495,7 @@ interface LibraryDao {
"(:starredFilter = 2 AND songs.starred IS NULL)) " + "(:starredFilter = 2 AND songs.starred IS NULL)) " +
"AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " + "AND (:search = '' OR instr(lower(songs.title), lower(:search)) > 0 " +
"OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " + "OR instr(lower(songs.artist), lower(:search)) > 0 OR instr(lower(songs.album), lower(:search)) > 0) " +
"ORDER BY songs.created ASC, songs.title COLLATE NOCASE, songs.id", "ORDER BY songs.created ASC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
) )
fun songsByAddedReversed( fun songsByAddedReversed(
sourceId: Long, sourceId: Long,
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.imePadding
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -137,6 +136,8 @@ private fun MainNavigation() {
LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f } LaunchedEffect(Unit) { if (nowPlayingOpen) nowPlayingProgress = 1f }
val playerVisible = playback.item != null val playerVisible = playback.item != null
var librarySearching by rememberSaveable { mutableStateOf(false) }
val showMiniPlayer = playerVisible && !librarySearching
LaunchedEffect(playerVisible) { LaunchedEffect(playerVisible) {
if (!playerVisible) { if (!playerVisible) {
settleJob?.cancel() settleJob?.cancel()
@@ -148,7 +149,7 @@ private fun MainNavigation() {
val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() } val navBarInset = with(density) { WindowInsets.navigationBars.getBottom(this).toDp() }
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize().imePadding()) { Column(Modifier.fillMaxSize()) {
NavHost( NavHost(
navController = navController, navController = navController,
modifier = modifier =
@@ -185,7 +186,8 @@ private fun MainNavigation() {
onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id, artist.coverArt)) }, onArtistClick = { artist -> navController.navigate(Routes.artist(artist.id, artist.coverArt)) },
onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) }, onPlaylistClick = { playlist -> navController.navigate(Routes.playlist(playlist.id)) },
onOpenSettings = { navController.navigate(Routes.SETTINGS) }, onOpenSettings = { navController.navigate(Routes.SETTINGS) },
bottomInset = if (playerVisible) 0.dp else navBarInset, onSearchActiveChange = { librarySearching = it },
bottomInset = if (showMiniPlayer) 0.dp else navBarInset,
) )
} }
composable(Routes.SETTINGS) { composable(Routes.SETTINGS) {
@@ -254,7 +256,7 @@ private fun MainNavigation() {
} }
} }
if (playerVisible) { if (showMiniPlayer) {
val miniArt = playbackController.coverArt(playback.item, thumbnail = true) val miniArt = playbackController.coverArt(playback.item, thumbnail = true)
MiniPlayer( MiniPlayer(
state = playback, state = playback,
@@ -50,12 +50,13 @@ 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.ModalBottomSheet import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.OutlinedTextFieldDefaults
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.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -164,6 +165,7 @@ fun LibraryRoute(
onArtistClick: (Artist) -> Unit, onArtistClick: (Artist) -> Unit,
onPlaylistClick: (Playlist) -> Unit, onPlaylistClick: (Playlist) -> Unit,
onOpenSettings: () -> Unit, onOpenSettings: () -> Unit,
onSearchActiveChange: (Boolean) -> Unit,
bottomInset: Dp, bottomInset: Dp,
viewModel: LibraryViewModel = koinViewModel(), viewModel: LibraryViewModel = koinViewModel(),
) { ) {
@@ -195,6 +197,7 @@ fun LibraryRoute(
onSongClick = viewModel::playSong, onSongClick = viewModel::playSong,
onSync = viewModel::sync, onSync = viewModel::sync,
onOpenSettings = onOpenSettings, onOpenSettings = onOpenSettings,
onSearchActiveChange = onSearchActiveChange,
playingSongId = playingSongId, playingSongId = playingSongId,
listQuery = listQuery, listQuery = listQuery,
resetKeys = resetKeys, resetKeys = resetKeys,
@@ -249,6 +252,7 @@ fun LibraryScreen(
onClearFilters: () -> Unit = {}, onClearFilters: () -> Unit = {},
search: String = "", search: String = "",
onSearchChange: (String) -> Unit = {}, onSearchChange: (String) -> Unit = {},
onSearchActiveChange: (Boolean) -> Unit = {},
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val pagerState = val pagerState =
@@ -260,6 +264,11 @@ fun LibraryScreen(
var searchActive by rememberSaveable { mutableStateOf(false) } var searchActive by rememberSaveable { mutableStateOf(false) }
val searchFocus = remember { FocusRequester() } val searchFocus = remember { FocusRequester() }
DisposableEffect(searchActive) {
onSearchActiveChange(searchActive)
onDispose { onSearchActiveChange(false) }
}
LaunchedEffect(pagerState) { LaunchedEffect(pagerState) {
snapshotFlow { pagerState.currentPage }.collect { page -> snapshotFlow { pagerState.currentPage }.collect { page ->
onTabSelected(LibraryTab.entries[page]) onTabSelected(LibraryTab.entries[page])
@@ -524,10 +533,11 @@ private fun ListOptionsSheet(
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
if (listQuery.starred != StarredFilter.Any) { IconButton(
IconButton(onClick = onClearFilters) { onClick = onClearFilters,
Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters") enabled = listQuery.starred != StarredFilter.Any,
} ) {
Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters")
} }
} }
FlowRow( FlowRow(
@@ -582,21 +592,24 @@ private fun SearchField(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
LaunchedEffect(Unit) { focusRequester.requestFocus() } LaunchedEffect(Unit) { focusRequester.requestFocus() }
OutlinedTextField( TextField(
value = value, value = value,
onValueChange = onValueChange, onValueChange = onValueChange,
singleLine = true, singleLine = true,
label = { Text("Search") }, placeholder = { Text("Search") },
leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) }, leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) },
trailingIcon = { trailingIcon = {
IconButton(onClick = onClose) { IconButton(onClick = onClose) {
Icon(Icons.Rounded.Close, contentDescription = "Close search") Icon(Icons.Rounded.Close, contentDescription = "Close search")
} }
}, },
shape = RoundedCornerShape(4.dp),
colors = colors =
OutlinedTextFieldDefaults.colors( TextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.background, focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
unfocusedContainerColor = MaterialTheme.colorScheme.background, unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
focusedIndicatorColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
), ),
modifier = modifier.focusRequester(focusRequester), modifier = modifier.focusRequester(focusRequester),
) )