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
|
package com.m57.hermescontrol.ui.achievements
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.m57.hermescontrol.data.model.Achievement
|
import com.m57.hermescontrol.data.model.Achievement
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.data.remote.safeApiCall
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
data class AchievementsUiState(
|
data class AchievementsUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
@@ -25,31 +21,25 @@ class AchievementsViewModel : ViewModel() {
|
|||||||
val uiState: StateFlow<AchievementsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<AchievementsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadAchievements() {
|
fun loadAchievements() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getAchievements() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getAchievements() }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
achievements = data.achievements.orEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
},
|
||||||
is NetworkResult.Success -> {
|
onError = { errorMsg ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
achievements = result.data.achievements.orEmpty(),
|
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
|
package com.m57.hermescontrol.ui.channels
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -27,7 +26,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -49,7 +48,6 @@ fun ChannelsScreen(
|
|||||||
viewModel: ChannelsViewModel = viewModel { ChannelsViewModel() },
|
viewModel: ChannelsViewModel = viewModel { ChannelsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -64,12 +62,7 @@ fun ChannelsScreen(
|
|||||||
viewModel.loadPlatforms()
|
viewModel.loadPlatforms()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_channels)) },
|
title = { Text(stringResource(R.string.screen_channels)) },
|
||||||
|
|||||||
@@ -1,19 +1,16 @@
|
|||||||
package com.m57.hermescontrol.ui.channels
|
package com.m57.hermescontrol.ui.channels
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatform
|
import com.m57.hermescontrol.data.model.MessagingPlatform
|
||||||
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
import com.m57.hermescontrol.data.model.MessagingPlatformUpdate
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
data class ChannelsUiState(
|
data class ChannelsUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
@@ -22,67 +19,55 @@ data class ChannelsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ChannelsViewModel : ViewModel() {
|
class ChannelsViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ChannelsUiState())
|
private val _uiState = MutableStateFlow(ChannelsUiState())
|
||||||
val uiState: StateFlow<ChannelsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ChannelsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadPlatforms() {
|
fun loadPlatforms() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getMessagingPlatforms() }
|
_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(
|
fun configurePlatform(
|
||||||
platformId: String,
|
platformId: String,
|
||||||
update: MessagingPlatformUpdate,
|
update: MessagingPlatformUpdate,
|
||||||
) {
|
) {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.configurePlatform(platformId, update) } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.configurePlatform(platformId, update) }
|
val message = "$platformId configured successfully — restart the gateway for changes to take effect"
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
toastMessage = message,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
loadPlatforms()
|
||||||
is NetworkResult.Success -> {
|
},
|
||||||
val message = "$platformId configured successfully — restart the gateway for changes to take effect"
|
onError = { errorMsg ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
toastMessage = message,
|
errorMessage = "Failed to configure platform: $errorMsg",
|
||||||
)
|
toastMessage = "Failed to configure platform: $errorMsg",
|
||||||
}
|
)
|
||||||
loadPlatforms()
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
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(
|
fun configurePlatform(
|
||||||
@@ -92,7 +77,7 @@ class ChannelsViewModel : ViewModel() {
|
|||||||
configurePlatform(platformId, MessagingPlatformUpdate(env = config))
|
configurePlatform(platformId, MessagingPlatformUpdate(env = config))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_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
|
package com.m57.hermescontrol.ui.config
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -25,7 +24,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
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.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -44,19 +43,13 @@ fun ConfigScreen(
|
|||||||
viewModel: ConfigViewModel = viewModel { ConfigViewModel() },
|
viewModel: ConfigViewModel = viewModel { ConfigViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
var editingText by remember(state.yamlText) { mutableStateOf(state.yamlText ?: "") }
|
var editingText by remember(state.yamlText) { mutableStateOf(state.yamlText ?: "") }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadRawConfig()
|
viewModel.loadRawConfig()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.config_screen_title)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -23,38 +25,32 @@ data class ConfigUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ConfigViewModel : ViewModel() {
|
class ConfigViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadRawConfig() {
|
fun loadRawConfig() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getRawConfig() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getRawConfig() }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
path = data.path,
|
||||||
|
yamlText = data.yaml,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
},
|
||||||
is NetworkResult.Success -> {
|
onError = { errorMsg ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
path = result.data.path,
|
errorMessage = "Failed to load config: $errorMsg",
|
||||||
yamlText = result.data.yaml,
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
is NetworkResult.Failure -> {
|
)
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load config: ${result.error.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun saveRawConfig(yamlText: String) {
|
fun saveRawConfig(yamlText: String) {
|
||||||
@@ -87,7 +83,7 @@ class ConfigViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.cron
|
package com.m57.hermescontrol.ui.cron
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -26,7 +25,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.StatusBadge
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
import com.m57.hermescontrol.util.CronExpressionFormatter
|
import com.m57.hermescontrol.util.CronExpressionFormatter
|
||||||
@@ -50,19 +49,13 @@ fun CronJobsScreen(
|
|||||||
viewModel: CronJobsViewModel = viewModel { CronJobsViewModel() },
|
viewModel: CronJobsViewModel = viewModel { CronJobsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadCronJobs()
|
viewModel.loadCronJobs()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_cron)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -21,32 +23,26 @@ data class CronJobsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class CronJobsViewModel : ViewModel() {
|
class CronJobsViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(CronJobsUiState())
|
private val _uiState = MutableStateFlow(CronJobsUiState())
|
||||||
val uiState: StateFlow<CronJobsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<CronJobsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadCronJobs() {
|
fun loadCronJobs() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getCronJobs() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getCronJobs() }
|
_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) {
|
fun pauseCronJob(id: String) {
|
||||||
@@ -135,7 +131,7 @@ class CronJobsViewModel : ViewModel() {
|
|||||||
_uiState.update { it.copy(jobs = originalJobs, toastMessage = errorMsg) }
|
_uiState.update { it.copy(jobs = originalJobs, toastMessage = errorMsg) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.gateway
|
package com.m57.hermescontrol.ui.gateway
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -31,7 +30,6 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -50,18 +49,12 @@ fun GatewayScreen(
|
|||||||
viewModel: GatewayViewModel = viewModel { GatewayViewModel() },
|
viewModel: GatewayViewModel = viewModel { GatewayViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadStatus()
|
viewModel.loadStatus()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_gateway)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -22,32 +24,26 @@ data class GatewayUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class GatewayViewModel : ViewModel() {
|
class GatewayViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(GatewayUiState())
|
private val _uiState = MutableStateFlow(GatewayUiState())
|
||||||
val uiState: StateFlow<GatewayUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<GatewayUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadStatus() {
|
fun loadStatus() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getStatus() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getStatus() }
|
_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() {
|
fun startGateway() {
|
||||||
@@ -92,7 +88,7 @@ class GatewayViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.kanban
|
package com.m57.hermescontrol.ui.kanban
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -38,7 +37,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
import com.m57.hermescontrol.ui.common.SearchBar
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
private const val DEFAULT_COLUMN = "todo"
|
private const val DEFAULT_COLUMN = "todo"
|
||||||
|
|
||||||
@@ -61,7 +60,6 @@ fun KanbanScreen(
|
|||||||
viewModel: KanbanViewModel = viewModel { KanbanViewModel() },
|
viewModel: KanbanViewModel = viewModel { KanbanViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -80,12 +78,7 @@ fun KanbanScreen(
|
|||||||
viewModel.loadBoards()
|
viewModel.loadBoards()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.kanban_board_title)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -26,38 +28,32 @@ data class KanbanUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class KanbanViewModel : ViewModel() {
|
class KanbanViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(KanbanUiState())
|
private val _uiState = MutableStateFlow(KanbanUiState())
|
||||||
val uiState: StateFlow<KanbanUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<KanbanUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadBoards() {
|
fun loadBoards() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getKanbanBoards() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getKanbanBoards() }
|
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 -> {
|
onError = { errorMsg ->
|
||||||
val boards = result.data.boards.orEmpty()
|
_uiState.update {
|
||||||
_uiState.update { it.copy(isLoading = false, boards = boards) }
|
it.copy(
|
||||||
val currentSlug = result.data.current
|
isLoading = false,
|
||||||
val currentBoard = boards.find { it.id == currentSlug } ?: boards.firstOrNull()
|
errorMessage = "Failed to load Kanban boards: $errorMsg",
|
||||||
if (currentBoard != null) {
|
)
|
||||||
selectBoard(currentBoard)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
is NetworkResult.Failure -> {
|
)
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load Kanban boards: ${result.error.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun selectBoard(board: KanbanBoard) {
|
fun selectBoard(board: KanbanBoard) {
|
||||||
@@ -183,7 +179,7 @@ class KanbanViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.keys
|
package com.m57.hermescontrol.ui.keys
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -34,7 +33,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -57,7 +56,6 @@ fun KeysScreen(
|
|||||||
viewModel: KeysViewModel = viewModel { KeysViewModel() },
|
viewModel: KeysViewModel = viewModel { KeysViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -73,12 +71,7 @@ fun KeysScreen(
|
|||||||
viewModel.loadKeys()
|
viewModel.loadKeys()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_keys)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -24,37 +26,31 @@ data class KeysUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class KeysViewModel : ViewModel() {
|
class KeysViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(KeysUiState())
|
private val _uiState = MutableStateFlow(KeysUiState())
|
||||||
val uiState: StateFlow<KeysUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<KeysUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadKeys(reveal: Boolean = false) {
|
fun loadKeys(reveal: Boolean = false) {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getEnvVars() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getEnvVars() }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
envVars = data.orEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
},
|
||||||
is NetworkResult.Success -> {
|
onError = { errorMsg ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
envVars = result.data.orEmpty(),
|
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) {
|
fun revealKey(key: String) {
|
||||||
@@ -110,7 +106,7 @@ class KeysViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.logs
|
package com.m57.hermescontrol.ui.logs
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
@@ -20,7 +19,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.unit.dp
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
import com.m57.hermescontrol.ui.common.SearchBar
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LogsScreen(
|
fun LogsScreen(
|
||||||
@@ -42,7 +41,6 @@ fun LogsScreen(
|
|||||||
viewModel: LogsViewModel = viewModel { LogsViewModel() },
|
viewModel: LogsViewModel = viewModel { LogsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
@@ -59,12 +57,7 @@ fun LogsScreen(
|
|||||||
viewModel.loadLogs()
|
viewModel.loadLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto scroll to bottom when new logs arrive
|
// Auto scroll to bottom when new logs arrive
|
||||||
LaunchedEffect(filteredLogs.size) {
|
LaunchedEffect(filteredLogs.size) {
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
package com.m57.hermescontrol.ui.logs
|
package com.m57.hermescontrol.ui.logs
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.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.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
data class LogsUiState(
|
data class LogsUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
@@ -20,37 +17,31 @@ data class LogsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class LogsViewModel : ViewModel() {
|
class LogsViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(LogsUiState())
|
private val _uiState = MutableStateFlow(LogsUiState())
|
||||||
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadLogs() {
|
fun loadLogs() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getLogs() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getLogs() }
|
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) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.mcp
|
package com.m57.hermescontrol.ui.mcp
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -29,7 +28,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -52,7 +51,6 @@ fun McpServersScreen(
|
|||||||
viewModel: McpServersViewModel = viewModel { McpServersViewModel() },
|
viewModel: McpServersViewModel = viewModel { McpServersViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -69,12 +67,7 @@ fun McpServersScreen(
|
|||||||
viewModel.loadServers()
|
viewModel.loadServers()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_mcp_servers)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -23,37 +25,31 @@ data class McpServersUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class McpServersViewModel : ViewModel() {
|
class McpServersViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(McpServersUiState())
|
private val _uiState = MutableStateFlow(McpServersUiState())
|
||||||
val uiState: StateFlow<McpServersUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<McpServersUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadServers() {
|
fun loadServers() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getMcpServers() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getMcpServers() }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
servers = data.servers.orEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
},
|
||||||
is NetworkResult.Success -> {
|
onError = { errorMsg ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
servers = result.data.servers.orEmpty(),
|
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) {
|
fun toggleServer(server: McpServer) {
|
||||||
@@ -167,7 +163,7 @@ class McpServersViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.model
|
package com.m57.hermescontrol.ui.model
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -34,7 +33,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
import com.m57.hermescontrol.ui.common.SearchBar
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -55,7 +54,6 @@ fun ModelScreen(
|
|||||||
viewModel: ModelViewModel = viewModel { ModelViewModel() },
|
viewModel: ModelViewModel = viewModel { ModelViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
var expandedProviderSlug by remember { mutableStateOf<String?>(null) }
|
var expandedProviderSlug by remember { mutableStateOf<String?>(null) }
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -74,12 +72,7 @@ fun ModelScreen(
|
|||||||
viewModel.loadModelOptions()
|
viewModel.loadModelOptions()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_models)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -24,7 +25,7 @@ data class ModelUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ModelViewModel : ViewModel() {
|
class ModelViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ModelUiState())
|
private val _uiState = MutableStateFlow(ModelUiState())
|
||||||
val uiState: StateFlow<ModelUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ModelUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -130,7 +131,7 @@ class ModelViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.pairing
|
package com.m57.hermescontrol.ui.pairing
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -27,7 +26,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -46,18 +45,12 @@ fun PairingScreen(
|
|||||||
viewModel: PairingViewModel = viewModel { PairingViewModel() },
|
viewModel: PairingViewModel = viewModel { PairingViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
viewModel.loadPairing()
|
viewModel.loadPairing()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.pairing_screen_title)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -24,32 +26,26 @@ data class PairingUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PairingViewModel : ViewModel() {
|
class PairingViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(PairingUiState())
|
private val _uiState = MutableStateFlow(PairingUiState())
|
||||||
val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadPairing() {
|
fun loadPairing() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getPairing() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getPairing() }
|
_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(
|
fun approvePairing(
|
||||||
@@ -106,7 +102,7 @@ class PairingViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.plugins
|
package com.m57.hermescontrol.ui.plugins
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -29,7 +28,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -51,7 +50,6 @@ fun PluginsScreen(
|
|||||||
viewModel: PluginsViewModel = viewModel { PluginsViewModel() },
|
viewModel: PluginsViewModel = viewModel { PluginsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -67,12 +65,7 @@ fun PluginsScreen(
|
|||||||
viewModel.loadPlugins()
|
viewModel.loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_plugins)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -21,33 +23,27 @@ data class PluginsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PluginsViewModel : ViewModel() {
|
class PluginsViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(PluginsUiState())
|
private val _uiState = MutableStateFlow(PluginsUiState())
|
||||||
val uiState: StateFlow<PluginsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PluginsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadPlugins() {
|
fun loadPlugins() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getPlugins() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getPlugins() }
|
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) {
|
fun togglePlugin(plugin: PluginInfo) {
|
||||||
@@ -165,7 +161,7 @@ class PluginsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.profiles
|
package com.m57.hermescontrol.ui.profiles
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -37,7 +36,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.ErrorState
|
||||||
import com.m57.hermescontrol.ui.common.HermesScaffold
|
import com.m57.hermescontrol.ui.common.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -57,7 +56,6 @@ fun ProfilesScreen(
|
|||||||
viewModel: ProfilesViewModel = viewModel { ProfilesViewModel() },
|
viewModel: ProfilesViewModel = viewModel { ProfilesViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var soulEditProfileName by remember { mutableStateOf<String?>(null) }
|
var soulEditProfileName by remember { mutableStateOf<String?>(null) }
|
||||||
var modelEditProfileName by remember { mutableStateOf<String?>(null) }
|
var modelEditProfileName by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -68,12 +66,7 @@ fun ProfilesScreen(
|
|||||||
viewModel.loadProfiles()
|
viewModel.loadProfiles()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_profiles)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastHost
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,7 +28,7 @@ data class ProfilesUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ProfilesViewModel : ViewModel() {
|
class ProfilesViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ProfilesUiState())
|
private val _uiState = MutableStateFlow(ProfilesUiState())
|
||||||
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -189,7 +190,7 @@ class ProfilesViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
package com.m57.hermescontrol.ui.sessions
|
package com.m57.hermescontrol.ui.sessions
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import com.m57.hermescontrol.data.model.SessionInfo
|
import com.m57.hermescontrol.data.model.SessionInfo
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
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.data.remote.safeApiCall
|
||||||
import kotlinx.coroutines.Dispatchers
|
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
data class SessionsUiState(
|
data class SessionsUiState(
|
||||||
val isLoading: Boolean = false,
|
val isLoading: Boolean = false,
|
||||||
@@ -25,31 +21,25 @@ class SessionsViewModel : ViewModel() {
|
|||||||
val uiState: StateFlow<SessionsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SessionsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadSessions() {
|
fun loadSessions() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getSessions() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getSessions() }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
sessions = data?.sessions.orEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
},
|
||||||
is NetworkResult.Success -> {
|
onError = { errorMsg ->
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
sessions = result.data?.sessions.orEmpty(),
|
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
|
package com.m57.hermescontrol.ui.skills
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -34,7 +33,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -62,7 +61,6 @@ fun SkillsScreen(
|
|||||||
viewModel: SkillsViewModel = viewModel { SkillsViewModel() },
|
viewModel: SkillsViewModel = viewModel { SkillsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
val statuses = listOf("All Statuses", "Enabled", "Disabled")
|
val statuses = listOf("All Statuses", "Enabled", "Disabled")
|
||||||
@@ -81,12 +79,7 @@ fun SkillsScreen(
|
|||||||
viewModel.loadSkills()
|
viewModel.loadSkills()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val filtered =
|
val filtered =
|
||||||
remember(state.skills, query, selectedStatus, selectedCategory) {
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -27,32 +29,26 @@ data class SkillsUiState(
|
|||||||
val saveContentSuccess: Boolean = false,
|
val saveContentSuccess: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
class SkillsViewModel : ViewModel() {
|
class SkillsViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(SkillsUiState())
|
private val _uiState = MutableStateFlow(SkillsUiState())
|
||||||
val uiState: StateFlow<SkillsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SkillsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadSkills() {
|
fun loadSkills() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getSkills() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getSkills() }
|
_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) {
|
fun toggleSkill(skill: Skill) {
|
||||||
@@ -191,7 +187,7 @@ class SkillsViewModel : ViewModel() {
|
|||||||
_uiState.update { it.copy(saveContentSuccess = false) }
|
_uiState.update { it.copy(saveContentSuccess = false) }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.system
|
package com.m57.hermescontrol.ui.system
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.FlowRow
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
@@ -26,7 +25,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
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.StatCard
|
||||||
import com.m57.hermescontrol.ui.common.StatusBadge
|
import com.m57.hermescontrol.ui.common.StatusBadge
|
||||||
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
import com.m57.hermescontrol.ui.common.StatusBadgeType
|
||||||
|
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||||
|
|
||||||
@OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class)
|
@OptIn(androidx.compose.foundation.layout.ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -51,7 +50,6 @@ fun SystemScreen(
|
|||||||
viewModel: SystemViewModel = viewModel { SystemViewModel() },
|
viewModel: SystemViewModel = viewModel { SystemViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
val spacing = LocalSpacing.current
|
val spacing = LocalSpacing.current
|
||||||
val statusColors = LocalHermesStatusColors.current
|
val statusColors = LocalHermesStatusColors.current
|
||||||
|
|
||||||
@@ -59,12 +57,7 @@ fun SystemScreen(
|
|||||||
viewModel.loadSystemData()
|
viewModel.loadSystemData()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_system)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -25,7 +27,7 @@ data class SystemUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class SystemViewModel : ViewModel() {
|
class SystemViewModel : ViewModel(), ToastHost {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SystemViewModel"
|
private const val TAG = "SystemViewModel"
|
||||||
}
|
}
|
||||||
@@ -34,34 +36,28 @@ class SystemViewModel : ViewModel() {
|
|||||||
val uiState: StateFlow<SystemUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<SystemUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadSystemData() {
|
fun loadSystemData() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getSystemStats() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getSystemStats() }
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
stats = data,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
when (result) {
|
// Doctor is optional; many servers do not expose it.
|
||||||
is NetworkResult.Success -> {
|
loadDoctorReport()
|
||||||
_uiState.update {
|
},
|
||||||
it.copy(
|
onError = { errorMsg ->
|
||||||
isLoading = false,
|
_uiState.update {
|
||||||
stats = result.data,
|
it.copy(
|
||||||
)
|
isLoading = false,
|
||||||
}
|
errorMessage = "Failed to load system stats: $errorMsg",
|
||||||
// Doctor is optional; many servers do not expose it.
|
)
|
||||||
loadDoctorReport()
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
is NetworkResult.Failure -> {
|
)
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load system stats: ${result.error.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun loadDoctorReport() {
|
private fun loadDoctorReport() {
|
||||||
@@ -96,7 +92,7 @@ class SystemViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.toolsets
|
package com.m57.hermescontrol.ui.toolsets
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -33,7 +32,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -56,7 +55,6 @@ fun ToolsetsScreen(
|
|||||||
viewModel: ToolsetsViewModel = viewModel { ToolsetsViewModel() },
|
viewModel: ToolsetsViewModel = viewModel { ToolsetsViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -73,12 +71,7 @@ fun ToolsetsScreen(
|
|||||||
viewModel.loadToolsets()
|
viewModel.loadToolsets()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_toolsets)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -22,32 +24,26 @@ data class ToolsetsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ToolsetsViewModel : ViewModel() {
|
class ToolsetsViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ToolsetsUiState())
|
private val _uiState = MutableStateFlow(ToolsetsUiState())
|
||||||
val uiState: StateFlow<ToolsetsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ToolsetsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadToolsets() {
|
fun loadToolsets() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getToolsets() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getToolsets() }
|
_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) {
|
fun toggleToolset(toolset: Toolset) {
|
||||||
@@ -91,7 +87,7 @@ class ToolsetsViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.m57.hermescontrol.ui.webhooks
|
package com.m57.hermescontrol.ui.webhooks
|
||||||
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -30,7 +29,6 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
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.HermesScaffold
|
||||||
import com.m57.hermescontrol.ui.common.LoadingState
|
import com.m57.hermescontrol.ui.common.LoadingState
|
||||||
import com.m57.hermescontrol.ui.common.SearchBar
|
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.listContentPadding
|
||||||
import com.m57.hermescontrol.ui.common.listItemSpacing
|
import com.m57.hermescontrol.ui.common.listItemSpacing
|
||||||
|
|
||||||
@@ -53,7 +52,6 @@ fun WebhooksScreen(
|
|||||||
viewModel: WebhooksViewModel = viewModel { WebhooksViewModel() },
|
viewModel: WebhooksViewModel = viewModel { WebhooksViewModel() },
|
||||||
) {
|
) {
|
||||||
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
val context = LocalContext.current
|
|
||||||
|
|
||||||
var query by remember { mutableStateOf("") }
|
var query by remember { mutableStateOf("") }
|
||||||
|
|
||||||
@@ -69,12 +67,7 @@ fun WebhooksScreen(
|
|||||||
viewModel.loadWebhooks()
|
viewModel.loadWebhooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(state.toastMessage) {
|
ToastEffect(toastMessage = state.toastMessage, onClearToast = viewModel::clearToast)
|
||||||
state.toastMessage?.let { msg ->
|
|
||||||
Toast.makeText(context, msg, Toast.LENGTH_SHORT).show()
|
|
||||||
viewModel.clearToast()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HermesScaffold(
|
HermesScaffold(
|
||||||
title = { Text(stringResource(R.string.screen_webhooks)) },
|
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.ApiClient
|
||||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
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.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
@@ -25,40 +27,34 @@ data class WebhooksUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class WebhooksViewModel : ViewModel() {
|
class WebhooksViewModel : ViewModel(), ToastHost {
|
||||||
private val _uiState = MutableStateFlow(WebhooksUiState())
|
private val _uiState = MutableStateFlow(WebhooksUiState())
|
||||||
val uiState: StateFlow<WebhooksUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<WebhooksUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun loadWebhooks() {
|
fun loadWebhooks() {
|
||||||
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
|
safeLaunchLoad(
|
||||||
viewModelScope.launch {
|
apiCall = { safeApiCall { ApiClient.hermesApi.getWebhooks() } },
|
||||||
val result =
|
onStart = { _uiState.update { it.copy(isLoading = true, errorMessage = null) } },
|
||||||
withContext(Dispatchers.IO) {
|
onSuccess = { data ->
|
||||||
safeApiCall { ApiClient.hermesApi.getWebhooks() }
|
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 -> {
|
onError = { errorMsg ->
|
||||||
val body = result.data
|
_uiState.update {
|
||||||
_uiState.update {
|
it.copy(
|
||||||
it.copy(
|
isLoading = false,
|
||||||
isLoading = false,
|
errorMessage = "Failed to load webhooks: $errorMsg",
|
||||||
enabled = body.enabled,
|
)
|
||||||
baseUrl = body.base_url,
|
|
||||||
subscriptions = body.subscriptions.orEmpty(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
is NetworkResult.Failure -> {
|
)
|
||||||
_uiState.update {
|
|
||||||
it.copy(
|
|
||||||
isLoading = false,
|
|
||||||
errorMessage = "Failed to load webhooks: ${result.error.message}",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleWebhooks(targetEnabled: Boolean) {
|
fun toggleWebhooks(targetEnabled: Boolean) {
|
||||||
@@ -92,7 +88,7 @@ class WebhooksViewModel : ViewModel() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun clearToast() {
|
override fun clearToast() {
|
||||||
_uiState.update { it.copy(toastMessage = null) }
|
_uiState.update { it.copy(toastMessage = null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user