mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
home: add an On repeat song row and move recently starred below genres
This commit is contained in:
@@ -304,6 +304,18 @@ interface LibraryDao {
|
||||
limit: Int,
|
||||
): Flow<List<AlbumSongItem>>
|
||||
|
||||
@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 songs.played IS NOT NULL AND songs.played >= :cutoff " +
|
||||
"ORDER BY songs.playCount DESC, songs.title COLLATE NOCASE, songs.id LIMIT :limit",
|
||||
)
|
||||
fun onRepeatSongs(
|
||||
sourceId: Long,
|
||||
cutoff: Long,
|
||||
limit: Int,
|
||||
): Flow<List<AlbumSongItem>>
|
||||
|
||||
@Query(
|
||||
"SELECT genre FROM (" +
|
||||
"SELECT song_genres.genre AS genre, songs.playCount AS plays FROM song_genres " +
|
||||
@@ -710,6 +722,26 @@ interface LibraryDao {
|
||||
)
|
||||
suspend fun starredSongIds(sourceId: Long): List<String>
|
||||
|
||||
@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 songs.played IS NOT NULL AND songs.played >= :cutoff " +
|
||||
"ORDER BY songs.playCount DESC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
fun homeOnRepeatSongs(
|
||||
sourceId: Long,
|
||||
cutoff: Long,
|
||||
): PagingSource<Int, AlbumSongItem>
|
||||
|
||||
@Query(
|
||||
"SELECT songs.id FROM songs WHERE sourceId = :sourceId AND played IS NOT NULL AND played >= :cutoff " +
|
||||
"ORDER BY playCount DESC, title COLLATE NOCASE, id",
|
||||
)
|
||||
suspend fun onRepeatSongIds(
|
||||
sourceId: Long,
|
||||
cutoff: Long,
|
||||
): List<String>
|
||||
|
||||
@Query(
|
||||
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
|
||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
|
||||
@@ -90,6 +90,16 @@ class LibraryRepository(
|
||||
|
||||
suspend fun starredSongIds(sourceId: Long): List<String> = db.libraryDao().starredSongIds(sourceId)
|
||||
|
||||
fun onRepeatSongs(
|
||||
sourceId: Long,
|
||||
limit: Int,
|
||||
): Flow<List<AlbumSongItem>> = db.libraryDao().onRepeatSongs(sourceId, weekAgoCutoff(), limit)
|
||||
|
||||
fun onRepeatSongsPage(sourceId: Long): Flow<PagingData<AlbumSongItem>> =
|
||||
pager(60) { db.libraryDao().homeOnRepeatSongs(sourceId, weekAgoCutoff()) }
|
||||
|
||||
suspend fun onRepeatSongIds(sourceId: Long): List<String> = db.libraryDao().onRepeatSongIds(sourceId, weekAgoCutoff())
|
||||
|
||||
fun songsByGenre(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
@@ -438,3 +448,5 @@ class LibraryRepository(
|
||||
const val REDISCOVER_WINDOW_MS = 60L * 24 * 60 * 60 * 1000
|
||||
|
||||
fun rediscoverCutoff(): Long = System.currentTimeMillis() - REDISCOVER_WINDOW_MS
|
||||
|
||||
fun weekAgoCutoff(): Long = System.currentTimeMillis() / 1000 - 7L * 24 * 60 * 60
|
||||
|
||||
@@ -169,7 +169,7 @@ fun HomeListScreen(
|
||||
)
|
||||
}
|
||||
|
||||
HomeSection.RecentlyStarredSongs -> {
|
||||
HomeSection.RecentlyStarredSongs, HomeSection.OnRepeatSongs -> {
|
||||
SongsList(
|
||||
songs = songs,
|
||||
coverArt = coverArt,
|
||||
|
||||
@@ -91,6 +91,7 @@ class HomeListViewModel(
|
||||
when {
|
||||
request.genre != null -> libraryRepository.songsByGenre(id, request.genre)
|
||||
request.section == HomeSection.RecentlyStarredSongs -> libraryRepository.starredSongs(id)
|
||||
request.section == HomeSection.OnRepeatSongs -> libraryRepository.onRepeatSongsPage(id)
|
||||
else -> flowOf(PagingData.empty<AlbumSongItem>())
|
||||
}
|
||||
}.cachedIn(viewModelScope)
|
||||
@@ -111,10 +112,10 @@ class HomeListViewModel(
|
||||
): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail)
|
||||
|
||||
fun play(song: Song) {
|
||||
if (request.section == HomeSection.RecentlyStarredSongs) {
|
||||
viewModelScope.playStarredList(libraryRepository, playbackController, song)
|
||||
} else {
|
||||
viewModelScope.playSongInContext(libraryRepository, playbackController, song)
|
||||
when (request.section) {
|
||||
HomeSection.RecentlyStarredSongs -> viewModelScope.playStarredList(libraryRepository, playbackController, song)
|
||||
HomeSection.OnRepeatSongs -> viewModelScope.playOnRepeatList(libraryRepository, playbackController, song)
|
||||
else -> viewModelScope.playSongInContext(libraryRepository, playbackController, song)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,6 +65,7 @@ enum class HomeSection(
|
||||
Genres("Genres"),
|
||||
Decades("Decades"),
|
||||
RecentlyStarredSongs("Recently starred"),
|
||||
OnRepeatSongs("On repeat"),
|
||||
RecentlyAddedAlbums("Recently added"),
|
||||
Rediscover("Rediscover"),
|
||||
;
|
||||
@@ -109,6 +110,7 @@ fun HomeRoute(
|
||||
onAlbumClick = onAlbumClick,
|
||||
onArtistClick = onArtistClick,
|
||||
onPlayStarred = viewModel::playStarred,
|
||||
onPlayOnRepeat = viewModel::playOnRepeat,
|
||||
onLongClick = { contextMenuHost?.show(it, actions) },
|
||||
onMore = onMore,
|
||||
onGenreClick = onGenreClick,
|
||||
@@ -127,6 +129,7 @@ fun HomeScreen(
|
||||
onAlbumClick: (Album) -> Unit = {},
|
||||
onArtistClick: (Artist) -> Unit = {},
|
||||
onPlayStarred: (Song) -> Unit = {},
|
||||
onPlayOnRepeat: (Song) -> Unit = {},
|
||||
onLongClick: (MenuTarget) -> Unit = {},
|
||||
onMore: (HomeSection) -> Unit = {},
|
||||
onGenreClick: (String) -> Unit = {},
|
||||
@@ -186,12 +189,12 @@ fun HomeScreen(
|
||||
onLongClick,
|
||||
onMore,
|
||||
)
|
||||
starredSongs(
|
||||
HomeSection.RecentlyStarredSongs,
|
||||
feed.recentlyStarredSongs,
|
||||
homeSongs(
|
||||
HomeSection.OnRepeatSongs,
|
||||
feed.onRepeatSongs,
|
||||
coverArt,
|
||||
playingSongId,
|
||||
onPlayStarred,
|
||||
onPlayOnRepeat,
|
||||
onLongClick,
|
||||
onMore,
|
||||
)
|
||||
@@ -204,6 +207,15 @@ fun HomeScreen(
|
||||
onMore,
|
||||
)
|
||||
genreBlock(HomeSection.Genres, feed.genres, onGenreClick, onMore)
|
||||
homeSongs(
|
||||
HomeSection.RecentlyStarredSongs,
|
||||
feed.recentlyStarredSongs,
|
||||
coverArt,
|
||||
playingSongId,
|
||||
onPlayStarred,
|
||||
onLongClick,
|
||||
onMore,
|
||||
)
|
||||
artistRow(
|
||||
HomeSection.MostPlayedArtists,
|
||||
feed.mostPlayedArtists,
|
||||
@@ -250,7 +262,7 @@ private fun HomeSectionHeader(
|
||||
onMore: (() -> Unit)?,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp, top = 28.dp, bottom = 0.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp, top = 24.dp, bottom = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
@@ -391,12 +403,12 @@ private fun LazyListScope.decadeRow(
|
||||
}
|
||||
}
|
||||
|
||||
private fun LazyListScope.starredSongs(
|
||||
private fun LazyListScope.homeSongs(
|
||||
section: HomeSection,
|
||||
songs: List<AlbumSongItem>,
|
||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||
playingSongId: String?,
|
||||
onPlayStarred: (Song) -> Unit,
|
||||
onSongPlay: (Song) -> Unit,
|
||||
onLongClick: (MenuTarget) -> Unit,
|
||||
onMore: (HomeSection) -> Unit,
|
||||
) {
|
||||
@@ -413,7 +425,7 @@ private fun LazyListScope.starredSongs(
|
||||
durationSeconds = item.song.duration,
|
||||
modifier =
|
||||
Modifier.combinedClickable(
|
||||
onClick = { onPlayStarred(item.song) },
|
||||
onClick = { onSongPlay(item.song) },
|
||||
onLongClick = { onLongClick(MenuTarget.Song(item.song, coverArt(item.coverArt, true))) },
|
||||
),
|
||||
)
|
||||
|
||||
@@ -35,6 +35,7 @@ data class HomeFeed(
|
||||
val genres: List<String> = emptyList(),
|
||||
val decades: List<Long> = emptyList(),
|
||||
val recentlyStarredSongs: List<AlbumSongItem> = emptyList(),
|
||||
val onRepeatSongs: List<AlbumSongItem> = emptyList(),
|
||||
val recentlyAddedAlbums: List<Album> = emptyList(),
|
||||
val rediscoverAlbums: List<Album> = emptyList(),
|
||||
val loading: Boolean = false,
|
||||
@@ -62,8 +63,9 @@ class HomeViewModel(
|
||||
libraryRepository.recentlyPlayedArtists(sourceId, HOME_ROW_LIMIT),
|
||||
libraryRepository.mostPlayedAlbums(sourceId, HOME_ROW_LIMIT),
|
||||
libraryRepository.mostPlayedArtists(sourceId, HOME_ROW_LIMIT),
|
||||
) { albums, artists, frequentAlbums, frequentArtists ->
|
||||
PlayRows(albums, artists, frequentAlbums, frequentArtists)
|
||||
libraryRepository.onRepeatSongs(sourceId, HOME_STARRED_LIMIT),
|
||||
) { albums, artists, frequentAlbums, frequentArtists, onRepeat ->
|
||||
PlayRows(albums, artists, frequentAlbums, frequentArtists, onRepeat)
|
||||
}
|
||||
val discovery =
|
||||
combine(
|
||||
@@ -86,6 +88,7 @@ class HomeViewModel(
|
||||
recentArtists = rows.recentArtists,
|
||||
frequentAlbums = rows.frequentAlbums,
|
||||
frequentArtists = rows.frequentArtists,
|
||||
onRepeatSongs = rows.onRepeatSongs,
|
||||
genres = found.genres,
|
||||
decades = found.decades,
|
||||
starredSongs = found.starredSongs,
|
||||
@@ -105,6 +108,10 @@ class HomeViewModel(
|
||||
viewModelScope.playStarredList(libraryRepository, playbackController, song)
|
||||
}
|
||||
|
||||
fun playOnRepeat(song: Song) {
|
||||
viewModelScope.playOnRepeatList(libraryRepository, playbackController, song)
|
||||
}
|
||||
|
||||
fun sync() = syncManager.requestSync()
|
||||
|
||||
private data class PlayRows(
|
||||
@@ -112,6 +119,7 @@ class HomeViewModel(
|
||||
val recentArtists: List<Artist>,
|
||||
val frequentAlbums: List<Album>,
|
||||
val frequentArtists: List<Artist>,
|
||||
val onRepeatSongs: List<AlbumSongItem>,
|
||||
)
|
||||
|
||||
private data class Discovery(
|
||||
@@ -134,6 +142,7 @@ internal fun buildHomeFeed(
|
||||
addedAlbums: List<Album>,
|
||||
rediscover: List<Album>,
|
||||
hasPlayData: Boolean,
|
||||
onRepeatSongs: List<AlbumSongItem> = emptyList(),
|
||||
): HomeFeed =
|
||||
HomeFeed(
|
||||
recentlyPlayedAlbums = recentAlbums.takeIf { hasPlayData }.orEmpty(),
|
||||
@@ -143,6 +152,7 @@ internal fun buildHomeFeed(
|
||||
genres = genres,
|
||||
decades = decades,
|
||||
recentlyStarredSongs = starredSongs,
|
||||
onRepeatSongs = onRepeatSongs,
|
||||
recentlyAddedAlbums = addedAlbums,
|
||||
rediscoverAlbums = rediscover,
|
||||
)
|
||||
@@ -173,3 +183,14 @@ internal fun CoroutineScope.playStarredList(
|
||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
|
||||
internal fun CoroutineScope.playOnRepeatList(
|
||||
libraryRepository: LibraryRepository,
|
||||
playbackController: PlaybackController,
|
||||
song: Song,
|
||||
) {
|
||||
launch {
|
||||
val ids = libraryRepository.onRepeatSongIds(song.sourceId)
|
||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,6 +335,24 @@ class LibraryHomeDaoTest {
|
||||
assertEquals(displayed, dao.starredSongIds(sourceId))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun onRepeatKeepsSongsPlayedThisWeekAndOrdersByPlayCount() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One", playCount = 3, played = 900),
|
||||
song(sourceId, "s2", "Two", playCount = 9, played = 950),
|
||||
song(sourceId, "s3", "Three", playCount = 100, played = 100),
|
||||
),
|
||||
)
|
||||
|
||||
val onRepeat = dao.onRepeatSongs(sourceId, cutoff = 500, limit = 5).first()
|
||||
assertEquals(listOf("s2", "s1"), onRepeat.map { it.song.id })
|
||||
assertEquals(listOf("s2", "s1"), dao.onRepeatSongIds(sourceId, cutoff = 500))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun songsByGenrePagingFiltersAndJoinsAlbumCoverArt() =
|
||||
runTest {
|
||||
|
||||
@@ -131,6 +131,7 @@ class HomeScreenshotTest {
|
||||
genres = listOf("Rock", "Jazz", "Classical", "Electronic", "Pop", "Folk", "Blues"),
|
||||
decades = listOf(1960L, 1970L, 1980L, 1990L, 2000L, 2010L),
|
||||
recentlyStarredSongs = songs(),
|
||||
onRepeatSongs = songs(),
|
||||
recentlyAddedAlbums = albums("Added"),
|
||||
rediscoverAlbums = albums("Repeat"),
|
||||
)
|
||||
|
||||
@@ -26,6 +26,7 @@ class HomeViewModelTest {
|
||||
addedAlbums = listOf(album("c")),
|
||||
rediscover = listOf(album("d")),
|
||||
hasPlayData = true,
|
||||
onRepeatSongs = listOf(starred("repeat")),
|
||||
)
|
||||
|
||||
assertEquals(listOf("a"), feed.recentlyPlayedAlbums.map { it.id })
|
||||
@@ -35,6 +36,7 @@ class HomeViewModelTest {
|
||||
assertEquals(listOf("Rock"), feed.genres)
|
||||
assertEquals(listOf(1970L), feed.decades)
|
||||
assertEquals(listOf("song"), feed.recentlyStarredSongs.map { it.song.id })
|
||||
assertEquals(listOf("repeat"), feed.onRepeatSongs.map { it.song.id })
|
||||
assertEquals(listOf("c"), feed.recentlyAddedAlbums.map { it.id })
|
||||
assertEquals(listOf("d"), feed.rediscoverAlbums.map { it.id })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user