Add save as feature

This commit is contained in:
Thomas Andres Gomez 2025-03-20 21:33:26 +01:00
parent e1fdb10793
commit 50c34c8520
10 changed files with 300 additions and 12 deletions

View file

@ -1,12 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:pathData="M0 0h24v24H0z" />
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#000000"
android:pathData="M17 3H5c-1.11 0-2 0.9-2 2v14c0 1.1 0.89 2 2 2h14c1.1 0 2-0.9 2-2V7l-4-4zm-5 16c-1.66 0-3-1.34-3-3s1.34-3 3-3 3 1.34 3 3-1.34 3-3 3zm3-10H5V5h10v4z" />
</vector>
android:fillColor="#5f6368"
android:pathData="M840,280v480q0,33 -23.5,56.5T760,840L200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h480l160,160ZM760,314L646,200L200,200v560h560v-446ZM480,720q50,0 85,-35t35,-85q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85q0,50 35,85t85,35ZM240,400h360v-160L240,240v160ZM200,314v446,-560 114Z" />
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:pathData="M200,840q-33,0 -56.5,-23.5T120,760v-560q0,-33 23.5,-56.5T200,120h480l160,160v212q-19,-8 -39.5,-10.5t-40.5,0.5v-169L647,200L200,200v560h240v80L200,840ZM200,200v560,-560ZM520,920v-123l221,-220q9,-9 20,-13t22,-4q12,0 23,4.5t20,13.5l37,37q8,9 12.5,20t4.5,22q0,11 -4,22.5T863,700L643,920L520,920ZM820,657 L783,620 820,657ZM580,860h38l121,-122 -18,-19 -19,-18 -122,121v38ZM721,719 L702,701 739,738 721,719ZM240,400h360v-160L240,240v160ZM480,720h4l116,-115v-5q0,-50 -35,-85t-85,-35q-50,0 -85,35t-35,85q0,50 35,85t85,35Z"
android:fillColor="#5f6368"/>
</vector>

View file

@ -123,6 +123,9 @@
<string name="character_sheet_edit__add_roll_action">Ajouter une action de lancer</string>
<string name="character_sheet_edit__delete__label">Supprimer</string>
<string name="character_sheet_edit__occupation__label">Compétence d'occupation</string>
<string name="character_sheet_edit__copy__title">Enregistrer une copie</string>
<string name="character_sheet_edit__copy__label">Nouvel identifiant</string>
<string name="character_sheet_edit__copy__error">Identifiant déjà utilisé.</string>
<string name="character_sheet__level">niv : %1$d</string>
<string name="character_sheet__diminished__label">État diminué</string>

View file

@ -1,7 +1,7 @@
package com.pixelized.desktop.lwa.repository.characterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheet
import com.pixelized.shared.lwa.model.characterSheet.CharacterSheetPreview
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -69,4 +69,10 @@ class CharacterSheetRepository(
) {
store.deleteCharacterSheet(characterId = characterId)
}
fun checkCharacterSheetIdValidity(
characterSheetId: String,
): Boolean {
return store.previewFlow.value.none { it.characterSheetId == characterSheetId }
}
}

View file

