mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
polish library controls and song ordering
This commit is contained in:
@@ -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),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user