Android: export and copy summaries and transcripts

- summary: SelectionContainer (select text directly), Kopieren
  (clipboard + toast), Teilen (share sheet) and Datei speichern (written
  to Download/meetrec via MediaStore, no permission needed on API 29+)
- transcript: Datei speichern alongside the existing share
- installed and verified building; sample of summary export names:
  meetrec-summary-<recording-id>.md / meetrec-transcript-<id>.txt
This commit is contained in:
2026-09-09 09:26:23 +02:00
parent 098d9b63fa
commit 9d3e553833
@@ -1,9 +1,16 @@
package com.meetrec.android
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentValues
import android.content.Context
import android.content.Intent
import android.media.MediaPlayer
import android.os.Environment
import android.provider.MediaStore
import android.widget.Toast
import androidx.compose.foundation.clickable
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
@@ -387,14 +394,42 @@ private fun LibraryDetail(
when {
m.summary == "pending" -> Text("wird erstellt …",
style = MaterialTheme.typography.bodySmall)
summaryText != null -> Text(
summaryText!!,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 240.dp)
.verticalScroll(rememberScrollState()),
)
summaryText != null -> {
SelectionContainer {
Text(
summaryText!!,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 240.dp)
.verticalScroll(rememberScrollState()),
)
}
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = {
copyToClipboard(context, summaryText!!)
}) { Text("Kopieren") }
OutlinedButton(onClick = {
shareText(context, summaryText!!, "MeetRec — Zusammenfassung")
}) { Text("Teilen") }
OutlinedButton(onClick = {
scope.launch {
try {
val name = "meetrec-summary-" +
(item as? LibraryItem.Server)?.meta?.id
.orEmpty() + ".md"
val path = withContext(Dispatchers.IO) {
saveTextToDownloads(context, name, summaryText!!)
}
Toast.makeText(context, "gespeichert: $path",
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
detailError = "save failed: ${e.message}"
}
}
}) { Text("Datei speichern") }
}
}
m.summary != null && m.summary.startsWith("error") -> {
Text(m.summary, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall)
@@ -481,8 +516,30 @@ private fun LibraryDetail(
Text("No transcript available for this recording.",
style = MaterialTheme.typography.bodySmall)
} else {
Text("Transcript (${d.segments.size} segments)",
style = MaterialTheme.typography.titleSmall)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Text("Transcript (${d.segments.size} segments)",
style = MaterialTheme.typography.titleSmall,
modifier = Modifier.align(androidx.compose.ui.Alignment.CenterVertically))
OutlinedButton(onClick = {
scope.launch {
try {
val text = d.segments.joinToString("\n") {
"[${fmtMs(it.first)}] ${it.second}"
}
val name = "meetrec-transcript-" +
(item as? LibraryItem.Server)?.meta?.id
.orEmpty() + ".txt"
val path = withContext(Dispatchers.IO) {
saveTextToDownloads(context, name, text)
}
Toast.makeText(context, "gespeichert: $path",
Toast.LENGTH_LONG).show()
} catch (e: Exception) {
detailError = "save failed: ${e.message}"
}
}
}) { Text("Datei speichern") }
}
LazyColumn(Modifier.fillMaxWidth().weight(1f)) {
items(d.segments) { (startMs, text) ->
Column(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
@@ -563,4 +620,36 @@ private fun MediaPlayer.stopSafely() {
if (isPlaying) stop()
} catch (_: IllegalStateException) {
}
}
private fun copyToClipboard(context: Context, text: String) {
val cb = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
cb.setPrimaryClip(ClipData.newPlainText("MeetRec", text))
Toast.makeText(context, "kopiert", Toast.LENGTH_SHORT).show()
}
private fun shareText(context: Context, text: String, title: String) {
context.startActivity(
Intent.createChooser(
Intent(Intent.ACTION_SEND)
.setType("text/plain")
.putExtra(Intent.EXTRA_TEXT, text),
title,
),
)
}
/** Writes text to Download/meetrec/<fileName>; returns the relative path. */
private fun saveTextToDownloads(context: Context, fileName: String, text: String): String {
val values = ContentValues().apply {
put(MediaStore.Downloads.DISPLAY_NAME, fileName)
put(MediaStore.Downloads.MIME_TYPE, "text/plain")
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS + "/meetrec")
}
val uri = context.contentResolver.insert(
MediaStore.Downloads.EXTERNAL_CONTENT_URI, values,
) ?: throw java.io.IOException("could not create $fileName")
context.contentResolver.openOutputStream(uri)?.use { it.write(text.toByteArray()) }
?: throw java.io.IOException("could not write $fileName")
return "${Environment.DIRECTORY_DOWNLOADS}/meetrec/$fileName"
}