mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
home polish: genres by most played in a wrapping flow, tighter album corners, spaced left-aligned artists
This commit is contained in:
@@ -306,17 +306,17 @@ interface LibraryDao {
|
||||
|
||||
@Query(
|
||||
"SELECT genre FROM (" +
|
||||
"SELECT song_genres.genre AS genre, songs.played AS played FROM song_genres " +
|
||||
"SELECT song_genres.genre AS genre, songs.playCount AS plays FROM song_genres " +
|
||||
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
||||
"WHERE song_genres.sourceId = :sourceId " +
|
||||
"UNION ALL " +
|
||||
"SELECT songs.genre AS genre, songs.played AS played FROM songs " +
|
||||
"SELECT songs.genre AS genre, songs.playCount AS plays FROM songs " +
|
||||
"WHERE songs.sourceId = :sourceId AND songs.genre IS NOT NULL AND songs.genre != '' " +
|
||||
"AND NOT EXISTS (SELECT 1 FROM song_genres " +
|
||||
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id)" +
|
||||
") GROUP BY genre ORDER BY MAX(played) DESC NULLS LAST, genre COLLATE NOCASE",
|
||||
") GROUP BY genre ORDER BY SUM(plays) DESC, genre COLLATE NOCASE",
|
||||
)
|
||||
fun genresByRecentPlay(sourceId: Long): Flow<List<String>>
|
||||
fun genresByMostPlayed(sourceId: Long): Flow<List<String>>
|
||||
|
||||
@Query(
|
||||
"SELECT DISTINCT (year / 10) * 10 AS decade FROM albums " +
|
||||
|
||||
@@ -79,7 +79,7 @@ class LibraryRepository(
|
||||
limit: Int,
|
||||
): Flow<List<AlbumSongItem>> = db.libraryDao().recentlyStarredSongs(sourceId, limit)
|
||||
|
||||
fun genresByRecentPlay(sourceId: Long): Flow<List<String>> = db.libraryDao().genresByRecentPlay(sourceId)
|
||||
fun genresByMostPlayed(sourceId: Long): Flow<List<String>> = db.libraryDao().genresByMostPlayed(sourceId)
|
||||
|
||||
fun decades(sourceId: Long): Flow<List<Long>> = db.libraryDao().decades(sourceId)
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ class HomeListViewModel(
|
||||
|
||||
val genres: StateFlow<List<String>> =
|
||||
sourceId
|
||||
.flatMapLatest { libraryRepository.genresByRecentPlay(it) }
|
||||
.flatMapLatest { libraryRepository.genresByMostPlayed(it) }
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
|
||||
|
||||
val decades: StateFlow<List<Long>> =
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.subtracks.ui.home
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -246,6 +248,7 @@ private fun HomeSectionHeader(
|
||||
private val TILE_SIZE = 132.dp
|
||||
private val ARTIST_TILE_SIZE = 104.dp
|
||||
private val TILE_SPACING = 8.dp
|
||||
private val ARTIST_SPACING = 16.dp
|
||||
|
||||
private fun LazyListScope.albumRow(
|
||||
section: HomeSection,
|
||||
@@ -292,7 +295,7 @@ private fun LazyListScope.artistRow(
|
||||
item(key = "${section.name}-row") {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(TILE_SPACING),
|
||||
horizontalArrangement = Arrangement.spacedBy(ARTIST_SPACING),
|
||||
) {
|
||||
items(artists, key = { it.id }) { artist ->
|
||||
val ref = coverArt(artist.coverArt, true)
|
||||
@@ -307,6 +310,7 @@ private fun LazyListScope.artistRow(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
private fun LazyListScope.genreBlock(
|
||||
section: HomeSection,
|
||||
genres: List<String>,
|
||||
@@ -318,16 +322,13 @@ private fun LazyListScope.genreBlock(
|
||||
HomeSectionHeader(section.title) { onMore(section) }
|
||||
}
|
||||
item(key = "${section.name}-row") {
|
||||
LazyRow(
|
||||
contentPadding = PaddingValues(horizontal = 16.dp),
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(TILE_SPACING),
|
||||
verticalArrangement = Arrangement.spacedBy(TILE_SPACING),
|
||||
) {
|
||||
items(genres.chunked(GENRE_COLUMN_HEIGHT), key = { it.first() }) { column ->
|
||||
Column(verticalArrangement = Arrangement.spacedBy(TILE_SPACING)) {
|
||||
column.forEach { genre ->
|
||||
AssistChip(onClick = { onGenreClick(genre) }, label = { Text(genre) })
|
||||
}
|
||||
}
|
||||
genres.forEach { genre ->
|
||||
AssistChip(onClick = { onGenreClick(genre) }, label = { Text(genre) })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -384,8 +385,6 @@ private fun LazyListScope.starredSongs(
|
||||
}
|
||||
}
|
||||
|
||||
private const val GENRE_COLUMN_HEIGHT = 3
|
||||
|
||||
@Composable
|
||||
private fun AlbumTile(
|
||||
album: Album,
|
||||
@@ -399,7 +398,7 @@ private fun AlbumTile(
|
||||
CoverArt(
|
||||
ref = ref,
|
||||
name = album.name,
|
||||
modifier = Modifier.size(TILE_SIZE).clip(RoundedCornerShape(4.dp)),
|
||||
modifier = Modifier.size(TILE_SIZE).clip(RoundedCornerShape(2.dp)),
|
||||
)
|
||||
Spacer(Modifier.height(6.dp))
|
||||
Text(
|
||||
@@ -427,7 +426,7 @@ private fun ArtistTile(
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.width(ARTIST_TILE_SIZE).combinedClickable(onClick = onClick, onLongClick = onLongClick),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
horizontalAlignment = Alignment.Start,
|
||||
) {
|
||||
CoverArt(
|
||||
ref = ref,
|
||||
|
||||
@@ -67,7 +67,7 @@ class HomeViewModel(
|
||||
}
|
||||
val discovery =
|
||||
combine(
|
||||
libraryRepository.genresByRecentPlay(sourceId),
|
||||
libraryRepository.genresByMostPlayed(sourceId),
|
||||
libraryRepository.decades(sourceId),
|
||||
libraryRepository.recentlyStarredSongs(sourceId, HOME_STARRED_LIMIT),
|
||||
libraryRepository.recentlyAddedAlbums(sourceId, HOME_ROW_LIMIT),
|
||||
|
||||
@@ -124,22 +124,22 @@ class LibraryHomeDaoTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun genresOrderByMostRecentPlayWithinTheGenre() =
|
||||
fun genresOrderByMostPlayed() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", genre = "Rock", played = 100),
|
||||
song(sourceId, "s2", "Two", genre = "Jazz", played = 400),
|
||||
song(sourceId, "s3", "Three", genre = "Rock", played = 300),
|
||||
song(sourceId, "s4", "Four", genre = "Classical", played = null),
|
||||
song(sourceId, "s1", "One", genre = "Rock", playCount = 5),
|
||||
song(sourceId, "s2", "Two", genre = "Jazz", playCount = 2),
|
||||
song(sourceId, "s3", "Three", genre = "Rock", playCount = 4),
|
||||
song(sourceId, "s4", "Four", genre = "Classical", playCount = 0),
|
||||
),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("Jazz", "Rock", "Classical"),
|
||||
dao.genresByRecentPlay(sourceId).first(),
|
||||
listOf("Rock", "Jazz", "Classical"),
|
||||
dao.genresByMostPlayed(sourceId).first(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -150,8 +150,8 @@ class LibraryHomeDaoTest {
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", genre = "Legacy", played = 100),
|
||||
song(sourceId, "s2", "Two", genre = "Legacy", played = 300),
|
||||
song(sourceId, "s1", "One", genre = "Legacy", playCount = 1),
|
||||
song(sourceId, "s2", "Two", genre = "Legacy", playCount = 3),
|
||||
),
|
||||
)
|
||||
insertGenre(sourceId, "s2", 0, "Rock")
|
||||
@@ -159,7 +159,7 @@ class LibraryHomeDaoTest {
|
||||
|
||||
assertEquals(
|
||||
listOf("Electronic", "Rock", "Legacy"),
|
||||
dao.genresByRecentPlay(sourceId).first(),
|
||||
dao.genresByMostPlayed(sourceId).first(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.test.junit4.v2.createAndroidComposeRule
|
||||
import androidx.compose.ui.test.onAllNodesWithText
|
||||
import androidx.compose.ui.test.onRoot
|
||||
import androidx.compose.ui.test.performTouchInput
|
||||
import androidx.compose.ui.test.swipeUp
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.github.takahirom.roborazzi.RobolectricDeviceQualifiers
|
||||
import com.github.takahirom.roborazzi.captureRoboImage
|
||||
@@ -48,6 +50,31 @@ class HomeScreenshotTest {
|
||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/home.png")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun homeGenres() {
|
||||
composeRule.setContent {
|
||||
SubtracksTheme {
|
||||
Surface(modifier = Modifier.fillMaxSize()) {
|
||||
HomeScreen(
|
||||
feed = feed(),
|
||||
coverArt = { _, _ -> null },
|
||||
playingSongId = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
composeRule.waitUntil(timeoutMillis = 5_000) {
|
||||
composeRule.onAllNodesWithText("Recently added").fetchSemanticsNodes().isNotEmpty()
|
||||
}
|
||||
repeat(2) {
|
||||
composeRule.onRoot().performTouchInput {
|
||||
swipeUp(startY = centerY + 600f, endY = centerY - 600f, durationMillis = 300)
|
||||
}
|
||||
composeRule.waitForIdle()
|
||||
}
|
||||
composeRule.onRoot().captureRoboImage("src/test/screenshots/home_genres.png")
|
||||
}
|
||||
|
||||
private fun feed() =
|
||||
HomeFeed(
|
||||
recentlyPlayedAlbums = albums("Recent"),
|
||||
|
||||
Reference in New Issue
Block a user