Link the CharacterSheet model to the UI.

This commit is contained in:
Thomas Andres Gomez 2024-11-05 14:16:57 +01:00
parent 65aa53890f
commit b71645a7a2
30 changed files with 1113 additions and 961 deletions

View file

@ -0,0 +1,21 @@
package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.pixelized.desktop.lwa.navigation.MainNavHost
import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
MainNavHost()
}
}
}

View file

@ -8,6 +8,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.rememberNavController
import com.pixelized.desktop.lwa.navigation.destination.MainDestination
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetEditPage
import com.pixelized.desktop.lwa.navigation.destination.composableCharacterSheetPage
import com.pixelized.desktop.lwa.navigation.destination.composableMainPage
val LocalScreen = compositionLocalOf<NavHostController> {
@ -28,6 +29,7 @@ fun MainNavHost(
) {
composableMainPage()
composableCharacterSheetPage()
composableCharacterSheetEditPage()
}
}

View file

@ -0,0 +1,48 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPage
import com.pixelized.desktop.lwa.utils.extention.ARG
object CharacterSheetDestination {
private const val ROUTE = "character.sheet"
private const val CHARACTER_ID = "id"
fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}"
fun navigationRoute(id: String) = "$ROUTE?$CHARACTER_ID=$id"
fun arguments() = listOf(
navArgument(CHARACTER_ID) {
nullable = true
}
)
data class Argument(
val id: String,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
id = savedStateHandle.get<String>(CHARACTER_ID) ?: error("missing character id")
)
}
}
fun NavGraphBuilder.composableCharacterSheetPage() {
composable(
route = CharacterSheetDestination.baseRoute(),
arguments = CharacterSheetDestination.arguments(),
) {
CharacterSheetPage()
}
}
fun NavHostController.navigateToCharacterSheet(
id: String,
) {
val route = CharacterSheetDestination.navigationRoute(id = id)
navigate(route = route)
}

View file

@ -0,0 +1,48 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.lifecycle.SavedStateHandle
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navArgument
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPage
import com.pixelized.desktop.lwa.utils.extention.ARG
object CharacterSheetEditDestination {
private const val ROUTE = "character.sheet.edit"
private const val CHARACTER_ID = "id"
fun baseRoute() = "$ROUTE?${CHARACTER_ID.ARG}"
fun navigationRoute(id: String?) = "$ROUTE?$CHARACTER_ID=$id"
fun arguments() = listOf(
navArgument(CHARACTER_ID) {
nullable = true
}
)
data class Argument(
val id: String?,
) {
constructor(savedStateHandle: SavedStateHandle) : this(
id = savedStateHandle.get<String>(CHARACTER_ID)
)
}
}
fun NavGraphBuilder.composableCharacterSheetEditPage() {
composable(
route = CharacterSheetEditDestination.baseRoute(),
arguments = CharacterSheetEditDestination.arguments(),
) {
CharacterSheetEditPage()
}
}
fun NavHostController.navigateToCharacterSheetEdit(
id: String? = null,
) {
val route = CharacterSheetEditDestination.navigationRoute(id = id)
navigate(route = route)
}

View file

