Add filter chip to the gamemaster character screen.
This commit is contained in:
parent
a59444c610
commit
662e270f3f
6 changed files with 196 additions and 75 deletions
|
@ -178,10 +178,10 @@
|
|||
<string name="level_up__skill_level">niv : %1$d -</string>
|
||||
|
||||
<string name="game_master__character_level__label">niv: %1$d</string>
|
||||
<string name="game_master__character_tag__character_search">joueur</string>
|
||||
<string name="game_master__character_tag__character_label">joueur: %1$d</string>
|
||||
<string name="game_master__character_tag__npc_search">npc</string>
|
||||
<string name="game_master__character_tag__npc_label">npc: %1$d</string>
|
||||
<string name="game_master__character_tag__character_search">Joueur</string>
|
||||
<string name="game_master__character_tag__character_label">Joueur-%1$d</string>
|
||||
<string name="game_master__character_tag__npc_search">Npc</string>
|
||||
<string name="game_master__character_tag__npc_label">Npc-%1$d</string>
|
||||
<string name="game_master__character_action__display_portrait">Afficher le portrait</string>
|
||||
<string name="game_master__character_action__add_to_group">Ajouter au groupe</string>
|
||||
<string name="game_master__character_action__remove_from_group">Retirer du groupe (id: %1$d)</string>
|
||||
|
|
|
@ -3,12 +3,11 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster
|
|||
import com.pixelized.desktop.lwa.repository.campaign.model.CharacterSheetPreview
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio.Action
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
|
||||
import com.pixelized.shared.lwa.model.campaign.Campaign
|
||||
import lwacharactersheet.composeapp.generated.resources.Res
|
||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_label
|
||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_search
|
||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_label
|
||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_search
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import java.text.Normalizer
|
||||
|
||||
|
@ -18,6 +17,7 @@ class GameMasterFactory {
|
|||
campaign: Campaign,
|
||||
characters: List<CharacterSheetPreview>,
|
||||
filter: String,
|
||||
tags: Map<GMTagUio.TagId, Boolean>,
|
||||
): List<GMCharacterPreviewUio> {
|
||||
val normalizedFilter = Normalizer.normalize(filter, Normalizer.Form.NFD)
|
||||
|
||||
|
@ -26,6 +26,7 @@ class GameMasterFactory {
|
|||
campaign = campaign,
|
||||
character = it,
|
||||
filter = normalizedFilter,
|
||||
tags = tags,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -34,71 +35,67 @@ class GameMasterFactory {
|
|||
campaign: Campaign,
|
||||
character: CharacterSheetPreview,
|
||||
filter: String,
|
||||
tags: Map<GMTagUio.TagId, Boolean>,
|
||||
): GMCharacterPreviewUio? {
|
||||
val characterId = campaign.characters.keys.firstOrNull {
|
||||
// get the characterInstanceId from the player list corresponding to this CharacterSheet if any
|
||||
val characterInstanceId: Campaign.CharacterInstance.Id? =
|
||||
campaign.characters.keys.firstOrNull {
|
||||
it.characterSheetId == character.characterSheetId
|
||||
}
|
||||
|
||||
// get all characterInstanceId from the npcs list corresponding to this CharacterSheet if any
|
||||
val npcIds = campaign.npcs.keys.filter {
|
||||
it.characterSheetId == character.characterSheetId
|
||||
}
|
||||
|
||||
var playerTagHighlighted = false
|
||||
var npcTagHighlighted = false
|
||||
|
||||
// Filter process.
|
||||
// Filter process : Name.
|
||||
if (filter.isNotEmpty()) {
|
||||
val normalizedName = Normalizer.normalize(character.name, Normalizer.Form.NFD)
|
||||
// If the filter is not empty and the character is not
|
||||
val playerTag = getString(Res.string.game_master__character_tag__character_search)
|
||||
val npcTag = getString(Res.string.game_master__character_tag__npc_search)
|
||||
|
||||
playerTagHighlighted = playerTag.contains(other = filter, ignoreCase = true)
|
||||
if (playerTagHighlighted && characterId == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
npcTagHighlighted = npcTag.contains(other = filter, ignoreCase = true)
|
||||
if (npcTagHighlighted && npcIds.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val nameHighlight = normalizedName.contains(other = filter, ignoreCase = true)
|
||||
if (nameHighlight.not() && playerTagHighlighted.not() && npcTagHighlighted.not()) {
|
||||
if (nameHighlight.not()) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
val tags = buildList {
|
||||
if (characterId != null) {
|
||||
// Tag filter process : Player.
|
||||
if (tags[GMTagUio.TagId.PLAYER] == true && characterInstanceId == null) {
|
||||
return null
|
||||
}
|
||||
// Tag filter process : Npc.
|
||||
if (tags[GMTagUio.TagId.NPC] == true && npcIds.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
// Build the call tag list.
|
||||
val previewTagsList = buildList {
|
||||
if (characterInstanceId != null) {
|
||||
add(
|
||||
GMCharacterPreviewUio.Tag(
|
||||
GMTagUio(
|
||||
id = GMTagUio.TagId.PLAYER,
|
||||
label = getString(
|
||||
Res.string.game_master__character_tag__character_label,
|
||||
characterId.instanceId,
|
||||
characterInstanceId.instanceId,
|
||||
),
|
||||
highlight = playerTagHighlighted,
|
||||
highlight = tags[GMTagUio.TagId.PLAYER] ?: false,
|
||||
)
|
||||
)
|
||||
}
|
||||
addAll(
|
||||
npcIds.map { npcId ->
|
||||
GMCharacterPreviewUio.Tag(
|
||||
GMTagUio(
|
||||
id = GMTagUio.TagId.NPC,
|
||||
label = getString(
|
||||
Res.string.game_master__character_tag__npc_label,
|
||||
npcId.instanceId
|
||||
),
|
||||
highlight = npcTagHighlighted,
|
||||
highlight = tags[GMTagUio.TagId.NPC] ?: false,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// build the cell action list
|
||||
val actions = buildList {
|
||||
add(
|
||||
when (characterId) {
|
||||
when (characterInstanceId) {
|
||||
null -> Action.AddToGroup
|
||||
else -> Action.RemoveFromGroup(instanceId = characterId.instanceId)
|
||||
else -> Action.RemoveFromGroup(instanceId = characterInstanceId.instanceId)
|
||||
}
|
||||
)
|
||||
add(Action.AddToNpc)
|
||||
|
@ -108,11 +105,11 @@ class GameMasterFactory {
|
|||
}
|
||||
)
|
||||
}
|
||||
|
||||
// return the cell UIO.
|
||||
return GMCharacterPreviewUio(
|
||||
characterSheetId = character.characterSheetId,
|
||||
name = character.name, level = character.level,
|
||||
tags = tags,
|
||||
tags = previewTagsList,
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,11 @@ package com.pixelized.desktop.lwa.ui.screen.gamemaster
|
|||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.gestures.Orientation
|
||||
import androidx.compose.foundation.gestures.draggable
|
||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||
import androidx.compose.foundation.gestures.scrollBy
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
@ -10,7 +15,10 @@ 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.LazyListState
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
|
@ -21,13 +29,18 @@ import androidx.compose.material.TopAppBar
|
|||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextField
|
||||
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreview
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTag
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
|
||||
import com.pixelized.desktop.lwa.ui.theme.lwa
|
||||
import kotlinx.coroutines.launch
|
||||
import lwacharactersheet.composeapp.generated.resources.Res
|
||||
import lwacharactersheet.composeapp.generated.resources.ic_cancel_24dp
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
@ -38,6 +51,7 @@ fun GameMasterScreen(
|
|||
viewModel: GameMasterViewModel = koinViewModel(),
|
||||
) {
|
||||
val characters = viewModel.characters.collectAsState()
|
||||
val tags = viewModel.tags.collectAsState()
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
@ -45,7 +59,9 @@ fun GameMasterScreen(
|
|||
GameMasterContent(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
filter = viewModel.filter,
|
||||
tags = tags,
|
||||
characters = characters,
|
||||
onTag = viewModel::onTag,
|
||||
onCharacterAction = viewModel::onCharacterAction,
|
||||
)
|
||||
}
|
||||
|
@ -54,10 +70,15 @@ fun GameMasterScreen(
|
|||
@Composable
|
||||
private fun GameMasterContent(
|
||||
modifier: Modifier = Modifier,
|
||||
filterChipsState: LazyListState = rememberLazyListState(),
|
||||
filter: LwaTextFieldUio,
|
||||
tags: State<List<GMTagUio>>,
|
||||
characters: State<List<GMCharacterPreviewUio>>,
|
||||
onTag: (GMTagUio.TagId) -> Unit,
|
||||
onCharacterAction: (String, GMCharacterPreviewUio.Action) -> Unit,
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
|
@ -95,9 +116,32 @@ private fun GameMasterContent(
|
|||
}
|
||||
}
|
||||
)
|
||||
LazyRow(
|
||||
modifier = Modifier.draggable(
|
||||
orientation = Orientation.Horizontal,
|
||||
state = rememberDraggableState { delta ->
|
||||
scope.launch {
|
||||
filterChipsState.scrollBy(-delta)
|
||||
}
|
||||
},
|
||||
),
|
||||
state = filterChipsState,
|
||||
contentPadding = remember { PaddingValues(all = 8.dp) },
|
||||
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
|
||||
) {
|
||||
items(
|
||||
items = tags.value,
|
||||
) { tag ->
|
||||
GMTag(
|
||||
style = MaterialTheme.lwa.typography.base.body1,
|
||||
tag = tag,
|
||||
onTag = { onTag(tag.id) },
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentPadding = PaddingValues(all = 8.dp),
|
||||
contentPadding = remember { PaddingValues(all = 8.dp) },
|
||||
verticalArrangement = Arrangement.spacedBy(space = 8.dp),
|
||||
) {
|
||||
items(
|
||||
|
|
|
@ -6,11 +6,18 @@ import com.pixelized.desktop.lwa.repository.campaign.CampaignRepository
|
|||
import com.pixelized.desktop.lwa.repository.characterSheet.CharacterSheetRepository
|
||||
import com.pixelized.desktop.lwa.ui.composable.textfield.LwaTextFieldUio
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMCharacterPreviewUio
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio
|
||||
import com.pixelized.desktop.lwa.ui.screen.gamemaster.items.GMTagUio.TagId
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.launch
|
||||
import lwacharactersheet.composeapp.generated.resources.Res
|
||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__character_search
|
||||
import lwacharactersheet.composeapp.generated.resources.game_master__character_tag__npc_search
|
||||
import org.jetbrains.compose.resources.getString
|
||||
|
||||
class GameMasterViewModel(
|
||||
private val campaignRepository: CampaignRepository,
|
||||
|
@ -28,10 +35,34 @@ class GameMasterViewModel(
|
|||
onValueChange = { _filter.value = it },
|
||||
)
|
||||
|
||||
private val _tags = MutableStateFlow(mapOf(TagId.PLAYER to false, TagId.NPC to false))
|
||||
val tags = _tags.map { it: Map<TagId, Boolean> ->
|
||||
it.map { (tag, highlight) ->
|
||||
when (tag) {
|
||||
TagId.PLAYER -> GMTagUio(
|
||||
id = TagId.PLAYER,
|
||||
label = getString(Res.string.game_master__character_tag__character_search),
|
||||
highlight = highlight,
|
||||
)
|
||||
|
||||
TagId.NPC -> GMTagUio(
|
||||
id = TagId.NPC,
|
||||
label = getString(Res.string.game_master__character_tag__npc_search),
|
||||
highlight = highlight,
|
||||
)
|
||||
}
|
||||
}
|
||||
}.stateIn(
|
||||
scope = viewModelScope,
|
||||
started = SharingStarted.Eagerly,
|
||||
initialValue = emptyList(),
|
||||
)
|
||||
|
||||
val characters = combine(
|
||||
campaignRepository.campaignFlow,
|
||||
characterSheetRepository.characterSheetPreviewFlow,
|
||||
filter.valueFlow,
|
||||
_tags,
|
||||
gameMasterFactory::convertToGMCharacterPreviewUio,
|
||||
).stateIn(
|
||||
scope = viewModelScope,
|
||||
|
@ -54,4 +85,12 @@ class GameMasterViewModel(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onTag(
|
||||
id: TagId,
|
||||
) {
|
||||
_tags.value = _tags.value.toMutableMap().also {
|
||||
it[id] = it.getOrPut(id) { true }.not()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,7 +8,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
|||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
|
@ -44,15 +43,9 @@ data class GMCharacterPreviewUio(
|
|||
val characterSheetId: String,
|
||||
val name: String,
|
||||
val level: Int,
|
||||
val tags: List<Tag>,
|
||||
val tags: List<GMTagUio>,
|
||||
val actions: List<Action>,
|
||||
) {
|
||||
@Stable
|
||||
data class Tag(
|
||||
val label: String,
|
||||
val highlight: Boolean,
|
||||
)
|
||||
|
||||
@Stable
|
||||
sealed class Action {
|
||||
@Stable
|
||||
|
@ -127,7 +120,10 @@ fun GMCharacterPreview(
|
|||
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
|
||||
) {
|
||||
character.tags.forEach { tag ->
|
||||
Tag(tag = tag)
|
||||
GMTag(
|
||||
style = MaterialTheme.lwa.typography.base.caption,
|
||||
tag = tag,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -201,26 +197,3 @@ private fun OverflowActionMenu(
|
|||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
private fun Tag(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = PaddingValues(horizontal = 8.dp, vertical = 2.dp),
|
||||
tag: GMCharacterPreviewUio.Tag,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier
|
||||
.background(
|
||||
color = MaterialTheme.lwa.colorScheme.elevated.base4dp,
|
||||
shape = CircleShape,
|
||||
)
|
||||
.padding(paddingValues = padding),
|
||||
style = MaterialTheme.lwa.typography.base.caption,
|
||||
color = when (tag.highlight) {
|
||||
true -> MaterialTheme.lwa.colorScheme.base.secondary
|
||||
else -> MaterialTheme.lwa.colorScheme.base.onSurface
|
||||
},
|
||||
text = tag.label,
|
||||
)
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package com.pixelized.desktop.lwa.ui.screen.gamemaster.items
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.desktop.lwa.ui.theme.lwa
|
||||
|
||||
|
||||
@Stable
|
||||
data class GMTagUio(
|
||||
val id: TagId,
|
||||
val label: String,
|
||||
val highlight: Boolean,
|
||||
) {
|
||||
@Stable
|
||||
enum class TagId {
|
||||
PLAYER, NPC
|
||||
}
|
||||
}
|
||||
|
||||
@Stable
|
||||
object GmTagDefault {
|
||||
val padding = PaddingValues(horizontal = 8.dp, vertical = 2.dp)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun GMTag(
|
||||
modifier: Modifier = Modifier,
|
||||
padding: PaddingValues = GmTagDefault.padding,
|
||||
shape: Shape = CircleShape,
|
||||
elevation: Dp = 2.dp,
|
||||
style: TextStyle,
|
||||
tag: GMTagUio,
|
||||
onTag: (() -> Unit)? = null,
|
||||
) {
|
||||
val animatedColor = animateColorAsState(
|
||||
when (tag.highlight) {
|
||||
true -> MaterialTheme.lwa.colorScheme.base.secondary
|
||||
else -> MaterialTheme.lwa.colorScheme.base.onSurface
|
||||
}
|
||||
)
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = shape,
|
||||
elevation = elevation,
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.clickable(enabled = onTag != null) { onTag?.invoke() }
|
||||
.padding(paddingValues = padding),
|
||||
style = style,
|
||||
color = animatedColor.value,
|
||||
text = tag.label,
|
||||
)
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue