From f62e8363de22ff98e1a8e5f78ba80ad79e273d6b Mon Sep 17 00:00:00 2001 From: austinried <4966622+austinried@users.noreply.github.com> Date: Sat, 3 Oct 2026 03:41:21 +0000 Subject: [PATCH] home: drop On repeat, restore starred, give artist lists the detail column --- .../com/subtracks/data/db/LibraryDao.kt | 32 ------ .../subtracks/data/repo/LibraryRepository.kt | 12 -- .../com/subtracks/ui/home/HomeListScreen.kt | 104 +++++++++++++++++- .../subtracks/ui/home/HomeListViewModel.kt | 9 +- .../com/subtracks/ui/home/HomeScreen.kt | 26 ++--- .../com/subtracks/ui/home/HomeViewModel.kt | 25 +---- .../subtracks/data/db/LibraryHomeDaoTest.kt | 18 --- .../com/subtracks/ui/home/HomeListInfoTest.kt | 24 ++++ .../subtracks/ui/home/HomeScreenshotTest.kt | 39 ++++++- .../subtracks/ui/home/HomeViewModelTest.kt | 2 - 10 files changed, 174 insertions(+), 117 deletions(-) diff --git a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt index 4944594d..54b48899 100644 --- a/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt +++ b/app/src/main/kotlin/com/subtracks/data/db/LibraryDao.kt @@ -304,18 +304,6 @@ interface LibraryDao { limit: Int, ): Flow> - @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> - @Query( "SELECT genre FROM (" + "SELECT song_genres.genre AS genre, songs.playCount AS plays FROM song_genres " + @@ -722,26 +710,6 @@ interface LibraryDao { ) suspend fun starredSongIds(sourceId: Long): List - @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 - - @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 - @Query( "SELECT songs.*, albums.coverArt AS coverArt FROM songs " + "LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " + diff --git a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt index ae06e90a..ffb83998 100644 --- a/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt +++ b/app/src/main/kotlin/com/subtracks/data/repo/LibraryRepository.kt @@ -90,16 +90,6 @@ class LibraryRepository( suspend fun starredSongIds(sourceId: Long): List = db.libraryDao().starredSongIds(sourceId) - fun onRepeatSongs( - sourceId: Long, - limit: Int, - ): Flow> = db.libraryDao().onRepeatSongs(sourceId, weekAgoCutoff(), limit) - - fun onRepeatSongsPage(sourceId: Long): Flow> = - pager(60) { db.libraryDao().homeOnRepeatSongs(sourceId, weekAgoCutoff()) } - - suspend fun onRepeatSongIds(sourceId: Long): List = db.libraryDao().onRepeatSongIds(sourceId, weekAgoCutoff()) - fun songsByGenre( sourceId: Long, genre: String, @@ -448,5 +438,3 @@ 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 diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt index c546f20d..d8c9e36c 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeListScreen.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack @@ -52,7 +53,6 @@ import com.subtracks.ui.components.CoverArt import com.subtracks.ui.components.ItemActions import com.subtracks.ui.components.LoadingState import com.subtracks.ui.components.MenuTarget -import com.subtracks.ui.library.ArtistsContent import com.subtracks.ui.library.SongRow import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinViewModel @@ -159,17 +159,17 @@ fun HomeListScreen( } HomeSection.RecentlyPlayedArtists, HomeSection.MostPlayedArtists -> { - ArtistsContent( - items = artists, + ArtistsList( + artists = artists, coverArt = coverArt, - bottomInset = 0.dp, + info = { artistListInfo(request, it, now) }, onArtistClick = onArtistClick, onLongClick = onLongClick, modifier = Modifier.padding(padding), ) } - HomeSection.RecentlyStarredSongs, HomeSection.OnRepeatSongs -> { + HomeSection.RecentlyStarredSongs -> { SongsList( songs = songs, coverArt = coverArt, @@ -351,6 +351,100 @@ private fun AlbumListRow( } } +@Composable +private fun ArtistsList( + artists: LazyPagingItems, + coverArt: (String?, Boolean) -> CoverArtRef?, + info: (Artist) -> String?, + onArtistClick: (Artist) -> Unit, + onLongClick: (MenuTarget) -> Unit, + modifier: Modifier = Modifier, +) { + if (artists.itemCount == 0 && artists.loadState.refresh is LoadState.Loading) { + LoadingState(modifier) + return + } + LazyColumn( + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(bottom = 24.dp), + ) { + items(count = artists.itemCount, key = artists.itemKey { it.id }) { index -> + val artist = artists[index] + if (artist != null) { + ArtistListRow( + artist = artist, + coverArt = coverArt, + info = info(artist), + onClick = { onArtistClick(artist) }, + onLongClick = { onLongClick(MenuTarget.Artist(artist, coverArt(artist.coverArt, true))) }, + ) + } + } + } +} + +@Composable +private fun ArtistListRow( + artist: Artist, + coverArt: (String?, Boolean) -> CoverArtRef?, + info: String?, + onClick: () -> Unit, + onLongClick: () -> Unit, +) { + Row( + modifier = + Modifier + .fillMaxWidth() + .combinedClickable(onClick = onClick, onLongClick = onLongClick) + .padding(horizontal = 16.dp, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CoverArt( + ref = coverArt(artist.coverArt, true), + name = artist.name, + modifier = Modifier.size(ALBUM_ROW_COVER).clip(CircleShape), + ) + Spacer(Modifier.width(12.dp)) + Column(Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(2.dp)) { + Text( + text = artist.name, + style = MaterialTheme.typography.bodyLarge, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "${artist.albumCount} ${if (artist.albumCount == 1L) "album" else "albums"}", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + modifier = Modifier.weight(1f), + ) + if (info != null) { + Text( + text = info, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + modifier = Modifier.padding(start = 8.dp), + ) + } + } + } + } +} + +internal fun artistListInfo( + request: HomeListRequest, + artist: Artist, + now: Long, +): String? = + when (request.section) { + HomeSection.RecentlyPlayedArtists -> artist.played?.let { relativeTimeLabel(it, now) } + HomeSection.MostPlayedArtists -> playCountLabel(artist.playCount) + else -> null + } + internal fun albumListInfo( request: HomeListRequest, album: Album, diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt index 31ce3f52..83f902c1 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeListViewModel.kt @@ -91,7 +91,6 @@ 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()) } }.cachedIn(viewModelScope) @@ -112,10 +111,10 @@ class HomeListViewModel( ): CoverArtRef? = sourceRepository.coverArt(coverArt, thumbnail) fun play(song: Song) { - when (request.section) { - HomeSection.RecentlyStarredSongs -> viewModelScope.playStarredList(libraryRepository, playbackController, song) - HomeSection.OnRepeatSongs -> viewModelScope.playOnRepeatList(libraryRepository, playbackController, song) - else -> viewModelScope.playSongInContext(libraryRepository, playbackController, song) + if (request.section == HomeSection.RecentlyStarredSongs) { + viewModelScope.playStarredList(libraryRepository, playbackController, song) + } else { + viewModelScope.playSongInContext(libraryRepository, playbackController, song) } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt index e7653286..ae17cc0e 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeScreen.kt @@ -65,7 +65,6 @@ enum class HomeSection( Genres("Genres"), Decades("Decades"), RecentlyStarredSongs("Recently starred"), - OnRepeatSongs("On repeat"), RecentlyAddedAlbums("Recently added"), Rediscover("Rediscover"), ; @@ -110,7 +109,6 @@ fun HomeRoute( onAlbumClick = onAlbumClick, onArtistClick = onArtistClick, onPlayStarred = viewModel::playStarred, - onPlayOnRepeat = viewModel::playOnRepeat, onLongClick = { contextMenuHost?.show(it, actions) }, onMore = onMore, onGenreClick = onGenreClick, @@ -129,7 +127,6 @@ fun HomeScreen( onAlbumClick: (Album) -> Unit = {}, onArtistClick: (Artist) -> Unit = {}, onPlayStarred: (Song) -> Unit = {}, - onPlayOnRepeat: (Song) -> Unit = {}, onLongClick: (MenuTarget) -> Unit = {}, onMore: (HomeSection) -> Unit = {}, onGenreClick: (String) -> Unit = {}, @@ -190,11 +187,11 @@ fun HomeScreen( onMore, ) homeSongs( - HomeSection.OnRepeatSongs, - feed.onRepeatSongs, + HomeSection.RecentlyStarredSongs, + feed.recentlyStarredSongs, coverArt, playingSongId, - onPlayOnRepeat, + onPlayStarred, onLongClick, onMore, ) @@ -207,12 +204,11 @@ fun HomeScreen( onMore, ) genreBlock(HomeSection.Genres, feed.genres, onGenreClick, onMore) - homeSongs( - HomeSection.RecentlyStarredSongs, - feed.recentlyStarredSongs, + albumRow( + HomeSection.RecentlyPlayedAlbums, + feed.recentlyPlayedAlbums, coverArt, - playingSongId, - onPlayStarred, + onAlbumClick, onLongClick, onMore, ) @@ -233,14 +229,6 @@ fun HomeScreen( onLongClick, onMore, ) - albumRow( - HomeSection.RecentlyPlayedAlbums, - feed.recentlyPlayedAlbums, - coverArt, - onAlbumClick, - onLongClick, - onMore, - ) } } } diff --git a/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt b/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt index 7b313da7..3989d8fa 100644 --- a/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt +++ b/app/src/main/kotlin/com/subtracks/ui/home/HomeViewModel.kt @@ -35,7 +35,6 @@ data class HomeFeed( val genres: List = emptyList(), val decades: List = emptyList(), val recentlyStarredSongs: List = emptyList(), - val onRepeatSongs: List = emptyList(), val recentlyAddedAlbums: List = emptyList(), val rediscoverAlbums: List = emptyList(), val loading: Boolean = false, @@ -63,9 +62,8 @@ class HomeViewModel( libraryRepository.recentlyPlayedArtists(sourceId, HOME_ROW_LIMIT), libraryRepository.mostPlayedAlbums(sourceId, HOME_ROW_LIMIT), libraryRepository.mostPlayedArtists(sourceId, HOME_ROW_LIMIT), - libraryRepository.onRepeatSongs(sourceId, HOME_STARRED_LIMIT), - ) { albums, artists, frequentAlbums, frequentArtists, onRepeat -> - PlayRows(albums, artists, frequentAlbums, frequentArtists, onRepeat) + ) { albums, artists, frequentAlbums, frequentArtists -> + PlayRows(albums, artists, frequentAlbums, frequentArtists) } val discovery = combine( @@ -88,7 +86,6 @@ class HomeViewModel( recentArtists = rows.recentArtists, frequentAlbums = rows.frequentAlbums, frequentArtists = rows.frequentArtists, - onRepeatSongs = rows.onRepeatSongs, genres = found.genres, decades = found.decades, starredSongs = found.starredSongs, @@ -108,10 +105,6 @@ class HomeViewModel( viewModelScope.playStarredList(libraryRepository, playbackController, song) } - fun playOnRepeat(song: Song) { - viewModelScope.playOnRepeatList(libraryRepository, playbackController, song) - } - fun sync() = syncManager.requestSync() private data class PlayRows( @@ -119,7 +112,6 @@ class HomeViewModel( val recentArtists: List, val frequentAlbums: List, val frequentArtists: List, - val onRepeatSongs: List, ) private data class Discovery( @@ -142,7 +134,6 @@ internal fun buildHomeFeed( addedAlbums: List, rediscover: List, hasPlayData: Boolean, - onRepeatSongs: List = emptyList(), ): HomeFeed = HomeFeed( recentlyPlayedAlbums = recentAlbums.takeIf { hasPlayData }.orEmpty(), @@ -152,7 +143,6 @@ internal fun buildHomeFeed( genres = genres, decades = decades, recentlyStarredSongs = starredSongs, - onRepeatSongs = onRepeatSongs, recentlyAddedAlbums = addedAlbums, rediscoverAlbums = rediscover, ) @@ -183,14 +173,3 @@ 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)) - } -} diff --git a/app/src/test/kotlin/com/subtracks/data/db/LibraryHomeDaoTest.kt b/app/src/test/kotlin/com/subtracks/data/db/LibraryHomeDaoTest.kt index ce9982fa..6681fca6 100644 --- a/app/src/test/kotlin/com/subtracks/data/db/LibraryHomeDaoTest.kt +++ b/app/src/test/kotlin/com/subtracks/data/db/LibraryHomeDaoTest.kt @@ -335,24 +335,6 @@ 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 { diff --git a/app/src/test/kotlin/com/subtracks/ui/home/HomeListInfoTest.kt b/app/src/test/kotlin/com/subtracks/ui/home/HomeListInfoTest.kt index 0c71866d..4d5f0c5c 100644 --- a/app/src/test/kotlin/com/subtracks/ui/home/HomeListInfoTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/home/HomeListInfoTest.kt @@ -1,6 +1,7 @@ package com.subtracks.ui.home import com.subtracks.data.model.Album +import com.subtracks.data.model.Artist import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test @@ -49,6 +50,15 @@ class HomeListInfoTest { assertEquals("1999", albumListInfo(HomeListRequest(title = "1990s", decade = 1990), album, now)) } + @Test + fun artistInfoMatchesTheSection() { + val artist = artist(played = secondsAgo(3), playCount = 7) + + assertEquals("3 days ago", artistListInfo(request(HomeSection.RecentlyPlayedArtists), artist, now)) + assertEquals("7 plays", artistListInfo(request(HomeSection.MostPlayedArtists), artist, now)) + assertNull(artistListInfo(request(HomeSection.MostPlayedArtists), artist.copy(playCount = 0, played = null), now)) + } + private fun secondsAgo(days: Long): Long = (now - days * 86_400_000L) / 1000L private fun request(section: HomeSection) = HomeListRequest(title = section.title, section = section) @@ -73,4 +83,18 @@ class HomeListInfoTest { playCount = playCount, played = played, ) + + private fun artist( + played: Long?, + playCount: Long, + ) = Artist( + sourceId = 1, + id = "ar", + name = "Artist", + albumCount = 1, + starred = null, + coverArt = null, + playCount = playCount, + played = played, + ) } diff --git a/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt b/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt index 24ec387e..9f948f23 100644 --- a/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/home/HomeScreenshotTest.kt @@ -105,6 +105,44 @@ class HomeScreenshotTest { composeRule.onRoot().captureRoboImage("src/test/screenshots/home_album_list.png") } + @Test + fun homeRecentlyPlayedArtistsList() { + val now = System.currentTimeMillis() + val artists = + listOf( + listArtist("Artist Yesterday", (now - 86_400_000L) / 1000L), + listArtist("Artist This Week", (now - 3 * 86_400_000L) / 1000L), + ) + composeRule.setContent { + SubtracksTheme { + HomeListScreen( + request = HomeListRequest(title = "Recently played artists", section = HomeSection.RecentlyPlayedArtists), + albums = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), + artists = remember { flowOf(PagingData.from(artists)) }.collectAsLazyPagingItems(), + songs = remember { flowOf(PagingData.empty()) }.collectAsLazyPagingItems(), + coverArt = { _, _ -> null }, + ) + } + } + composeRule.waitUntil(timeoutMillis = 5_000) { + composeRule.onAllNodesWithText("Artist Yesterday").fetchSemanticsNodes().isNotEmpty() + } + composeRule.onRoot().captureRoboImage("src/test/screenshots/home_artist_list.png") + } + + private fun listArtist( + name: String, + played: Long, + ) = Artist( + sourceId = 1, + id = name, + name = name, + albumCount = 4, + starred = null, + coverArt = null, + played = played, + ) + private fun listAlbum( name: String, created: Long, @@ -131,7 +169,6 @@ 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"), ) diff --git a/app/src/test/kotlin/com/subtracks/ui/home/HomeViewModelTest.kt b/app/src/test/kotlin/com/subtracks/ui/home/HomeViewModelTest.kt index c9f2cfc9..d65141ab 100644 --- a/app/src/test/kotlin/com/subtracks/ui/home/HomeViewModelTest.kt +++ b/app/src/test/kotlin/com/subtracks/ui/home/HomeViewModelTest.kt @@ -26,7 +26,6 @@ class HomeViewModelTest { addedAlbums = listOf(album("c")), rediscover = listOf(album("d")), hasPlayData = true, - onRepeatSongs = listOf(starred("repeat")), ) assertEquals(listOf("a"), feed.recentlyPlayedAlbums.map { it.id }) @@ -36,7 +35,6 @@ 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 }) }