@ -10,28 +10,31 @@ import kotlinx.coroutines.flow.stateIn
object CharacterSheetRepository {
private val scope = CoroutineScope(Dispatchers.IO)
private val preferences = CharacterSheetPreference(
dataStore = createDataStore { "characterssheet.preferences_pb" }
)
fun characterSheet(): StateFlow<List<CharacterSheet>> {
return preferences.loadFlow()
private val sheets = preferences.loadFlow()
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = emptyList()
)
fun characterSheetFlow(): StateFlow<List<CharacterSheet>> {
return sheets
}
fun characterSheet(id: String): StateFlow<CharacterSheet?> {
return preferences.loadFlow()
fun characterSheetFlow(id: String?): StateFlow<CharacterSheet?> {
return sheets
.map { sheets ->
sheets.firstOrNull { sheet -> sheet.id == id }
}
.stateIn(
scope = scope,
started = SharingStarted.Eagerly,
initialValue = null
initialValue = sheets.value.firstOrNull { it.id == id }
)
}
@ -48,5 +51,11 @@ object CharacterSheetRepository {
// save the list of characters sheet.
preferences.save(sheets = savedSheets)
}
suspend fun delete(id: String) {
val savedSheets = preferences.load().toMutableList()
savedSheets.removeIf { it.id == id }
preferences.save(sheets = savedSheets)
}
}

View file

@ -0,0 +1,70 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio.Characteristic
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio.Node
class CharacterSheetFactory {
fun convertToUio(model: CharacterSheet): CharacterSheetPageUio {
return CharacterSheetPageUio(
id = model.id,
name = model.name,
characteristics = listOf(
Characteristic(label = "Force", value = "${model.strength}"),
Characteristic(label = "Dextérité", value = "${model.dexterity}"),
Characteristic(label = "Constitution", value = "${model.constitution}"),
Characteristic(label = "Taille", value = "${model.height}"),
Characteristic(label = "Intelligence", value = "${model.intelligence}"),
Characteristic(label = "Pouvoir", value = "${model.power}"),
Characteristic(label = "Charisme", value = "${model.charisma}"),
),
subCharacteristics = listOf(
Characteristic(label = "Déplacement ", value = "${model.movement}"),
Characteristic(
label = "Points de vie",
value = "${model.currentHp}/${model.maxHp}"
),
Characteristic(
label = "Points de pouvoir",
value = "${model.currentPP}/${model.maxPP}"
),
Characteristic(label = "Bonus aux dégâts", value = model.damageBonus),
Characteristic(label = "Armure", value = "${model.armor}"),
),
skills = model.skills.mapNotNull {
if (it.value > 0) {
Node(
type = Node.Type.SKILLS,
label = it.label,
value = it.value,
)
} else {
null
}
},
occupations = model.occupations.mapNotNull {
if (it.value > 0) {
Node(
type = Node.Type.OCCUPATIONS,
label = it.label,
value = it.value,
)
} else {
null
}
},
magics = model.magics.mapNotNull {
if (it.value > 0) {
Node(
type = Node.Type.MAGICS,
label = it.label,
value = it.value,
)
} else {
null
}
},
)
}
}

View file

@ -0,0 +1,377 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.composable.overlay.BlurOverlay
import com.pixelized.desktop.lwa.composable.overlay.BlurOverlayViewModel
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
import com.pixelized.desktop.lwa.screen.roll.RollPage
import com.pixelized.desktop.lwa.screen.roll.RollViewModel
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetPageUio(
val id: String,
val name: String,
val characteristics: List<Characteristic>,
val subCharacteristics: List<Characteristic>,
val skills: List<Node>,
val occupations: List<Node>,
val magics: List<Node>,
) {
@Stable
data class Characteristic(
val label: String,
val value: String,
)
@Stable
data class Node(
val type: Type,
val label: String,
val value: Int,
) {
@Stable
enum class Type {
SKILLS, OCCUPATIONS, MAGICS,
}
}
}
@Composable
fun CharacterSheetPage(
viewModel: CharacterSheetViewModel = viewModel {
CharacterSheetViewModel(savedStateHandle = createSavedStateHandle())
},
overlayViewModel: BlurOverlayViewModel = viewModel { BlurOverlayViewModel() },
rollViewModel: RollViewModel = viewModel { RollViewModel() },
) {
val screen = LocalScreen.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
BlurOverlay(
viewModel = overlayViewModel,
overlay = {
RollPage(
viewModel = rollViewModel,
onDismissRequest = overlayViewModel::hide,
)
},
content = {
viewModel.sheet.value?.let {
CharacterSheetPageContent(
modifier = Modifier.fillMaxSize(),
characterSheet = it,
onBack = {
screen.popBackStack()
},
onEdit = {
screen.navigateToCharacterSheetEdit(id = it.id)
},
onDelete = {
scope.launch {
viewModel.deleteCharacter(id = it.id)
screen.popBackStack()
}
},
onCharacteristic = { characteristic ->
rollViewModel.prepareRoll(characteristic = characteristic)
overlayViewModel.show()
},
onSkill = { node ->
rollViewModel.prepareRoll(node = node)
overlayViewModel.show()
},
)
}
},
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CharacterSheetPageContent(
modifier: Modifier = Modifier,
scrollState: ScrollState = rememberScrollState(),
width: Dp = 320.dp,
characterSheet: CharacterSheetPageUio,
onBack: () -> Unit,
onEdit: () -> Unit,
onDelete: () -> Unit,
onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit,
onSkill: (skill: CharacterSheetPageUio.Node) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characterSheet.name,
)
},
actions = {
IconButton(
onClick = onEdit,
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
}
IconButton(
onClick = onDelete,
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
)
}
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = scrollState).padding(all = 16.dp)
.padding(paddingValues)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
FlowRow(
maxItemsInEachRow = 3,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally,
),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
characterSheet.characteristics.forEach {
Stat(
modifier = Modifier.width(width = width / 3 - 32.dp)
.height(height = 112.dp),
characteristic = it,
onClick = { onCharacteristic(it) },
)
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Charactéristiques dérivées"
)
characterSheet.subCharacteristics.forEach {
Characteristics(
modifier = Modifier.fillMaxWidth(),
characteristic = it,
)
}
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Compétences",
)
characterSheet.skills.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Occupations"
)
characterSheet.occupations.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
DecoratedBox(
modifier = Modifier.width(width = width).padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Compétences magiques"
)
characterSheet.magics.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
}
}
)
}
@Composable
private fun Stat(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(all = 8.dp),
characteristic: CharacterSheetPageUio.Characteristic,
onClick: () -> Unit,
) {
DecoratedBox(
modifier = Modifier.clickable(onClick = onClick).padding(paddingValues = paddingValues)
.then(other = modifier),
) {
Text(
modifier = Modifier.align(alignment = Alignment.TopCenter),
style = MaterialTheme.typography.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.label,
)
Text(
modifier = Modifier.align(alignment = Alignment.Center),
style = MaterialTheme.typography.h4,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.value
)
}
}
@Composable
private fun Characteristics(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
characteristic: CharacterSheetPageUio.Characteristic,
) {
Row(
modifier = Modifier.padding(paddingValues = paddingValues).then(other = modifier),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
text = characteristic.label
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = characteristic.value,
)
}
}
@Composable
private fun Skill(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
label: String,
value: Any,
onClick: () -> Unit,
) {
Row(
modifier = Modifier.clickable(onClick = onClick).padding(paddingValues = paddingValues)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = label
)
Text(
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = "$value",
)
Checkbox(modifier = Modifier.size(size = 32.dp), checked = false, onCheckedChange = { })
}
}

View file

@ -0,0 +1,32 @@
package com.pixelized.desktop.lwa.screen.characterSheet.detail
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetDestination
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.utils.extention.collectAsState
class CharacterSheetViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val argument = CharacterSheetDestination.Argument(savedStateHandle)
private val repository = CharacterSheetRepository
private val factory = CharacterSheetFactory()
val sheet: State<CharacterSheetPageUio?>
@Composable
@Stable
get() = repository
.characterSheetFlow(id = argument.id)
.collectAsState { sheet ->
sheet?.let { model -> factory.convertToUio(model = model) }
}
suspend fun deleteCharacter(id: String) {
repository.delete(id = id)
}
}

View file

@ -0,0 +1,196 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.createSavedStateHandle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetEditPageUio(
val id: String,
val name: FieldUio,
val skills: List<SkillGroup>,
) {
@Stable
data class SkillGroup(
val title: String,
val type: Type,
val editable: Boolean = false,
val fields: List<FieldUio>,
) {
@Stable
enum class Type {
CHARACTERISTICS,
SUB_CHARACTERISTICS,
SKILLS,
OCCUPATIONS,
MAGICS,
OTHER,
}
}
}
@Composable
fun CharacterSheetEditPage(
viewModel: CharacterSheetEditViewModel = viewModel {
CharacterSheetEditViewModel(
savedStateHandle = createSavedStateHandle()
)
},
) {
val screen = LocalScreen.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
CharacterSheetEdit(
form = viewModel.characterSheet.value,
onSkill = viewModel::onSkill,
onBack = { screen.popBackStack() },
onSave = {
scope.launch {
viewModel.save()
screen.popBackStack()
}
},
)
}
}
@Composable
fun CharacterSheetEdit(
form: CharacterSheetEditPageUio,
onSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit,
onBack: () -> Unit,
onSave: () -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = "Création de personnage",
)
},
navigationIcon = {
IconButton(
onClick = onBack,
) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = null,
)
}
}
)
},
content = { paddingValues ->
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(paddingValues = paddingValues)
.padding(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Form(
modifier = Modifier.fillMaxWidth(),
field = form.name,
)
form.skills.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
style = MaterialTheme.typography.caption,
text = it.title,
)
it.fields.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
if (it.editable) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
)
) {
TextButton(
onClick = { onSkill(it) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
text = "Ajouter une ligne",
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.End,
) {
TextButton(
onClick = onSave,
) {
Text(text = "Sauvegarder")
}
}
}
}
)
}

View file

@ -0,0 +1,64 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.navigation.destination.CharacterSheetEditDestination
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
class CharacterSheetEditViewModel(
savedStateHandle: SavedStateHandle,
) : ViewModel() {
private val argument = CharacterSheetEditDestination.Argument(savedStateHandle)
private val repository = CharacterSheetRepository
private val factory = CharacterSheetFactory()
private val _characterSheet = repository
.characterSheetFlow(id = argument.id).value
.let { sheet -> mutableStateOf(factory.convertToUio(sheet = sheet)) }
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
fun onSkill(skill: SkillGroup) {
val sheet = _characterSheet.value
_characterSheet.value = sheet.copy(
skills = sheet.skills.map { group ->
if (skill.title == group.title) {
group.copy(
fields = mutableListOf<FieldUio>().apply {
addAll(group.fields)
add(
FieldUio.create(
label = "",
isLabelEditable = true,
valuePlaceHolder = {
when (group.type) {
SkillGroup.Type.CHARACTERISTICS -> ""
SkillGroup.Type.SUB_CHARACTERISTICS -> ""
SkillGroup.Type.SKILLS -> "0"
SkillGroup.Type.OCCUPATIONS -> "40"
SkillGroup.Type.MAGICS -> "0"
SkillGroup.Type.OTHER -> ""
}
},
)
)
}
)
} else {
group
}
}
)
}
suspend fun save() {
val sheet = _characterSheet.value
val model = factory.convertToModel(sheet = sheet)
repository.save(characterSheet = model)
}
}

View file

@ -1,96 +1,116 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.business.SkillNormalizerUseCase.normalize
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPageUio.SkillGroup
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import java.util.UUID
import kotlin.math.max
class CharacterSheetEditViewModel : ViewModel() {
class CharacterSheetFactory {
private val repository = CharacterSheetRepository
private val factory = CharacterSheetFactory()
private val _characterSheet: MutableState<CharacterSheetEditPageUio>
val characterSheet: State<CharacterSheetEditPageUio> get() = _characterSheet
init {
_characterSheet = mutableStateOf(createEmptyCharacterSheet())
}
fun onSkill(skill: SkillGroup) {
val sheet = _characterSheet.value
_characterSheet.value = sheet.copy(
skills = sheet.skills.map { group ->
if (skill.title == group.title) {
group.copy(
fields = mutableListOf<FieldUio>().apply {
addAll(group.fields)
add(
FieldUio.create(
label = "",
isLabelEditable = true,
valuePlaceHolder = {
when (group.type) {
SkillGroup.Type.CHARACTERISTICS -> ""
SkillGroup.Type.SUB_CHARACTERISTICS -> ""
SkillGroup.Type.SKILLS -> "0"
SkillGroup.Type.OCCUPATIONS -> "40"
SkillGroup.Type.MAGICS -> "0"
SkillGroup.Type.OTHER -> ""
}
fun convertToModel(sheet: CharacterSheetEditPageUio): CharacterSheet {
return CharacterSheet(
id = sheet.id,
name = sheet.name.value.value,
strength = sheet.skills[0].fields[0].unpack(),
dexterity = sheet.skills[0].fields[1].unpack(),
constitution = sheet.skills[0].fields[2].unpack(),
height = sheet.skills[0].fields[3].unpack(),
intelligence = sheet.skills[0].fields[4].unpack(),
power = sheet.skills[0].fields[5].unpack(),
charisma = sheet.skills[0].fields[6].unpack(),
movement = sheet.skills[1].fields[0].unpack(),
currentHp = sheet.skills[1].fields[1].unpack(),
maxHp = sheet.skills[1].fields[1].unpack(),
currentPP = sheet.skills[1].fields[2].unpack(),
maxPP = sheet.skills[1].fields[2].unpack(),
damageBonus = sheet.skills[1].fields[3].unpack(),
armor = sheet.skills[1].fields[4].unpack(),
skills = sheet.skills[2].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
)
},
occupations = sheet.skills[3].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
)
},
magics = sheet.skills[4].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.unpack(),
used = false,
)
}
)
} else {
group
}
}
},
attacks = emptyList(),
)
}
suspend fun save() {
val sheet = _characterSheet.value
val model = factory.convertToModel(sheet = sheet)
repository.save(characterSheet = model)
}
fun convertToUio(
sheet: CharacterSheet?,
): CharacterSheetEditPageUio {
val str = FieldUio.create(
label = "Force",
valuePlaceHolder = { "0" },
initialValue = sheet?.strength?.toString() ?: ""
)
val dex = FieldUio.create(
label = "Dextérité",
valuePlaceHolder = { "0" },
initialValue = sheet?.dexterity?.toString() ?: ""
)
val con = FieldUio.create(
label = "Constitution",
valuePlaceHolder = { "0" },
initialValue = sheet?.constitution?.toString() ?: ""
)
val hei = FieldUio.create(
label = "Taille",
valuePlaceHolder = { "0" },
initialValue = sheet?.height?.toString() ?: ""
)
val int = FieldUio.create(
label = "Intelligence",
valuePlaceHolder = { "0" },
initialValue = sheet?.intelligence?.toString() ?: ""
)
val pow = FieldUio.create(
label = "Pouvoir",
valuePlaceHolder = { "0" },
initialValue = sheet?.power?.toString() ?: ""
)
val cha = FieldUio.create(
label = "Charisme",
valuePlaceHolder = { "0" },
initialValue = sheet?.charisma?.toString() ?: ""
)
private fun createEmptyCharacterSheet(): CharacterSheetEditPageUio {
val str = FieldUio.create(label = "Force", valuePlaceHolder = { "0" })
val dex = FieldUio.create(label = "Dextérité", valuePlaceHolder = { "0" })
val con = FieldUio.create(label = "Constitution", valuePlaceHolder = { "0" })
val vit = FieldUio.create(label = "Taille", valuePlaceHolder = { "0" })
val int = FieldUio.create(label = "Intelligence", valuePlaceHolder = { "0" })
val pow = FieldUio.create(label = "Pouvoir", valuePlaceHolder = { "0" })
val cha = FieldUio.create(label = "Charisme", valuePlaceHolder = { "0" })
fun str(): Int = str.value.value.toIntOrNull() ?: 0
fun dex(): Int = dex.value.value.toIntOrNull() ?: 0
fun con(): Int = con.value.value.toIntOrNull() ?: 0
fun vit(): Int = vit.value.value.toIntOrNull() ?: 0
fun int(): Int = int.value.value.toIntOrNull() ?: 0
fun pow(): Int = pow.value.value.toIntOrNull() ?: 0
fun cha(): Int = cha.value.value.toIntOrNull() ?: 0
fun str(): Int = str.unpack() ?: 0
fun dex(): Int = dex.unpack() ?: 0
fun con(): Int = con.unpack() ?: 0
fun hei(): Int = hei.unpack() ?: 0
fun int(): Int = int.unpack() ?: 0
fun pow(): Int = pow.unpack() ?: 0
fun cha(): Int = cha.unpack() ?: 0
return CharacterSheetEditPageUio(
id = UUID.randomUUID().toString(),
id = sheet?.id ?: UUID.randomUUID().toString(),
name = FieldUio.create(
useLabelAsPlaceholder = true,
label = "Name",
initialValue = sheet?.name ?: ""
),
skills = listOf(
SkillGroup(
title = "Charactéristiques",
type = SkillGroup.Type.CHARACTERISTICS,
fields = listOf(str, dex, con, vit, int, pow, cha),
fields = listOf(str, dex, con, hei, int, pow, cha),
),
SkillGroup(
title = "Charactéristiques dérivées",
@ -99,19 +119,22 @@ class CharacterSheetEditViewModel : ViewModel() {
FieldUio.create(
label = "Déplacement",
valuePlaceHolder = { "10" },
initialValue = sheet?.movement?.toString() ?: ""
),
FieldUio.create(
label = "Points de vie",
valuePlaceHolder = { "${(con() + vit()) / 2}" },
valuePlaceHolder = { "${(con() + hei()) / 2}" },
initialValue = sheet?.maxHp?.toString() ?: ""
),
FieldUio.create(
label = "Points de pouvoir",
valuePlaceHolder = { "${pow()}" },
initialValue = sheet?.maxPP?.toString() ?: ""
),
FieldUio.create(
label = "Bonus aux dégats",
valuePlaceHolder = {
val bonus = str() + vit()
val bonus = str() + hei()
when {
bonus < 12 -> "-1d6"
bonus in 12..17 -> "-1d4"
@ -121,26 +144,37 @@ class CharacterSheetEditViewModel : ViewModel() {
else -> "2d6"
}
},
initialValue = sheet?.damageBonus ?: ""
),
FieldUio.create(
label = "Armure",
valuePlaceHolder = { "0" },
initialValue = sheet?.armor?.toString() ?: ""
),
FieldUio.create(label = "Armure", valuePlaceHolder = { "0" }),
),
),
SkillGroup(
title = "Compétances",
type = SkillGroup.Type.SKILLS,
editable = true,
fields = listOf(
fields = sheet?.skills?.map {
FieldUio.create(
label = it.label,
valuePlaceHolder = { "" },
initialValue = it.value.toString(),
)
} ?: listOf(
FieldUio.create(
label = "Bagarre",
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Esquive",
valuePlaceHolder = { "${normalize(dex() * 2)}" }
valuePlaceHolder = { "${normalize(dex() * 2)}" },
),
FieldUio.create(
label = "Saisie",
valuePlaceHolder = { "${normalize(str() + vit())}" },
valuePlaceHolder = { "${normalize(str() + hei())}" },
),
FieldUio.create(
label = "Lancer",
@ -172,16 +206,7 @@ class CharacterSheetEditViewModel : ViewModel() {
),
FieldUio.create(
label = "Intimidation",
valuePlaceHolder = {
"${
normalize(
cha() + max(
pow(),
vit()
) * 2
)
}"
},
valuePlaceHolder = { "${normalize(cha() + max(pow(), hei()) * 2)}" },
),
FieldUio.create(
label = "Baratin",
@ -193,7 +218,7 @@ class CharacterSheetEditViewModel : ViewModel() {
),
FieldUio.create(
label = "Discrétion",
valuePlaceHolder = { "${normalize(cha() + dex() * 2 - vit())}" },
valuePlaceHolder = { "${normalize(cha() + dex() * 2 - hei())}" },
),
FieldUio.create(
label = "Escamotage",
@ -209,15 +234,35 @@ class CharacterSheetEditViewModel : ViewModel() {
title = "Occupations",
type = SkillGroup.Type.OCCUPATIONS,
editable = true,
fields = emptyList(),
fields = sheet?.occupations?.map {
FieldUio.create(
label = it.label,
valuePlaceHolder = { "40" },
initialValue = it.value.toString()
)
} ?: emptyList(),
),
SkillGroup(
title = "Compétences magiques",
type = SkillGroup.Type.MAGICS,
editable = true,
fields = emptyList(),
fields = sheet?.magics?.map {
FieldUio.create(
label = it.label,
valuePlaceHolder = { "" },
initialValue = it.value.toString()
)
} ?: emptyList(),
),
)
)
}
private inline fun <reified T> FieldUio.unpack(): T {
val tmp = value.value.ifBlank { valuePlaceHolder.value }
return when (T::class) {
Int::class -> (tmp.toIntOrNull() ?: 0) as T
else -> tmp as T
}
}
}

View file

@ -0,0 +1,95 @@
package com.pixelized.desktop.lwa.screen.main
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheet
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
@Stable
data class CharacterUio(
val id: String,
val name: String,
)
@Composable
fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) {
val screen = LocalScreen.current
Surface {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
MainPageContent(
characters = viewModel.characters,
onCharacter = {
screen.navigateToCharacterSheet(id = it.id)
},
onCreateCharacter = {
screen.navigateToCharacterSheetEdit()
},
)
}
}
}
@Composable
fun MainPageContent(
modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit,
) {
Column(
modifier = modifier.padding(horizontal = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 32.dp),
) {
Column {
characters.value.forEach { sheet ->
TextButton(
onClick = { onCharacter(sheet) }
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = sheet.name,
)
}
}
}
TextButton(
onClick = { onCreateCharacter() },
) {
Text(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
textAlign = TextAlign.Start,
text = "Créer une feuille de personnage",
)
}
}
}

View file

@ -14,8 +14,9 @@ class MainPageViewModel : ViewModel() {
val characters: State<List<CharacterUio>>
@Composable
@Stable
get() {
return characterSheetRepository.characterSheet().collectAsState { sheets ->
get() = characterSheetRepository
.characterSheetFlow()
.collectAsState { sheets ->
sheets.map { sheet ->
CharacterUio(
id = sheet.id,
@ -24,6 +25,3 @@ class MainPageViewModel : ViewModel() {
}
}
}
}

View file

@ -8,7 +8,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import com.pixelized.desktop.lwa.business.RollUseCase
import com.pixelized.desktop.lwa.business.SkillStepUseCase
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetPageUio
import com.pixelized.desktop.lwa.screen.characterSheet.detail.CharacterSheetPageUio
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay

View file

@ -0,0 +1,3 @@
package com.pixelized.desktop.lwa.utils.extention
val String.ARG: String get() = "$this={$this}"

View file

@ -1,143 +0,0 @@
package com.pixelized.desktop.lwa
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.pixelized.desktop.lwa.navigation.MainNavHost
import com.pixelized.desktop.lwa.screen.main.MainPage
import com.pixelized.desktop.lwa.theme.LwaTheme
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
LwaTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
MainNavHost()
}
}
}
//
// val sheetViewModel = viewModel { CharacterSheetViewModel() }
// val overlayViewModel = viewModel { BlurOverlayViewModel() }
// val rollViewModel = viewModel { RollViewModel() }
//
// val edit = remember { mutableStateOf<CharacterSheetEditUio?>(null) }
//
//
//
// Column(
// modifier = Modifier.padding(all = 16.dp),
// ) {
// Row {
// Button(
// onClick = sheetViewModel::showCharacterSheet,
// ) {
// Text(text = "Koryas Tissenpa")
// }
// IconButton(
// onClick = {
// edit.value = CharacterSheetEditUio.create(
// sheet = CharacterSheetUio.Koryas,
// )
// }
// ) {
// Icon(
// imageVector = Icons.Default.Edit,
// contentDescription = null
// )
// }
// }
// Button(
// onClick = { edit.value = CharacterSheetEditUio.Default },
// ) {
// Text(text = "Créer une feuille de personnage")
// }
// }
//
// sheetViewModel.sheet.value?.let { sheet ->
// Window(
// onCloseRequest = sheetViewModel::hideCharacterSheet,
// state = rememberWindowState(
// width = 320.dp + 64.dp,
// height = 900.dp,
// ),
// title = "LwaCharacterSheet",
// ) {
// Surface(
// modifier = Modifier.fillMaxSize()
// ) {
// BlurOverlay(
// viewModel = overlayViewModel,
// overlay = {
// RollPage(
// viewModel = rollViewModel,
// onDismissRequest = overlayViewModel::hide,
// )
// },
// content = {
// CharacterSheet(
// modifier = Modifier.fillMaxWidth(),
// width = 320.dp,
// characterSheet = sheet,
// onCharacteristic = { characteristic ->
// rollViewModel.prepareRoll(characteristic = characteristic)
// overlayViewModel.show()
// },
// onSkill = { node ->
// rollViewModel.prepareRoll(node = node)
// overlayViewModel.show()
// }
// )
// },
// )
// }
// }
// }
//
// edit.value?.let { sheet ->
// Window(
// onCloseRequest = { edit.value = null },
// state = rememberWindowState(
// width = 320.dp + 64.dp,
// height = 900.dp,
// ),
// title = "LwaCharacterSheet",
// ) {
// Surface(
// modifier = Modifier.fillMaxSize(),
// ) {
// CharacterSheetEdit(
// form = sheet,
// onSkill = { skill ->
// edit.value = sheet.copy(
// groups = sheet.groups.map { group ->
// if (skill.title == group.title) {
// group.copy(
// fields = mutableListOf<FieldUio>().apply {
// addAll(group.fields)
// add(
// FieldUio.create(
// label = "",
// valuePlaceHolder = { "40" },
// )
// )
// }
// )
// } else {
// group
// }
// }
// )
// }
// )
// }
// }
// }
// }
// }
//}

View file

@ -1,27 +0,0 @@
package com.pixelized.desktop.lwa.navigation.destination
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEdit
import com.pixelized.desktop.lwa.screen.characterSheet.edit.CharacterSheetEditPage
object CharacterSheetEditDestination {
private const val ROUTE = "character.sheet.edit"
fun baseRoute() = ROUTE
fun navigationRoute() = ROUTE
}
fun NavGraphBuilder.composableCharacterSheetEditPage() {
composable(
route = CharacterSheetEditDestination.baseRoute(),
) {
CharacterSheetEditPage()
}
}
fun NavHostController.navigateToCharacterSheetEdit() {
val route = CharacterSheetEditDestination.navigationRoute()
navigate(route = route)
}

View file

@ -1,335 +0,0 @@
package com.pixelized.desktop.lwa.screen.characterSheet
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Checkbox
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.screen.characterSheet.CharacterSheetPageUio.Node.Type
@Stable
data class CharacterSheetPageUio(
val name: String,
val characteristics: List<Characteristic>,
val subCharacteristics: List<Characteristic>,
val skills: List<Node>,
val occupations: List<Node>,
val magics: List<Node>,
) {
@Stable
data class Characteristic(
val label: String,
val value: String,
)
@Stable
data class Node(
val type: Type,
val label: String,
val value: Int,
) {
@Stable
enum class Type {
SKILLS,
OCCUPATIONS,
MAGICS,
}
}
companion object {
val Koryas = CharacterSheetPageUio(
name = "Koryas Tissenpa",
characteristics = listOf(
Characteristic(label = "Force", value = "10"),
Characteristic(label = "Dextérité", value = "11"),
Characteristic(label = "Constitution", value = "15"),
Characteristic(label = "Taille", value = "13"),
Characteristic(label = "Intelligence", value = "9"),
Characteristic(label = "Pouvoir", value = "15"),
Characteristic(label = "Charisme", value = "7"),
),
subCharacteristics = listOf(
Characteristic(label = "Déplacement ", value = "10"),
Characteristic(label = "Points de vie", value = "14/14"),
Characteristic(label = "Points de pouvoir", value = "13/13"),
Characteristic(label = "Bonus aux dégâts", value = "1d4"),
Characteristic(label = "Armure", value = "0"),
),
skills = listOf(
Node(type = Type.SKILLS, label = "Bagarre", value = 75),
Node(type = Type.SKILLS, label = "Esquive", value = 60),
Node(type = Type.SKILLS, label = "Saisie", value = 20),
Node(type = Type.SKILLS, label = "Lancer", value = 20),
Node(type = Type.SKILLS, label = "Athlétisme", value = 60),
Node(type = Type.SKILLS, label = "Acrobatie", value = 50),
Node(type = Type.SKILLS, label = "Perception", value = 55),
Node(type = Type.SKILLS, label = "Recherche", value = 25),
Node(type = Type.SKILLS, label = "Empathie", value = 15),
Node(type = Type.SKILLS, label = "Persuasion", value = 20),
Node(type = Type.SKILLS, label = "Intimidation", value = 50),
Node(type = Type.SKILLS, label = "Baratin", value = 20),
Node(type = Type.SKILLS, label = "Marchandage", value = 10),
Node(type = Type.SKILLS, label = "Escamotage", value = 20),
Node(type = Type.SKILLS, label = "Premiers soins", value = 20),
),
occupations = listOf(
Node(type = Type.OCCUPATIONS, label = "Survie", value = 80),
Node(type = Type.OCCUPATIONS, label = "Empathie (Animal)", value = 60),
Node(type = Type.OCCUPATIONS, label = "Pistage", value = 60),
Node(type = Type.OCCUPATIONS, label = "Discrétion", value = 60),
Node(type = Type.OCCUPATIONS, label = "Connaissance (Herboristerie)", value = 40),
Node(type = Type.OCCUPATIONS, label = "Artisanat (Onguent)", value = 60),
),
magics = listOf(
Node(type = Type.MAGICS, label = "Métamorphose (Loup)", value = 100),
),
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CharacterSheetPage(
modifier: Modifier,
scrollState: ScrollState = rememberScrollState(),
width: Dp = 320.dp,
characterSheet: CharacterSheetPageUio = CharacterSheetPageUio.Koryas,
onCharacteristic: (characteristic: CharacterSheetPageUio.Characteristic) -> Unit,
onSkill: (skill: CharacterSheetPageUio.Node) -> Unit,
) {
Column(
modifier = Modifier
.verticalScroll(state = scrollState)
.padding(all = 16.dp)
.then(other = modifier),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(bottom = 16.dp),
style = MaterialTheme.typography.h4,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characterSheet.name,
)
FlowRow(
maxItemsInEachRow = 3,
horizontalArrangement = Arrangement.spacedBy(
space = 16.dp,
alignment = Alignment.CenterHorizontally,
),
verticalArrangement = Arrangement.spacedBy(space = 16.dp),
) {
characterSheet.characteristics.forEach {
Stat(
modifier = Modifier
.width(width = width / 3 - 32.dp)
.height(height = 112.dp),
characteristic = it,
onClick = { onCharacteristic(it) },
)
}
}
DecoratedBox(
modifier = Modifier
.width(width = width)
.padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Charactéristiques dérivées"
)
characterSheet.subCharacteristics.forEach {
Characteristics(
modifier = Modifier.fillMaxWidth(),
characteristic = it,
)
}
}
}
DecoratedBox(
modifier = Modifier
.width(width = width)
.padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Compétences",
)
characterSheet.skills.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
DecoratedBox(
modifier = Modifier
.width(width = width)
.padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Occupations"
)
characterSheet.occupations.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
DecoratedBox(
modifier = Modifier
.width(width = width)
.padding(vertical = 8.dp),
) {
Column {
Text(
modifier = Modifier.fillMaxWidth().padding(bottom = 8.dp),
style = MaterialTheme.typography.caption,
textAlign = TextAlign.Center,
text = "Compétences magiques"
)
characterSheet.magics.forEach {
Skill(
modifier = Modifier.fillMaxWidth(),
label = it.label,
value = it.value,
onClick = { onSkill(it) },
)
}
}
}
}
}
@Composable
private fun Stat(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(all = 8.dp),
characteristic: CharacterSheetPageUio.Characteristic,
onClick: () -> Unit,
) {
DecoratedBox(
modifier = Modifier
.clickable(onClick = onClick)
.padding(paddingValues = paddingValues)
.then(other = modifier),
) {
Text(
modifier = Modifier.align(alignment = Alignment.TopCenter),
style = MaterialTheme.typography.caption,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.label,
)
Text(
modifier = Modifier.align(alignment = Alignment.Center),
style = MaterialTheme.typography.h4,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = characteristic.value
)
}
}
@Composable
private fun Characteristics(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
characteristic: CharacterSheetPageUio.Characteristic,
) {
Row(
modifier = Modifier
.padding(paddingValues = paddingValues)
.then(other = modifier),
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
text = characteristic.label
)
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = characteristic.value,
)
}
}
@Composable
private fun Skill(
modifier: Modifier = Modifier,
paddingValues: PaddingValues = PaddingValues(horizontal = 8.dp),
label: String,
value: Any,
onClick: () -> Unit,
) {
Row(
modifier = Modifier
.clickable(onClick = onClick)
.padding(paddingValues = paddingValues)
.then(other = modifier),
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
modifier = Modifier.weight(1f),
style = MaterialTheme.typography.body1,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = label
)
Text(
style = MaterialTheme.typography.body1,
fontWeight = FontWeight.Bold,
text = "$value",
)
Checkbox(
modifier = Modifier.size(size = 32.dp),
checked = false,
onCheckedChange = { }
)
}
}

View file

@ -1,19 +0,0 @@
package com.pixelized.desktop.lwa.screen.characterSheet
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
class CharacterSheetViewModel : ViewModel() {
private val _sheet = mutableStateOf<CharacterSheetPageUio?>(null)
val sheet: State<CharacterSheetPageUio?> get() = _sheet
fun showCharacterSheet() {
_sheet.value = CharacterSheetPageUio.Koryas
}
fun hideCharacterSheet() {
_sheet.value = null
}
}

View file

@ -1,166 +0,0 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Arrangement
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.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Button
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.Form
import kotlinx.coroutines.launch
@Stable
data class CharacterSheetEditPageUio(
val id: String,
val name: FieldUio,
val skills: List<SkillGroup>,
) {
@Stable
data class SkillGroup(
val title: String,
val type: Type,
val editable: Boolean = false,
val fields: List<FieldUio>,
) {
@Stable
enum class Type {
CHARACTERISTICS,
SUB_CHARACTERISTICS,
SKILLS,
OCCUPATIONS,
MAGICS,
OTHER,
}
}
}
@Composable
fun CharacterSheetEditPage(
viewModel: CharacterSheetEditViewModel = viewModel { CharacterSheetEditViewModel() },
) {
val screen = LocalScreen.current
val scope = rememberCoroutineScope()
Surface(
modifier = Modifier.fillMaxSize(),
) {
CharacterSheetEdit(
form = viewModel.characterSheet.value,
onSkill = viewModel::onSkill,
onCancel = { screen.popBackStack() },
onSave = {
scope.launch {
viewModel.save()
screen.popBackStack()
}
},
)
}
}
@Composable
fun CharacterSheetEdit(
form: CharacterSheetEditPageUio,
onSkill: (CharacterSheetEditPageUio.SkillGroup) -> Unit,
onCancel: () -> Unit,
onSave: () -> Unit,
) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
.padding(all = 24.dp),
verticalArrangement = Arrangement.spacedBy(space = 16.dp)
) {
Form(
modifier = Modifier.fillMaxWidth(),
field = form.name,
)
form.skills.forEach {
DecoratedBox(
modifier = Modifier.animateContentSize(),
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
modifier = Modifier.padding(vertical = 8.dp),
style = MaterialTheme.typography.caption,
text = it.title,
)
it.fields.forEach {
Form(
modifier = Modifier.fillMaxWidth(),
field = it,
)
}
if (it.editable) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(
space = 8.dp,
alignment = Alignment.End
)
) {
Button(
colors = ButtonDefaults.textButtonColors(),
onClick = { onSkill(it) },
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
) {
Text(
style = MaterialTheme.typography.caption,
text = "Ajouter une ligne",
)
Icon(
imageVector = Icons.Default.Add,
contentDescription = null,
)
}
}
}
}
}
}
}
Row {
TextButton(
onClick = onCancel,
) {
Text(text = "Annuler")
}
TextButton(
onClick = onSave,
) {
Text(text = "Sauvegarder")
}
}
}
}

View file

@ -1,58 +0,0 @@
package com.pixelized.desktop.lwa.screen.characterSheet.edit
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheet
import com.pixelized.desktop.lwa.screen.characterSheet.edit.composable.FieldUio
class CharacterSheetFactory {
fun convertToModel(sheet: CharacterSheetEditPageUio): CharacterSheet {
return CharacterSheet(
id = sheet.id,
name = sheet.name.value.value,
strength = sheet.skills[0].fields[0].unpack(),
dexterity = sheet.skills[0].fields[1].unpack(),
constitution = sheet.skills[0].fields[2].unpack(),
height = sheet.skills[0].fields[3].unpack(),
intelligence = sheet.skills[0].fields[4].unpack(),
power = sheet.skills[0].fields[5].unpack(),
charisma = sheet.skills[0].fields[6].unpack(),
movement = sheet.skills[1].fields[0].unpack(),
currentHp = sheet.skills[1].fields[1].unpack(),
maxHp = sheet.skills[1].fields[1].unpack(),
currentPP = sheet.skills[1].fields[2].unpack(),
maxPP = sheet.skills[1].fields[2].unpack(),
damageBonus = sheet.skills[1].fields[3].unpack(),
armor = sheet.skills[1].fields[4].unpack(),
skills = sheet.skills[2].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.value.value.toIntOrNull() ?: 0,
used = false,
)
},
occupations = sheet.skills[3].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.value.value.toIntOrNull() ?: 0,
used = false,
)
},
magics = sheet.skills[4].fields.map {
CharacterSheet.Skill(
label = it.label.value,
value = it.value.value.toIntOrNull() ?: 0,
used = false,
)
},
attacks = emptyList(),
)
}
private inline fun <reified T> FieldUio.unpack(): T {
val tmp = value.value.ifBlank { valuePlaceHolder.value }
return when (T::class) {
Int::class -> (tmp.toIntOrNull() ?: 0) as T
else -> tmp as T
}
}
}

