diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt index 5676fc4b9e..eaa1e88c1c 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/types/Ps1Save.kt @@ -37,22 +37,22 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameMillis import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.FilterQuality import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.vitorpamplona.amethyst.R import com.vitorpamplona.amethyst.model.Note +import com.vitorpamplona.amethyst.ui.stringRes import com.vitorpamplona.amethyst.ui.theme.Size24dp import com.vitorpamplona.amethyst.ui.theme.placeholderText import com.vitorpamplona.amethyst.ui.theme.replyModifier import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveEvent import com.vitorpamplona.quartz.experimental.ps1saves.Ps1SaveIcon -import kotlinx.coroutines.delay /** How many hex characters of the block to preview (the full block is ~16K chars). */ private const val HEX_PREVIEW_CHARS = 192 @@ -102,7 +102,7 @@ fun RenderPs1Save(baseNote: Note) { Spacer(Modifier.width(8.dp)) } Text( - text = (if (save.icon == null) FLOPPY_PREFIX else "") + (save.title ?: stringResource(R.string.ps1_save_title)), + text = (if (save.icon == null) FLOPPY_PREFIX else "") + (save.title ?: stringRes(R.string.ps1_save_title)), style = MaterialTheme.typography.titleMedium, maxLines = 2, overflow = TextOverflow.Ellipsis, @@ -113,7 +113,7 @@ fun RenderPs1Save(baseNote: Note) { listOfNotNull( save.filename, save.region, - save.blockNumber?.let { stringResource(R.string.ps1_save_block, it) }, + save.blockNumber?.let { stringRes(R.string.ps1_save_block, it) }, ).joinToString(" · ") if (details.isNotEmpty()) { @@ -151,7 +151,8 @@ private fun Ps1SaveIconImage(icon: Ps1SaveIcon) { remember(icon) { icon.frames.map { pixels -> Bitmap - .createBitmap(pixels, Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE, Bitmap.Config.ARGB_8888) + .createBitmap(Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE, Bitmap.Config.ARGB_8888) + .apply { setPixels(pixels, 0, Ps1SaveIcon.SIZE, 0, 0, Ps1SaveIcon.SIZE, Ps1SaveIcon.SIZE) } .asImageBitmap() } } @@ -159,10 +160,14 @@ private fun Ps1SaveIconImage(icon: Ps1SaveIcon) { var frameIndex by remember(icon) { mutableIntStateOf(0) } if (frames.size > 1) { + // Driven by the frame clock (not delay) so the ticker suspends whenever + // the composition stops drawing (backgrounded, behind another screen). LaunchedEffect(icon) { + val start = withFrameMillis { it } while (true) { - delay(ICON_FRAME_MILLIS) - frameIndex = (frameIndex + 1) % frames.size + withFrameMillis { now -> + frameIndex = (((now - start) / ICON_FRAME_MILLIS) % frames.size).toInt() + } } } } diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt index ad7061809a..6e1612fd41 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEvent.kt @@ -71,18 +71,9 @@ class Ps1SaveEvent( /** Game region, from the `region` tag (may be null). */ fun region() = tags.firstTagValue("region") - /** Identifier of the memory card this block belongs to, from the `m` tag (may be null). */ - fun memoryCardId() = tags.firstTagValue("m") - /** Block number within the card, from the `block` tag (null when missing or not a number). */ fun blockNumber() = tags.firstTagValue("block")?.toIntOrNull() - /** Position of this block in a multi-block save chain, from the `state` tag (may be null). */ - fun blockState() = tags.firstTagValue("state") - - /** SHA-256 of the block data, from the `x` tag (may be null). */ - fun blockHash() = tags.firstTagValue("x") - /** Publisher-provided human-readable summary, from the NIP-31 `alt` tag (may be null). */ fun summary() = tags.alt() diff --git a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveIcon.kt b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveIcon.kt index 9bc5b22ead..45b52b6b78 100644 --- a/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveIcon.kt +++ b/quartz/src/commonMain/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveIcon.kt @@ -35,12 +35,15 @@ import com.vitorpamplona.quartz.nip01Core.core.hexToByteArrayOrNull * (bits 0-4 red, 5-9 green, 10-14 blue); raw `0x0000` is transparent. * - `0x80` — the frames, each 16×16 pixels at 4bpp (128 bytes), low nibble * first; pixel values index the CLUT. - * - * [frames] holds one 256-entry row-major ARGB pixel array per animation frame, - * ready to blit into a bitmap. */ @Immutable class Ps1SaveIcon( + /** + * One 256-entry row-major ARGB pixel array per animation frame. Treat as + * frozen: the arrays back the class's `@Immutable` contract, so mutating + * them in place would not recompose — build a new [Ps1SaveIcon] to change + * pixels. + */ val frames: List, ) { companion object { diff --git a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt index 063db33c00..03a65563ae 100644 --- a/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt +++ b/quartz/src/commonTest/kotlin/com/vitorpamplona/quartz/experimental/ps1saves/Ps1SaveEventTest.kt @@ -72,10 +72,7 @@ class Ps1SaveEventTest { assertEquals("SYNCED OVER NOST Card", event.saveTitle()) assertEquals("BASCUS-00001SOFTCARD", event.filename()) assertEquals("America", event.region()) - assertEquals("synctest-f1fc95d1", event.memoryCardId()) assertEquals(1, event.blockNumber()) - assertEquals("first", event.blockState()) - assertEquals("4235e8c1d7164927b78dd50e0a12acbea04b2d80f0864fc3298e7d84225d8f41", event.blockHash()) assertEquals("PS1 save 'SYNCED OVER NOST Card' (BASCUS-00001SOFTCARD)", event.summary()) assertEquals("synctest-f1fc95d1-1", event.dTag()) }