diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt index efaf02b972..4eb39ae3c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/AppNavigation.kt @@ -59,6 +59,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.isSameRoute import com.vitorpamplona.amethyst.ui.note.PayViaIntentScreen import com.vitorpamplona.amethyst.ui.note.UpdateReactionTypeScreen import com.vitorpamplona.amethyst.ui.note.nip22Comments.ReplyCommentPostScreen +import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageFileScreen import com.vitorpamplona.amethyst.ui.note.share.ShareNoteAsImageScreen import com.vitorpamplona.amethyst.ui.screen.AccountSessionManager import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountSwitcherAndLeftDrawerLayout @@ -417,6 +418,7 @@ fun BuildNavigation( composableFromEndArgs { ProfileScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ThreadScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ShareNoteAsImageScreen(it.id, accountViewModel, nav) } + composableFromEndArgs { ShareNoteAsImageFileScreen(it.id, accountViewModel, nav) } composableFromEndArgs { ContactListUsersScreen(it.noteId, accountViewModel, nav) } composableFromEndArgs { HashtagScreen(it, accountViewModel, nav) } composableFromEndArgs { GeoHashScreen(it, accountViewModel, nav) } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt index 53fcb6cb15..a7792979c5 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/navigation/routes/Routes.kt @@ -436,6 +436,10 @@ sealed class Route { val id: String, ) : Route() + @Serializable data class ShareNoteAsImageFile( + val id: String, + ) : Route() + @Serializable data class ContactListUsers( val noteId: String, ) : Route() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt index ded21d5121..20af25f5f1 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/elements/DropDownMenu.kt @@ -240,6 +240,11 @@ fun NoteDropDownMenu( onDismiss() } M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(R.string.share_as_image)) { + val shareId = if (note is AddressableNote) note.address.toValue() else note.idHex + nav.nav(Route.ShareNoteAsImageFile(shareId)) + onDismiss() + } + M3ActionRow(icon = MaterialSymbols.Image, text = stringRes(R.string.share_as_image_url)) { val shareId = if (note is AddressableNote) note.address.toValue() else note.idHex nav.nav(Route.ShareNoteAsImage(shareId)) onDismiss() diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt index 2e697ace58..87349908f8 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/share/ShareNoteAsImageScreen.kt @@ -120,6 +120,89 @@ fun ShareNoteAsImageScreen( } } +/** + * Renders [id]'s note into the same framed card as [ShareNoteAsImageScreen], captures it to a + * bitmap, writes it to the cache as a PNG and hands the local file straight to the Android share + * sheet — no preview, no upload. The screen only shows a brief progress indicator while the card + * is being captured. + */ +@Composable +fun ShareNoteAsImageFileScreen( + id: String, + accountViewModel: AccountViewModel, + nav: INav, +) { + LoadNote(id, accountViewModel) { note -> + if (note != null) { + ShareNoteAsImageFileScreen(note, accountViewModel, nav) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ShareNoteAsImageFileScreen( + note: Note, + accountViewModel: AccountViewModel, + nav: INav, +) { + val context = LocalContext.current + val graphicsLayer = rememberGraphicsLayer() + + // Guards against capturing/sharing more than once across recompositions. + var shared by remember { mutableStateOf(false) } + + Scaffold( + topBar = { TopBarWithBackButton(stringRes(R.string.share_as_image), nav) }, + ) { pad -> + Box( + modifier = + Modifier + .padding(pad) + .fillMaxSize() + .background( + Brush.verticalGradient( + listOf( + MaterialTheme.colorScheme.surface, + MaterialTheme.colorScheme.surfaceVariant, + ), + ), + ), + contentAlignment = Alignment.Center, + ) { + // Rendered (but unpainted) only to feed the snapshot; the user never sees this card. + CaptureSource( + note = note, + graphicsLayer = graphicsLayer, + accountViewModel = accountViewModel, + nav = nav, + ) + + GeneratingPreview() + + LaunchedEffect(note) { + // Wait a couple of frames so the card is measured and drawn, then let network + // media settle before the final snapshot. + withFrameNanos {} + withFrameNanos {} + graphicsLayer.toImageBitmap() + delay(IMAGE_SETTLE_MS) + val bitmap = graphicsLayer.toImageBitmap() + + if (!shared) { + shared = true + val uri = + withContext(Dispatchers.IO) { + saveBitmapToCache(context, bitmap.asAndroidBitmap()) + } + startShareImageFileIntent(context, uri) + nav.popBack() + } + } + } + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun ShareNoteAsImageScreen( @@ -208,7 +291,7 @@ fun ShareNoteAsImageScreen( } Scaffold( - topBar = { TopBarWithBackButton(stringRes(R.string.share_as_image), nav) }, + topBar = { TopBarWithBackButton(stringRes(R.string.share_as_image_url), nav) }, bottomBar = { ShareBottomBar( serverName = selectedServer.name, @@ -451,6 +534,21 @@ private fun startShareUrlIntent( type = "text/plain" putExtra(Intent.EXTRA_TEXT, url) } + val shareIntent = Intent.createChooser(sendIntent, stringRes(context, R.string.share_as_image_url)) + context.startActivity(shareIntent) +} + +private fun startShareImageFileIntent( + context: Context, + uri: Uri, +) { + val sendIntent = + Intent().apply { + action = Intent.ACTION_SEND + type = PNG_MIME + putExtra(Intent.EXTRA_STREAM, uri) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } val shareIntent = Intent.createChooser(sendIntent, stringRes(context, R.string.share_as_image)) context.startActivity(shareIntent) } diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 3acb23326d..8ecd0164dd 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -474,6 +474,7 @@ Share Browser Link Share Share as Image + Share as Image Url Generating preview… Shared via Amethyst Author ID