feat(common): add ConfirmDialog and safeLaunchAction helper
Add reusable ConfirmDialog with destructive action styling. Add safeLaunchAction to ViewModelExt for mutating calls with unit tests.
This commit is contained in:
1 parent
70f331b931
commit
e6e79be10d
3 files changed
+202
No files matched your search
@@ -0,0 +1,101 @@
|
||||
package com.m57.hermescontrol.ui.common
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.m57.hermescontrol.R
|
||||
|
||||
/**
|
||||
* Standard confirmation dialog across the app.
|
||||
* Supports destructive actions with error styling, optional title icons, and optional extra body content.
|
||||
*/
|
||||
@Composable
|
||||
fun ConfirmDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
onConfirm: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
confirmText: String = stringResource(R.string.action_confirm),
|
||||
dismissText: String = stringResource(R.string.action_cancel),
|
||||
isDestructive: Boolean = false,
|
||||
icon: ImageVector? = null,
|
||||
extraContent: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
modifier = modifier,
|
||||
title = {
|
||||
if (icon != null) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint =
|
||||
if (isDestructive) {
|
||||
MaterialTheme.colorScheme.error
|
||||
} else {
|
||||
MaterialTheme.colorScheme.primary
|
||||
},
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(title)
|
||||
}
|
||||
} else {
|
||||
Text(title)
|
||||
}
|
||||
},
|
||||
text = {
|
||||
if (extraContent != null) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(message)
|
||||
extraContent()
|
||||
}
|
||||
} else {
|
||||
Text(message)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
if (isDestructive) {
|
||||
Button(
|
||||
onClick = onConfirm,
|
||||
colors =
|
||||
ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.error,
|
||||
contentColor = MaterialTheme.colorScheme.onError,
|
||||
),
|
||||
) {
|
||||
Text(confirmText)
|
||||
}
|
||||
} else {
|
||||
Button(onClick = onConfirm) {
|
||||
Text(confirmText)
|
||||
}
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(dismissText)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -34,6 +34,33 @@ inline fun <T> ViewModel.safeLaunchLoad(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes a mutating API call inside viewModelScope with onStart, onSuccess, onError, and onComplete lifecycle callbacks.
|
||||
* Like safeLaunchLoad, avoids Dispatchers.IO hop to protect tests from dispatcher poisoning.
|
||||
*/
|
||||
inline fun <T> ViewModel.safeLaunchAction(
|
||||
currentJob: Job? = null,
|
||||
crossinline onStart: () -> Unit = {},
|
||||
crossinline apiCall: suspend () -> NetworkResult<T>,
|
||||
crossinline onSuccess: (T) -> Unit = {},
|
||||
crossinline onError: (String) -> Unit = {},
|
||||
crossinline onComplete: () -> Unit = {},
|
||||
): Job {
|
||||
if (currentJob?.isActive == true) return currentJob
|
||||
onStart()
|
||||
return viewModelScope.launch {
|
||||
try {
|
||||
val result = apiCall()
|
||||
when (result) {
|
||||
is NetworkResult.Success -> onSuccess(result.data)
|
||||
is NetworkResult.Failure -> onError(result.error.message)
|
||||
}
|
||||
} finally {
|
||||
onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect a gateway change event ([com.m57.hermescontrol.data.ws.ChangeEvents])
|
||||
* from [ChangeEventHub] and run a **silent** refresh: no loading spinner, no
|
||||
|
||||
Reference in new issue
Block a user