Make landing page dynamic based on the character sheets

This commit is contained in:
Andres Gomez, Thomas (ITDV RL) 2024-06-05 16:08:39 +02:00
parent ddfc9a4cc6
commit 71b0556a74
16 changed files with 240 additions and 146 deletions

View file

@ -13,7 +13,7 @@ object AlterationSortUseCase {
}.thenByDescending { }.thenByDescending {
PLAYER == it PLAYER == it
}.thenByDescending { }.thenByDescending {
sheet?.characterClass?.any { clazz -> clazz.value == it } sheet?.characterClass?.any { clazz -> clazz.id == it }
}.thenByDescending { }.thenByDescending {
sheet?.race == it sheet?.race == it
}.thenByDescending { }.thenByDescending {

View file

@ -1,6 +1,7 @@
package com.pixelized.rplexicon.data.model package com.pixelized.rplexicon.data.model
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
data class CharacterSheet( data class CharacterSheet(
@ -57,45 +58,84 @@ data class CharacterSheet(
val others: List<String>, // others masteries val others: List<String>, // others masteries
) { ) {
val isWarlock: Boolean get() = characterClass.contains(Class.WARLOCK) val isWarlock: Boolean get() = characterClass.contains(Class.WARLOCK)
val resource: String? get() = characterClass.firstNotNullOfOrNull { it.resource } val resource: String? get() = characterClass.firstNotNullOfOrNull { it.resourceId }
enum class Class( enum class Class(
val value: String, val id: String,
val resource: String? = null, @StringRes
val label: Int? = null, val label: Int,
val resourceId: String? = null,
@StringRes
val resourceLabel: Int? = null,
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
) { ) {
BARBARIAN( BARBARIAN(
value = "Barbare", id = "Barbare",
resource = "Rage", label = R.string.class_barbarian,
label = R.string.character_sheet_title_rage, resourceId = "Rage",
icon = R.drawable.ic_class_barbarian_24 resourceLabel = R.string.character_sheet_title_rage,
icon = R.drawable.icbg_class_barbarian,
), ),
BARD( BARD(
value = "Barde", id = "Barde",
resource = "Inspiration bardique", label = R.string.class_bard,
label = R.string.character_sheet_title_inspiration, resourceId = "Inspiration bardique",
icon = R.drawable.ic_class_bard_24 resourceLabel = R.string.character_sheet_title_inspiration,
icon = R.drawable.icbg_class_bard,
), ),
CLERIC( CLERIC(
value = "Clerc", id = "Clerc",
resource = "Conduit divin", label = R.string.class_cleric,
label = R.string.character_sheet_title_conduit, resourceId = "Conduit divin",
icon = R.drawable.ic_class_cleric_24 resourceLabel = R.string.character_sheet_title_conduit,
icon = R.drawable.icbg_class_cleric,
), ),
DRUID( DRUID(
value = "Druide", id = "Druide",
resource = "Forme sauvage", label = R.string.class_druid,
label = R.string.character_sheet_title_wild_shape, resourceId = "Forme sauvage",
resourceLabel = R.string.character_sheet_title_wild_shape,
icon = R.drawable.ic_class_druid_24, icon = R.drawable.ic_class_druid_24,
), ),
FIGHTER(value = "Guerrier", icon = R.drawable.ic_class_fighter_24), FIGHTER(
MONK(value = "Moine", icon = R.drawable.ic_class_monk_24), id = "Guerrier",
PALADIN(value = "Paladin", icon = R.drawable.ic_class_paladin_24), label = R.string.class_fighter,
RANGER(value = "Rodeur", icon = R.drawable.ic_class_ranger_24), icon = R.drawable.ic_class_fighter_24,
ROGUE(value = "Roublard", icon = R.drawable.ic_class_rogue_24), ),
SORCERER(value = "Ensorceleur", icon = R.drawable.ic_class_sorcerer_24), MONK(
WARLOCK(value = "Occultiste", icon = R.drawable.ic_class_warlock_24), id = "Moine",
WIZARD(value = "Magicien", icon = R.drawable.ic_class_wizard_24), label = R.string.class_monk,
icon = R.drawable.ic_class_monk_24,
),
PALADIN(
id = "Paladin",
label = R.string.class_paladin,
icon = R.drawable.ic_class_paladin_24,
),
RANGER(
id = "Rodeur",
label = R.string.class_ranger,
icon = R.drawable.icbg_class_ranger,
),
ROGUE(
id = "Roublard",
label = R.string.class_rogue,
icon = R.drawable.ic_class_rogue_24,
),
SORCERER(
id = "Ensorceleur",
label = R.string.class_sorcerer,
icon = R.drawable.ic_class_sorcerer_24,
),
WARLOCK(
id = "Occultiste",
label = R.string.class_warlock,
icon = R.drawable.icbg_class_warlock,
),
WIZARD(
id = "Magicien",
label = R.string.class_wizard,
icon = R.drawable.ic_class_wizard_24,
),
} }
} }

View file

@ -51,7 +51,7 @@ class AlterationParser @Inject constructor(
characterSheets characterSheets
.filter { // check if the alteration is applicable to the character .filter { // check if the alteration is applicable to the character
alteration.target.let { target -> alteration.target.let { target ->
target == ALL || target == it.name || target == it.race || it.characterClass.any { it.value == target } target == ALL || target == it.name || target == it.race || it.characterClass.any { it.id == target }
} }
} }
.forEach { sheet -> // check the default alteration state for that character .forEach { sheet -> // check the default alteration state for that character

View file

@ -12,18 +12,18 @@ class CharacterClassParser @Inject constructor() {
?.map { it.trim() } ?.map { it.trim() }
?.mapNotNull { ?.mapNotNull {
when (it) { when (it) {
CharacterSheet.Class.BARBARIAN.value -> CharacterSheet.Class.BARBARIAN CharacterSheet.Class.BARBARIAN.id -> CharacterSheet.Class.BARBARIAN
CharacterSheet.Class.BARD.value -> CharacterSheet.Class.BARD CharacterSheet.Class.BARD.id -> CharacterSheet.Class.BARD
CharacterSheet.Class.CLERIC.value -> CharacterSheet.Class.CLERIC CharacterSheet.Class.CLERIC.id -> CharacterSheet.Class.CLERIC
CharacterSheet.Class.DRUID.value -> CharacterSheet.Class.DRUID CharacterSheet.Class.DRUID.id -> CharacterSheet.Class.DRUID
CharacterSheet.Class.FIGHTER.value -> CharacterSheet.Class.FIGHTER CharacterSheet.Class.FIGHTER.id -> CharacterSheet.Class.FIGHTER
CharacterSheet.Class.MONK.value -> CharacterSheet.Class.MONK CharacterSheet.Class.MONK.id -> CharacterSheet.Class.MONK
CharacterSheet.Class.PALADIN.value -> CharacterSheet.Class.PALADIN CharacterSheet.Class.PALADIN.id -> CharacterSheet.Class.PALADIN
CharacterSheet.Class.RANGER.value -> CharacterSheet.Class.RANGER CharacterSheet.Class.RANGER.id -> CharacterSheet.Class.RANGER
CharacterSheet.Class.ROGUE.value -> CharacterSheet.Class.ROGUE CharacterSheet.Class.ROGUE.id -> CharacterSheet.Class.ROGUE
CharacterSheet.Class.SORCERER.value -> CharacterSheet.Class.SORCERER CharacterSheet.Class.SORCERER.id -> CharacterSheet.Class.SORCERER
CharacterSheet.Class.WARLOCK.value -> CharacterSheet.Class.WARLOCK CharacterSheet.Class.WARLOCK.id -> CharacterSheet.Class.WARLOCK
CharacterSheet.Class.WIZARD.value -> CharacterSheet.Class.WIZARD CharacterSheet.Class.WIZARD.id -> CharacterSheet.Class.WIZARD
else -> null else -> null
} }
} }

View file

@ -18,10 +18,10 @@ class CharacterSheetHeaderUioFactory @Inject constructor(
sheetHeaderData: HeaderViewModel.SheetHeaderData?, sheetHeaderData: HeaderViewModel.SheetHeaderData?,
fireHeaderData: HeaderViewModel.FireHeaderData?, fireHeaderData: HeaderViewModel.FireHeaderData?,
): CharacterSheetHeaderUio { ): CharacterSheetHeaderUio {
val classWithResource = sheetHeaderData?.characterClass?.firstOrNull { it.resource != null } val classWithResource = sheetHeaderData?.characterClass?.firstOrNull { it.resourceId != null }
val favoriteSkill = skillRepository.find( val favoriteSkill = skillRepository.find(
character = character, character = character,
skill = classWithResource?.resource skill = classWithResource?.resourceId
) )
return CharacterSheetHeaderUio( return CharacterSheetHeaderUio(
initiative = LabelPointUio( initiative = LabelPointUio(
@ -47,10 +47,10 @@ class CharacterSheetHeaderUioFactory @Inject constructor(
value = "$it", value = "$it",
) )
}, },
resource = if (classWithResource?.label != null && favoriteSkill?.amount != null) { resource = if (classWithResource?.resourceLabel != null && favoriteSkill?.amount != null) {
ResourcePointUio( ResourcePointUio(
id = favoriteSkill.name, id = favoriteSkill.name,
label = classWithResource.label, label = classWithResource.resourceLabel,
value = fireHeaderData?.resource ?: 0, value = fireHeaderData?.resource ?: 0,
max = favoriteSkill.amount, max = favoriteSkill.amount,
) )

View file

@ -3,6 +3,7 @@ package com.pixelized.rplexicon.ui.screens.landing
import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_NO
import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_NIGHT_YES
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.FloatRange
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
@ -42,7 +43,7 @@ import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Stable @Stable
data class LandingItemUio( data class LandingItemUio(
@DrawableRes val icon: Int, @DrawableRes val icon: Int?,
val title: String?, val title: String?,
val subTitle: String?, val subTitle: String?,
) )
@ -54,6 +55,9 @@ fun LandingItem(
paddings: PaddingValues = PaddingValues(), paddings: PaddingValues = PaddingValues(),
imagePadding: PaddingValues = PaddingValues(all = 16.dp), imagePadding: PaddingValues = PaddingValues(all = 16.dp),
shape: Shape = remember { RoundedCornerShape(size = 8.dp) }, shape: Shape = remember { RoundedCornerShape(size = 8.dp) },
backgroundFilter: ColorFilter? = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface),
@FloatRange(from = 0.0, to = 1.0) backgroundGradientFrom: Float = 0.5f,
@FloatRange(from = 0.0, to = 1.0) backgroundGradientTo: Float = 1f,
rotation: Float = 0f, rotation: Float = 0f,
alpha: Float = 0.6f, alpha: Float = 0.6f,
onClick: () -> Unit, onClick: () -> Unit,
@ -66,23 +70,30 @@ fun LandingItem(
.then(other = modifier), .then(other = modifier),
contentAlignment = Alignment.BottomCenter contentAlignment = Alignment.BottomCenter
) { ) {
Image( item.icon?.let {
modifier = Modifier Image(
.matchParentSize() modifier = Modifier
.padding(paddingValues = imagePadding) .matchParentSize()
.rotate(degrees = rotation), .padding(paddingValues = imagePadding)
painter = painterResource(id = item.icon), .rotate(degrees = rotation),
alpha = alpha, painter = painterResource(id = it),
contentScale = ContentScale.FillHeight, alpha = alpha,
alignment = Alignment.TopCenter, contentScale = ContentScale.Fit,
colorFilter = ColorFilter.tint(color = MaterialTheme.colorScheme.onSurface), alignment = Alignment.TopCenter,
contentDescription = null, colorFilter = backgroundFilter,
) contentDescription = null,
)
}
Box( Box(
modifier = Modifier modifier = Modifier
.matchParentSize() .matchParentSize()
.background(brush = rememberBackgroundGradient()) .background(
brush = rememberBackgroundGradient(
from = backgroundGradientFrom,
to = backgroundGradientTo,
)
)
) )
Column( Column(
@ -118,13 +129,16 @@ fun LandingItem(
} }
@Composable @Composable
private fun rememberBackgroundGradient(): Brush { private fun rememberBackgroundGradient(
@FloatRange(from = 0.0, to = 1.0) from: Float = 0.5f,
@FloatRange(from = 0.0, to = 1.0) to: Float = 1f,
): Brush {
val colorScheme = MaterialTheme.colorScheme val colorScheme = MaterialTheme.colorScheme
return remember { return remember {
Brush.verticalGradient( Brush.verticalGradient(
colors = listOf( colors = listOf(
colorScheme.surface.copy(alpha = 0.5f), colorScheme.surface.copy(alpha = from),
colorScheme.surface.copy(alpha = 1.0f), colorScheme.surface.copy(alpha = to),
) )
) )
} }

View file

@ -30,6 +30,10 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.composed import androidx.compose.ui.composed
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
@ -44,6 +48,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.pixelized.rplexicon.R import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.ui.composable.rememberBackgroundGradient import com.pixelized.rplexicon.ui.composable.rememberBackgroundGradient
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
@ -59,7 +64,9 @@ import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.modifier.scrollOffset import com.pixelized.rplexicon.utilitary.extentions.modifier.scrollOffset
@Composable @Composable
fun LandingScreen() { fun LandingScreen(
viewModel: LandingViewModel = hiltViewModel(),
) {
val screen = LocalScreenNavHost.current val screen = LocalScreenNavHost.current
Surface( Surface(
@ -75,7 +82,10 @@ fun LandingScreen() {
end = 16.dp, end = 16.dp,
bottom = 16.dp, bottom = 16.dp,
), ),
onCharacter = { screen.navigateToCharacterSheet(name = it) }, characters = viewModel.landing,
onCharacter = { character ->
character.title?.let { screen.navigateToCharacterSheet(name = it) }
},
onSearch = { onSearch = {
screen.navigateToSearch( screen.navigateToSearch(
enableLexicon = true, enableLexicon = true,
@ -103,13 +113,22 @@ private fun LandingContent(
bottom = 16.dp, bottom = 16.dp,
), ),
sectionPadding: Dp = 32.dp, sectionPadding: Dp = 32.dp,
onCharacter: (String) -> Unit, characters: State<List<LandingItemUio>>,
onCharacter: (LandingItemUio) -> Unit,
onSearch: () -> Unit, onSearch: () -> Unit,
onGameMaster: () -> Unit, onGameMaster: () -> Unit,
onLexicon: () -> Unit, onLexicon: () -> Unit,
onQuest: () -> Unit, onQuest: () -> Unit,
onMap: () -> Unit, onMap: () -> Unit,
) { ) {
val charactersSection = remember {
derivedStateOf {
characters.value
.groupBy { characters.value.indexOf(it) / 3 }
.values.toList()
}
}
Box( Box(
modifier = modifier.verticalScroll(scrollState), modifier = modifier.verticalScroll(scrollState),
) { ) {
@ -174,86 +193,28 @@ private fun LandingContent(
text = stringResource(id = R.string.landing__caterogy__character), text = stringResource(id = R.string.landing__caterogy__character),
) )
Row( charactersSection.value.forEach { charactersGroup ->
modifier = Modifier.fillMaxWidth(), Row(
horizontalArrangement = Arrangement.spacedBy(space = 8.dp), modifier = Modifier.fillMaxWidth(),
) { horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
stringResource(id = R.string.landing__character_brulkhai).let { character -> ) {
LandingItem( charactersGroup.forEach { character ->
modifier = Modifier LandingItem(
.height(128.dp) modifier = Modifier
.weight(1f), .aspectRatio(0.8f)
imagePadding = PaddingValues(all = 8.dp), .weight(1f),
item = LandingItemUio( imagePadding = PaddingValues(bottom = 8.dp),
icon = R.drawable.ic_class_barbarian_24, item = character,
title = character, backgroundFilter = null,
subTitle = stringResource(id = R.string.class_barbarian), backgroundGradientFrom = 0f,
), backgroundGradientTo = 0.5f,
onClick = { onCharacter(character) }, onClick = { onCharacter(character) },
) )
}
repeat(3 - charactersGroup.size) {
Spacer(modifier = Modifier.weight(1f))
}
} }
stringResource(id = R.string.landing__character_leandre).let { character ->
LandingItem(
modifier = Modifier
.height(128.dp)
.weight(1f),
imagePadding = PaddingValues(all = 8.dp),
item = LandingItemUio(
icon = R.drawable.ic_class_cleric_24,
title = character,
subTitle = stringResource(id = R.string.class_cleric),
),
onClick = { onCharacter(character) },
)
}
stringResource(id = R.string.landing__character_nelia).let { character ->
LandingItem(
modifier = Modifier
.height(128.dp)
.weight(1f),
imagePadding = PaddingValues(all = 8.dp),
item = LandingItemUio(
icon = R.drawable.ic_class_ranger_24,
title = character,
subTitle = stringResource(id = R.string.class_ranger),
),
onClick = { onCharacter(character) },
)
}
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
) {
stringResource(id = R.string.landing__character_tigrane).let { character ->
LandingItem(
modifier = Modifier
.height(128.dp)
.weight(1f),
imagePadding = PaddingValues(all = 8.dp),
item = LandingItemUio(
icon = R.drawable.ic_class_warlock_24,
title = character,
subTitle = stringResource(id = R.string.class_warlock),
),
onClick = { onCharacter(character) },
)
}
stringResource(id = R.string.landing__character_unathana).let { character ->
LandingItem(
modifier = Modifier
.height(128.dp)
.weight(1f),
imagePadding = PaddingValues(all = 8.dp),
item = LandingItemUio(
icon = R.drawable.ic_class_bard_24,
title = character,
subTitle = stringResource(id = R.string.class_bard),
),
onClick = { onCharacter(character) },
)
}
Spacer(modifier = Modifier.weight(1f))
} }
Text( Text(
@ -392,6 +353,37 @@ private fun LandingPreview() {
Surface { Surface {
LandingContent( LandingContent(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
characters = remember {
mutableStateOf(
listOf(
LandingItemUio(
icon = R.drawable.icbg_class_barbarian,
title = "Brulkhai",
subTitle = "Barbarian, Warrior",
),
LandingItemUio(
icon = R.drawable.icbg_class_cleric,
title = "Léandre",
subTitle = "Cleric",
),
LandingItemUio(
icon = R.drawable.icbg_class_ranger,
title = "Nelia",
subTitle = "Ranger, Druid",
),
LandingItemUio(
icon = R.drawable.icbg_class_warlock,
title = "Tigrane",
subTitle = "Warlock",
),
LandingItemUio(
icon = R.drawable.icbg_class_bard,
title = "Unathana",
subTitle = "Bard",
),
)
)
},
onCharacter = { }, onCharacter = { },
onSearch = { }, onSearch = { },
onGameMaster = { }, onGameMaster = { },

View file

@ -0,0 +1,48 @@
package com.pixelized.rplexicon.ui.screens.landing
import android.app.Application
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository
import com.pixelized.rplexicon.utilitary.extentions.context
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import javax.inject.Inject
@HiltViewModel
class LandingViewModel @Inject constructor(
private val sheetRepository: CharacterSheetRepository,
application: Application,
) : AndroidViewModel(application = application) {
private val _landing = mutableStateOf<List<LandingItemUio>>(emptyList())
val landing: State<List<LandingItemUio>> = _landing
init {
viewModelScope.launch(Dispatchers.Default) {
sheetRepository.data.collectLatest { sheets ->
val landing = sheets.mapNotNull { entry ->
val icon = entry.value.characterClass.firstOrNull()?.icon
val title = entry.key
val subTitle = entry.value.characterClass
.map { context.getString(it.label) }
.joinToString(separator = ", ") { it }
LandingItemUio(
icon = icon,
title = title,
subTitle = subTitle,
)
}
withContext(Dispatchers.Main) {
_landing.value = landing
}
}
}
}
}

View file

@ -546,7 +546,7 @@ class SummaryFactory @Inject constructor(
} ?: none() } ?: none()
// Update spells // Update spells
val skill = sheet.characterClass.firstOrNull()?.resource?.let { skill -> val skill = sheet.characterClass.firstOrNull()?.resourceId?.let { skill ->
fire?.skills?.get(skill) fire?.skills?.get(skill)
}?.let { skill -> }?.let { skill ->
label(label = "$skill") label(label = "$skill")

View file

@ -146,7 +146,7 @@ private object ResourcesCache {
"Transe" to R.drawable.icbg_focused_conjuration, "Transe" to R.drawable.icbg_focused_conjuration,
"Attaque supplémentaire" to R.drawable.icbg_extra_attack, "Attaque supplémentaire" to R.drawable.icbg_extra_attack,
"Apanage de la Noblesse" to R.drawable.icbg_natures_ward, "Apanage de la Noblesse" to R.drawable.icbg_natures_ward,
"Druidique" to R.drawable.icbg_class_druid_badge, "Druidique" to R.drawable.icbg_class_druid,
"Appel de familier" to R.drawable.icbg_find_familiar_cat, "Appel de familier" to R.drawable.icbg_find_familiar_cat,
"Détection de la magie" to R.drawable.icbg_generic_psychic, "Détection de la magie" to R.drawable.icbg_generic_psychic,
"Vision dans le noir (Tieffelin)" to R.drawable.icbg_darkvision, "Vision dans le noir (Tieffelin)" to R.drawable.icbg_darkvision,

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View file

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB