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, + ) + } + } +}