fix(android): replace clipboard auto-detect with manual Paste button (#344)

Android 13+ restricts clipboard access for background-to-foreground
transitions — the auto-detect banner never appeared on resume.
Replace with a permanent Paste button that reads clipboard on tap
(user interaction grants clipboard permission).

Also: Export button is now icon-only (share icon) to save space.

Co-authored-by: yyoyoian-pixel <279225925+yyoyoian-pixel@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
yyoyoian-pixel
2026-04-27 22:22:52 +02:00
committed by GitHub
parent 915dba76cd
commit f5bd82b64e
@@ -63,13 +63,9 @@ fun ConfigSharingBar(
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val clipText = clipboard.getText()?.text.orEmpty()
val hasConfigInClipboard = clipText.isNotEmpty() && ConfigStore.looksLikeConfig(clipText)
var showExportDialog by remember { mutableStateOf(false) } var showExportDialog by remember { mutableStateOf(false) }
var showImportConfirm by remember { mutableStateOf(false) } var showImportConfirm by remember { mutableStateOf(false) }
var pendingImport by remember { mutableStateOf<MhrvConfig?>(null) } var pendingImport by remember { mutableStateOf<MhrvConfig?>(null) }
var showQrDialog by remember { mutableStateOf(false) }
// QR scanner launcher — fires the ZXing embedded scanner activity. // QR scanner launcher — fires the ZXing embedded scanner activity.
val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result -> val scanLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
@@ -83,58 +79,32 @@ fun ConfigSharingBar(
} }
} }
// --- Paste from clipboard banner --- // --- Export + Paste + Scan row ---
if (hasConfigInClipboard) {
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
),
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
"Config detected in clipboard",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.weight(1f),
)
FilledTonalButton(
onClick = {
val decoded = ConfigStore.decode(clipText)
if (decoded != null) {
pendingImport = decoded
showImportConfirm = true
} else {
scope.launch { onSnackbar(ctx.getString(R.string.snack_invalid_config)) }
}
},
) {
Icon(Icons.Default.ContentPaste, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.btn_import_clipboard))
}
}
}
}
// --- Export + Scan row ---
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp),
) { ) {
IconButton(onClick = { showExportDialog = true }) {
Icon(Icons.Default.Share, contentDescription = stringResource(R.string.btn_export_config))
}
// Manual paste — reads clipboard on tap. Android 13+ restricts
// background clipboard access, so auto-detect doesn't work.
// User interaction (tap) grants clipboard permission.
OutlinedButton( OutlinedButton(
onClick = { showExportDialog = true }, onClick = {
modifier = Modifier.weight(1f), val text = clipboard.getText()?.text.orEmpty()
val decoded = ConfigStore.decode(text)
if (decoded != null) {
pendingImport = decoded
showImportConfirm = true
} else {
scope.launch { onSnackbar(ctx.getString(R.string.snack_invalid_config)) }
}
},
) { ) {
Icon(Icons.Default.Share, null, modifier = Modifier.size(18.dp)) Icon(Icons.Default.ContentPaste, null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(4.dp)) Spacer(Modifier.width(4.dp))
Text(stringResource(R.string.btn_export_config)) Text("Paste")
} }
OutlinedButton( OutlinedButton(
onClick = { onClick = {