In-app preview of the doctor summary before sharing

New SummaryPreviewScreen renders the generated PDF page by page with
the platform PdfRenderer (2x scale, no new dependencies) and puts share
in the top bar — see before you send. The settings button now opens the
preview instead of jumping straight to the share sheet; the share path
reuses the already-rendered file via DoctorSummaryPdf.shareIntentFor.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-06-11 17:27:59 +02:00
commit e16fd72d06
5 changed files with 152 additions and 13 deletions

View file

@ -46,8 +46,10 @@ class DoctorSummaryPdf(private val context: Context) {
)
/** Generates the PDF and returns a ready-to-launch share chooser intent. */
suspend fun buildShareIntent(): Intent {
val file = render()
suspend fun buildShareIntent(): Intent = shareIntentFor(render())
/** Share chooser for an already-rendered summary (the preview screen's path). */
fun shareIntentFor(file: File): Intent {
val uri = FileProvider.getUriForFile(context, "${context.packageName}.fileprovider", file)
val send = Intent(Intent.ACTION_SEND)
.setType("application/pdf")

View file

@ -21,6 +21,7 @@ sealed interface Screen {
/** itemId == null → create new. */
data class ItemEdit(val itemId: Long?) : Screen
data object Settings : Screen
data object SummaryPreview : Screen
}
class Nav(initial: Screen) {
@ -50,5 +51,6 @@ fun AppRoot() {
is Screen.Inventory -> InventoryScreen(nav)
is Screen.ItemEdit -> ItemEditScreen(nav, screen.itemId)
is Screen.Settings -> SettingsScreen(nav)
is Screen.SummaryPreview -> SummaryPreviewScreen(nav)
}
}

View file

@ -45,7 +45,6 @@ import no.naiv.meddetsamme.R
import no.naiv.meddetsamme.alarm.AlarmScheduler
import no.naiv.meddetsamme.backup.BackupManager
import no.naiv.meddetsamme.fest.FestRepository
import no.naiv.meddetsamme.pdf.DoctorSummaryPdf
import no.naiv.meddetsamme.settings.SettingsStore
@OptIn(ExperimentalMaterial3Api::class)
@ -204,17 +203,9 @@ fun SettingsScreen(nav: Nav) {
HorizontalDivider()
Text("Legeoversikt", style = MaterialTheme.typography.titleMedium)
OutlinedButton(
onClick = {
scope.launch {
try {
context.startActivity(DoctorSummaryPdf(context).buildShareIntent())
} catch (e: Exception) {
toast("Kunne ikke lage PDF: ${e.message}")
}
}
},
onClick = { nav.push(Screen.SummaryPreview) },
modifier = Modifier.fillMaxWidth(),
) { Text("Del medisinoversikt (PDF)") }
) { Text("Vis medisinoversikt (PDF)") }
}
}

View file

@ -0,0 +1,136 @@
package no.naiv.meddetsamme.ui
import android.graphics.Bitmap
import android.graphics.pdf.PdfRenderer
import android.os.ParcelFileDescriptor
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import java.io.File
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import no.naiv.meddetsamme.R
import no.naiv.meddetsamme.pdf.DoctorSummaryPdf
/**
* In-app preview of the doctor summary: the generated PDF rendered page by
* page with the platform PdfRenderer (no viewer app needed, no dependencies),
* with share in the top bar. See before you send.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SummaryPreviewScreen(nav: Nav) {
val context = LocalContext.current
var pages by remember { mutableStateOf<List<Bitmap>>(emptyList()) }
var pdfFile by remember { mutableStateOf<File?>(null) }
var error by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
withContext(Dispatchers.IO) {
try {
val pdf = DoctorSummaryPdf(context)
val file = pdf.render()
val rendered = mutableListOf<Bitmap>()
ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY).use { fd ->
PdfRenderer(fd).use { renderer ->
for (i in 0 until renderer.pageCount) {
renderer.openPage(i).use { page ->
// 2x point size — crisp on phone screens without
// burning memory on huge bitmaps.
val bmp = Bitmap.createBitmap(page.width * 2, page.height * 2, Bitmap.Config.ARGB_8888)
bmp.eraseColor(android.graphics.Color.WHITE)
page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
rendered += bmp
}
}
}
}
pdfFile = file
pages = rendered
} catch (e: Exception) {
error = e.message ?: "Ukjent feil"
}
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Medisinoversikt") },
navigationIcon = {
IconButton(onClick = { nav.pop() }) {
Icon(painterResource(R.drawable.ic_back), contentDescription = "Tilbake")
}
},
actions = {
IconButton(
onClick = {
pdfFile?.let {
context.startActivity(DoctorSummaryPdf(context).shareIntentFor(it))
}
},
enabled = pdfFile != null,
) {
Icon(painterResource(R.drawable.ic_share), contentDescription = "Del PDF")
}
},
)
},
) { padding ->
when {
error != null -> Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
Text("Kunne ikke lage oversikten: $error", color = MaterialTheme.colorScheme.error)
}
pages.isEmpty() -> Box(Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
CircularProgressIndicator()
}
else -> LazyColumn(
modifier = Modifier.fillMaxSize().padding(padding),
contentPadding = androidx.compose.foundation.layout.PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
itemsIndexed(pages) { index, bmp ->
Surface(
shadowElevation = 2.dp,
shape = MaterialTheme.shapes.small,
modifier = Modifier.fillMaxWidth(),
) {
Image(
bitmap = bmp.asImageBitmap(),
contentDescription = "Side ${index + 1} av ${pages.size} av medisinoversikten",
contentScale = ContentScale.FillWidth,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
}
}

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:height="24dp"
android:viewportWidth="24" android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92s2.92,-1.31 2.92,-2.92 -1.31,-2.92 -2.92,-2.92z" />
</vector>