27 files changed
+399
-15
No files matched your search
@@ -2,20 +2,29 @@ package com.m57.hermescontrol.ui.skills
|
||||
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material.icons.filled.Extension
|
||||
import androidx.compose.material.icons.filled.Save
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -25,8 +34,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.m57.hermescontrol.theme.LocalSpacing
|
||||
@@ -161,10 +174,24 @@ fun SkillsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
Switch(
|
||||
checked = skill.enabled,
|
||||
onCheckedChange = { viewModel.toggleSkill(skill) },
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { viewModel.loadSkillContent(skill.name) },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Edit,
|
||||
contentDescription = "Edit Skill File",
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(
|
||||
checked = skill.enabled,
|
||||
onCheckedChange = { viewModel.toggleSkill(skill) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -172,4 +199,155 @@ fun SkillsScreen(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state.editingSkillName?.let { skillName ->
|
||||
SkillEditorDialog(
|
||||
skillName = skillName,
|
||||
isLoading = state.isLoadingContent,
|
||||
initialContent = state.skillContent,
|
||||
isSaving = state.isSavingContent,
|
||||
saveSuccess = state.saveContentSuccess,
|
||||
onSave = { content -> viewModel.saveSkillContent(skillName, content) },
|
||||
onDismiss = { viewModel.clearEditor() },
|
||||
onClearSaveSuccess = { viewModel.clearSaveSuccess() },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SkillEditorDialog(
|
||||
skillName: String,
|
||||
isLoading: Boolean,
|
||||
initialContent: String?,
|
||||
isSaving: Boolean,
|
||||
saveSuccess: Boolean,
|
||||
onSave: (String) -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
onClearSaveSuccess: () -> Unit,
|
||||
) {
|
||||
var contentText by remember(initialContent) { mutableStateOf(initialContent.orEmpty()) }
|
||||
var showDiscardConfirm by remember { mutableStateOf(false) }
|
||||
|
||||
val hasChanges =
|
||||
remember(initialContent, contentText) {
|
||||
val original = initialContent.orEmpty()
|
||||
original != contentText
|
||||
}
|
||||
|
||||
LaunchedEffect(saveSuccess) {
|
||||
if (saveSuccess) {
|
||||
onClearSaveSuccess()
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
|
||||
Dialog(
|
||||
onDismissRequest = {
|
||||
if (hasChanges) {
|
||||
showDiscardConfirm = true
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||
) {
|
||||
Card(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
colors =
|
||||
CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
) {
|
||||
HermesScaffold(
|
||||
title = { Text("Edit: $skillName") },
|
||||
onBack = {
|
||||
if (hasChanges) {
|
||||
showDiscardConfirm = true
|
||||
} else {
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
showBack = true,
|
||||
actions = {
|
||||
if (!isLoading) {
|
||||
IconButton(
|
||||
onClick = { onSave(contentText) },
|
||||
enabled = !isSaving,
|
||||
) {
|
||||
if (isSaving) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier.fillMaxSize(0.6f),
|
||||
strokeWidth = 2.dp,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Save,
|
||||
contentDescription = "Save Changes",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
) { padding ->
|
||||
Box(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding),
|
||||
) {
|
||||
when {
|
||||
isLoading -> {
|
||||
LoadingState()
|
||||
}
|
||||
|
||||
else -> {
|
||||
OutlinedTextField(
|
||||
value = contentText,
|
||||
onValueChange = { contentText = it },
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
textStyle =
|
||||
TextStyle(
|
||||
fontFamily = FontFamily.Monospace,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
placeholder = {
|
||||
Text("Enter skill content...")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showDiscardConfirm) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { showDiscardConfirm = false },
|
||||
title = { Text("Discard changes?") },
|
||||
text = { Text("You have unsaved changes. Are you sure you want to discard them?") },
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
showDiscardConfirm = false
|
||||
onDismiss()
|
||||
},
|
||||
) {
|
||||
Text("Discard")
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = { showDiscardConfirm = false }) {
|
||||
Text("Cancel")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,11 @@ data class SkillsUiState(
|
||||
val skills: List<Skill> = emptyList(),
|
||||
val errorMessage: String? = null,
|
||||
val toastMessage: String? = null,
|
||||
val isLoadingContent: Boolean = false,
|
||||
val editingSkillName: String? = null,
|
||||
val skillContent: String? = null,
|
||||
val isSavingContent: Boolean = false,
|
||||
val saveContentSuccess: Boolean = false,
|
||||
)
|
||||
|
||||
class SkillsViewModel : ViewModel() {
|
||||
@@ -37,6 +42,7 @@ class SkillsViewModel : ViewModel() {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update { it.copy(isLoading = false, skills = result.data.orEmpty()) }
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
@@ -90,6 +96,99 @@ class SkillsViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
fun loadSkillContent(skillName: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingContent = true,
|
||||
editingSkillName = skillName,
|
||||
skillContent = null,
|
||||
saveContentSuccess = false,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getSkillContent(skillName) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingContent = false,
|
||||
skillContent = result.data?.content.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingContent = false,
|
||||
editingSkillName = null,
|
||||
toastMessage = "Failed to load skill content: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun saveSkillContent(
|
||||
skillName: String,
|
||||
content: String,
|
||||
) {
|
||||
_uiState.update { it.copy(isSavingContent = true, saveContentSuccess = false) }
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.saveSkillContent(
|
||||
skillName,
|
||||
com.m57.hermescontrol.data.model
|
||||
.SaveSkillContentRequest(content),
|
||||
)
|
||||
}
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isSavingContent = false,
|
||||
saveContentSuccess = true,
|
||||
skillContent = content,
|
||||
toastMessage = "Skill saved successfully",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isSavingContent = false,
|
||||
toastMessage = "Failed to save skill content: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearEditor() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
editingSkillName = null,
|
||||
skillContent = null,
|
||||
isLoadingContent = false,
|
||||
isSavingContent = false,
|
||||
saveContentSuccess = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSaveSuccess() {
|
||||
_uiState.update { it.copy(saveContentSuccess = false) }
|
||||
}
|
||||
|
||||
fun clearToast() {
|
||||
_uiState.update { it.copy(toastMessage = null) }
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user