diff --git a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt index 0e709858..f70c90e9 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/achievements/AchievementsViewModel.kt @@ -1,18 +1,14 @@ package com.m57.hermescontrol.ui.achievements import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.Achievement import com.m57.hermescontrol.data.remote.ApiClient -import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall -import kotlinx.coroutines.Dispatchers +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext data class AchievementsUiState( val isLoading: Boolean = false, @@ -25,31 +21,25 @@ class AchievementsViewModel : ViewModel() { val uiState: StateFlow = _uiState.asStateFlow() fun loadAchievements() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getAchievements() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getAchievements() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { + it.copy( + isLoading = false, + achievements = data.achievements.orEmpty(), + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { - it.copy( - isLoading = false, - achievements = result.data.achievements.orEmpty(), - ) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load achievements: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load achievements: ${result.error.message}", - ) - } - } - } - } + }, + ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt index 8d3f55d3..7885d926 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.channels -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -27,7 +26,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -39,6 +37,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -49,7 +48,6 @@ fun ChannelsScreen( viewModel: ChannelsViewModel = viewModel { ChannelsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -64,12 +62,7 @@ fun ChannelsScreen( viewModel.loadPlatforms() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_channels)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt index 020fceae..ee85468b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/channels/ChannelsViewModel.kt @@ -1,19 +1,16 @@ package com.m57.hermescontrol.ui.channels import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.MessagingPlatform import com.m57.hermescontrol.data.model.MessagingPlatformUpdate import com.m57.hermescontrol.data.remote.ApiClient -import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall -import kotlinx.coroutines.Dispatchers +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext data class ChannelsUiState( val isLoading: Boolean = false, @@ -22,67 +19,55 @@ data class ChannelsUiState( val toastMessage: String? = null, ) -class ChannelsViewModel : ViewModel() { +class ChannelsViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(ChannelsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadPlatforms() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { it.copy(isLoading = false, platforms = data?.platforms.orEmpty()) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load platforms: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { it.copy(isLoading = false, platforms = result.data?.platforms.orEmpty()) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load platforms: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun configurePlatform( platformId: String, update: MessagingPlatformUpdate, ) { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.configurePlatform(platformId, update) } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.configurePlatform(platformId, update) } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + val message = "$platformId configured successfully — restart the gateway for changes to take effect" + _uiState.update { + it.copy( + isLoading = false, + toastMessage = message, + ) } - when (result) { - is NetworkResult.Success -> { - val message = "$platformId configured successfully — restart the gateway for changes to take effect" - _uiState.update { - it.copy( - isLoading = false, - toastMessage = message, - ) - } - loadPlatforms() + loadPlatforms() + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to configure platform: $errorMsg", + toastMessage = "Failed to configure platform: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to configure platform: ${result.error.message}", - toastMessage = "Failed to configure platform: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun configurePlatform( @@ -92,7 +77,7 @@ class ChannelsViewModel : ViewModel() { configurePlatform(platformId, MessagingPlatformUpdate(env = config)) } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt index b8d19649..b76406a8 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/SharedComponents.kt @@ -343,3 +343,19 @@ fun InfoRow( ) } } + +// ── ToastEffect — helper for one-shot toast messages ─────────────────── + +@Composable +fun ToastEffect( + toastMessage: String?, + onClearToast: () -> Unit, +) { + val context = androidx.compose.ui.platform.LocalContext.current + androidx.compose.runtime.LaunchedEffect(toastMessage) { + toastMessage?.let { msg -> + android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_SHORT).show() + onClearToast() + } + } +} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/common/ViewModelExt.kt b/app/src/main/java/com/m57/hermescontrol/ui/common/ViewModelExt.kt new file mode 100644 index 00000000..118e4ead --- /dev/null +++ b/app/src/main/java/com/m57/hermescontrol/ui/common/ViewModelExt.kt @@ -0,0 +1,28 @@ +package com.m57.hermescontrol.ui.common + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.m57.hermescontrol.data.remote.NetworkResult +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface ToastHost { + fun clearToast() +} + +inline fun ViewModel.safeLaunchLoad( + crossinline apiCall: suspend () -> NetworkResult, + crossinline onStart: () -> Unit, + crossinline onSuccess: (T) -> Unit, + crossinline onError: (String) -> Unit, +) { + onStart() + viewModelScope.launch { + val result = withContext(Dispatchers.IO) { apiCall() } + when (result) { + is NetworkResult.Success -> onSuccess(result.data) + is NetworkResult.Failure -> onError(result.error.message ?: "Unknown error") + } + } +} diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt index 2a410214..7dcc9f65 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.config -import android.widget.Toast import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -25,7 +24,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp @@ -35,6 +33,7 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.ToastEffect @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -44,19 +43,13 @@ fun ConfigScreen( viewModel: ConfigViewModel = viewModel { ConfigViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var editingText by remember(state.yamlText) { mutableStateOf(state.yamlText ?: "") } LaunchedEffect(Unit) { viewModel.loadRawConfig() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.config_screen_title)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt index d18f5744..5a42040a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/config/ConfigViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.UpdateRawConfigRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,38 +25,32 @@ data class ConfigUiState( val toastMessage: String? = null, ) -class ConfigViewModel : ViewModel() { +class ConfigViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(ConfigUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadRawConfig() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getRawConfig() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getRawConfig() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { + it.copy( + isLoading = false, + path = data.path, + yamlText = data.yaml, + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { - it.copy( - isLoading = false, - path = result.data.path, - yamlText = result.data.yaml, - ) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load config: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load config: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun saveRawConfig(yamlText: String) { @@ -87,7 +83,7 @@ class ConfigViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt index ca1b546f..aac78545 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.cron -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -26,7 +25,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -39,6 +37,7 @@ import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadgeType +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing import com.m57.hermescontrol.util.CronExpressionFormatter @@ -50,19 +49,13 @@ fun CronJobsScreen( viewModel: CronJobsViewModel = viewModel { CronJobsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current val spacing = LocalSpacing.current LaunchedEffect(Unit) { viewModel.loadCronJobs() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_cron)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt index 7fa43f7b..cd7fd8cc 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/cron/CronJobsViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.CronJob import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,32 +23,26 @@ data class CronJobsUiState( val toastMessage: String? = null, ) -class CronJobsViewModel : ViewModel() { +class CronJobsViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(CronJobsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadCronJobs() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getCronJobs() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getCronJobs() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { it.copy(isLoading = false, jobs = data.orEmpty()) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load cron jobs: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { it.copy(isLoading = false, jobs = result.data.orEmpty()) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load cron jobs: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun pauseCronJob(id: String) { @@ -135,7 +131,7 @@ class CronJobsViewModel : ViewModel() { _uiState.update { it.copy(jobs = originalJobs, toastMessage = errorMsg) } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt index 5c9e6641..dc00964d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.gateway -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -31,7 +30,6 @@ import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -41,6 +39,7 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.ToastEffect @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -50,18 +49,12 @@ fun GatewayScreen( viewModel: GatewayViewModel = viewModel { GatewayViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current LaunchedEffect(Unit) { viewModel.loadStatus() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_gateway)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt index f3da7fc1..60deb3f4 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/gateway/GatewayViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.StatusResponse import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -22,32 +24,26 @@ data class GatewayUiState( val toastMessage: String? = null, ) -class GatewayViewModel : ViewModel() { +class GatewayViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(GatewayUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadStatus() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getStatus() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getStatus() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { it.copy(isLoading = false, status = data) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load status: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { it.copy(isLoading = false, status = result.data) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load status: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun startGateway() { @@ -92,7 +88,7 @@ class GatewayViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt index e6aff9bc..883d00f0 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.kanban -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -38,7 +37,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -50,6 +48,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect private const val DEFAULT_COLUMN = "todo" @@ -61,7 +60,6 @@ fun KanbanScreen( viewModel: KanbanViewModel = viewModel { KanbanViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -80,12 +78,7 @@ fun KanbanScreen( viewModel.loadBoards() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.kanban_board_title)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt index 2bf9908e..0d25ff6c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/kanban/KanbanViewModel.kt @@ -8,6 +8,8 @@ import com.m57.hermescontrol.data.model.KanbanTask import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -26,38 +28,32 @@ data class KanbanUiState( val toastMessage: String? = null, ) -class KanbanViewModel : ViewModel() { +class KanbanViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(KanbanUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadBoards() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getKanbanBoards() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getKanbanBoards() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + val boards = data.boards.orEmpty() + _uiState.update { it.copy(isLoading = false, boards = boards) } + val currentSlug = data.current + val currentBoard = boards.find { it.id == currentSlug } ?: boards.firstOrNull() + if (currentBoard != null) { + selectBoard(currentBoard) } - when (result) { - is NetworkResult.Success -> { - val boards = result.data.boards.orEmpty() - _uiState.update { it.copy(isLoading = false, boards = boards) } - val currentSlug = result.data.current - val currentBoard = boards.find { it.id == currentSlug } ?: boards.firstOrNull() - if (currentBoard != null) { - selectBoard(currentBoard) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load Kanban boards: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load Kanban boards: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun selectBoard(board: KanbanBoard) { @@ -183,7 +179,7 @@ class KanbanViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt index 6a068f29..684f9173 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.keys -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -34,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp @@ -46,6 +44,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -57,7 +56,6 @@ fun KeysScreen( viewModel: KeysViewModel = viewModel { KeysViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -73,12 +71,7 @@ fun KeysScreen( viewModel.loadKeys() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_keys)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt index 839a94f2..a02778c5 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/keys/KeysViewModel.kt @@ -8,6 +8,8 @@ import com.m57.hermescontrol.data.model.EnvVarUpdate import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -24,37 +26,31 @@ data class KeysUiState( val toastMessage: String? = null, ) -class KeysViewModel : ViewModel() { +class KeysViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(KeysUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadKeys(reveal: Boolean = false) { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getEnvVars() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getEnvVars() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { + it.copy( + isLoading = false, + envVars = data.orEmpty(), + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { - it.copy( - isLoading = false, - envVars = result.data.orEmpty(), - ) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load keys: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load keys: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun revealKey(key: String) { @@ -110,7 +106,7 @@ class KeysViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt index 9ab64175..ab228e5d 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.logs -import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -20,7 +19,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp @@ -34,6 +32,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect @Composable fun LogsScreen( @@ -42,7 +41,6 @@ fun LogsScreen( viewModel: LogsViewModel = viewModel { LogsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current val spacing = LocalSpacing.current val listState = rememberLazyListState() @@ -59,12 +57,7 @@ fun LogsScreen( viewModel.loadLogs() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) // Auto scroll to bottom when new logs arrive LaunchedEffect(filteredLogs.size) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt index f538ce0e..c791e1bb 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/logs/LogsViewModel.kt @@ -1,17 +1,14 @@ package com.m57.hermescontrol.ui.logs import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.remote.ApiClient -import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall -import kotlinx.coroutines.Dispatchers +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext data class LogsUiState( val isLoading: Boolean = false, @@ -20,37 +17,31 @@ data class LogsUiState( val toastMessage: String? = null, ) -class LogsViewModel : ViewModel() { +class LogsViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(LogsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadLogs() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getLogs() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getLogs() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + val body = data + val logsList = body.lines ?: body.logs ?: emptyList() + _uiState.update { it.copy(isLoading = false, logs = logsList) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load logs: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - val body = result.data - val logsList = body.lines ?: body.logs ?: emptyList() - _uiState.update { it.copy(isLoading = false, logs = logsList) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load logs: ${result.error.message}", - ) - } - } - } - } + }, + ) } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index 8577d5de..0b5016c7 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.mcp -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,7 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -41,6 +39,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -52,7 +51,6 @@ fun McpServersScreen( viewModel: McpServersViewModel = viewModel { McpServersViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -69,12 +67,7 @@ fun McpServersScreen( viewModel.loadServers() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_mcp_servers)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt index 25374a95..d4db6bad 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt @@ -7,6 +7,8 @@ import com.m57.hermescontrol.data.model.McpServerToggleRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -23,37 +25,31 @@ data class McpServersUiState( val toastMessage: String? = null, ) -class McpServersViewModel : ViewModel() { +class McpServersViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(McpServersUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadServers() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getMcpServers() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getMcpServers() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { + it.copy( + isLoading = false, + servers = data.servers.orEmpty(), + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { - it.copy( - isLoading = false, - servers = result.data.servers.orEmpty(), - ) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load MCP servers: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load MCP servers: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun toggleServer(server: McpServer) { @@ -167,7 +163,7 @@ class McpServersViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt index 9e8deb79..d4c7fb44 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.model -import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.layout.Arrangement @@ -34,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -46,6 +44,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -55,7 +54,6 @@ fun ModelScreen( viewModel: ModelViewModel = viewModel { ModelViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var expandedProviderSlug by remember { mutableStateOf(null) } var query by remember { mutableStateOf("") } @@ -74,12 +72,7 @@ fun ModelScreen( viewModel.loadModelOptions() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_models)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt index 3a2c2801..9b9ca8a8 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/model/ModelViewModel.kt @@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.model.UpdateProfileModelRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -24,7 +25,7 @@ data class ModelUiState( val toastMessage: String? = null, ) -class ModelViewModel : ViewModel() { +class ModelViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(ModelUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -130,7 +131,7 @@ class ModelViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt index dc606c42..5b501b45 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.pairing -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -27,7 +26,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -37,6 +35,7 @@ import com.m57.hermescontrol.R import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.ToastEffect @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -46,18 +45,12 @@ fun PairingScreen( viewModel: PairingViewModel = viewModel { PairingViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current LaunchedEffect(Unit) { viewModel.loadPairing() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.pairing_screen_title)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt index 12ebf6e8..4ea2c868 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/pairing/PairingViewModel.kt @@ -8,6 +8,8 @@ import com.m57.hermescontrol.data.model.PairingRevokeRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -24,32 +26,26 @@ data class PairingUiState( val toastMessage: String? = null, ) -class PairingViewModel : ViewModel() { +class PairingViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(PairingUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadPairing() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getPairing() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getPairing() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { it.copy(isLoading = false, pairing = data) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load pairing info: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { it.copy(isLoading = false, pairing = result.data) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load pairing info: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun approvePairing( @@ -106,7 +102,7 @@ class PairingViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt index bac6281e..48926943 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.plugins -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -29,7 +28,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -40,6 +38,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -51,7 +50,6 @@ fun PluginsScreen( viewModel: PluginsViewModel = viewModel { PluginsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -67,12 +65,7 @@ fun PluginsScreen( viewModel.loadPlugins() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_plugins)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt index 85fa039c..1789eacd 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/plugins/PluginsViewModel.kt @@ -6,6 +6,8 @@ import com.m57.hermescontrol.data.model.PluginInfo import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -21,33 +23,27 @@ data class PluginsUiState( val toastMessage: String? = null, ) -class PluginsViewModel : ViewModel() { +class PluginsViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(PluginsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadPlugins() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getPlugins() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getPlugins() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + val plugins = data.plugins.orEmpty() + _uiState.update { it.copy(isLoading = false, plugins = plugins) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load plugins: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - val plugins = result.data.plugins.orEmpty() - _uiState.update { it.copy(isLoading = false, plugins = plugins) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load plugins: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun togglePlugin(plugin: PluginInfo) { @@ -165,7 +161,7 @@ class PluginsViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt index a28ac21f..0bcda1da 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.profiles -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -37,7 +36,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -48,6 +46,7 @@ import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState +import com.m57.hermescontrol.ui.common.ToastEffect @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -57,7 +56,6 @@ fun ProfilesScreen( viewModel: ProfilesViewModel = viewModel { ProfilesViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var soulEditProfileName by remember { mutableStateOf(null) } var modelEditProfileName by remember { mutableStateOf(null) } @@ -68,12 +66,7 @@ fun ProfilesScreen( viewModel.loadProfiles() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_profiles)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt index fe38a430..94880efd 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt @@ -9,6 +9,7 @@ import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,7 +28,7 @@ data class ProfilesUiState( val toastMessage: String? = null, ) -class ProfilesViewModel : ViewModel() { +class ProfilesViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(ProfilesUiState()) val uiState: StateFlow = _uiState.asStateFlow() @@ -189,7 +190,7 @@ class ProfilesViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt index 0599013b..399127ce 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/sessions/SessionsViewModel.kt @@ -1,18 +1,14 @@ package com.m57.hermescontrol.ui.sessions import androidx.lifecycle.ViewModel -import androidx.lifecycle.viewModelScope import com.m57.hermescontrol.data.model.SessionInfo import com.m57.hermescontrol.data.remote.ApiClient -import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall -import kotlinx.coroutines.Dispatchers +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext data class SessionsUiState( val isLoading: Boolean = false, @@ -25,31 +21,25 @@ class SessionsViewModel : ViewModel() { val uiState: StateFlow = _uiState.asStateFlow() fun loadSessions() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getSessions() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getSessions() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { + it.copy( + isLoading = false, + sessions = data?.sessions.orEmpty(), + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { - it.copy( - isLoading = false, - sessions = result.data?.sessions.orEmpty(), - ) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load sessions: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load sessions: ${result.error.message}", - ) - } - } - } - } + }, + ) } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt index db6bea87..9d5abd1f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.skills -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -34,7 +33,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily @@ -52,6 +50,7 @@ import com.m57.hermescontrol.ui.common.FilterChipRow import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -62,7 +61,6 @@ fun SkillsScreen( viewModel: SkillsViewModel = viewModel { SkillsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current val spacing = LocalSpacing.current var query by remember { mutableStateOf("") } val statuses = listOf("All Statuses", "Enabled", "Disabled") @@ -81,12 +79,7 @@ fun SkillsScreen( viewModel.loadSkills() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) val filtered = remember(state.skills, query, selectedStatus, selectedCategory) { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt index bae187d1..5b2e6ec9 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/skills/SkillsViewModel.kt @@ -7,6 +7,8 @@ import com.m57.hermescontrol.data.model.ToggleSkillRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -27,32 +29,26 @@ data class SkillsUiState( val saveContentSuccess: Boolean = false, ) -class SkillsViewModel : ViewModel() { +class SkillsViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(SkillsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadSkills() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getSkills() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getSkills() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { it.copy(isLoading = false, skills = data.orEmpty()) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load skills: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { it.copy(isLoading = false, skills = result.data.orEmpty()) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load skills: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun toggleSkill(skill: Skill) { @@ -191,7 +187,7 @@ class SkillsViewModel : ViewModel() { _uiState.update { it.copy(saveContentSuccess = false) } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt index 861c1bb5..cfd3027b 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.system -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow @@ -26,7 +25,6 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel @@ -42,6 +40,7 @@ import com.m57.hermescontrol.ui.common.SectionHeader import com.m57.hermescontrol.ui.common.StatCard import com.m57.hermescontrol.ui.common.StatusBadge import com.m57.hermescontrol.ui.common.StatusBadgeType +import com.m57.hermescontrol.ui.common.ToastEffect @OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class) @Composable @@ -51,7 +50,6 @@ fun SystemScreen( viewModel: SystemViewModel = viewModel { SystemViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current val spacing = LocalSpacing.current val statusColors = LocalHermesStatusColors.current @@ -59,12 +57,7 @@ fun SystemScreen( viewModel.loadSystemData() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_system)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt index da45b5cf..13ce996c 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/system/SystemViewModel.kt @@ -9,6 +9,8 @@ import com.m57.hermescontrol.data.model.SystemStatsResponse import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,7 +27,7 @@ data class SystemUiState( val toastMessage: String? = null, ) -class SystemViewModel : ViewModel() { +class SystemViewModel : ViewModel(), ToastHost { companion object { private const val TAG = "SystemViewModel" } @@ -34,34 +36,28 @@ class SystemViewModel : ViewModel() { val uiState: StateFlow = _uiState.asStateFlow() fun loadSystemData() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getSystemStats() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getSystemStats() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { + it.copy( + isLoading = false, + stats = data, + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { - it.copy( - isLoading = false, - stats = result.data, - ) - } - // Doctor is optional; many servers do not expose it. - loadDoctorReport() + // Doctor is optional; many servers do not expose it. + loadDoctorReport() + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load system stats: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load system stats: ${result.error.message}", - ) - } - } - } - } + }, + ) } private fun loadDoctorReport() { @@ -96,7 +92,7 @@ class SystemViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt index 0ff27f55..50231d91 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.toolsets -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -33,7 +32,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @@ -45,6 +43,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -56,7 +55,6 @@ fun ToolsetsScreen( viewModel: ToolsetsViewModel = viewModel { ToolsetsViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -73,12 +71,7 @@ fun ToolsetsScreen( viewModel.loadToolsets() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_toolsets)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt index 76d24ca7..e25fa670 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/toolsets/ToolsetsViewModel.kt @@ -7,6 +7,8 @@ import com.m57.hermescontrol.data.model.ToolsetToggleRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -22,32 +24,26 @@ data class ToolsetsUiState( val toastMessage: String? = null, ) -class ToolsetsViewModel : ViewModel() { +class ToolsetsViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(ToolsetsUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadToolsets() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getToolsets() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getToolsets() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + _uiState.update { it.copy(isLoading = false, toolsets = data.orEmpty()) } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load toolsets: $errorMsg", + ) } - when (result) { - is NetworkResult.Success -> { - _uiState.update { it.copy(isLoading = false, toolsets = result.data.orEmpty()) } - } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load toolsets: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun toggleToolset(toolset: Toolset) { @@ -91,7 +87,7 @@ class ToolsetsViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt index 8c312279..a85aef75 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksScreen.kt @@ -1,6 +1,5 @@ package com.m57.hermescontrol.ui.webhooks -import android.widget.Toast import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -30,7 +29,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight @@ -42,6 +40,7 @@ import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.HermesScaffold import com.m57.hermescontrol.ui.common.LoadingState import com.m57.hermescontrol.ui.common.SearchBar +import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.listContentPadding import com.m57.hermescontrol.ui.common.listItemSpacing @@ -53,7 +52,6 @@ fun WebhooksScreen( viewModel: WebhooksViewModel = viewModel { WebhooksViewModel() }, ) { val state by viewModel.uiState.collectAsStateWithLifecycle() - val context = LocalContext.current var query by remember { mutableStateOf("") } @@ -69,12 +67,7 @@ fun WebhooksScreen( viewModel.loadWebhooks() } - LaunchedEffect(state.toastMessage) { - state.toastMessage?.let { msg -> - Toast.makeText(context, msg, Toast.LENGTH_SHORT).show() - viewModel.clearToast() - } - } + ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast) HermesScaffold( title = { Text(stringResource(R.string.screen_webhooks)) }, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt index 238002d3..e7fde637 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/webhooks/WebhooksViewModel.kt @@ -7,6 +7,8 @@ import com.m57.hermescontrol.data.model.WebhooksToggleRequest import com.m57.hermescontrol.data.remote.ApiClient import com.m57.hermescontrol.data.remote.NetworkResult import com.m57.hermescontrol.data.remote.safeApiCall +import com.m57.hermescontrol.ui.common.ToastHost +import com.m57.hermescontrol.ui.common.safeLaunchLoad import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -25,40 +27,34 @@ data class WebhooksUiState( val toastMessage: String? = null, ) -class WebhooksViewModel : ViewModel() { +class WebhooksViewModel : ViewModel(), ToastHost { private val _uiState = MutableStateFlow(WebhooksUiState()) val uiState: StateFlow = _uiState.asStateFlow() fun loadWebhooks() { - _uiState.update { it.copy(isLoading = true, errorMessage = null) } - viewModelScope.launch { - val result = - withContext(Dispatchers.IO) { - safeApiCall { ApiClient.hermesApi.getWebhooks() } + safeLaunchLoad( + apiCall = { safeApiCall { ApiClient.hermesApi.getWebhooks() } }, + onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } }, + onSuccess = { data -> + val body = data + _uiState.update { + it.copy( + isLoading = false, + enabled = body.enabled, + baseUrl = body.base_url, + subscriptions = body.subscriptions.orEmpty(), + ) } - when (result) { - is NetworkResult.Success -> { - val body = result.data - _uiState.update { - it.copy( - isLoading = false, - enabled = body.enabled, - baseUrl = body.base_url, - subscriptions = body.subscriptions.orEmpty(), - ) - } + }, + onError = { errorMsg -> + _uiState.update { + it.copy( + isLoading = false, + errorMessage = "Failed to load webhooks: $errorMsg", + ) } - - is NetworkResult.Failure -> { - _uiState.update { - it.copy( - isLoading = false, - errorMessage = "Failed to load webhooks: ${result.error.message}", - ) - } - } - } - } + }, + ) } fun toggleWebhooks(targetEnabled: Boolean) { @@ -92,7 +88,7 @@ class WebhooksViewModel : ViewModel() { } } - fun clearToast() { + override fun clearToast() { _uiState.update { it.copy(toastMessage = null) } } }