View file

@ -1,108 +0,0 @@
package com.pixelized.desktop.lwa.screen.main
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.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Edit
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.lifecycle.viewmodel.compose.viewModel
import com.pixelized.desktop.lwa.composable.decoratedBox.DecoratedBox
import com.pixelized.desktop.lwa.navigation.LocalScreen
import com.pixelized.desktop.lwa.navigation.destination.navigateToCharacterSheetEdit
@Stable
data class CharacterUio(
val id: String,
val name: String,
)
@Composable
fun MainPage(
viewModel: MainPageViewModel = viewModel { MainPageViewModel() },
) {
val screen = LocalScreen.current
Surface {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center,
) {
MainPageContent(
characters = viewModel.characters,
onCharacter = {
},
onCharacterEdit = {
},
onCharacterDelete = {
},
onCreateCharacter = {
screen.navigateToCharacterSheetEdit()
},
)
}
}
}
@Composable
fun MainPageContent(
modifier: Modifier = Modifier,
characters: State<List<CharacterUio>>,
onCharacter: (CharacterUio) -> Unit,
onCharacterEdit: (CharacterUio) -> Unit,
onCharacterDelete: (CharacterUio) -> Unit,
onCreateCharacter: () -> Unit,
) {
Column(
modifier = modifier,
) {
DecoratedBox {
Column {
characters.value.forEach { sheet ->
Row {
TextButton(
onClick = { onCharacter(sheet) },
) {
Text(text = sheet.name)
}
IconButton(
onClick = { onCharacterEdit(sheet) },
) {
Icon(
imageVector = Icons.Default.Edit,
contentDescription = null,
)
}
IconButton(
onClick = { onCharacterDelete(sheet) },
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = null,
)
}
}
}
TextButton(
onClick = { onCreateCharacter() },
) {
Text(text = "Créer une feuille de personnage")
}
}
}
}
}