refactor: Replace MaterialAlertDialogBuilder with Compose AlertDialog

Replace all 11 instances of MaterialAlertDialogBuilder across 5 files with Compose AlertDialog. Use sealed dialog state per screen to prevent dialog stacking. Add shared ConfirmationDialog composable. Delete dead ErrorDialog.kt.
This commit is contained in:
darken
2026-03-09 12:46:23 +00:00
committed by Matthias Urhahn
parent cba239f411
commit d7d9d53ff3
6 changed files with 272 additions and 176 deletions
@@ -0,0 +1,32 @@
package eu.darken.capod.common.compose
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@Composable
fun ConfirmationDialog(
title: String,
message: String,
confirmLabel: String,
dismissLabel: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = title) },
text = { Text(text = message) },
confirmButton = {
TextButton(onClick = onConfirm) {
Text(text = confirmLabel)
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = dismissLabel)
}
},
)
}
@@ -17,10 +17,11 @@ import androidx.lifecycle.compose.LifecycleResumeEffect
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.res.stringResource
import androidx.core.view.WindowCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import eu.darken.capod.R
import eu.darken.capod.common.compose.ConfirmationDialog
import eu.darken.capod.common.debug.logging.logTag
import eu.darken.capod.common.theming.CapodTheme
import eu.darken.capod.common.uix.Activity2
@@ -29,6 +30,11 @@ import eu.darken.capod.main.core.currentThemeState
import eu.darken.capod.main.core.themeState
import javax.inject.Inject
private sealed interface RecorderDialog {
data object SentConfirm : RecorderDialog
data object DeleteConfirm : RecorderDialog
}
@AndroidEntryPoint
class RecorderActivity : Activity2() {
@@ -58,6 +64,7 @@ class RecorderActivity : Activity2() {
}
var hasShared by remember { mutableStateOf(false) }
var dialog by remember { mutableStateOf<RecorderDialog?>(null) }
LaunchedEffect(Unit) {
vm.events.collect { event ->
@@ -71,50 +78,44 @@ class RecorderActivity : Activity2() {
}
}
var showSentConfirm by remember { mutableStateOf(false) }
LifecycleResumeEffect(hasShared) {
if (hasShared) {
showSentConfirm = true
dialog = RecorderDialog.SentConfirm
hasShared = false
}
onPauseOrDispose {}
}
if (showSentConfirm) {
LaunchedEffect(Unit) {
MaterialAlertDialogBuilder(this@RecorderActivity).apply {
setTitle(R.string.support_debuglog_sent_title)
setMessage(R.string.support_debuglog_sent_message)
setPositiveButton(R.string.general_done_action) { _, _ ->
showSentConfirm = false
when (dialog) {
is RecorderDialog.SentConfirm -> {
ConfirmationDialog(
title = stringResource(R.string.support_debuglog_sent_title),
message = stringResource(R.string.support_debuglog_sent_message),
confirmLabel = stringResource(R.string.general_done_action),
dismissLabel = stringResource(R.string.general_cancel_action),
onConfirm = {
dialog = null
vm.discard()
}
setNegativeButton(R.string.general_cancel_action) { _, _ ->
showSentConfirm = false
}
setOnCancelListener { showSentConfirm = false }
}.show()
},
onDismiss = { dialog = null },
)
}
}
var showDeleteConfirm by remember { mutableStateOf(false) }
if (showDeleteConfirm) {
LaunchedEffect(Unit) {
MaterialAlertDialogBuilder(this@RecorderActivity).apply {
setTitle(R.string.support_debuglog_session_delete_title)
setMessage(R.string.support_debuglog_session_delete_message)
setPositiveButton(R.string.profiles_delete_action) { _, _ ->
showDeleteConfirm = false
is RecorderDialog.DeleteConfirm -> {
ConfirmationDialog(
title = stringResource(R.string.support_debuglog_session_delete_title),
message = stringResource(R.string.support_debuglog_session_delete_message),
confirmLabel = stringResource(R.string.profiles_delete_action),
dismissLabel = stringResource(R.string.general_cancel_action),
onConfirm = {
dialog = null
vm.discard()
}
setNegativeButton(R.string.general_cancel_action) { _, _ ->
showDeleteConfirm = false
}
setOnCancelListener { showDeleteConfirm = false }
}.show()
},
onDismiss = { dialog = null },
)
}
null -> {}
}
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
@@ -123,7 +124,7 @@ class RecorderActivity : Activity2() {
state = it,
onShare = { vm.share() },
onKeep = { vm.keep() },
onDiscard = { showDeleteConfirm = true },
onDiscard = { dialog = RecorderDialog.DeleteConfirm },
onPrivacyPolicy = { vm.goPrivacyPolicy() },
)
}
@@ -1,24 +1,55 @@
package eu.darken.capod.common.debug.recording.ui
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
class RecorderConsentDialog(
private val context: Context,
private val webpageTool: WebpageTool
@Composable
fun RecorderConsentDialog(
onStartRecord: () -> Unit,
onOpenPrivacyPolicy: () -> Unit,
onDismiss: () -> Unit,
) {
fun showDialog(onStartRecord: () -> Unit) {
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.support_debuglog_label)
setMessage(R.string.settings_debuglog_explanation)
setPositiveButton(R.string.debug_debuglog_record_action) { _, _ -> onStartRecord() }
setNegativeButton(R.string.general_cancel_action) { _, _ -> }
setNeutralButton(R.string.settings_privacy_policy_label) { _, _ ->
webpageTool.open(PrivacyPolicy.URL)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text(text = stringResource(R.string.support_debuglog_label)) },
text = {
Column(modifier = Modifier.fillMaxWidth()) {
Text(text = stringResource(R.string.settings_debuglog_explanation))
Spacer(modifier = Modifier.height(8.dp))
TextButton(
onClick = {
onOpenPrivacyPolicy()
onDismiss()
},
modifier = Modifier.align(Alignment.End),
) {
Text(text = stringResource(R.string.settings_privacy_policy_label))
}
}
}.show()
}
}
},
confirmButton = {
TextButton(onClick = {
onDismiss()
onStartRecord()
}) {
Text(text = stringResource(R.string.debug_debuglog_record_action))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(text = stringResource(R.string.general_cancel_action))
}
},
)
}
@@ -1,19 +0,0 @@
package eu.darken.capod.common.error
import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
fun Throwable.asErrorDialogBuilder(
context: Context
) = MaterialAlertDialogBuilder(context).apply {
val error = this@asErrorDialogBuilder
val localizedError = error.localized(context)
setTitle(localizedError.label)
setMessage(localizedError.description)
setPositiveButton(android.R.string.ok) { _, _ ->
}
}
@@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.twotone.ArrowBack
import androidx.compose.material.icons.automirrored.twotone.MenuBook
@@ -25,6 +24,7 @@ import androidx.compose.material.icons.twotone.Delete
import androidx.compose.material.icons.twotone.FiberManualRecord
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.material.icons.twotone.Warning
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
@@ -32,6 +32,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
@@ -50,10 +51,10 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.compose.ConfirmationDialog
import eu.darken.capod.common.compose.Preview2
import eu.darken.capod.common.compose.PreviewWrapper
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -65,6 +66,13 @@ import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.common.settings.SettingsBaseItem
import eu.darken.capod.common.settings.SettingsCategoryHeader
private sealed interface SupportDialog {
data object Consent : SupportDialog
data object ShortRecordingWarning : SupportDialog
data class DeleteSession(val sessionId: String) : SupportDialog
data object ClearLogs : SupportDialog
}
@Composable
fun SupportScreenHost(vm: SupportViewModel = hiltViewModel()) {
ErrorEventHandler(vm)
@@ -78,19 +86,17 @@ fun SupportScreenHost(vm: SupportViewModel = hiltViewModel()) {
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
var showShortRecordingWarning by remember { mutableStateOf(false) }
var dialog by remember { mutableStateOf<SupportDialog?>(null) }
LaunchedEffect(Unit) {
vm.events.collect { event ->
when (event) {
SupportViewModel.Event.ShowConsentDialog -> {
RecorderConsentDialog(context, WebpageTool(context)).showDialog {
vm.startDebugLog()
}
dialog = SupportDialog.Consent
}
SupportViewModel.Event.ShowShortRecordingWarning -> {
showShortRecordingWarning = true
dialog = SupportDialog.ShortRecordingWarning
}
is SupportViewModel.Event.OpenRecorderActivity -> {
@@ -103,12 +109,69 @@ fun SupportScreenHost(vm: SupportViewModel = hiltViewModel()) {
}
}
if (showShortRecordingWarning) {
ShortRecordingWarningDialog(
context = context,
onDismiss = { showShortRecordingWarning = false },
onStopAnyway = { vm.forceStopDebugLog() },
)
when (val d = dialog) {
is SupportDialog.Consent -> {
RecorderConsentDialog(
onStartRecord = {
vm.startDebugLog()
},
onOpenPrivacyPolicy = {
WebpageTool(context).open(PrivacyPolicy.URL)
},
onDismiss = { dialog = null },
)
}
is SupportDialog.ShortRecordingWarning -> {
AlertDialog(
onDismissRequest = { dialog = null },
title = { Text(text = stringResource(R.string.debug_debuglog_short_recording_title)) },
text = { Text(text = stringResource(R.string.debug_debuglog_short_recording_message)) },
confirmButton = {
TextButton(onClick = { dialog = null }) {
Text(text = stringResource(R.string.debug_debuglog_short_recording_continue))
}
},
dismissButton = {
TextButton(onClick = {
dialog = null
vm.forceStopDebugLog()
}) {
Text(text = stringResource(R.string.debug_debuglog_short_recording_stop))
}
},
)
}
is SupportDialog.DeleteSession -> {
ConfirmationDialog(
title = stringResource(R.string.support_debuglog_session_delete_title),
message = stringResource(R.string.support_debuglog_session_delete_message),
confirmLabel = stringResource(R.string.profiles_delete_action),
dismissLabel = stringResource(R.string.general_cancel_action),
onConfirm = {
dialog = null
vm.deleteSession(d.sessionId)
},
onDismiss = { dialog = null },
)
}
is SupportDialog.ClearLogs -> {
ConfirmationDialog(
title = stringResource(R.string.support_debuglog_session_delete_title),
message = stringResource(R.string.support_debuglog_clear_all_message),
confirmLabel = stringResource(R.string.profiles_delete_action),
dismissLabel = stringResource(R.string.general_cancel_action),
onConfirm = {
dialog = null
vm.clearDebugLogs()
},
onDismiss = { dialog = null },
)
}
null -> {}
}
state?.let {
@@ -122,53 +185,13 @@ fun SupportScreenHost(vm: SupportViewModel = hiltViewModel()) {
onTroubleShooter = { vm.goToTroubleShooter() },
onDebugLogToggle = { vm.onDebugLogToggle() },
onOpenSession = { vm.openSession(it) },
onDeleteSession = { id ->
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.support_debuglog_session_delete_title)
setMessage(R.string.support_debuglog_session_delete_message)
setPositiveButton(R.string.profiles_delete_action) { _, _ ->
vm.deleteSession(id)
}
setNegativeButton(R.string.general_cancel_action) { _, _ -> }
}.show()
},
onDeleteSession = { id -> dialog = SupportDialog.DeleteSession(id) },
onStopRecording = { vm.onDebugLogToggle() },
onClearLogs = {
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.support_debuglog_session_delete_title)
setMessage(R.string.support_debuglog_clear_all_message)
setPositiveButton(R.string.profiles_delete_action) { _, _ ->
vm.clearDebugLogs()
}
setNegativeButton(R.string.general_cancel_action) { _, _ -> }
}.show()
},
onClearLogs = { dialog = SupportDialog.ClearLogs },
)
}
}
@Composable
private fun ShortRecordingWarningDialog(
context: android.content.Context,
onDismiss: () -> Unit,
onStopAnyway: () -> Unit,
) {
LaunchedEffect(Unit) {
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.debug_debuglog_short_recording_title)
setMessage(R.string.debug_debuglog_short_recording_message)
setPositiveButton(R.string.debug_debuglog_short_recording_continue) { _, _ ->
onDismiss()
}
setNegativeButton(R.string.debug_debuglog_short_recording_stop) { _, _ ->
onDismiss()
onStopAnyway()
}
setOnCancelListener { onDismiss() }
}.show()
}
}
@Composable
fun SupportScreen(
state: SupportViewModel.State,
@@ -23,6 +23,7 @@ import androidx.compose.material.icons.twotone.BugReport
import androidx.compose.material.icons.twotone.Cancel
import androidx.compose.material.icons.twotone.Delete
import androidx.compose.material.icons.twotone.Email
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
@@ -37,6 +38,7 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -55,15 +57,23 @@ import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.LifecycleResumeEffect
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.darken.capod.R
import eu.darken.capod.common.PrivacyPolicy
import eu.darken.capod.common.WebpageTool
import eu.darken.capod.common.compose.ConfirmationDialog
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import eu.darken.capod.common.debug.recording.ui.RecorderConsentDialog
import eu.darken.capod.common.error.ErrorEventHandler
import eu.darken.capod.common.navigation.NavigationEventHandler
import eu.darken.capod.main.ui.settings.support.contactform.ContactFormViewModel.Category
private sealed interface ContactFormDialog {
data object SentConfirm : ContactFormDialog
data object Consent : ContactFormDialog
data object ShortRecordingWarning : ContactFormDialog
data class DeleteSession(val sessionId: String) : ContactFormDialog
}
@Composable
fun ContactFormScreenHost(vm: ContactFormViewModel = hiltViewModel()) {
ErrorEventHandler(vm)
@@ -73,36 +83,17 @@ fun ContactFormScreenHost(vm: ContactFormViewModel = hiltViewModel()) {
val snackbarHostState = remember { SnackbarHostState() }
var hasSentEmail by remember { mutableStateOf(false) }
var showSentConfirm by remember { mutableStateOf(false) }
var dialog by remember { mutableStateOf<ContactFormDialog?>(null) }
LifecycleResumeEffect(hasSentEmail) {
vm.refreshLogSessions()
if (hasSentEmail) {
showSentConfirm = true
dialog = ContactFormDialog.SentConfirm
hasSentEmail = false
}
onPauseOrDispose {}
}
if (showSentConfirm) {
LaunchedEffect(Unit) {
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.support_contact_sent_title)
setMessage(R.string.support_contact_sent_message)
setPositiveButton(R.string.general_done_action) { _, _ ->
showSentConfirm = false
vm.confirmSent()
}
setNegativeButton(R.string.general_cancel_action) { _, _ ->
showSentConfirm = false
}
setOnCancelListener { showSentConfirm = false }
}.show()
}
}
var showShortRecordingWarning by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
vm.events.collect { event ->
when (event) {
@@ -123,33 +114,79 @@ fun ContactFormScreenHost(vm: ContactFormViewModel = hiltViewModel()) {
}
ContactFormViewModel.Event.ShowConsentDialog -> {
RecorderConsentDialog(context, WebpageTool(context)).showDialog {
vm.doStartRecording()
}
dialog = ContactFormDialog.Consent
}
ContactFormViewModel.Event.ShowShortRecordingWarning -> {
showShortRecordingWarning = true
dialog = ContactFormDialog.ShortRecordingWarning
}
}
}
}
if (showShortRecordingWarning) {
LaunchedEffect(Unit) {
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.debug_debuglog_short_recording_title)
setMessage(R.string.debug_debuglog_short_recording_message)
setPositiveButton(R.string.debug_debuglog_short_recording_continue) { _, _ ->
showShortRecordingWarning = false
}
setNegativeButton(R.string.debug_debuglog_short_recording_stop) { _, _ ->
showShortRecordingWarning = false
vm.forceStopRecording()
}
setOnCancelListener { showShortRecordingWarning = false }
}.show()
when (val d = dialog) {
is ContactFormDialog.SentConfirm -> {
ConfirmationDialog(
title = stringResource(R.string.support_contact_sent_title),
message = stringResource(R.string.support_contact_sent_message),
confirmLabel = stringResource(R.string.general_done_action),
dismissLabel = stringResource(R.string.general_cancel_action),
onConfirm = {
dialog = null
vm.confirmSent()
},
onDismiss = { dialog = null },
)
}
is ContactFormDialog.Consent -> {
RecorderConsentDialog(
onStartRecord = {
vm.doStartRecording()
},
onOpenPrivacyPolicy = {
WebpageTool(context).open(PrivacyPolicy.URL)
},
onDismiss = { dialog = null },
)
}
is ContactFormDialog.ShortRecordingWarning -> {
AlertDialog(
onDismissRequest = { dialog = null },
title = { Text(text = stringResource(R.string.debug_debuglog_short_recording_title)) },
text = { Text(text = stringResource(R.string.debug_debuglog_short_recording_message)) },
confirmButton = {
TextButton(onClick = { dialog = null }) {
Text(text = stringResource(R.string.debug_debuglog_short_recording_continue))
}
},
dismissButton = {
TextButton(onClick = {
dialog = null
vm.forceStopRecording()
}) {
Text(text = stringResource(R.string.debug_debuglog_short_recording_stop))
}
},
)
}
is ContactFormDialog.DeleteSession -> {
ConfirmationDialog(
title = stringResource(R.string.support_contact_debuglog_delete_title),
message = stringResource(R.string.support_contact_debuglog_delete_message),
confirmLabel = stringResource(R.string.profiles_delete_action),
dismissLabel = stringResource(R.string.general_cancel_action),
onConfirm = {
dialog = null
vm.deleteLogSession(d.sessionId)
},
onDismiss = { dialog = null },
)
}
null -> {}
}
val state by vm.state.collectAsStateWithLifecycle(initialValue = null)
@@ -162,16 +199,7 @@ fun ContactFormScreenHost(vm: ContactFormViewModel = hiltViewModel()) {
onDescriptionChange = { vm.updateDescription(it) },
onExpectedChange = { vm.updateExpectedBehavior(it) },
onSelectSession = { vm.selectLogSession(it) },
onDeleteSession = { id ->
MaterialAlertDialogBuilder(context).apply {
setTitle(R.string.support_contact_debuglog_delete_title)
setMessage(R.string.support_contact_debuglog_delete_message)
setPositiveButton(R.string.profiles_delete_action) { _, _ ->
vm.deleteLogSession(id)
}
setNegativeButton(R.string.general_cancel_action) { _, _ -> }
}.show()
},
onDeleteSession = { id -> dialog = ContactFormDialog.DeleteSession(id) },
onStartRecording = { vm.startRecording() },
onStopRecording = { vm.stopRecording() },
onSend = { vm.send() },