@ -18,7 +18,8 @@ import kotlinx.coroutines.flow.StateFlow
@Stable
data class LwaTextFieldUio(
val enable: Boolean,
val enable: Boolean = true,
val isError: StateFlow<Boolean>,
val labelFlow: StateFlow<String?>,
val valueFlow: StateFlow<String>,
val placeHolderFlow: StateFlow<String?>,
@ -45,6 +46,7 @@ fun LwaTextField(
val label = field.labelFlow.collectAsState()
val value = field.valueFlow.collectAsState()
val placeHolder = field.placeHolderFlow.collectAsState()
val isError = field.isError.collectAsState()
TextField(
modifier = localModifier.then(other = modifier),
@ -63,6 +65,7 @@ fun LwaTextField(
)
}
},
isError = isError.value,
label = label.value?.let {
{
Text(

View file

@ -0,0 +1,187 @@
package com.pixelized.desktop.lwa.ui.screen.characterSheet.copy
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SizeTransform
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.Key
import androidx.compose.ui.input.key.key
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.theme.lwa
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__copy__error
import lwacharactersheet.composeapp.generated.resources.dialog__cancel_action
import lwacharactersheet.composeapp.generated.resources.dialog__confirm_action
import org.jetbrains.compose.resources.stringResource
@Stable
data class CharacterSheetCopyDialogUio(
val label: String,
val value: LwaTextFieldUio,
val validate: () -> Boolean
)
@Composable
fun CharacterSheetCopyDialog(
dialog: State<CharacterSheetCopyDialogUio?>,
onConfirm: (CharacterSheetCopyDialogUio) -> Unit,
onDismissRequest: () -> Unit,
) {
AnimatedContent(
modifier = Modifier.fillMaxSize(),
targetState = dialog.value,
transitionSpec = {
val enter = fadeIn() + slideInVertically { 32 }
val exit = fadeOut() + slideOutVertically { 32 }
enter togetherWith exit using SizeTransform(clip = false)
},
) {
Box(
modifier = Modifier.fillMaxSize()
) {
when (it) {
null -> Box(
modifier = Modifier,
)
else -> {
val focusRequester = remember {
FocusRequester()
}
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
Dialog(
dialog = it,
focusRequester = focusRequester,
onConfirm = onConfirm,
onDismissRequest = onDismissRequest,
)
}
}
}
}
}
@Composable
private fun Dialog(
dialog: CharacterSheetCopyDialogUio,
focusRequester: FocusRequester = remember { FocusRequester() },
onConfirm: (CharacterSheetCopyDialogUio) -> Unit,
onDismissRequest: () -> Unit,
) {
Box(
modifier = Modifier
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
onClick = onDismissRequest,
)
.onPreviewKeyEvent {
when {
it.key == Key.Escape -> {
onDismissRequest()
true
}
else -> false
}
}
.fillMaxSize()
.padding(all = 32.dp),
contentAlignment = Alignment.Center,
) {
DecoratedBox {
Surface {
Column(
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(top = 16.dp, start = 24.dp, end = 24.dp),
style = MaterialTheme.typography.caption,
text = dialog.label,
)
LwaTextField(
modifier = Modifier
.focusRequester(focusRequester = focusRequester)
.width(512.dp),
field = dialog.value,
)
AnimatedVisibility(
visible = dialog.value.isError.collectAsState().value,
enter = fadeIn(),
exit = fadeOut(),
) {
Text(
style = MaterialTheme.lwa.typography.base.caption,
color = MaterialTheme.lwa.colorScheme.base.error,
text = stringResource(Res.string.character_sheet_edit__copy__error)
)
}
Row(
modifier = Modifier
.padding(bottom = 4.dp)
.padding(horizontal = 16.dp)
.align(alignment = Alignment.End),
horizontalArrangement = Arrangement.spacedBy(
space = 4.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = onDismissRequest,
) {
Text(
color = MaterialTheme.colors.primary.copy(alpha = .7f),
text = stringResource(Res.string.dialog__cancel_action)
)
}
TextButton(
onClick = { if(dialog.validate()) {onConfirm(dialog)} },
) {
Text(
text = stringResource(Res.string.dialog__confirm_action)
)
}
}
}
}
}
}
}

View file

@ -6,6 +6,7 @@ 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.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
@ -31,6 +32,7 @@ import com.pixelized.desktop.lwa.LocalWindowController
import com.pixelized.desktop.lwa.ui.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController
import com.pixelized.desktop.lwa.ui.navigation.window.LocalWindow
import com.pixelized.desktop.lwa.ui.screen.characterSheet.copy.CharacterSheetCopyDialog
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionField
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.BaseSkillFieldUio
@ -55,6 +57,7 @@ import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__sa
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_action
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_action
import lwacharactersheet.composeapp.generated.resources.ic_save_24dp
import lwacharactersheet.composeapp.generated.resources.ic_save_as_24dp
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
@ -121,6 +124,11 @@ fun CharacterSheetEditPage(
viewModel.onNewAction()
}
},
onCopy = {
scope.launch {
viewModel.showCopyCharacterSheetDialog()
}
},
onSave = {
scope.launch {
viewModel.save()
@ -131,6 +139,22 @@ fun CharacterSheetEditPage(
},
)
}
CharacterSheetCopyDialog(
dialog = viewModel.copyCharacterSheetDialog,
onConfirm = { dialog ->
scope.launch {
val characterSheetId = dialog.value.valueFlow.value
viewModel.saveAs(characterSheetId = characterSheetId)
if (screen.popBackStack().not()) {
windowController.hideWindow(window = window)
}
}
},
onDismissRequest = {
viewModel.hideCopyCharacterSheetDialog()
}
)
}
@Composable
@ -141,6 +165,7 @@ fun CharacterSheetEdit(
onNewSpecialSkill: () -> Unit,
onNewMagicSkill: () -> Unit,
onNewAction: () -> Unit,
onCopy: () -> Unit,
onSave: () -> Unit,
) {
Scaffold(
@ -155,6 +180,15 @@ fun CharacterSheetEdit(
)
},
actions = {
IconButton(
onClick = onCopy,
) {
Icon(
painter = painterResource(Res.drawable.ic_save_as_24dp),
tint = MaterialTheme.colors.primary,
contentDescription = null,
)
}
IconButton(
onClick = onSave,
) {
@ -200,7 +234,7 @@ fun CharacterSheetEdit(
verticalAlignment = Alignment.CenterVertically,
) {
LevelUpField(
modifier = Modifier.weight(1f),
modifier = Modifier.weight(1f).offset(y = 4.dp),
field = form.levelUp,
)
SimpleField(

View file

@ -5,13 +5,18 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
import com.pixelized.desktop.lwa.ui.navigation.screen.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.ui.screen.characterSheet.copy.CharacterSheetCopyDialogUio
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.common.SkillFieldFactory
import com.pixelized.desktop.lwa.ui.screen.characterSheet.edit.composable.ActionFieldUio
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.runBlocking
import lwacharactersheet.composeapp.generated.resources.Res
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__action_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__actions__name_label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__copy__label
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__copy__title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__magic_title
import lwacharactersheet.composeapp.generated.resources.character_sheet_edit__skills__special_title
import org.jetbrains.compose.resources.getString
@ -26,6 +31,9 @@ class CharacterSheetEditViewModel(
private val argument = CharacterSheetEditDestination.Argument(savedStateHandle)
private val copyDialog = mutableStateOf<CharacterSheetCopyDialogUio?>(null)
val copyCharacterSheetDialog: State<CharacterSheetCopyDialogUio?> = copyDialog
private val _characterSheet = mutableStateOf(
runBlocking {
sheetFactory.convertToUio(
@ -36,6 +44,34 @@ class CharacterSheetEditViewModel(
)
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
// TODO
suspend fun showCopyCharacterSheetDialog() {
val characterSheetId = MutableStateFlow(argument.id ?: "")
val error = MutableStateFlow(false)
copyDialog.value = CharacterSheetCopyDialogUio(
label = getString(Res.string.character_sheet_edit__copy__title),
value = LwaTextFieldUio(
labelFlow = MutableStateFlow(getString(Res.string.character_sheet_edit__copy__label)),
isError = error,
valueFlow = characterSheetId,
placeHolderFlow = MutableStateFlow(null),
onValueChange = { characterSheetId.value = it },
),
validate = {
characterSheetRepository.checkCharacterSheetIdValidity(
characterSheetId = characterSheetId.value
).also {
error.value = it.not()
}
}
)
}
// TODO
fun hideCopyCharacterSheetDialog() {
copyDialog.value = null
}
suspend fun onNewSpecialSkill() {
val id = UUID.randomUUID().toString()
val skill = skillFactory.createSkill(
@ -115,4 +151,16 @@ class CharacterSheetEditViewModel(
characterSheet = updatedSheet,
)
}
suspend fun saveAs(
characterSheetId: String,
) {
val updatedSheet = sheetFactory.updateCharacterSheet(
currentSheet = characterSheetRepository.characterDetail(characterSheetId = _characterSheet.value.id.value),
editedSheet = _characterSheet.value,
)
characterSheetRepository.updateCharacter(
characterSheet = updatedSheet.copy(id = characterSheetId),
)
}
}

View file

@ -29,7 +29,7 @@ fun LevelUpField(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
style = MaterialTheme.lwa.typography.base.caption,
style = MaterialTheme.typography.body1,
text = field.label,
)
Checkbox(

View file

@ -38,6 +38,7 @@ class GameMasterViewModel(
enable = true,
labelFlow = MutableStateFlow(runBlocking { getString(Res.string.game_master__character__filter) }),
valueFlow = _filter,
isError = MutableStateFlow(false),
placeHolderFlow = MutableStateFlow(null),
onValueChange = { _filter.value = it },
)