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 package com.meetrec.android
import android.content.ClipData
import android.content.ClipboardManager
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.media.MediaPlayer 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.clickable
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -387,14 +394,42 @@ private fun LibraryDetail(
when { when {
m.summary == "pending" -> Text("wird erstellt …", m.summary == "pending" -> Text("wird erstellt …",
style = MaterialTheme.typography.bodySmall) style = MaterialTheme.typography.bodySmall)
summaryText != null -> Text( summaryText != null -> {
summaryText!!, SelectionContainer {
style = MaterialTheme.typography.bodyMedium, Text(
modifier = Modifier summaryText!!,
.fillMaxWidth() style = MaterialTheme.typography.bodyMedium,
.heightIn(max = 240.dp) modifier = Modifier
.verticalScroll(rememberScrollState()), .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") -> { m.summary != null && m.summary.startsWith("error") -> {
Text(m.summary, color = MaterialTheme.colorScheme.error, Text(m.summary, color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall) style = MaterialTheme.typography.bodySmall)
@@ -481,8 +516,30 @@ private fun LibraryDetail(
Text("No transcript available for this recording.", Text("No transcript available for this recording.",
style = MaterialTheme.typography.bodySmall) style = MaterialTheme.typography.bodySmall)
} else { } else {
Text("Transcript (${d.segments.size} segments)", Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
style = MaterialTheme.typography.titleSmall) 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)) { LazyColumn(Modifier.fillMaxWidth().weight(1f)) {
items(d.segments) { (startMs, text) -> items(d.segments) { (startMs, text) ->
Column(Modifier.fillMaxWidth().padding(vertical = 4.dp)) { Column(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
@@ -563,4 +620,36 @@ private fun MediaPlayer.stopSafely() {
if (isPlaying) stop() if (isPlaying) stop()
} catch (_: IllegalStateException) { } 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"
} }