mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
tristate starred filter, filter-aware empty state, keyboard-safe search bar
This commit is contained in:
@@ -91,193 +91,193 @@ interface LibraryDao {
|
|||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY name COLLATE NOCASE",
|
"ORDER BY name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByName(
|
fun albumsByName(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY name COLLATE NOCASE DESC",
|
"ORDER BY name COLLATE NOCASE DESC",
|
||||||
)
|
)
|
||||||
fun albumsByNameReversed(
|
fun albumsByNameReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY albumArtist COLLATE NOCASE, year, name COLLATE NOCASE",
|
"ORDER BY albumArtist COLLATE NOCASE, year, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByArtist(
|
fun albumsByArtist(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY albumArtist COLLATE NOCASE DESC, year DESC, name COLLATE NOCASE DESC",
|
"ORDER BY albumArtist COLLATE NOCASE DESC, year DESC, name COLLATE NOCASE DESC",
|
||||||
)
|
)
|
||||||
fun albumsByArtistReversed(
|
fun albumsByArtistReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY year DESC, name COLLATE NOCASE",
|
"ORDER BY year DESC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByYear(
|
fun albumsByYear(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY year ASC, name COLLATE NOCASE",
|
"ORDER BY year ASC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByYearReversed(
|
fun albumsByYearReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY created DESC, name COLLATE NOCASE",
|
"ORDER BY created DESC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByRecentlyAdded(
|
fun albumsByRecentlyAdded(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY created ASC, name COLLATE NOCASE",
|
"ORDER BY created ASC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByRecentlyAddedReversed(
|
fun albumsByRecentlyAddedReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY starred IS NULL, starred DESC, name COLLATE NOCASE",
|
"ORDER BY starred IS NULL, starred DESC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByStarred(
|
fun albumsByStarred(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%' OR albumArtist LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY starred IS NULL, starred ASC, name COLLATE NOCASE",
|
"ORDER BY starred IS NULL, starred ASC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun albumsByStarredReversed(
|
fun albumsByStarredReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Album>
|
): PagingSource<Int, Album>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY name COLLATE NOCASE",
|
"ORDER BY name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun artistsByName(
|
fun artistsByName(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Artist>
|
): PagingSource<Int, Artist>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY name COLLATE NOCASE DESC",
|
"ORDER BY name COLLATE NOCASE DESC",
|
||||||
)
|
)
|
||||||
fun artistsByNameReversed(
|
fun artistsByNameReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Artist>
|
): PagingSource<Int, Artist>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY albumCount DESC, name COLLATE NOCASE",
|
"ORDER BY albumCount DESC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun artistsByAlbumCount(
|
fun artistsByAlbumCount(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Artist>
|
): PagingSource<Int, Artist>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY albumCount ASC, name COLLATE NOCASE",
|
"ORDER BY albumCount ASC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun artistsByAlbumCountReversed(
|
fun artistsByAlbumCountReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Artist>
|
): PagingSource<Int, Artist>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY starred IS NULL, starred DESC, name COLLATE NOCASE",
|
"ORDER BY starred IS NULL, starred DESC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun artistsByStarred(
|
fun artistsByStarred(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Artist>
|
): PagingSource<Int, Artist>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
"SELECT * FROM artists WHERE sourceId = :sourceId " +
|
||||||
"AND (:starredOnly = 0 OR starred IS NOT NULL) " +
|
"AND (:starredFilter = 0 OR (:starredFilter = 1 AND starred IS NOT NULL) OR (:starredFilter = 2 AND starred IS NULL)) " +
|
||||||
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
"AND (:search = '' OR name LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY starred IS NULL, starred ASC, name COLLATE NOCASE",
|
"ORDER BY starred IS NULL, starred ASC, name COLLATE NOCASE",
|
||||||
)
|
)
|
||||||
fun artistsByStarredReversed(
|
fun artistsByStarredReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, Artist>
|
): PagingSource<Int, Artist>
|
||||||
|
|
||||||
@@ -344,7 +344,9 @@ interface LibraryDao {
|
|||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"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, " +
|
||||||
@@ -352,91 +354,103 @@ interface LibraryDao {
|
|||||||
)
|
)
|
||||||
fun songs(
|
fun songs(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByTitle(
|
fun songsByTitle(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY songs.title COLLATE NOCASE DESC, songs.id",
|
"ORDER BY songs.title COLLATE NOCASE DESC, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByTitleReversed(
|
fun songsByTitleReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY songs.artist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
|
"ORDER BY songs.artist COLLATE NOCASE, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByArtist(
|
fun songsByArtist(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"ORDER BY songs.artist COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
|
"ORDER BY songs.artist COLLATE NOCASE DESC, songs.album COLLATE NOCASE, songs.disc, songs.track, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByArtistReversed(
|
fun songsByArtistReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"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.title COLLATE NOCASE, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByStarred(
|
fun songsByStarred(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||||
"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 AND (:starredOnly = 0 OR songs.starred IS NOT NULL) " +
|
"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 || '%' " +
|
"AND (:search = '' OR songs.title LIKE '%' || :search || '%' " +
|
||||||
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
"OR songs.artist LIKE '%' || :search || '%' OR songs.album LIKE '%' || :search || '%') " +
|
||||||
"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.title COLLATE NOCASE, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByStarredReversed(
|
fun songsByStarredReversed(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
starredOnly: Boolean,
|
starredFilter: Int,
|
||||||
search: String,
|
search: String,
|
||||||
): PagingSource<Int, SongListItem>
|
): PagingSource<Int, SongListItem>
|
||||||
|
|
||||||
|
|||||||
@@ -29,10 +29,12 @@ enum class LibraryListTab(
|
|||||||
Songs("songs", SongSort.Album.name, true),
|
Songs("songs", SongSort.Album.name, true),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class StarredFilter { Any, Starred, NotStarred }
|
||||||
|
|
||||||
data class ListQuery(
|
data class ListQuery(
|
||||||
val sort: String,
|
val sort: String,
|
||||||
val descending: Boolean = false,
|
val descending: Boolean = false,
|
||||||
val starredOnly: Boolean = false,
|
val starred: StarredFilter = StarredFilter.Any,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
|
private val Context.preferences: DataStore<Preferences> by preferencesDataStore("user_prefs")
|
||||||
@@ -65,7 +67,7 @@ class UserPreferences(
|
|||||||
|
|
||||||
private fun listQueryKey(tab: LibraryListTab) = stringPreferencesKey("list_query_${tab.key}")
|
private fun listQueryKey(tab: LibraryListTab) = stringPreferencesKey("list_query_${tab.key}")
|
||||||
|
|
||||||
private fun encode(query: ListQuery) = "${query.sort}|${if (query.descending) 1 else 0}|${if (query.starredOnly) 1 else 0}"
|
private fun encode(query: ListQuery) = "${query.sort}|${if (query.descending) 1 else 0}|${query.starred.ordinal}"
|
||||||
|
|
||||||
private fun decode(
|
private fun decode(
|
||||||
stored: String?,
|
stored: String?,
|
||||||
@@ -76,7 +78,7 @@ class UserPreferences(
|
|||||||
return ListQuery(
|
return ListQuery(
|
||||||
sort = parts[0].ifEmpty { defaultSort },
|
sort = parts[0].ifEmpty { defaultSort },
|
||||||
descending = parts[1] == "1",
|
descending = parts[1] == "1",
|
||||||
starredOnly = parts[2] == "1",
|
starred = StarredFilter.entries.getOrElse(parts[2].toIntOrNull() ?: 0) { StarredFilter.Any },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.subtracks.data.prefs.AlbumSort
|
|||||||
import com.subtracks.data.prefs.ArtistSort
|
import com.subtracks.data.prefs.ArtistSort
|
||||||
import com.subtracks.data.prefs.PlaylistSort
|
import com.subtracks.data.prefs.PlaylistSort
|
||||||
import com.subtracks.data.prefs.SongSort
|
import com.subtracks.data.prefs.SongSort
|
||||||
|
import com.subtracks.data.prefs.StarredFilter
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class LibraryRepository(
|
class LibraryRepository(
|
||||||
@@ -26,7 +27,7 @@ class LibraryRepository(
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
sort: AlbumSort,
|
sort: AlbumSort,
|
||||||
descending: Boolean,
|
descending: Boolean,
|
||||||
starredOnly: Boolean,
|
starred: StarredFilter,
|
||||||
search: String,
|
search: String,
|
||||||
): Flow<PagingData<Album>> =
|
): Flow<PagingData<Album>> =
|
||||||
pager(40) {
|
pager(40) {
|
||||||
@@ -34,41 +35,41 @@ class LibraryRepository(
|
|||||||
when (sort) {
|
when (sort) {
|
||||||
AlbumSort.Name -> {
|
AlbumSort.Name -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.albumsByNameReversed(sourceId, starredOnly, search)
|
dao.albumsByNameReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.albumsByName(sourceId, starredOnly, search)
|
dao.albumsByName(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumSort.Artist -> {
|
AlbumSort.Artist -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.albumsByArtistReversed(sourceId, starredOnly, search)
|
dao.albumsByArtistReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.albumsByArtist(sourceId, starredOnly, search)
|
dao.albumsByArtist(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumSort.Year -> {
|
AlbumSort.Year -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.albumsByYearReversed(sourceId, starredOnly, search)
|
dao.albumsByYearReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.albumsByYear(sourceId, starredOnly, search)
|
dao.albumsByYear(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumSort.Added -> {
|
AlbumSort.Added -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.albumsByRecentlyAddedReversed(sourceId, starredOnly, search)
|
dao.albumsByRecentlyAddedReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.albumsByRecentlyAdded(sourceId, starredOnly, search)
|
dao.albumsByRecentlyAdded(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
AlbumSort.Starred -> {
|
AlbumSort.Starred -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.albumsByStarredReversed(sourceId, starredOnly, search)
|
dao.albumsByStarredReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.albumsByStarred(sourceId, starredOnly, search)
|
dao.albumsByStarred(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,7 +79,7 @@ class LibraryRepository(
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
sort: ArtistSort,
|
sort: ArtistSort,
|
||||||
descending: Boolean,
|
descending: Boolean,
|
||||||
starredOnly: Boolean,
|
starred: StarredFilter,
|
||||||
search: String,
|
search: String,
|
||||||
): Flow<PagingData<Artist>> =
|
): Flow<PagingData<Artist>> =
|
||||||
pager(60) {
|
pager(60) {
|
||||||
@@ -86,25 +87,25 @@ class LibraryRepository(
|
|||||||
when (sort) {
|
when (sort) {
|
||||||
ArtistSort.Name -> {
|
ArtistSort.Name -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.artistsByNameReversed(sourceId, starredOnly, search)
|
dao.artistsByNameReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.artistsByName(sourceId, starredOnly, search)
|
dao.artistsByName(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistSort.AlbumCount -> {
|
ArtistSort.AlbumCount -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.artistsByAlbumCountReversed(sourceId, starredOnly, search)
|
dao.artistsByAlbumCountReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.artistsByAlbumCount(sourceId, starredOnly, search)
|
dao.artistsByAlbumCount(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ArtistSort.Starred -> {
|
ArtistSort.Starred -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.artistsByStarredReversed(sourceId, starredOnly, search)
|
dao.artistsByStarredReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.artistsByStarred(sourceId, starredOnly, search)
|
dao.artistsByStarred(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,37 +150,37 @@ class LibraryRepository(
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
sort: SongSort,
|
sort: SongSort,
|
||||||
descending: Boolean,
|
descending: Boolean,
|
||||||
starredOnly: Boolean,
|
starred: StarredFilter,
|
||||||
search: String,
|
search: String,
|
||||||
): Flow<PagingData<SongListItem>> =
|
): Flow<PagingData<SongListItem>> =
|
||||||
pager(60) {
|
pager(60) {
|
||||||
val dao = db.libraryDao()
|
val dao = db.libraryDao()
|
||||||
when (sort) {
|
when (sort) {
|
||||||
SongSort.Album -> {
|
SongSort.Album -> {
|
||||||
dao.songs(sourceId, starredOnly, search)
|
dao.songs(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
|
|
||||||
SongSort.Title -> {
|
SongSort.Title -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.songsByTitleReversed(sourceId, starredOnly, search)
|
dao.songsByTitleReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.songsByTitle(sourceId, starredOnly, search)
|
dao.songsByTitle(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SongSort.Artist -> {
|
SongSort.Artist -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.songsByArtistReversed(sourceId, starredOnly, search)
|
dao.songsByArtistReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.songsByArtist(sourceId, starredOnly, search)
|
dao.songsByArtist(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
SongSort.Starred -> {
|
SongSort.Starred -> {
|
||||||
if (descending) {
|
if (descending) {
|
||||||
dao.songsByStarredReversed(sourceId, starredOnly, search)
|
dao.songsByStarredReversed(sourceId, starred.ordinal, search)
|
||||||
} else {
|
} else {
|
||||||
dao.songsByStarred(sourceId, starredOnly, search)
|
dao.songsByStarred(sourceId, starred.ordinal, search)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,16 @@ package com.subtracks.ui.components
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxScope
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@@ -182,3 +185,29 @@ fun EmptyState(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilteredEmptyState(
|
||||||
|
onClearFilters: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier.fillMaxSize().padding(32.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "Filters are hiding everything.",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
Button(onClick = onClearFilters) {
|
||||||
|
Text("Clear filters")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.subtracks.data.model.Album
|
|||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
|
import com.subtracks.ui.components.FilteredEmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.components.rememberViewportFill
|
import com.subtracks.ui.components.rememberViewportFill
|
||||||
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
||||||
@@ -39,6 +40,8 @@ fun AlbumsContent(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
filtered: Boolean = false,
|
||||||
|
onClearFilters: () -> Unit = {},
|
||||||
resetKey: Any? = null,
|
resetKey: Any? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -48,7 +51,11 @@ fun AlbumsContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.itemCount == 0 -> {
|
items.itemCount == 0 -> {
|
||||||
EmptyState("No albums yet.\nSync with your server to fill your library.", modifier)
|
if (filtered) {
|
||||||
|
FilteredEmptyState(onClearFilters, modifier)
|
||||||
|
} else {
|
||||||
|
EmptyState("No albums yet.\nSync with your server to fill your library.", modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.subtracks.data.model.Artist
|
|||||||
import com.subtracks.data.model.CoverArtRef
|
import com.subtracks.data.model.CoverArtRef
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
|
import com.subtracks.ui.components.FilteredEmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.components.rememberViewportFill
|
import com.subtracks.ui.components.rememberViewportFill
|
||||||
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
import com.subtracks.ui.theme.PrefetchArtworkSeeds
|
||||||
@@ -37,6 +38,8 @@ fun ArtistsContent(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onArtistClick: (Artist) -> Unit,
|
onArtistClick: (Artist) -> Unit,
|
||||||
|
filtered: Boolean = false,
|
||||||
|
onClearFilters: () -> Unit = {},
|
||||||
resetKey: Any? = null,
|
resetKey: Any? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -46,7 +49,11 @@ fun ArtistsContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.itemCount == 0 -> {
|
items.itemCount == 0 -> {
|
||||||
EmptyState("No artists yet.", modifier)
|
if (filtered) {
|
||||||
|
FilteredEmptyState(onClearFilters, modifier)
|
||||||
|
} else {
|
||||||
|
EmptyState("No artists yet.", modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.WindowInsets
|
|||||||
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.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.imePadding
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -27,18 +28,21 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.rounded.PlaylistPlay
|
import androidx.compose.material.icons.automirrored.rounded.PlaylistPlay
|
||||||
import androidx.compose.material.icons.filled.Sort
|
import androidx.compose.material.icons.filled.Sort
|
||||||
|
import androidx.compose.material.icons.rounded.Add
|
||||||
import androidx.compose.material.icons.rounded.Album
|
import androidx.compose.material.icons.rounded.Album
|
||||||
import androidx.compose.material.icons.rounded.ArrowDownward
|
import androidx.compose.material.icons.rounded.ArrowDownward
|
||||||
import androidx.compose.material.icons.rounded.ArrowUpward
|
import androidx.compose.material.icons.rounded.ArrowUpward
|
||||||
import androidx.compose.material.icons.rounded.Close
|
import androidx.compose.material.icons.rounded.Close
|
||||||
import androidx.compose.material.icons.rounded.MusicNote
|
import androidx.compose.material.icons.rounded.MusicNote
|
||||||
import androidx.compose.material.icons.rounded.Person
|
import androidx.compose.material.icons.rounded.Person
|
||||||
|
import androidx.compose.material.icons.rounded.Remove
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
import androidx.compose.material.icons.rounded.Settings
|
import androidx.compose.material.icons.rounded.Settings
|
||||||
import androidx.compose.material.icons.rounded.Sync
|
import androidx.compose.material.icons.rounded.Sync
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -93,6 +97,7 @@ import com.subtracks.data.prefs.LibraryListTab
|
|||||||
import com.subtracks.data.prefs.ListQuery
|
import com.subtracks.data.prefs.ListQuery
|
||||||
import com.subtracks.data.prefs.PlaylistSort
|
import com.subtracks.data.prefs.PlaylistSort
|
||||||
import com.subtracks.data.prefs.SongSort
|
import com.subtracks.data.prefs.SongSort
|
||||||
|
import com.subtracks.data.prefs.StarredFilter
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
@@ -190,8 +195,19 @@ fun LibraryRoute(
|
|||||||
starredSupported = listTab.supportsStarred,
|
starredSupported = listTab.supportsStarred,
|
||||||
onSortChange = { viewModel.setListQuery(listTab, listQuery.copy(sort = it)) },
|
onSortChange = { viewModel.setListQuery(listTab, listQuery.copy(sort = it)) },
|
||||||
onToggleSortDirection = { viewModel.setListQuery(listTab, listQuery.copy(descending = !listQuery.descending)) },
|
onToggleSortDirection = { viewModel.setListQuery(listTab, listQuery.copy(descending = !listQuery.descending)) },
|
||||||
onToggleStarred = { viewModel.setListQuery(listTab, listQuery.copy(starredOnly = !listQuery.starredOnly)) },
|
onCycleStarred = {
|
||||||
onClearFilters = { viewModel.setListQuery(listTab, listQuery.copy(starredOnly = false)) },
|
val next =
|
||||||
|
when (listQuery.starred) {
|
||||||
|
StarredFilter.Any -> StarredFilter.Starred
|
||||||
|
StarredFilter.Starred -> StarredFilter.NotStarred
|
||||||
|
StarredFilter.NotStarred -> StarredFilter.Any
|
||||||
|
}
|
||||||
|
viewModel.setListQuery(listTab, listQuery.copy(starred = next))
|
||||||
|
},
|
||||||
|
onClearFilters = {
|
||||||
|
viewModel.setListQuery(listTab, listQuery.copy(starred = StarredFilter.Any))
|
||||||
|
viewModel.setSearch(listTab, "")
|
||||||
|
},
|
||||||
search = search,
|
search = search,
|
||||||
onSearchChange = { viewModel.setSearch(listTab, it) },
|
onSearchChange = { viewModel.setSearch(listTab, it) },
|
||||||
)
|
)
|
||||||
@@ -221,7 +237,7 @@ fun LibraryScreen(
|
|||||||
starredSupported: Boolean = false,
|
starredSupported: Boolean = false,
|
||||||
onSortChange: (String) -> Unit = {},
|
onSortChange: (String) -> Unit = {},
|
||||||
onToggleSortDirection: () -> Unit = {},
|
onToggleSortDirection: () -> Unit = {},
|
||||||
onToggleStarred: () -> Unit = {},
|
onCycleStarred: () -> Unit = {},
|
||||||
onClearFilters: () -> Unit = {},
|
onClearFilters: () -> Unit = {},
|
||||||
search: String = "",
|
search: String = "",
|
||||||
onSearchChange: (String) -> Unit = {},
|
onSearchChange: (String) -> Unit = {},
|
||||||
@@ -261,8 +277,9 @@ fun LibraryScreen(
|
|||||||
val titleFraction = scrollBehavior.state.collapsedFraction
|
val titleFraction = scrollBehavior.state.collapsedFraction
|
||||||
|
|
||||||
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
val statusBarTop = with(density) { WindowInsets.statusBars.getTop(this).toDp() }
|
||||||
val listBottomInset = bottomInset + if (searchActive) 0.dp else FAB_CLEARANCE
|
val filtersActive = listQuery.starred != StarredFilter.Any || search.isNotEmpty()
|
||||||
val resetKey = listOf(listQuery.sort, listQuery.descending, listQuery.starredOnly, search)
|
val listBottomInset = bottomInset + if (searchActive) SEARCH_BAR_CLEARANCE else FAB_CLEARANCE
|
||||||
|
val resetKey = listOf(listQuery.sort, listQuery.descending, listQuery.starred, search)
|
||||||
|
|
||||||
Box(modifier.fillMaxSize()) {
|
Box(modifier.fillMaxSize()) {
|
||||||
Column(
|
Column(
|
||||||
@@ -312,11 +329,27 @@ fun LibraryScreen(
|
|||||||
) { page ->
|
) { page ->
|
||||||
when (LibraryTab.entries[page]) {
|
when (LibraryTab.entries[page]) {
|
||||||
LibraryTab.Albums -> {
|
LibraryTab.Albums -> {
|
||||||
AlbumsContent(albums, coverArt, listBottomInset, onAlbumClick, resetKey = resetKey)
|
AlbumsContent(
|
||||||
|
albums,
|
||||||
|
coverArt,
|
||||||
|
listBottomInset,
|
||||||
|
onAlbumClick,
|
||||||
|
filtered = filtersActive,
|
||||||
|
onClearFilters = onClearFilters,
|
||||||
|
resetKey = resetKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryTab.Artists -> {
|
LibraryTab.Artists -> {
|
||||||
ArtistsContent(artists, coverArt, listBottomInset, onArtistClick, resetKey = resetKey)
|
ArtistsContent(
|
||||||
|
artists,
|
||||||
|
coverArt,
|
||||||
|
listBottomInset,
|
||||||
|
onArtistClick,
|
||||||
|
filtered = filtersActive,
|
||||||
|
onClearFilters = onClearFilters,
|
||||||
|
resetKey = resetKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryTab.Songs -> {
|
LibraryTab.Songs -> {
|
||||||
@@ -326,28 +359,43 @@ fun LibraryScreen(
|
|||||||
listBottomInset,
|
listBottomInset,
|
||||||
onSongClick,
|
onSongClick,
|
||||||
playingSongId,
|
playingSongId,
|
||||||
|
filtered = filtersActive,
|
||||||
|
onClearFilters = onClearFilters,
|
||||||
resetKey = resetKey,
|
resetKey = resetKey,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
LibraryTab.Playlists -> {
|
LibraryTab.Playlists -> {
|
||||||
PlaylistsContent(playlists, coverArt, listBottomInset, onPlaylistClick, resetKey = resetKey)
|
PlaylistsContent(
|
||||||
|
playlists,
|
||||||
|
coverArt,
|
||||||
|
listBottomInset,
|
||||||
|
onPlaylistClick,
|
||||||
|
filtered = filtersActive,
|
||||||
|
onClearFilters = onClearFilters,
|
||||||
|
resetKey = resetKey,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (searchActive) {
|
if (searchActive) {
|
||||||
SearchField(
|
SearchField(
|
||||||
value = search,
|
value = search,
|
||||||
onValueChange = onSearchChange,
|
onValueChange = onSearchChange,
|
||||||
onClose = {
|
onClose = {
|
||||||
searchActive = false
|
searchActive = false
|
||||||
onSearchChange("")
|
onSearchChange("")
|
||||||
},
|
},
|
||||||
focusRequester = searchFocus,
|
focusRequester = searchFocus,
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp),
|
modifier =
|
||||||
)
|
Modifier
|
||||||
}
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.imePadding()
|
||||||
|
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!searchActive) {
|
if (!searchActive) {
|
||||||
@@ -359,7 +407,7 @@ fun LibraryScreen(
|
|||||||
) {
|
) {
|
||||||
Box {
|
Box {
|
||||||
Icon(Icons.Filled.Sort, contentDescription = "List options")
|
Icon(Icons.Filled.Sort, contentDescription = "List options")
|
||||||
if (listQuery.starredOnly) {
|
if (listQuery.starred != StarredFilter.Any) {
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
@@ -383,7 +431,7 @@ fun LibraryScreen(
|
|||||||
starredSupported = starredSupported,
|
starredSupported = starredSupported,
|
||||||
onSortChange = onSortChange,
|
onSortChange = onSortChange,
|
||||||
onToggleSortDirection = onToggleSortDirection,
|
onToggleSortDirection = onToggleSortDirection,
|
||||||
onToggleStarred = onToggleStarred,
|
onCycleStarred = onCycleStarred,
|
||||||
onClearFilters = onClearFilters,
|
onClearFilters = onClearFilters,
|
||||||
onSearch = {
|
onSearch = {
|
||||||
showOptions = false
|
showOptions = false
|
||||||
@@ -402,7 +450,7 @@ private fun ListOptionsSheet(
|
|||||||
starredSupported: Boolean,
|
starredSupported: Boolean,
|
||||||
onSortChange: (String) -> Unit,
|
onSortChange: (String) -> Unit,
|
||||||
onToggleSortDirection: () -> Unit,
|
onToggleSortDirection: () -> Unit,
|
||||||
onToggleStarred: () -> Unit,
|
onCycleStarred: () -> Unit,
|
||||||
onClearFilters: () -> Unit,
|
onClearFilters: () -> Unit,
|
||||||
onSearch: () -> Unit,
|
onSearch: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -456,12 +504,38 @@ private fun ListOptionsSheet(
|
|||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||||
) {
|
) {
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = listQuery.starredOnly,
|
selected = listQuery.starred != StarredFilter.Any,
|
||||||
onClick = onToggleStarred,
|
onClick = onCycleStarred,
|
||||||
label = { Text("Starred") },
|
label = { Text("Starred") },
|
||||||
|
leadingIcon =
|
||||||
|
when (listQuery.starred) {
|
||||||
|
StarredFilter.Any -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredFilter.Starred -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StarredFilter.NotStarred -> {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Remove,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if (listQuery.starredOnly) {
|
if (listQuery.starred != StarredFilter.Any) {
|
||||||
TextButton(onClick = onClearFilters) { Text("Clear") }
|
TextButton(onClick = onClearFilters) { Text("Clear filters") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -469,6 +543,7 @@ private fun ListOptionsSheet(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val FAB_CLEARANCE = 80.dp
|
private val FAB_CLEARANCE = 80.dp
|
||||||
|
private val SEARCH_BAR_CLEARANCE = 80.dp
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SearchField(
|
private fun SearchField(
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ class LibraryViewModel(
|
|||||||
combine(listQueries.getValue(LibraryListTab.Albums), searches.getValue(LibraryListTab.Albums)) { query, search ->
|
combine(listQueries.getValue(LibraryListTab.Albums), searches.getValue(LibraryListTab.Albums)) { query, search ->
|
||||||
query to search
|
query to search
|
||||||
}.flatMapLatest { (query, search) ->
|
}.flatMapLatest { (query, search) ->
|
||||||
libraryRepository.albums(sourceId, query.albumSort(), query.descending, query.starredOnly, search)
|
libraryRepository.albums(sourceId, query.albumSort(), query.descending, query.starred, search)
|
||||||
}
|
}
|
||||||
}.cachedIn(viewModelScope)
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ class LibraryViewModel(
|
|||||||
combine(listQueries.getValue(LibraryListTab.Artists), searches.getValue(LibraryListTab.Artists)) { query, search ->
|
combine(listQueries.getValue(LibraryListTab.Artists), searches.getValue(LibraryListTab.Artists)) { query, search ->
|
||||||
query to search
|
query to search
|
||||||
}.flatMapLatest { (query, search) ->
|
}.flatMapLatest { (query, search) ->
|
||||||
libraryRepository.artists(sourceId, query.artistSort(), query.descending, query.starredOnly, search)
|
libraryRepository.artists(sourceId, query.artistSort(), query.descending, query.starred, search)
|
||||||
}
|
}
|
||||||
}.cachedIn(viewModelScope)
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ class LibraryViewModel(
|
|||||||
combine(listQueries.getValue(LibraryListTab.Songs), searches.getValue(LibraryListTab.Songs)) { query, search ->
|
combine(listQueries.getValue(LibraryListTab.Songs), searches.getValue(LibraryListTab.Songs)) { query, search ->
|
||||||
query to search
|
query to search
|
||||||
}.flatMapLatest { (query, search) ->
|
}.flatMapLatest { (query, search) ->
|
||||||
libraryRepository.songs(sourceId, query.songSort(), query.descending, query.starredOnly, search)
|
libraryRepository.songs(sourceId, query.songSort(), query.descending, query.starred, search)
|
||||||
}
|
}
|
||||||
}.cachedIn(viewModelScope)
|
}.cachedIn(viewModelScope)
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import com.subtracks.data.model.CoverArtRef
|
|||||||
import com.subtracks.data.model.Playlist
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
|
import com.subtracks.ui.components.FilteredEmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.components.rememberViewportFill
|
import com.subtracks.ui.components.rememberViewportFill
|
||||||
|
|
||||||
@@ -36,6 +37,8 @@ fun PlaylistsContent(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
|
filtered: Boolean = false,
|
||||||
|
onClearFilters: () -> Unit = {},
|
||||||
resetKey: Any? = null,
|
resetKey: Any? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -45,7 +48,11 @@ fun PlaylistsContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.itemCount == 0 -> {
|
items.itemCount == 0 -> {
|
||||||
EmptyState("No playlists yet.", modifier)
|
if (filtered) {
|
||||||
|
FilteredEmptyState(onClearFilters, modifier)
|
||||||
|
} else {
|
||||||
|
EmptyState("No playlists yet.", modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import com.subtracks.data.model.Song
|
|||||||
import com.subtracks.data.model.SongListItem
|
import com.subtracks.data.model.SongListItem
|
||||||
import com.subtracks.ui.components.CoverArt
|
import com.subtracks.ui.components.CoverArt
|
||||||
import com.subtracks.ui.components.EmptyState
|
import com.subtracks.ui.components.EmptyState
|
||||||
|
import com.subtracks.ui.components.FilteredEmptyState
|
||||||
import com.subtracks.ui.components.LoadingState
|
import com.subtracks.ui.components.LoadingState
|
||||||
import com.subtracks.ui.components.rememberViewportFill
|
import com.subtracks.ui.components.rememberViewportFill
|
||||||
|
|
||||||
@@ -48,6 +49,8 @@ fun SongsContent(
|
|||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onSongClick: (Int) -> Unit,
|
onSongClick: (Int) -> Unit,
|
||||||
playingSongId: String? = null,
|
playingSongId: String? = null,
|
||||||
|
filtered: Boolean = false,
|
||||||
|
onClearFilters: () -> Unit = {},
|
||||||
resetKey: Any? = null,
|
resetKey: Any? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
@@ -57,7 +60,11 @@ fun SongsContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
items.itemCount == 0 -> {
|
items.itemCount == 0 -> {
|
||||||
EmptyState("No songs yet.", modifier)
|
if (filtered) {
|
||||||
|
FilteredEmptyState(onClearFilters, modifier)
|
||||||
|
} else {
|
||||||
|
EmptyState("No songs yet.", modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -50,17 +50,17 @@ class LibraryDaoTest {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Alpha", "Beta", "Gamma"),
|
listOf("Alpha", "Beta", "Gamma"),
|
||||||
dao.albumsByName(sourceId, starredOnly = false, search = "").page().map { it.name },
|
dao.albumsByName(sourceId, starredFilter = 0, search = "").page().map { it.name },
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Gamma", "Beta", "Alpha"),
|
listOf("Gamma", "Beta", "Alpha"),
|
||||||
dao.albumsByNameReversed(sourceId, starredOnly = false, search = "").page().map { it.name },
|
dao.albumsByNameReversed(sourceId, starredFilter = 0, search = "").page().map { it.name },
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Alpha", "Beta", "Gamma"),
|
listOf("Alpha", "Beta", "Gamma"),
|
||||||
dao.albumsByYear(sourceId, starredOnly = false, search = "").page().map { it.name },
|
dao.albumsByYear(sourceId, starredFilter = 0, search = "").page().map { it.name },
|
||||||
)
|
)
|
||||||
assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, starredOnly = true, search = "").page().map { it.name })
|
assertEquals(listOf("Alpha"), dao.albumsByName(sourceId, starredFilter = 1, search = "").page().map { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -77,13 +77,13 @@ class LibraryDaoTest {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Apple", "Zebra"),
|
listOf("Apple", "Zebra"),
|
||||||
dao.songsByTitle(sourceId, starredOnly = false, search = "").page().map { it.song.title },
|
dao.songsByTitle(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Zebra", "Apple"),
|
listOf("Zebra", "Apple"),
|
||||||
dao.songsByTitleReversed(sourceId, starredOnly = false, search = "").page().map { it.song.title },
|
dao.songsByTitleReversed(sourceId, starredFilter = 0, search = "").page().map { it.song.title },
|
||||||
)
|
)
|
||||||
assertEquals(listOf("Apple"), dao.songsByTitle(sourceId, starredOnly = true, search = "").page().map { it.song.title })
|
assertEquals(listOf("Apple"), dao.songsByTitle(sourceId, starredFilter = 1, search = "").page().map { it.song.title })
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -101,13 +101,14 @@ class LibraryDaoTest {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Beta", "Gamma", "Alpha"),
|
listOf("Beta", "Gamma", "Alpha"),
|
||||||
dao.albumsByRecentlyAdded(sourceId, starredOnly = false, search = "").page().map { it.name },
|
dao.albumsByRecentlyAdded(sourceId, starredFilter = 0, search = "").page().map { it.name },
|
||||||
)
|
)
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Alpha", "Gamma", "Beta"),
|
listOf("Alpha", "Gamma", "Beta"),
|
||||||
dao.albumsByStarred(sourceId, starredOnly = false, search = "").page().map { it.name },
|
dao.albumsByStarred(sourceId, starredFilter = 0, search = "").page().map { it.name },
|
||||||
)
|
)
|
||||||
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, starredOnly = false, search = "gam").page().map { it.name })
|
assertEquals(listOf("Gamma"), dao.albumsByName(sourceId, starredFilter = 0, search = "gam").page().map { it.name })
|
||||||
|
assertEquals(listOf("Beta"), dao.albumsByName(sourceId, starredFilter = 2, search = "").page().map { it.name })
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun source(): Long =
|
private suspend fun source(): Long =
|
||||||
|
|||||||
@@ -23,9 +23,12 @@ class UserPreferencesTest {
|
|||||||
assertEquals(ListQuery("Name"), prefs.listQuery(LibraryListTab.Albums).first())
|
assertEquals(ListQuery("Name"), prefs.listQuery(LibraryListTab.Albums).first())
|
||||||
assertEquals(ListQuery("Album"), prefs.listQuery(LibraryListTab.Songs).first())
|
assertEquals(ListQuery("Album"), prefs.listQuery(LibraryListTab.Songs).first())
|
||||||
|
|
||||||
prefs.setListQuery(LibraryListTab.Albums, ListQuery("Added", descending = true, starredOnly = true))
|
prefs.setListQuery(LibraryListTab.Albums, ListQuery("Added", descending = true, starred = StarredFilter.Starred))
|
||||||
|
|
||||||
assertEquals(ListQuery("Added", descending = true, starredOnly = true), prefs.listQuery(LibraryListTab.Albums).first())
|
assertEquals(
|
||||||
|
ListQuery("Added", descending = true, starred = StarredFilter.Starred),
|
||||||
|
prefs.listQuery(LibraryListTab.Albums).first(),
|
||||||
|
)
|
||||||
assertEquals(ListQuery("Album"), prefs.listQuery(LibraryListTab.Songs).first())
|
assertEquals(ListQuery("Album"), prefs.listQuery(LibraryListTab.Songs).first())
|
||||||
|
|
||||||
file.delete()
|
file.delete()
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class SyncServiceTest {
|
|||||||
2,
|
2,
|
||||||
db
|
db
|
||||||
.libraryDao()
|
.libraryDao()
|
||||||
.artistsByName(1, false, "")
|
.artistsByName(1, 0, "")
|
||||||
.allRows()
|
.allRows()
|
||||||
.size,
|
.size,
|
||||||
)
|
)
|
||||||
@@ -68,7 +68,7 @@ class SyncServiceTest {
|
|||||||
2,
|
2,
|
||||||
db
|
db
|
||||||
.libraryDao()
|
.libraryDao()
|
||||||
.songs(1, false, "")
|
.songs(1, 0, "")
|
||||||
.allRows()
|
.allRows()
|
||||||
.size,
|
.size,
|
||||||
)
|
)
|
||||||
@@ -90,7 +90,7 @@ class SyncServiceTest {
|
|||||||
listOf("a1"),
|
listOf("a1"),
|
||||||
db
|
db
|
||||||
.libraryDao()
|
.libraryDao()
|
||||||
.artistsByName(1, false, "")
|
.artistsByName(1, 0, "")
|
||||||
.allRows()
|
.allRows()
|
||||||
.map { it.id },
|
.map { it.id },
|
||||||
)
|
)
|
||||||
@@ -98,7 +98,7 @@ class SyncServiceTest {
|
|||||||
listOf("s1"),
|
listOf("s1"),
|
||||||
db
|
db
|
||||||
.libraryDao()
|
.libraryDao()
|
||||||
.songs(1, false, "")
|
.songs(1, 0, "")
|
||||||
.allRows()
|
.allRows()
|
||||||
.map { it.song.id },
|
.map { it.song.id },
|
||||||
)
|
)
|
||||||
@@ -200,7 +200,7 @@ class SyncServiceTest {
|
|||||||
"Old",
|
"Old",
|
||||||
db
|
db
|
||||||
.libraryDao()
|
.libraryDao()
|
||||||
.artistsByName(1, false, "")
|
.artistsByName(1, 0, "")
|
||||||
.allRows()
|
.allRows()
|
||||||
.single()
|
.single()
|
||||||
.name,
|
.name,
|
||||||
@@ -213,7 +213,7 @@ class SyncServiceTest {
|
|||||||
"New",
|
"New",
|
||||||
db
|
db
|
||||||
.libraryDao()
|
.libraryDao()
|
||||||
.artistsByName(1, false, "")
|
.artistsByName(1, 0, "")
|
||||||
.allRows()
|
.allRows()
|
||||||
.single()
|
.single()
|
||||||
.name,
|
.name,
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package com.subtracks.ui
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
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.onNodeWithText
|
||||||
|
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.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.prefs.StarredFilter
|
||||||
|
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.sortOptionsFor
|
||||||
|
import com.subtracks.ui.theme.SubtracksTheme
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
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 LibraryOptionsTest {
|
||||||
|
@get:Rule
|
||||||
|
val composeRule = createAndroidComposeRule<ComponentActivity>()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun theOptionsSheetAppliesSortFilterAndClear() {
|
||||||
|
var query by mutableStateOf(ListQuery("Name"))
|
||||||
|
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 = query,
|
||||||
|
sortOptions = sortOptionsFor(LibraryTab.Albums),
|
||||||
|
starredSupported = true,
|
||||||
|
onSortChange = { query = query.copy(sort = it) },
|
||||||
|
onCycleStarred = {
|
||||||
|
query =
|
||||||
|
query.copy(
|
||||||
|
starred =
|
||||||
|
when (query.starred) {
|
||||||
|
StarredFilter.Any -> StarredFilter.Starred
|
||||||
|
StarredFilter.Starred -> StarredFilter.NotStarred
|
||||||
|
StarredFilter.NotStarred -> StarredFilter.Any
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClearFilters = { query = query.copy(starred = StarredFilter.Any) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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.onNodeWithText("Added").performClick()
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
assertEquals("Added", query.sort)
|
||||||
|
|
||||||
|
composeRule.onAllNodesWithText("Starred")[1].performClick()
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
assertEquals(StarredFilter.Starred, query.starred)
|
||||||
|
|
||||||
|
composeRule.onNodeWithText("Clear filters").performClick()
|
||||||
|
composeRule.waitForIdle()
|
||||||
|
assertEquals(StarredFilter.Any, query.starred)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun albums() =
|
||||||
|
(1..3).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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user