mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
song added sort, robust scroll reset, restyled search and filter controls
This commit is contained in:
@@ -454,6 +454,38 @@ interface LibraryDao {
|
||||
search: String,
|
||||
): PagingSource<Int, SongListItem>
|
||||
|
||||
@Query(
|
||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"WHERE songs.sourceId = :sourceId " +
|
||||
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
|
||||
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
|
||||
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||
"ORDER BY songs.created DESC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
fun songsByAdded(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
): PagingSource<Int, SongListItem>
|
||||
|
||||
@Query(
|
||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"WHERE songs.sourceId = :sourceId " +
|
||||
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND songs.starred IS NOT NULL) OR " +
|
||||
"(:starredFilter = 2 AND songs.starred IS NULL)) " +
|
||||
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||
"ORDER BY songs.created ASC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
fun songsByAddedReversed(
|
||||
sourceId: Long,
|
||||
starredFilter: Int,
|
||||
search: String,
|
||||
): PagingSource<Int, SongListItem>
|
||||
|
||||
@Query(PLAYLIST_SONGS_SQL)
|
||||
fun playlistSongs(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -79,5 +79,12 @@ val MIGRATION_7_8 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_8_9 =
|
||||
object : Migration(8, 9) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL("ALTER TABLE `songs` ADD COLUMN `created` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATIONS: Array<Migration> =
|
||||
arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8)
|
||||
arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9)
|
||||
|
||||
@@ -32,7 +32,7 @@ import com.subtracks.data.model.SubsonicSource
|
||||
ShuffleOrder::class,
|
||||
ArtworkSeed::class,
|
||||
],
|
||||
version = 8,
|
||||
version = 9,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(QueueKindConverter::class)
|
||||
|
||||
@@ -160,6 +160,7 @@ data class Song(
|
||||
val disc: Long?,
|
||||
val starred: Long?,
|
||||
val genre: String?,
|
||||
@ColumnInfo(defaultValue = "0") val created: Long = 0,
|
||||
)
|
||||
|
||||
data class SongListItem(
|
||||
|
||||
@@ -16,7 +16,7 @@ enum class ArtistSort { Name, AlbumCount, Starred }
|
||||
|
||||
enum class PlaylistSort { Name, Added, Updated }
|
||||
|
||||
enum class SongSort { Album, Title, Artist, Starred }
|
||||
enum class SongSort { Album, Title, Artist, Starred, Added }
|
||||
|
||||
enum class LibraryListTab(
|
||||
val key: String,
|
||||
|
||||
@@ -183,6 +183,14 @@ class LibraryRepository(
|
||||
dao.songsByStarred(sourceId, starred.ordinal, search)
|
||||
}
|
||||
}
|
||||
|
||||
SongSort.Added -> {
|
||||
if (descending) {
|
||||
dao.songsByAddedReversed(sourceId, starred.ordinal, search)
|
||||
} else {
|
||||
dao.songsByAdded(sourceId, starred.ordinal, search)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -77,6 +77,7 @@ object SubsonicXml {
|
||||
disc = element.longAttr("discNumber"),
|
||||
starred = element.dateAttr("starred"),
|
||||
genre = element.attr("genre").ifEmpty { null },
|
||||
created = element.dateAttr("created") ?: 0L,
|
||||
)
|
||||
|
||||
fun playlists(
|
||||
|
||||
@@ -117,6 +117,7 @@ private val songColumns =
|
||||
"disc",
|
||||
"starred",
|
||||
"genre",
|
||||
"created",
|
||||
)
|
||||
|
||||
private val playlistColumns = listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration")
|
||||
@@ -130,7 +131,8 @@ private fun Artist.values() = listOf<Any?>(sourceId, id, name, albumCount, starr
|
||||
private fun Album.values() =
|
||||
listOf<Any?>(sourceId, id, artistId, name, albumArtist, created, coverArt, genre, year, starred, songCount, frequentRank, recentRank)
|
||||
|
||||
private fun Song.values() = listOf<Any?>(sourceId, id, albumId, artistId, title, album, artist, duration, track, disc, starred, genre)
|
||||
private fun Song.values() =
|
||||
listOf<Any?>(sourceId, id, albumId, artistId, title, album, artist, duration, track, disc, starred, genre, created)
|
||||
|
||||
private fun Playlist.values() = listOf<Any?>(sourceId, id, name, comment, coverArt, songCount, created, changed, duration)
|
||||
|
||||
|
||||
@@ -3,14 +3,49 @@ package com.subtracks.ui.components
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.paging.LoadState
|
||||
|
||||
private const val FILL_EPSILON_PX = 1
|
||||
|
||||
/**
|
||||
* Scrolls back to the top once a new filter/sort has actually loaded.
|
||||
*
|
||||
* Scrolling immediately after [resetKey] changes does not stick: the lazy layout keeps the
|
||||
* previously visible item in place by key while the paged list reloads, which snaps the scroll
|
||||
* back. Waiting for the refresh triggered by the change and resetting afterwards avoids that.
|
||||
*/
|
||||
@Composable
|
||||
fun ResetScrollOnChange(
|
||||
resetKey: Any?,
|
||||
refreshState: () -> LoadState,
|
||||
onReset: suspend () -> Unit,
|
||||
) {
|
||||
var pending by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(resetKey) { pending = true }
|
||||
LaunchedEffect(Unit) {
|
||||
var refreshes = 0
|
||||
var handled = 0
|
||||
snapshotFlow(refreshState).collect { state ->
|
||||
if (state is LoadState.Loading) {
|
||||
refreshes++
|
||||
} else if (pending && refreshes > handled) {
|
||||
onReset()
|
||||
handled = refreshes
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun rememberViewportFill(state: LazyListState): Dp {
|
||||
val density = LocalDensity.current
|
||||
|
||||
@@ -14,7 +14,6 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.testTag
|
||||
@@ -29,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
||||
|
||||
@@ -60,7 +60,7 @@ fun AlbumsContent(
|
||||
|
||||
else -> {
|
||||
val gridState = rememberLazyGridState()
|
||||
LaunchedEffect(resetKey) { gridState.scrollToItem(0) }
|
||||
ResetScrollOnChange(resetKey, { items.loadState.refresh }) { gridState.scrollToItem(0) }
|
||||
val fill = rememberViewportFill(gridState)
|
||||
LazyVerticalGrid(
|
||||
state = gridState,
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -29,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
||||
|
||||
@@ -58,7 +58,7 @@ fun ArtistsContent(
|
||||
|
||||
else -> {
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(resetKey) { listState.scrollToItem(0) }
|
||||
ResetScrollOnChange(resetKey, { items.loadState.refresh }) { listState.scrollToItem(0) }
|
||||
val fill = rememberViewportFill(listState)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.subtracks.ui.library
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -32,6 +33,7 @@ import androidx.compose.material.icons.rounded.Album
|
||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.FilterAltOff
|
||||
import androidx.compose.material.icons.rounded.MusicNote
|
||||
import androidx.compose.material.icons.rounded.Person
|
||||
import androidx.compose.material.icons.rounded.Remove
|
||||
@@ -46,20 +48,17 @@ import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalBottomSheet
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.OutlinedTextFieldDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -153,6 +152,7 @@ fun sortOptionsFor(tab: LibraryTab): List<SortOption> =
|
||||
SortOption(SongSort.Title.name, "Title"),
|
||||
SortOption(SongSort.Artist.name, "Artist"),
|
||||
SortOption(SongSort.Starred.name, "Starred"),
|
||||
SortOption(SongSort.Added.name, "Added"),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -327,56 +327,54 @@ fun LibraryScreen(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxWidth().weight(1f),
|
||||
) { page ->
|
||||
key(resetKey) {
|
||||
when (LibraryTab.entries[page]) {
|
||||
LibraryTab.Albums -> {
|
||||
AlbumsContent(
|
||||
albums,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onAlbumClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
when (LibraryTab.entries[page]) {
|
||||
LibraryTab.Albums -> {
|
||||
AlbumsContent(
|
||||
albums,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onAlbumClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
|
||||
LibraryTab.Artists -> {
|
||||
ArtistsContent(
|
||||
artists,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onArtistClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
LibraryTab.Artists -> {
|
||||
ArtistsContent(
|
||||
artists,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onArtistClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
|
||||
LibraryTab.Songs -> {
|
||||
SongsContent(
|
||||
songs,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onSongClick,
|
||||
playingSongId,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
LibraryTab.Songs -> {
|
||||
SongsContent(
|
||||
songs,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onSongClick,
|
||||
playingSongId,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
|
||||
LibraryTab.Playlists -> {
|
||||
PlaylistsContent(
|
||||
playlists,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onPlaylistClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
LibraryTab.Playlists -> {
|
||||
PlaylistsContent(
|
||||
playlists,
|
||||
coverArt,
|
||||
listBottomInset,
|
||||
onPlaylistClick,
|
||||
filtered = filtersActive,
|
||||
onClearFilters = onClearFilters,
|
||||
resetKey = resetKey,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -456,12 +454,22 @@ private fun ListOptionsSheet(
|
||||
onSearch: () -> Unit,
|
||||
) {
|
||||
Column(Modifier.fillMaxWidth().padding(bottom = 24.dp)) {
|
||||
ListItem(
|
||||
headlineContent = { Text("Search this list") },
|
||||
leadingContent = { Icon(Icons.Rounded.Search, contentDescription = null) },
|
||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||
modifier = Modifier.clickable(onClick = onSearch),
|
||||
)
|
||||
Surface(
|
||||
onClick = onSearch,
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
color = Color.Transparent,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 14.dp),
|
||||
) {
|
||||
Icon(Icons.Rounded.Search, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text("Search this list", color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
}
|
||||
}
|
||||
HorizontalDivider()
|
||||
|
||||
Row(
|
||||
@@ -488,18 +496,30 @@ private fun ListOptionsSheet(
|
||||
sortOptions.forEach { option ->
|
||||
FilterChip(
|
||||
selected = listQuery.sort == option.value,
|
||||
onClick = { onSortChange(option.value) },
|
||||
onClick = {
|
||||
if (listQuery.sort == option.value) onToggleSortDirection() else onSortChange(option.value)
|
||||
},
|
||||
label = { Text(option.label) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (starredSupported) {
|
||||
Text(
|
||||
text = "Filters",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 4.dp),
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 8.dp, top = 16.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Filters",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
if (listQuery.starred != StarredFilter.Any) {
|
||||
IconButton(onClick = onClearFilters) {
|
||||
Icon(Icons.Rounded.FilterAltOff, contentDescription = "Clear filters")
|
||||
}
|
||||
}
|
||||
}
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
@@ -535,9 +555,6 @@ private fun ListOptionsSheet(
|
||||
}
|
||||
},
|
||||
)
|
||||
if (listQuery.starred != StarredFilter.Any) {
|
||||
TextButton(onClick = onClearFilters) { Text("Clear filters") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -555,11 +572,11 @@ private fun SearchField(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
LaunchedEffect(Unit) { focusRequester.requestFocus() }
|
||||
TextField(
|
||||
OutlinedTextField(
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
singleLine = true,
|
||||
placeholder = { Text("Search") },
|
||||
label = { Text("Search") },
|
||||
leadingIcon = { Icon(Icons.Rounded.Search, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
IconButton(onClick = onClose) {
|
||||
@@ -567,11 +584,9 @@ private fun SearchField(
|
||||
}
|
||||
},
|
||||
colors =
|
||||
TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
focusedIndicatorColor = Color.Transparent,
|
||||
unfocusedIndicatorColor = Color.Transparent,
|
||||
OutlinedTextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.background,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.background,
|
||||
),
|
||||
modifier = modifier.focusRequester(focusRequester),
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
@@ -29,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
|
||||
@Composable
|
||||
@@ -57,7 +57,7 @@ fun PlaylistsContent(
|
||||
|
||||
else -> {
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(resetKey) { listState.scrollToItem(0) }
|
||||
ResetScrollOnChange(resetKey, { items.loadState.refresh }) { listState.scrollToItem(0) }
|
||||
val fill = rememberViewportFill(listState)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.draw.clip
|
||||
@@ -40,6 +39,7 @@ import com.subtracks.ui.components.CoverArt
|
||||
import com.subtracks.ui.components.EmptyState
|
||||
import com.subtracks.ui.components.FilteredEmptyState
|
||||
import com.subtracks.ui.components.LoadingState
|
||||
import com.subtracks.ui.components.ResetScrollOnChange
|
||||
import com.subtracks.ui.components.rememberViewportFill
|
||||
|
||||
@Composable
|
||||
@@ -69,7 +69,7 @@ fun SongsContent(
|
||||
|
||||
else -> {
|
||||
val listState = rememberLazyListState()
|
||||
LaunchedEffect(resetKey) { listState.scrollToItem(0) }
|
||||
ResetScrollOnChange(resetKey, { items.loadState.refresh }) { listState.scrollToItem(0) }
|
||||
val fill = rememberViewportFill(listState)
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
|
||||
@@ -111,6 +111,28 @@ class LibraryDaoTest {
|
||||
assertEquals(listOf("Beta"), dao.albumsByName(sourceId, starredFilter = 2, search = "").page().map { it.name })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun songAddedSortOrdersByCreated() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "Zebra", starred = null, created = 100),
|
||||
song(sourceId, "s2", "Apple", starred = null, created = 200),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("Apple", "Zebra"),
|
||||
dao.songsByAdded(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
|
||||
)
|
||||
assertEquals(
|
||||
listOf("Zebra", "Apple"),
|
||||
dao.songsByAddedReversed(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun source(): Long =
|
||||
db.sourcesDao().insertSource(Source(name = "Navidrome", address = "http://localhost", isActive = true, createdAt = 0))
|
||||
|
||||
@@ -149,6 +171,7 @@ class LibraryDaoTest {
|
||||
id: String,
|
||||
title: String,
|
||||
starred: Long?,
|
||||
created: Long = 0,
|
||||
) = Song(
|
||||
sourceId = sourceId,
|
||||
id = id,
|
||||
@@ -162,5 +185,6 @@ class LibraryDaoTest {
|
||||
disc = 1,
|
||||
starred = starred,
|
||||
genre = null,
|
||||
created = created,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ class LibraryOptionsTest {
|
||||
composeRule.waitForIdle()
|
||||
assertEquals(StarredFilter.Starred, query.starred)
|
||||
|
||||
composeRule.onNodeWithText("Clear filters").performClick()
|
||||
composeRule.onNodeWithContentDescription("Clear filters").performClick()
|
||||
composeRule.waitForIdle()
|
||||
assertEquals(StarredFilter.Any, query.starred)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user