add sort, filters and search to the library

This commit is contained in:
austinried
2026-10-04 00:29:55 +00:00
parent 120d180220
commit 9a99977342
13 changed files with 1127 additions and 96 deletions
@@ -0,0 +1,125 @@
package com.subtracks.data.db
import android.content.Context
import androidx.paging.PagingSource
import androidx.room3.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.subtracks.data.model.Album
import com.subtracks.data.model.Song
import com.subtracks.data.model.Source
import kotlinx.coroutines.test.runTest
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class LibraryDaoTest {
private lateinit var db: SubtracksDatabase
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<Context>()
db =
Room
.inMemoryDatabaseBuilder(context, SubtracksDatabase::class.java)
.setDriver(BundledSQLiteDriver())
.build()
}
@After
fun tearDown() {
db.close()
}
@Test
fun albumSortsAndStarredFilter() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertAlbums(
listOf(
album(sourceId, "a", "Beta", year = 2000, starred = null),
album(sourceId, "b", "Alpha", year = 2010, starred = 5),
album(sourceId, "c", "Gamma", year = 1990, starred = null),
),
)
assertEquals(listOf("Alpha", "Beta", "Gamma"), dao.albumsByName(sourceId, starredOnly = false).page().map { it.name })
assertEquals(listOf("Gamma", "Beta", "Alpha"), dao.albumsByNameReversed(sourceId, starredOnly = false).page().map { it.name })
assertEquals(listOf("Alpha", "Beta", "Gamma"), dao.albumsByYear(sourceId, starredOnly = false).page().map { it.name })
assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, starredOnly = true).page().map { it.name })
}
@Test
fun songSortsByTitleAndStarredFilter() =
runTest {
val sourceId = source()
val dao = db.libraryDao()
dao.upsertSongs(
listOf(
song(sourceId, "s1", "Zebra", starred = null),
song(sourceId, "s2", "Apple", starred = 7),
),
)
assertEquals(listOf("Apple", "Zebra"), dao.songsByTitle(sourceId, starredOnly = false).page().map { it.song.title })
assertEquals(listOf("Zebra", "Apple"), dao.songsByTitleReversed(sourceId, starredOnly = false).page().map { it.song.title })
assertEquals(listOf("Apple"), dao.songsByTitle(sourceId, starredOnly = true).page().map { it.song.title })
}
private suspend fun source(): Long =
db.sourcesDao().insertSource(Source(name = "Navidrome", address = "http://localhost", isActive = true, createdAt = 0))
private suspend fun <T : Any> PagingSource<Int, T>.page(): List<T> =
(
load(
PagingSource.LoadParams.Refresh(key = null, loadSize = 100, placeholdersEnabled = false),
) as PagingSource.LoadResult.Page
).data
private fun album(
sourceId: Long,
id: String,
name: String,
year: Long?,
starred: Long?,
) = Album(
sourceId = sourceId,
id = id,
artistId = "ar-1",
name = name,
albumArtist = "Artist",
created = 0,
coverArt = null,
genre = null,
year = year,
starred = starred,
songCount = 1,
frequentRank = null,
recentRank = null,
)
private fun song(
sourceId: Long,
id: String,
title: String,
starred: Long?,
) = Song(
sourceId = sourceId,
id = id,
albumId = "al-1",
artistId = "ar-1",
title = title,
album = "Album",
artist = "Artist",
duration = 200,
track = 1,
disc = 1,
starred = starred,
genre = null,
)
}
@@ -60,7 +60,7 @@ class SyncServiceTest {
2,
db
.libraryDao()
.artists(1)
.artistsByName(1, false)
.allRows()
.size,
)
@@ -68,7 +68,7 @@ class SyncServiceTest {
2,
db
.libraryDao()
.songs(1)
.songs(1, false)
.allRows()
.size,
)
@@ -90,7 +90,7 @@ class SyncServiceTest {
listOf("a1"),
db
.libraryDao()
.artists(1)
.artistsByName(1, false)
.allRows()
.map { it.id },
)
@@ -98,7 +98,7 @@ class SyncServiceTest {
listOf("s1"),
db
.libraryDao()
.songs(1)
.songs(1, false)
.allRows()
.map { it.song.id },
)
@@ -200,7 +200,7 @@ class SyncServiceTest {
"Old",
db
.libraryDao()
.artists(1)
.artistsByName(1, false)
.allRows()
.single()
.name,
@@ -213,7 +213,7 @@ class SyncServiceTest {
"New",
db
.libraryDao()
.artists(1)
.artistsByName(1, false)
.allRows()
.single()
.name,
@@ -0,0 +1,122 @@
package com.subtracks.ui
import androidx.activity.ComponentActivity
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
import androidx.compose.ui.test.onAllNodesWithTag
import androidx.compose.ui.test.onAllNodesWithText
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onRoot
import androidx.compose.ui.test.performClick
import androidx.paging.PagingData
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
import com.github.takahirom.roborazzi.captureRoboImage
import com.subtracks.data.model.Album
import com.subtracks.data.model.Artist
import com.subtracks.data.model.Playlist
import com.subtracks.data.model.SongListItem
import com.subtracks.data.prefs.ListQuery
import com.subtracks.data.repo.SearchHit
import com.subtracks.ui.library.ALBUM_COVER_TAG
import com.subtracks.ui.library.LibraryScreen
import com.subtracks.ui.library.LibraryTab
import com.subtracks.ui.library.SearchScreen
import com.subtracks.ui.library.sortOptionsFor
import com.subtracks.ui.theme.SubtracksTheme
import kotlinx.coroutines.flow.flowOf
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import org.robolectric.annotation.GraphicsMode
@RunWith(AndroidJUnit4::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@Config(qualifiers = RobolectricDeviceQualifiers.PixelXL)
class LibraryListsScreenshotTest {
@get:Rule
val composeRule = createAndroidComposeRule<ComponentActivity>()
@Test
fun libraryOptions() {
composeRule.setContent {
SubtracksTheme {
LibraryScreen(
selectedTab = LibraryTab.Albums,
onTabSelected = {},
albums = remember { flowOf(PagingData.from(albums())) }.collectAsLazyPagingItems(),
artists = remember { flowOf(PagingData.empty<Artist>()) }.collectAsLazyPagingItems(),
songs = remember { flowOf(PagingData.empty<SongListItem>()) }.collectAsLazyPagingItems(),
playlists = remember { flowOf(PagingData.empty<Playlist>()) }.collectAsLazyPagingItems(),
coverArt = { _, _ -> null },
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
onSync = {},
onOpenSettings = {},
listQuery = ListQuery("Name"),
sortOptions = sortOptionsFor(LibraryTab.Albums),
starredSupported = true,
)
}
}
composeRule.waitUntil(timeoutMillis = 5_000) {
composeRule.onAllNodesWithTag(ALBUM_COVER_TAG).fetchSemanticsNodes().isNotEmpty()
}
composeRule.onNodeWithContentDescription("List options").performClick()
composeRule.mainClock.advanceTimeBy(1_000)
composeRule.waitForIdle()
composeRule.onRoot().captureRoboImage("src/test/screenshots/library_options.png")
}
@Test
fun search() {
composeRule.setContent {
SubtracksTheme {
SearchScreen(
query = "radio",
results =
listOf(
SearchHit("song", "s-eiirp", "Everything In Its Right Place"),
SearchHit("album", "al-kid-a", "Kid A"),
SearchHit("artist", "ar-radiohead", "Radiohead"),
SearchHit("playlist", "pl-late-night", "Late Night"),
),
onQueryChange = {},
onBack = {},
onAlbumClick = {},
onArtistClick = {},
onPlaylistClick = {},
onSongClick = {},
)
}
}
composeRule.waitUntil(timeoutMillis = 5_000) {
composeRule.onAllNodesWithText("Radiohead").fetchSemanticsNodes().isNotEmpty()
}
composeRule.onRoot().captureRoboImage("src/test/screenshots/search.png")
}
private fun albums() =
(1..7).map { index ->
Album(
sourceId = 1,
id = "al-$index",
artistId = "ar-1",
name = "Album $index",
albumArtist = "Artist",
created = 0,
coverArt = null,
genre = null,
year = null,
starred = null,
songCount = 1,
frequentRank = null,
recentRank = null,
)
}
}