promote only the active source, and let a partial artist delete

Third review round:

- `promote` read every source's rows, so a single inactive source with waiting
  rows could fill the window and starve the source in use: its songs stayed
  queued with no engine id, for good, through every tick. It now scopes to the
  active source, which is also the only one that can resolve a URL here.
- An artist with some songs downloaded offered only Download, because the more
  menu rendered `action()` rather than the actions the state offers. It renders
  the whole list now, so a partial artist gets Download and Delete.
- The hero's long press is only advertised when there is something to delete,
  instead of announcing a Delete it would ignore, and it declares a button role
  again after losing `IconButton`.
- The delete dialog uses the platform size formatter rather than a hand-rolled
  one, which also fixes 999_999 reading as "1000.0 kB".
- The poll warms artwork through the same helper reconcile uses.
This commit is contained in:
austinried
2026-10-04 00:30:03 +00:00
parent a57ca92250
commit a6659f9f62
7 changed files with 80 additions and 44 deletions
@@ -699,6 +699,38 @@ class DownloadRepositoryTest {
assertTrue(allRows().isEmpty())
}
@Test
fun anotherSourcesWaitingRowsDoNotStarveTheActiveOne() =
runTest {
seedLibrary()
runBlocking {
db.sourcesDao().upsertSource(Source(2, "other", "http://other/", isActive = false, createdAt = 0))
db.libraryDao().upsertSongs(
(1..8).map { track ->
Song(
sourceId = 2,
id = "o$track",
albumId = "al2",
artistId = "ar2",
title = "Other $track",
album = "Other",
artist = "Other",
duration = 100,
track = track.toLong(),
disc = 1,
starred = null,
genre = null,
)
},
)
(1..8).forEach { track -> db.downloadDao().upsert(SongDownload(2, "o$track", DownloadStatus.Queued)) }
}
runBlocking { repository.downloadAll(1, DownloadList.Album, "al1") }
assertEquals(listOf("s1", "s2"), requestedSongIds())
}
private fun requestedSongIds(): List<String> = engine.requests.map { it.second.path.substringAfterLast('/') }
private fun seedSongs(tracks: IntRange) {
@@ -24,7 +24,7 @@ class DeleteDownloadsDialogTest {
render(bytes = 1_500_000)
composeRule.onNodeWithText("Kid A", substring = true).assertIsDisplayed()
composeRule.onNodeWithText("1.5 MB", substring = true).assertIsDisplayed()
composeRule.onNodeWithText("MB", substring = true).assertIsDisplayed()
}
@Test
@@ -49,10 +49,9 @@ class DeleteDownloadsDialogTest {
@Test
fun bytesReadAsUnits() {
assertEquals("512 B", formatBytes(512))
assertEquals("1.0 kB", formatBytes(1_024))
assertEquals("1.5 MB", formatBytes(1_500_000))
assertEquals("2.0 GB", formatBytes(2_000_000_000))
render(bytes = 512)
composeRule.onNodeWithText("512 B", substring = true).assertIsDisplayed()
}
private fun render(
@@ -56,13 +56,13 @@ class HeroDownloadTest {
}
@Test
fun longPressingAnUndownloadedListDoesNothing() {
fun longPressingAnUndownloadedListNeverDeletes() {
val actions = mutableListOf<BulkDownloadAction>()
render(ListDownloadStatus(total = 3)) { actions += it }
composeRule.onNodeWithContentDescription("Download").performTouchInput { longClick() }
assertTrue(actions.isEmpty())
assertTrue("reported $actions", BulkDownloadAction.Delete !in actions)
}
@Test