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 54d4a4ab25..36b1c0f199 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
@@ -21,15 +21,7 @@
package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -37,43 +29,26 @@ 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.text.font.FontFamily
-import androidx.compose.ui.text.font.FontStyle
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
import androidx.core.graphics.createBitmap
-import com.vitorpamplona.amethyst.R
+import com.vitorpamplona.amethyst.commons.ui.note.Ps1SaveCard
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
/** How many hex characters of the block to preview (the full block is ~16K chars). */
private const val HEX_PREVIEW_CHARS = 192
-/** Floppy-disk emoji fallback for blocks without a decodable save icon. */
-private const val FLOPPY_PREFIX = "💾 "
-
/** Milliseconds per animation frame, roughly the BIOS memory-card screen cadence. */
private const val ICON_FRAME_MILLIS = 250L
/**
- * Minimal, fixed-size card for a PS1 memory-card save block (kind 38192).
- *
- * The content is a full 8 KiB memory-card block, hex-encoded (~16K characters),
- * so the card never renders it whole: it shows the save's own 16×16 icon from
- * the title frame (animated exactly like the PS1 BIOS memory-card screen; a
- * floppy emoji when the block has none), the save title, a metadata line
- * (game product code, region, block number), and a short monospace hex preview
- * clamped to a few lines — no expansion, no network calls. The card is
- * identical in the feed and the opened view, so it takes no makeItShort flag.
+ * Entry for a PS1 memory-card save block: decodes the [Note] and hands the parsed
+ * values to the shared commons [Ps1SaveCard]. The save's 16×16 icon needs Android
+ * bitmap decoding, so it is supplied here as the card's icon slot.
*/
@Composable
fun RenderPs1Save(baseNote: Note) {
@@ -97,58 +72,15 @@ fun RenderPs1Save(baseNote: Note) {
)
}
- Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- if (save.icon != null) {
- Ps1SaveIconImage(save.icon)
- Spacer(Modifier.width(8.dp))
- }
- Text(
- 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,
- )
- }
-
- val details =
- listOfNotNull(
- save.filename,
- save.region,
- save.blockNumber?.let { stringRes(R.string.ps1_save_block, it) },
- ).joinToString(" · ")
-
- if (details.isNotEmpty()) {
- Spacer(Modifier.height(6.dp))
- Text(
- text = details,
- style = MaterialTheme.typography.bodyMedium,
- color = MaterialTheme.colorScheme.placeholderText,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis,
- )
- }
-
- if (save.isBlank) {
- Spacer(Modifier.height(6.dp))
- Text(
- text = stringRes(R.string.ps1_save_empty_slot),
- style = MaterialTheme.typography.bodyMedium,
- fontStyle = FontStyle.Italic,
- color = MaterialTheme.colorScheme.placeholderText,
- )
- } else if (save.hexPreview != null) {
- Spacer(Modifier.height(6.dp))
- Text(
- text = save.hexPreview,
- style = MaterialTheme.typography.bodySmall,
- fontFamily = FontFamily.Monospace,
- color = MaterialTheme.colorScheme.placeholderText,
- maxLines = 3,
- overflow = TextOverflow.Ellipsis,
- )
- }
- }
+ Ps1SaveCard(
+ title = save.title,
+ filename = save.filename,
+ region = save.region,
+ blockNumber = save.blockNumber,
+ isBlank = save.isBlank,
+ hexPreview = save.hexPreview,
+ icon = save.icon?.let { ic -> { Ps1SaveIconImage(ic) } },
+ )
}
/**
diff --git a/amethyst/src/main/res/values-cs/strings.xml b/amethyst/src/main/res/values-cs/strings.xml
index d54d5ca6ff..9ad85e1ddd 100644
--- a/amethyst/src/main/res/values-cs/strings.xml
+++ b/amethyst/src/main/res/values-cs/strings.xml
@@ -3800,9 +3800,6 @@
Končí %1$s
On-chain dar
- Uložení na paměťovou kartu PS1
- blok %1$d
- Prázdný slot
Policie
Rychlostní radar
diff --git a/amethyst/src/main/res/values-de-rDE/strings.xml b/amethyst/src/main/res/values-de-rDE/strings.xml
index 1298b253f4..7602ce3d44 100644
--- a/amethyst/src/main/res/values-de-rDE/strings.xml
+++ b/amethyst/src/main/res/values-de-rDE/strings.xml
@@ -3692,9 +3692,6 @@
Endet %1$s
On-Chain-Spende
- PS1-Memory-Card-Spielstand
- Block %1$d
- Leerer Slot
Polizei
Blitzer
diff --git a/amethyst/src/main/res/values-hi-rIN/strings.xml b/amethyst/src/main/res/values-hi-rIN/strings.xml
index 0d4b6151d1..5e60ed6ed8 100644
--- a/amethyst/src/main/res/values-hi-rIN/strings.xml
+++ b/amethyst/src/main/res/values-hi-rIN/strings.xml
@@ -3606,9 +3606,6 @@
समाप्ति %1$s
खण्डश्रृंखलाबद्ध दान
- पीएस१ स्मृति पत्रक अभिलेखन
- खण्ड %1$d
- रिक्त छेद
रक्षापालिका
गति चित्रग्राहक
diff --git a/amethyst/src/main/res/values-hu-rHU/strings.xml b/amethyst/src/main/res/values-hu-rHU/strings.xml
index 6c1fb314ee..24eecb5793 100644
--- a/amethyst/src/main/res/values-hu-rHU/strings.xml
+++ b/amethyst/src/main/res/values-hu-rHU/strings.xml
@@ -3578,9 +3578,6 @@
Véget ér ekkor: %1$s
Láncon belüli adomány
- PS1 memóriakártya-mentés
- %1$d blokk
- Üres hely
Rendőrség
Sebességmérő kamera
diff --git a/amethyst/src/main/res/values-nl-rNL/strings.xml b/amethyst/src/main/res/values-nl-rNL/strings.xml
index ea089f4835..c94b7f06be 100644
--- a/amethyst/src/main/res/values-nl-rNL/strings.xml
+++ b/amethyst/src/main/res/values-nl-rNL/strings.xml
@@ -3133,9 +3133,6 @@
Eindigt %1$s
On-chain donatie
- PS1 memory card-save
- blok %1$d
- Leeg slot
Politie
Flitser
diff --git a/amethyst/src/main/res/values-pl-rPL/strings.xml b/amethyst/src/main/res/values-pl-rPL/strings.xml
index cef2a0446c..eca52c63ce 100644
--- a/amethyst/src/main/res/values-pl-rPL/strings.xml
+++ b/amethyst/src/main/res/values-pl-rPL/strings.xml
@@ -3795,9 +3795,6 @@ Zaplanowane posty z innych kont nie zostaną opublikowane, dopóki to konto jest
Kończy się %1$s
Darowizna on-chain
- Zapis karty pamięci PS1
- blok %1$d
- Pusty slot
Policja
Fotoradar
diff --git a/amethyst/src/main/res/values-pt-rBR/strings.xml b/amethyst/src/main/res/values-pt-rBR/strings.xml
index 3712af6f63..9335be6456 100644
--- a/amethyst/src/main/res/values-pt-rBR/strings.xml
+++ b/amethyst/src/main/res/values-pt-rBR/strings.xml
@@ -3690,9 +3690,6 @@
Termina %1$s
Doação on-chain
- Salvamento em cartão de memória PS1
- bloco %1$d
- Espaço vazio
Polícia
Radar de velocidade
diff --git a/amethyst/src/main/res/values-sl-rSI/strings.xml b/amethyst/src/main/res/values-sl-rSI/strings.xml
index a9cb466b6b..c076ad822e 100644
--- a/amethyst/src/main/res/values-sl-rSI/strings.xml
+++ b/amethyst/src/main/res/values-sl-rSI/strings.xml
@@ -3396,9 +3396,6 @@ Za ohranitev zasebnosti to denarnico polni in prazni prek ne-zasebnih računov,
Izteče se: %1$s
On-chain donacija
- Shrani na pomnilniško kartico PS1
- %1$d blok
- Prazen prostor
Policija
Hitrostna kamera
diff --git a/amethyst/src/main/res/values-sv-rSE/strings.xml b/amethyst/src/main/res/values-sv-rSE/strings.xml
index 95df130236..205d83b6e9 100644
--- a/amethyst/src/main/res/values-sv-rSE/strings.xml
+++ b/amethyst/src/main/res/values-sv-rSE/strings.xml
@@ -3690,9 +3690,6 @@
Slutar %1$s
On-chain-donation
- PS1-minneskortsparning
- block %1$d
- Tom plats
Polis
Fartkamera
diff --git a/amethyst/src/main/res/values-zh-rCN/strings.xml b/amethyst/src/main/res/values-zh-rCN/strings.xml
index 008fb3b150..632f14c5e0 100644
--- a/amethyst/src/main/res/values-zh-rCN/strings.xml
+++ b/amethyst/src/main/res/values-zh-rCN/strings.xml
@@ -3556,9 +3556,6 @@
结束 %1$s
链上捐助
- PS1 内存卡保存
- 块 %1$d
- 空槽位
警察
测速照相
diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml
index d0466056f7..c2b27b95af 100644
--- a/amethyst/src/main/res/values/strings.xml
+++ b/amethyst/src/main/res/values/strings.xml
@@ -4058,9 +4058,6 @@
Ends %1$s
On-chain donation
- PS1 memory card save
- block %1$d
- Empty slot
Police
Speed camera
diff --git a/commons/src/commonMain/composeResources/values-cs/strings.xml b/commons/src/commonMain/composeResources/values-cs/strings.xml
index be6387a03d..6ac54dd1f3 100644
--- a/commons/src/commonMain/composeResources/values-cs/strings.xml
+++ b/commons/src/commonMain/composeResources/values-cs/strings.xml
@@ -24,4 +24,7 @@
- %1$s +%2$d dalších
- %1$s +%2$d dalších
+ Uložení na paměťovou kartu PS1
+ blok %1$d
+ Prázdný slot
diff --git a/commons/src/commonMain/composeResources/values-de-rDE/strings.xml b/commons/src/commonMain/composeResources/values-de-rDE/strings.xml
index b0fcb8ca97..343b579293 100644
--- a/commons/src/commonMain/composeResources/values-de-rDE/strings.xml
+++ b/commons/src/commonMain/composeResources/values-de-rDE/strings.xml
@@ -18,4 +18,7 @@
- %1$s +%2$d weitere
- %1$s +%2$d weitere
+ PS1-Memory-Card-Spielstand
+ Block %1$d
+ Leerer Slot
diff --git a/commons/src/commonMain/composeResources/values-hi-rIN/strings.xml b/commons/src/commonMain/composeResources/values-hi-rIN/strings.xml
index d807fe64de..a75cb4a7d5 100644
--- a/commons/src/commonMain/composeResources/values-hi-rIN/strings.xml
+++ b/commons/src/commonMain/composeResources/values-hi-rIN/strings.xml
@@ -18,4 +18,7 @@
- %1$s सं॰ %2$d अधिक
- %1$s सं॰ %2$d अधिक
+ पीएस१ स्मृति पत्रक अभिलेखन
+ खण्ड %1$d
+ रिक्त छेद
diff --git a/commons/src/commonMain/composeResources/values-hu-rHU/strings.xml b/commons/src/commonMain/composeResources/values-hu-rHU/strings.xml
index 58c1d912f6..c62c6e4053 100644
--- a/commons/src/commonMain/composeResources/values-hu-rHU/strings.xml
+++ b/commons/src/commonMain/composeResources/values-hu-rHU/strings.xml
@@ -18,4 +18,7 @@
- %1$s +%2$d további
- %1$s +%2$d további
+ PS1 memóriakártya-mentés
+ %1$d blokk
+ Üres hely
diff --git a/commons/src/commonMain/composeResources/values-nl-rNL/strings.xml b/commons/src/commonMain/composeResources/values-nl-rNL/strings.xml
index 6b7f3932a2..ad9925209a 100644
--- a/commons/src/commonMain/composeResources/values-nl-rNL/strings.xml
+++ b/commons/src/commonMain/composeResources/values-nl-rNL/strings.xml
@@ -18,4 +18,7 @@
- %1$s +%2$d meer
- %1$s +%2$d meer
+ PS1 memory card-save
+ blok %1$d
+ Leeg slot
diff --git a/commons/src/commonMain/composeResources/values-pl-rPL/strings.xml b/commons/src/commonMain/composeResources/values-pl-rPL/strings.xml
index cbc3b51b7b..cdf1ce08dc 100644
--- a/commons/src/commonMain/composeResources/values-pl-rPL/strings.xml
+++ b/commons/src/commonMain/composeResources/values-pl-rPL/strings.xml
@@ -23,4 +23,7 @@
- %1$s +%2$d więcej
- %1$s +%2$d więcej
+ Zapis karty pamięci PS1
+ blok %1$d
+ Pusty slot
diff --git a/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml b/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml
index b99f46e723..33662af34c 100644
--- a/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml
+++ b/commons/src/commonMain/composeResources/values-pt-rBR/strings.xml
@@ -18,4 +18,7 @@
- %1$s +%2$d a mais
- %1$s +%2$d a mais
+ Salvamento em cartão de memória PS1
+ bloco %1$d
+ Espaço vazio
diff --git a/commons/src/commonMain/composeResources/values-sl-rSI/strings.xml b/commons/src/commonMain/composeResources/values-sl-rSI/strings.xml
index abeb6f5f26..401bab39c0 100644
--- a/commons/src/commonMain/composeResources/values-sl-rSI/strings.xml
+++ b/commons/src/commonMain/composeResources/values-sl-rSI/strings.xml
@@ -24,4 +24,7 @@
- %1$s in še %2$d druge
- %1$s in še %2$d drugih
+ Shrani na pomnilniško kartico PS1
+ %1$d blok
+ Prazen prostor
diff --git a/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml b/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml
index 9bd2051608..cdde6011a3 100644
--- a/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml
+++ b/commons/src/commonMain/composeResources/values-sv-rSE/strings.xml
@@ -18,4 +18,6 @@
- %1$s +%2$d till
- %1$s +%2$d till
+ PS1-minneskortsparning
+ Tom plats
diff --git a/commons/src/commonMain/composeResources/values-zh-rCN/strings.xml b/commons/src/commonMain/composeResources/values-zh-rCN/strings.xml
index d2d301218a..4a5b4d7621 100644
--- a/commons/src/commonMain/composeResources/values-zh-rCN/strings.xml
+++ b/commons/src/commonMain/composeResources/values-zh-rCN/strings.xml
@@ -15,4 +15,7 @@
- %1$s + 另%2$d
+ PS1 内存卡保存
+ 块 %1$d
+ 空槽位
diff --git a/commons/src/commonMain/composeResources/values/strings.xml b/commons/src/commonMain/composeResources/values/strings.xml
index c284b6612f..5305079a35 100644
--- a/commons/src/commonMain/composeResources/values/strings.xml
+++ b/commons/src/commonMain/composeResources/values/strings.xml
@@ -119,4 +119,7 @@
- %1$s +%2$d more
- %1$s +%2$d more
+ PS1 memory card save
+ block %1$d
+ Empty slot
diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/Ps1SaveCard.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/Ps1SaveCard.kt
new file mode 100644
index 0000000000..a6f4a55bc7
--- /dev/null
+++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/note/Ps1SaveCard.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright (c) 2025 Vitor Pamplona
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy of
+ * this software and associated documentation files (the "Software"), to deal in
+ * the Software without restriction, including without limitation the rights to use,
+ * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
+ * Software, and to permit persons to whom the Software is furnished to do so,
+ * subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
+ * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ */
+package com.vitorpamplona.amethyst.commons.ui.note
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.font.FontFamily
+import androidx.compose.ui.text.font.FontStyle
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import com.vitorpamplona.amethyst.commons.resources.Res
+import com.vitorpamplona.amethyst.commons.resources.ps1_save_block
+import com.vitorpamplona.amethyst.commons.resources.ps1_save_empty_slot
+import com.vitorpamplona.amethyst.commons.resources.ps1_save_title
+import com.vitorpamplona.amethyst.commons.ui.theme.placeholderText
+import com.vitorpamplona.amethyst.commons.ui.theme.replyModifier
+import org.jetbrains.compose.resources.stringResource
+
+/** Floppy-disk emoji fallback for blocks without a decodable save icon. */
+private const val FLOPPY_PREFIX = "💾 "
+
+/**
+ * Minimal, fixed-size card for a PS1 memory-card save block (kind 38192).
+ *
+ * The content is a full 8 KiB memory-card block, hex-encoded (~16K characters),
+ * so the card never renders it whole: it shows the save title, a metadata line
+ * (game product code, region, block number), and a short monospace hex preview
+ * clamped to a few lines — no expansion, no network calls. The card is identical
+ * in the feed and the opened view, so it takes no makeItShort flag.
+ *
+ * The save's animated 16×16 icon is decoded from raw memory-card pixels, which is
+ * platform-specific bitmap work, so it arrives as the [icon] slot supplied by the
+ * host. A null [icon] falls back to a floppy-disk emoji prefix on the title.
+ */
+@Composable
+fun Ps1SaveCard(
+ title: String?,
+ filename: String?,
+ region: String?,
+ blockNumber: Int?,
+ isBlank: Boolean,
+ hexPreview: String?,
+ icon: (@Composable () -> Unit)?,
+) {
+ Column(MaterialTheme.colorScheme.replyModifier.padding(10.dp)) {
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ if (icon != null) {
+ icon()
+ Spacer(Modifier.width(8.dp))
+ }
+ Text(
+ text = (if (icon == null) FLOPPY_PREFIX else "") + (title ?: stringResource(Res.string.ps1_save_title)),
+ style = MaterialTheme.typography.titleMedium,
+ maxLines = 2,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ val details =
+ listOfNotNull(
+ filename,
+ region,
+ blockNumber?.let { stringResource(Res.string.ps1_save_block, it) },
+ ).joinToString(" · ")
+
+ if (details.isNotEmpty()) {
+ Spacer(Modifier.height(6.dp))
+ Text(
+ text = details,
+ style = MaterialTheme.typography.bodyMedium,
+ color = MaterialTheme.colorScheme.placeholderText,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+
+ if (isBlank) {
+ Spacer(Modifier.height(6.dp))
+ Text(
+ text = stringResource(Res.string.ps1_save_empty_slot),
+ style = MaterialTheme.typography.bodyMedium,
+ fontStyle = FontStyle.Italic,
+ color = MaterialTheme.colorScheme.placeholderText,
+ )
+ } else if (hexPreview != null) {
+ Spacer(Modifier.height(6.dp))
+ Text(
+ text = hexPreview,
+ style = MaterialTheme.typography.bodySmall,
+ fontFamily = FontFamily.Monospace,
+ color = MaterialTheme.colorScheme.placeholderText,
+ maxLines = 3,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ }
+}