Refactor: Extract boilerplate: safeLaunchLoad, clearToast, ToastEffect (CODE-21/22/23) (#312)
- Extracted `safeLaunchLoad` inline extension function to `ViewModelExt.kt` to encapsulate API call success/failure handling logic and replaced usages across 18 viewmodels. - Extracted `ToastHost` interface to standardize `clearToast()` behavior across all ViewModels displaying toasts. - Extracted `ToastEffect` composable to `SharedComponents.kt` and applied it to 16 screens, reducing repetitive side effect code blocks. Fixes #286
This commit is contained in:
36 files changed
+427
-585
No files matched your search
@@ -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<AchievementsUiState> = _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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<ChannelsUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 <T> ViewModel.safeLaunchLoad(
|
||||
crossinline apiCall: suspend () -> NetworkResult<T>,
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<ConfigUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<CronJobsUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<GatewayUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<KanbanUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<KeysUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<LogsUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<McpServersUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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<String?>(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)) },
|
||||
|
||||
@@ -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<ModelUiState> = _uiState.asStateFlow()
|
||||
|
||||
@@ -130,7 +131,7 @@ class ModelViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearToast() {
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<PairingUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<PluginsUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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<String?>(null) }
|
||||
var modelEditProfileName by remember { mutableStateOf<String?>(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)) },
|
||||
|
||||
@@ -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<ProfilesUiState> = _uiState.asStateFlow()
|
||||
|
||||
@@ -189,7 +190,7 @@ class ProfilesViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearToast() {
|
||||
override fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
|
||||
|
||||
@@ -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<SessionsUiState> = _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}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<SkillsUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<SystemUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<ToolsetsUiState> = _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) }
|
||||
}
|
||||
}
|
||||
@@ -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)) },
|
||||
|
||||
@@ -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<WebhooksUiState> = _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) }
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user