feat(android): generated placeholder art and a skeleton library

A title without artwork shows the accent as a gradient with its initials
instead of a black card with a play triangle, so a library of unmatched
titles is no longer a grid of identical cards. Art with the name already
over it, and episode stills, get the gradient without initials. While
artwork loads, and while the catalog first loads, the page shows pulsing
shapes where the posters will be rather than a spinner.

The screenshot tests turn artwork loading off, so whether a failed fetch
lands before the capture no longer decides what the image shows.
This commit is contained in:
nvrl committed 2026-10-03 12:45:34 +02:00
1 parent 0563c91b9e
commit 110f97caf9
13 files changed
+177 -16

No files matched your search

@@ -0,0 +1,28 @@
package io.narl.protonstream.ui
import org.junit.Assert.assertEquals
import org.junit.Test
class ArtworkTest {
@Test
fun `initials skip the lower-case particles of a romanised name`() {
assertEquals("OK", initials("Oshi no Ko"))
assertEquals("AH", initials("Ano Hi Mita Hana no Namae o Bokutachi wa Mada Shiranai."))
}
@Test
fun `a one-word name has one initial`() {
assertEquals("A", initials("Akira"))
}
@Test
fun `a name with no capitals falls back to its first two words`() {
assertEquals("BW", initials("blue world order"))
}
@Test
fun `punctuation and separators are not words`() {
assertEquals("S2", initials("Steins;Gate - 2"))
assertEquals("", initials(" - "))
}
}
@@ -36,8 +36,9 @@ import uniffi.pstr_android.TitleType
* The screens are about to be redesigned one by one, and a layout regression —
* a row that wraps, a button pushed off the edge — compiles and passes every
* other test. Here it is a changed PNG in review. No artwork is loaded: the
* bridge is not available on the host, so every tile shows its placeholder,
* which is also what a title without metadata looks like on a phone.
* bridge is not available on the host, so every tile shows its generated
* placeholder, which is also what a title without metadata looks like on a
* phone.
*/
@RunWith(RobolectricTestRunner::class)
@GraphicsMode(GraphicsMode.Mode.NATIVE)
@@ -53,7 +54,9 @@ class ScreenshotTest {
private fun capture(name: String, content: @Composable () -> Unit) {
compose.setContent {
ProtonStreamTheme {
Surface(color = MaterialTheme.colorScheme.background) { content() }
CompositionLocalProvider(LocalArtworkLoading provides false) {
Surface(color = MaterialTheme.colorScheme.background) { content() }
}
}
}
compose.onRoot().captureRoboImage("src/test/screenshots/$name.png")
@@ -85,6 +88,11 @@ class ScreenshotTest {
LibraryScreen(AppUiState(loading = false), {}, { _, _ -> }, {}, {}, { _, _ -> }, { _, _ -> }, PaddingValues())
}
@Test
fun `the library while the catalog loads`() = capture("library-loading") {
LibraryScreen(AppUiState(), {}, { _, _ -> }, {}, {}, { _, _ -> }, { _, _ -> }, PaddingValues())
}
@Test
fun `a title with seasons`() = capture("title-phone") {
TitleScreen(SERIES, true, { _, _ -> }, {}, {}, {}, { _, _ -> }, PaddingValues())
Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 154 KiB

After

Width:  |  Height:  |  Size: 854 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 649 KiB