Refactor a bit the header part of the character sheet.

This commit is contained in:
Thomas Andres Gomez 2023-10-30 12:25:17 +01:00
parent 854458903d
commit ae5493336b
6 changed files with 148 additions and 99 deletions

View file

@ -4,6 +4,7 @@ package com.pixelized.rplexicon.ui.screens.character
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.annotation.StringRes
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
@ -42,6 +43,9 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.minimumInteractiveComponentSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
@ -57,14 +61,21 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalRollOverlay
import com.pixelized.rplexicon.LocalSnack
import com.pixelized.rplexicon.NO_WINDOW_INSETS
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.Loader
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.screens.character.CharacterHeader.Action
import com.pixelized.rplexicon.ui.screens.character.CharacterHeader.Alteration
import com.pixelized.rplexicon.ui.screens.character.CharacterHeader.Inventory
import com.pixelized.rplexicon.ui.screens.character.CharacterHeader.Proficiency
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.character.ProficiencyUio.ID.*
import com.pixelized.rplexicon.ui.screens.character.composable.character.StatUio.ID.*
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPage
import com.pixelized.rplexicon.ui.screens.character.pages.actions.ActionPagePreview
import com.pixelized.rplexicon.ui.screens.character.pages.actions.AttacksViewModel
@ -87,6 +98,14 @@ import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.extentions.lexiconShadow
import kotlinx.coroutines.launch
enum class CharacterHeader(@StringRes val label: Int) {
Action(R.string.character_sheet_tab_actions),
Alteration(R.string.character_sheet_tab_alteration),
Inventory(R.string.character_sheet_tab_inventory),
Proficiency(R.string.character_sheet_tab_proficiency),
}
@OptIn(
ExperimentalMaterialApi::class,
ExperimentalFoundationApi::class,
@ -103,7 +122,6 @@ fun CharacterSheetScreen(
skillViewModel: SkillsViewModel = hiltViewModel(),
alterationsViewModel: AlterationViewModel = hiltViewModel(),
) {
val snack = LocalSnack.current
val screen = LocalScreenNavHost.current
val overlay = LocalRollOverlay.current
val scope = rememberCoroutineScope()
@ -115,14 +133,9 @@ fun CharacterSheetScreen(
refreshing = false,
onRefresh = { scope.launch { viewModel.update(force = true) } },
)
val pagerState = rememberPagerState {
val haveSheet = proficiencyViewModel.sheet.value != null
val haveAction = attacksViewModel.attacks.value.isNotEmpty()
val haveSpell = spellsViewModel.spells.value.isNotEmpty()
val haveAlteration = alterationsViewModel.alterations.value.isNotEmpty()
val haveInventory = true
haveSheet.toInt() + (haveAction || haveSpell).toInt() + haveAlteration.toInt() + haveInventory.toInt()
}
val tabs = rememberHeaderTabsState()
val pagerState = rememberPagerState { tabs.value.size }
Surface(
modifier = Modifier.fillMaxSize(),
) {
@ -132,18 +145,21 @@ fun CharacterSheetScreen(
sheetState = sheetState,
refreshState = refresh,
name = viewModel.character,
onRefresh = {
scope.launch { viewModel.update(force = true) }
},
onFullRefresh = {
scope.launch { viewModel.update(force = true, full = true) }
},
tabs = tabs,
header = headerViewModel.header,
onBack = {
screen.popBackStack()
},
onTab = {
scope.launch { pagerState.animateScrollToPage(it) }
},
onHitPoint = headerViewModel::toggleHitPointDialog,
onRefresh = {
scope.launch { viewModel.update(force = true) }
},
onFullRefresh = {
scope.launch { viewModel.update(force = true, full = true) }
},
loader = {
Loader(
modifier = Modifier.align(Alignment.TopCenter),
@ -159,7 +175,6 @@ fun CharacterSheetScreen(
actions = {
ActionPage(
sheetState = sheetState,
headerViewModel = headerViewModel,
attacksViewModel = attacksViewModel,
objectsViewModel = objectsViewModel,
spellsViewModel = spellsViewModel,
@ -189,6 +204,12 @@ fun CharacterSheetScreen(
},
)
HandleHitPointEditDialog(
dialog = headerViewModel.hitPointDialog,
onDismissRequest = headerViewModel::toggleHitPointDialog,
onConfirm = headerViewModel::applyHitPointChange,
)
HandleFetchError(
errors = viewModel.error,
)
@ -217,6 +238,9 @@ private fun CharacterSheetContent(
onRefresh: () -> Unit,
onFullRefresh: () -> Unit,
name: String,
tabs: State<List<CharacterHeader>>,
header: State<CharacterSheetHeaderUio?>,
onHitPoint: () -> Unit,
onBack: () -> Unit,
onTab: (Int) -> Unit,
loader: @Composable BoxScope.() -> Unit,
@ -276,19 +300,28 @@ private fun CharacterSheetContent(
}
},
content = {
Box(
Column(
modifier = Modifier
.fillMaxSize()
.navigationBarsPadding(),
) {
Column {
Column(
modifier = Modifier
.zIndex(1f)
.lexiconShadow(),
) {
CharacterSheetHeader(
modifier = Modifier.fillMaxWidth(),
header = header,
onHitPoint = onHitPoint,
)
PagerHeader(
modifier = Modifier
.zIndex(1f)
.lexiconShadow(),
pagerState = pagerState,
tabs = tabs,
onTab = onTab,
)
}
Box {
HorizontalPager(
modifier = Modifier
.fillMaxWidth()
@ -297,16 +330,16 @@ private fun CharacterSheetContent(
beyondBoundsPageCount = 0,
verticalAlignment = Alignment.Top,
pageContent = { page ->
when (page) {
0 -> inventory()
1 -> proficiencies()
2 -> actions()
3 -> alterations()
when (tabs.value[page]) {
Action -> actions()
Alteration -> alterations()
Inventory -> inventory()
Proficiency -> proficiencies()
}
}
)
loader()
}
loader()
}
}
)
@ -320,41 +353,78 @@ private fun CharacterSheetContent(
private fun PagerHeader(
modifier: Modifier = Modifier,
pagerState: PagerState,
tabs: List<String> = headers(),
tabs: State<List<CharacterHeader>>,
onTab: (Int) -> Unit,
) {
ScrollableTabRow(
modifier = modifier,
selectedTabIndex = pagerState.currentPage,
divider = { },
) {
tabs.forEachIndexed { index, tab ->
Tab(
selected = pagerState.currentPage == index,
selectedContentColor = MaterialTheme.colorScheme.onSurface,
unselectedContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
onClick = { onTab(index) },
) {
Text(
modifier = Modifier.padding(all = 8.dp),
style = MaterialTheme.typography.labelSmall,
text = tab,
)
if (tabs.value.isNotEmpty()) {
ScrollableTabRow(
modifier = modifier,
selectedTabIndex = pagerState.currentPage,
edgePadding = 16.dp,
divider = { },
) {
tabs.value.forEachIndexed { index, tab ->
Tab(
selected = pagerState.currentPage == index,
selectedContentColor = MaterialTheme.colorScheme.onSurface,
unselectedContentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.5f),
onClick = { onTab(index) },
) {
Text(
modifier = Modifier.padding(all = 8.dp),
style = MaterialTheme.typography.labelSmall,
text = stringResource(id = tab.label),
)
}
}
}
}
}
@Stable
@Composable
private fun headers(): List<String> {
val inventory = stringResource(id = R.string.character_sheet_tab_inventory)
val proficiency = stringResource(id = R.string.character_sheet_tab_proficiency)
val actions = stringResource(id = R.string.character_sheet_tab_actions)
val alteration = stringResource(id = R.string.character_sheet_tab_alteration)
return remember {
listOf(inventory, proficiency, actions, alteration)
@Stable
private fun rememberHeaderTabsState(
proficiencyViewModel: ProficiencyViewModel = hiltViewModel(),
attacksViewModel: AttacksViewModel = hiltViewModel(),
inventoryViewModel: InventoryViewModel = hiltViewModel(),
spellsViewModel: SpellsViewModel = hiltViewModel(),
skillViewModel: SkillsViewModel = hiltViewModel(),
alterationsViewModel: AlterationViewModel = hiltViewModel(),
): State<List<CharacterHeader>> {
val headers = remember {
derivedStateOf {
mutableListOf<CharacterHeader>().apply {
addAll(
when {
proficiencyViewModel.sheet.value != null -> listOf(Proficiency)
else -> emptyList()
}
)
addAll(
when {
attacksViewModel.attacks.value.isNotEmpty() ||
spellsViewModel.spells.value.isNotEmpty() ||
skillViewModel.skills.value.isNotEmpty() -> listOf(Action)
else -> emptyList()
}
)
addAll(
when {
alterationsViewModel.alterations.value.isNotEmpty() -> listOf(Alteration)
else -> emptyList()
}
)
addAll(
when {
inventoryViewModel.inventory.value.isNotEmpty() -> listOf(Inventory)
else -> emptyList()
}
)
}
}
}
return headers
}
@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class)
@ -366,23 +436,20 @@ private fun CharacterScreenPreview(
) {
LexiconTheme {
Surface {
val sheetState = rememberModalBottomSheetState(
initialValue = when (preview == 3) {
true -> ModalBottomSheetValue.Expanded
else -> ModalBottomSheetValue.Hidden
},
)
CharacterSheetContent(
modifier = Modifier.fillMaxSize(),
sheetState = sheetState,
sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden),
pagerState = rememberPagerState(initialPage = preview) { 4 },
refreshState = rememberPullRefreshState(refreshing = false, onRefresh = { }),
name = "Brulkhai",
header = rememberCharacterHeaderStatePreview(),
tabs = rememberHeaderPreview(),
onBack = { },
onTab = { },
onRefresh = { },
onFullRefresh = { },
loader = { },
onHitPoint = { },
proficiencies = { ProficiencyPreview() },
actions = { ActionPagePreview() },
alterations = { AlterationPagePreview() },
@ -393,7 +460,18 @@ private fun CharacterScreenPreview(
}
}
private fun Boolean.toInt(): Int = if (this) 1 else 0
@Composable
@Stable
private fun rememberHeaderPreview(): State<List<CharacterHeader>> = remember {
mutableStateOf(
listOf(
Proficiency,
Action,
Alteration,
Inventory,
)
)
}
private class CharacterScreenPreviewProvider : PreviewParameterProvider<Int> {
override val values: Sequence<Int> = sequenceOf(0, 1, 2, 3)

View file

@ -32,7 +32,7 @@ data class CharacterSheetHeaderUio(
@Composable
fun CharacterSheetHeader(
modifier: Modifier = Modifier,
padding : PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
padding : PaddingValues = PaddingValues(start = 16.dp, end = 16.dp, bottom = 4.dp),
header: State<CharacterSheetHeaderUio?>,
onHitPoint : () -> Unit,
) {

View file

@ -36,8 +36,8 @@ data class LabelPointUio(
fun LabelPoint(
modifier: Modifier = Modifier,
labelStyle: TextStyle = MaterialTheme.typography.labelSmall,
valueStyle: TextStyle = MaterialTheme.typography.headlineMedium,
maxStyle: TextStyle = MaterialTheme.typography.titleMedium,
valueStyle: TextStyle = MaterialTheme.typography.headlineSmall,
maxStyle: TextStyle = MaterialTheme.typography.titleSmall,
label: LabelPointUio,
onClick: (() -> Unit)? = null,
) {

View file

@ -4,9 +4,9 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
@ -24,7 +24,6 @@ import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.LocalRollOverlay
import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.edit.HandleHitPointEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleSkillEditDialog
import com.pixelized.rplexicon.ui.composable.edit.HandleSpellEditDialog
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
@ -41,10 +40,7 @@ import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHead
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.SpellUio
import com.pixelized.rplexicon.ui.screens.character.composable.actions.rememberTokenListStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeader
import com.pixelized.rplexicon.ui.screens.character.composable.character.CharacterSheetHeaderUio
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberAttackListStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberCharacterHeaderStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberObjectListStatePreview
import com.pixelized.rplexicon.ui.screens.character.composable.preview.rememberSpellListStatePreview
import com.pixelized.rplexicon.ui.theme.LexiconTheme
@ -54,7 +50,6 @@ import kotlinx.coroutines.launch
@Composable
fun ActionPage(
sheetState: ModalBottomSheetState,
headerViewModel: HeaderViewModel = hiltViewModel(),
attacksViewModel: AttacksViewModel = hiltViewModel(),
objectsViewModel: ObjectsViewModel = hiltViewModel(),
spellsViewModel: SpellsViewModel = hiltViewModel(),
@ -66,12 +61,10 @@ fun ActionPage(
ActionsPageContent(
modifier = Modifier.fillMaxSize(),
header = headerViewModel.header,
attacks = attacksViewModel.attacks,
objects = objectsViewModel.objects,
tokens = skillViewModel.skills,
spells = spellsViewModel.spells,
onHitPoint = headerViewModel::toggleHitPointDialog,
onAttackHit = { id ->
attacksViewModel.onHitRoll(id)?.let {
overlay.prepareRoll(diceThrow = it)
@ -84,7 +77,7 @@ fun ActionPage(
overlay.showOverlay()
}
},
onObject = {
onObject = { id ->
},
onUseObject = { id ->
@ -108,7 +101,7 @@ fun ActionPage(
},
onSpell = { spell ->
screen.navigateToSpellDetail(
character = headerViewModel.character,
character = spellsViewModel.characterName,
spell = spell,
)
},
@ -131,12 +124,6 @@ fun ActionPage(
},
)
HandleHitPointEditDialog(
dialog = headerViewModel.hitPointDialog,
onDismissRequest = headerViewModel::toggleHitPointDialog,
onConfirm = headerViewModel::applyHitPointChange,
)
HandleSpellEditDialog(
dialog = spellsViewModel.spellEditDialog,
onDismissRequest = spellsViewModel::hideSpellEditDialog,
@ -160,12 +147,10 @@ fun ActionPage(
fun ActionsPageContent(
modifier: Modifier = Modifier,
lazyListState: LazyListState = rememberLazyListState(),
header: State<CharacterSheetHeaderUio?>,
attacks: State<List<AttackUio>>,
objects: State<List<ObjectItemUio>>,
tokens: State<List<SkillItemUio>>,
spells: State<List<Pair<SpellHeaderUio, List<SpellUio>>>>,
onHitPoint: () -> Unit,
onAttackHit: (id: String) -> Unit,
onAttackDamage: (id: String) -> Unit,
onObject: (id: String) -> Unit,
@ -182,14 +167,9 @@ fun ActionsPageContent(
Column(
modifier = modifier,
) {
CharacterSheetHeader(
modifier = Modifier.fillMaxWidth(),
header = header,
onHitPoint = onHitPoint,
)
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(vertical = 8.dp),
) {
if (attacks.value.isNotEmpty()) {
stickyHeader {
@ -262,9 +242,6 @@ fun ActionsPageContent(
onCast = onCast,
)
}
items(count = 1) {
Spacer(modifier = Modifier.height(height = 16.dp))
}
}
}
}
@ -278,12 +255,10 @@ fun ActionPagePreview() {
Surface {
ActionsPageContent(
modifier = Modifier.fillMaxSize(),
header = rememberCharacterHeaderStatePreview(),
attacks = rememberAttackListStatePreview(),
objects = rememberObjectListStatePreview(),
tokens = rememberTokenListStatePreview(),
spells = rememberSpellListStatePreview(),
onHitPoint = { },
onAttackHit = { },
onAttackDamage = { },
onObject = { },

View file

@ -44,10 +44,6 @@ class ObjectsViewModel @Inject constructor(
}
}
fun onObject(name: String) {
}
fun onUse(name: String): DiceThrow? {
val item = objectsRepository.find(character = character, item = name)
return item?.let {

View file

@ -45,9 +45,9 @@ class SpellsViewModel @Inject constructor(
spellFactory: SpellUioFactory,
savedStateHandle: SavedStateHandle,
) : AndroidViewModel(application) {
private val characterName = savedStateHandle.characterSheetArgument.name
private var character: CharacterSheet? = null
private var characterFire: CharacterSheetFire? = null
val characterName = savedStateHandle.characterSheetArgument.name
private val _editDialog = mutableStateOf<SpellEditDialogUio?>(null)
val spellEditDialog: State<SpellEditDialogUio?> get() = _editDialog