Add spell to the search engine.

This commit is contained in:
Thomas Andres Gomez 2023-11-21 14:44:54 +01:00
parent 7de925a431
commit 719ebd4c5e
17 changed files with 547 additions and 72 deletions

View file

@ -420,7 +420,7 @@ class DiceThrowUseCase @Inject constructor(
}
is DiceThrow.SpellAttack -> {
val spell = spellRepository.find(
val spell = spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell,
)
@ -435,7 +435,7 @@ class DiceThrowUseCase @Inject constructor(
}
is DiceThrow.SpellDamage -> {
val spell = spellRepository.find(
val spell = spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell,
)
@ -450,7 +450,7 @@ class DiceThrowUseCase @Inject constructor(
}
is DiceThrow.SpellEffect -> {
val spell = spellRepository.find(
val spell = spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell,
)

View file

@ -6,11 +6,14 @@ import com.pixelized.rplexicon.R
import com.pixelized.rplexicon.data.model.Lexicon
import com.pixelized.rplexicon.data.model.Location
import com.pixelized.rplexicon.data.model.Quest
import com.pixelized.rplexicon.data.model.Spell
import com.pixelized.rplexicon.data.repository.character.DescriptionRepository
import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio
import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.LocationSearchItemUio
import com.pixelized.rplexicon.ui.theme.typography.LexiconTypography
import com.pixelized.rplexicon.utilitary.annotate
import com.pixelized.rplexicon.utilitary.dropCapRegex
import com.pixelized.rplexicon.utilitary.extentions.local.label
import com.pixelized.rplexicon.utilitary.extentions.prefix
import com.pixelized.rplexicon.utilitary.extractSentenceRegex
import com.pixelized.rplexicon.utilitary.extractWordRegex
@ -19,7 +22,9 @@ import com.pixelized.rplexicon.utilitary.nullableAnnotate
import com.pixelized.rplexicon.utilitary.styleWith
import javax.inject.Inject
class SearchUseCase @Inject constructor() {
class SearchUseCase @Inject constructor(
private val descriptionRepository: DescriptionRepository,
) {
fun filterLexicon(
context: Context,
@ -289,4 +294,126 @@ class SearchUseCase @Inject constructor() {
)
}
}
fun filterSpells(
context: Context,
typography: LexiconTypography,
spells: List<Spell>,
criterion: List<String>
): List<SearchItemUio.SpellSearchItemUio> {
val dropCapRegex = dropCapRegex()
val highlightRegex = highlightRegex(terms = criterion)
val extractSentence = extractSentenceRegex(terms = criterion)
val ritual = context.getString(R.string.spell_detail_ritual)
val categoryPrefix = context.getString(R.string.search_category_prefix_spell)
val levelPrefix = context.getString(R.string.search_spell_item_level)
val schoolPrefix = context.getString(R.string.search_spell_item_school)
val castingTimePrefix = context.getString(R.string.search_spell_item_castingTime)
val rangePrefix = context.getString(R.string.search_spell_item_range)
val requirementPrefix = context.getString(R.string.search_spell_item_requirement)
val durationPrefix = context.getString(R.string.search_spell_item_duration)
val descriptionPrefix = context.getString(R.string.search_spell_item_description)
return spells.filter {
criterion.map { criteria ->
val schoolName = context.getString(it.school.label)
val itemDescription = descriptionRepository.find(name = it.name)
val name = it.name.contains(criteria, true)
val translated = itemDescription?.original?.contains(criteria, true) == true
val school = schoolName.contains(criteria, true)
val level = "${it.level}".contains(criteria, true)
val castingTime = it.castingTime.contains(criteria, true)
val range = it.range.contains(criteria, true)
val requirement = it.requirement.contains(criteria, true)
val duration = it.duration.contains(criteria, true)
val isRitual = it.ritual && ritual.contains(criteria, true)
val description = itemDescription?.description?.contains(criteria, true) == true
name || translated || school || level || castingTime || range || requirement || duration || isRitual || description
}.all { it }
}.map { item ->
val school = context.getString(item.school.label)
val itemDescription = descriptionRepository.find(name = item.name)
SearchItemUio.SpellSearchItemUio(
id = item.name,
category = nullableAnnotate(
text = school.prefix(categoryPrefix),
highlightRegex styleWith typography.search.categoryHighlight,
),
name = annotate(
text = item.name,
highlightRegex styleWith typography.search.titleHighlight,
dropCapRegex styleWith typography.titleMediumDropCap,
),
translated = nullableAnnotate(
text = itemDescription?.original,
highlightRegex styleWith typography.search.extractHighlight,
),
level = item.level.let {
AnnotatedString(
text = "$levelPrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = "$it",
highlightRegex styleWith typography.search.extractHighlight,
)
},
school = school.let {
AnnotatedString(
text = "$schoolPrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = if (item.ritual) ritual.prefix(it) ?: "" else it,
highlightRegex styleWith typography.search.extractHighlight,
)
},
castingTime = item.castingTime.let {
AnnotatedString(
text = "$castingTimePrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = it,
highlightRegex styleWith typography.search.extractHighlight,
)
},
range = item.range.let {
AnnotatedString(
text = "$rangePrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = it,
highlightRegex styleWith typography.search.extractHighlight,
)
},
requirement = item.requirement.let {
AnnotatedString(
text = "$requirementPrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = it,
highlightRegex styleWith typography.search.extractHighlight,
)
},
duration = item.duration.let {
AnnotatedString(
text = "$durationPrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = it,
highlightRegex styleWith typography.search.extractHighlight,
)
},
description = itemDescription?.description?.let { extractSentence.find(it) }?.let {
AnnotatedString(
text = "$descriptionPrefix ",
spanStyle = typography.search.extractBold,
) + annotate(
text = it.value,
highlightRegex styleWith typography.search.extractHighlight,
)
},
)
}
}
}

View file

@ -21,19 +21,24 @@ class SpellRepository @Inject constructor(
private val spellBookParser: SpellBookParser,
private val assignedSpellParser: AssignedSpellParser,
) {
private var spellsBook: List<Spell>? = null
private var _spellsBook = MutableStateFlow<List<Spell>>(emptyList())
val spellsBook: StateFlow<List<Spell>> get() = _spellsBook
private val _spells = MutableStateFlow<Map<String, List<AssignedSpell>>>(emptyMap())
val spells: StateFlow<Map<String, List<AssignedSpell>>> get() = _spells
var lastSuccessFullUpdate: Update = Update.INITIAL
private set
fun find(character: String?): List<AssignedSpell>? {
fun findSpell(name: String?): Spell? {
return spellsBook.value.find { it.name == name }
}
fun findAssignedSpell(character: String?): List<AssignedSpell>? {
return character?.let { _spells.value[it] }
}
fun find(character: String?, spell: String): AssignedSpell? {
return find(character)?.find { assigned -> assigned.spell.name == spell }
fun findAssignedSpell(character: String?, spell: String): AssignedSpell? {
return findAssignedSpell(character)?.find { assigned -> assigned.spell.name == spell }
}
@Throws(IncompatibleSheetStructure::class, Exception::class)
@ -43,9 +48,9 @@ class SpellRepository @Inject constructor(
async { sheet.get(CharacterBinder.ID, CharacterBinder.MAGIC_LEXICON).execute() },
async { sheet.get(CharacterBinder.ID, CharacterBinder.MAGIC).execute() },
)
val spellsBook = spellBookParser.parse(sheet = lexicon)
val assignedSpells = assignedSpellParser.parse(sheet = magic, spells = spellsBook)
this@SpellRepository.spellsBook = spellsBook
val spells = spellBookParser.parse(sheet = lexicon)
val assignedSpells = assignedSpellParser.parse(sheet = magic, spells = spells)
_spellsBook.emit(spells)
_spells.emit(assignedSpells)
lastSuccessFullUpdate = Update.currentTime()
}

View file

@ -17,11 +17,13 @@ private const val ROUTE = "search"
private const val ARG_ENABLE_LEXICON = "ARG_ENABLE_LEXICON"
private const val ARG_ENABLE_QUESTS = "ARG_ENABLE_QUESTS"
private const val ARG_ENABLE_LOCATIONS = "ARG_ENABLE_LOCATIONS"
private const val ARG_ENABLE_SPELLS = "ARG_ENABLE_SPELLS"
val SEARCH_ROUTE = ROUTE +
"?${ARG_ENABLE_LEXICON.ARG}" +
"&${ARG_ENABLE_QUESTS.ARG}" +
"&${ARG_ENABLE_LOCATIONS.ARG}"
"&${ARG_ENABLE_LOCATIONS.ARG}" +
"&${ARG_ENABLE_SPELLS.ARG}"
@Stable
@Immutable
@ -29,6 +31,7 @@ data class SearchArgument(
val enableLexicon: Boolean = false,
val enableQuests: Boolean = false,
val enableLocations: Boolean = false,
val enableSpells: Boolean = false,
)
val SavedStateHandle.searchArgument: SearchArgument
@ -36,6 +39,7 @@ val SavedStateHandle.searchArgument: SearchArgument
enableLexicon = get(ARG_ENABLE_LEXICON) ?: false,
enableQuests = get(ARG_ENABLE_QUESTS) ?: false,
enableLocations = get(ARG_ENABLE_LOCATIONS) ?: false,
enableSpells = get(ARG_ENABLE_SPELLS) ?: false,
)
fun NavGraphBuilder.composableSearch() {
@ -54,6 +58,10 @@ fun NavGraphBuilder.composableSearch() {
type = NavType.BoolType
nullable = false
},
navArgument(ARG_ENABLE_SPELLS) {
type = NavType.BoolType
nullable = false
}
),
animation = NavigationAnimation.Push,
) {
@ -66,11 +74,13 @@ fun NavHostController.navigateToSearch(
enableLexicon: Boolean = false,
enableQuests: Boolean = false,
enableLocations: Boolean = false,
enableSpells: Boolean = false,
) {
val route = ROUTE +
"?$ARG_ENABLE_LEXICON=$enableLexicon" +
"&$ARG_ENABLE_QUESTS=$enableQuests" +
"&$ARG_ENABLE_LOCATIONS=$enableLocations"
"&$ARG_ENABLE_LOCATIONS=$enableLocations" +
"&$ARG_ENABLE_SPELLS=$enableSpells"
navigate(route = route, builder = option)
}

View file

@ -13,35 +13,35 @@ import com.pixelized.rplexicon.ui.screens.spell.SpellDetailScreen
import com.pixelized.rplexicon.utilitary.extentions.ARG
private const val ROUTE = "spellDetail"
private const val ARG_CHARACTER = "character"
private const val ARG_SPELL = "spell"
private const val ARG_HIGHLIGHT = "highlight"
val SPELL_DETAIL_ROUTE = "$ROUTE?${ARG_CHARACTER.ARG}&${ARG_SPELL.ARG}"
val SPELL_DETAIL_ROUTE = "$ROUTE?${ARG_SPELL.ARG}&${ARG_HIGHLIGHT.ARG}"
@Stable
data class SpellDetailArgument(
val character: String,
val spell: String,
val highlight: String?,
)
val SavedStateHandle.spellDetailArgument: SpellDetailArgument
get() = SpellDetailArgument(
character = get(ARG_CHARACTER) ?: error("Missing $ARG_CHARACTER argument from $this"),
spell = get(ARG_SPELL) ?: error("Missing $ARG_SPELL argument from $this"),
highlight = get(ARG_HIGHLIGHT),
)
fun NavGraphBuilder.composableSpellDetail() {
animatedComposable(
route = SPELL_DETAIL_ROUTE,
arguments = listOf(
navArgument(name = ARG_CHARACTER) {
type = NavType.StringType
nullable = false
},
navArgument(name = ARG_SPELL) {
type = NavType.StringType
nullable = false
}
},
navArgument(name = ARG_HIGHLIGHT) {
type = NavType.StringType
nullable = true
},
),
animation = NavigationAnimation.Push,
) {
@ -50,12 +50,12 @@ fun NavGraphBuilder.composableSpellDetail() {
}
fun NavHostController.navigateToSpellDetail(
character: String,
spell: String,
highlight: String? = null,
option: NavOptionsBuilder.() -> Unit = {},
) {
val route = ROUTE +
"?${ARG_CHARACTER}=$character" +
"&${ARG_SPELL}=$spell"
"?${ARG_SPELL}=$spell" +
"&${ARG_HIGHLIGHT}=$highlight"
navigate(route = route, builder = option)
}

View file

@ -100,10 +100,7 @@ fun ActionPage(
spellsViewModel.showSpellEditDialog(level = level, value = value, max = max)
},
onSpell = { spell ->
screen.navigateToSpellDetail(
character = spellsViewModel.characterName,
spell = spell,
)
screen.navigateToSpellDetail(spell = spell)
},
onSpellHit = { id ->
overlay.prepareRoll(diceThrow = spellsViewModel.onSpellHitRoll(id))

View file

@ -47,7 +47,7 @@ class SpellsViewModel @Inject constructor(
) : AndroidViewModel(application) {
private var character: CharacterSheet? = null
private var characterFire: CharacterSheetFire? = null
val characterName = savedStateHandle.characterSheetArgument.name
private val characterName = savedStateHandle.characterSheetArgument.name
private val _editDialog = mutableStateOf<SpellEditDialogUio?>(null)
val spellEditDialog: State<SpellEditDialogUio?> get() = _editDialog
@ -83,7 +83,7 @@ class SpellsViewModel @Inject constructor(
}
fun shouldDisplaySpellLevelChooser(name: String): Boolean {
val assignedSpell = spellRepository.find(
val assignedSpell = spellRepository.findAssignedSpell(
character = characterName,
spell = name,
)
@ -96,7 +96,7 @@ class SpellsViewModel @Inject constructor(
fun prepareSpellCast(name: String) {
val character = character
val characterFire = characterFire
val assignedSpell = spellRepository.find(
val assignedSpell = spellRepository.findAssignedSpell(
character = characterName,
spell = name,
)
@ -129,7 +129,7 @@ class SpellsViewModel @Inject constructor(
}
fun onCastSpell(id: String): DiceThrow {
val spell = spellRepository.find(character = characterName, spell = id)
val spell = spellRepository.findAssignedSpell(character = characterName, spell = id)
return onCastSpell(
id = id,
level = when (character?.isWarlock ?: false) {

View file

@ -87,17 +87,17 @@ class AlterationFactory @Inject constructor(
}
is DiceThrow.SpellAttack -> {
val spell = spellRepository.find(diceThrow.character, spell = diceThrow.spell)
val spell = spellRepository.findAssignedSpell(diceThrow.character, spell = diceThrow.spell)
listOf(Property.SPELL_ATTACK) + (spell?.hit?.modifier ?: emptyList())
}
is DiceThrow.SpellDamage -> {
val spell = spellRepository.find(diceThrow.character, spell = diceThrow.spell)
val spell = spellRepository.findAssignedSpell(diceThrow.character, spell = diceThrow.spell)
listOf(Property.SPELL_DAMAGE) + (spell?.effect?.modifier ?: emptyList())
}
is DiceThrow.SpellEffect -> {
val spell = spellRepository.find(diceThrow.character, spell = diceThrow.spell)
val spell = spellRepository.findAssignedSpell(diceThrow.character, spell = diceThrow.spell)
spell?.effect?.modifier ?: emptyList()
}

View file

@ -38,14 +38,14 @@ class DiceFactory @Inject constructor(
RollDiceUio(icon = it)
}
is DiceThrow.SpellDamage -> spellRepository.find(
is DiceThrow.SpellDamage -> spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell
)?.effect?.faces?.icon?.let {
RollDiceUio(icon = it)
}
is DiceThrow.SpellEffect -> spellRepository.find(
is DiceThrow.SpellEffect -> spellRepository.findAssignedSpell(
character = diceThrow.character,
spell = diceThrow.spell
)?.effect?.faces?.icon?.let {

View file

@ -33,6 +33,7 @@ import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLocationDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToQuestDetail
import com.pixelized.rplexicon.ui.navigation.screens.navigateToSpellDetail
import com.pixelized.rplexicon.ui.screens.search.item.LexiconSearchItem
import com.pixelized.rplexicon.ui.screens.search.item.LocationSearchItem
import com.pixelized.rplexicon.ui.screens.search.item.QuestSearchItem
@ -40,6 +41,8 @@ import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio
import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.LexiconSearchItemUio
import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.LocationSearchItemUio
import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.QuestSearchItemUio
import com.pixelized.rplexicon.ui.screens.search.item.SearchItemUio.SpellSearchItemUio
import com.pixelized.rplexicon.ui.screens.search.item.SpellSearchItem
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.PUC_FULL
import com.pixelized.rplexicon.utilitary.extentions.lexiconShadow
@ -80,6 +83,9 @@ fun SearchScreen(
onLocation = {
screen.navigateToLocationDetail(id = it, highlight = form.highlight)
},
onSpell = {
screen.navigateToSpellDetail(spell = it, highlight = form.highlight)
},
)
}
}
@ -96,6 +102,7 @@ private fun SearchScreenContent(
onLexicon: (String) -> Unit,
onQuest: (String) -> Unit,
onLocation: (String) -> Unit,
onSpell: (String) -> Unit,
) {
Scaffold(
modifier = modifier,
@ -145,6 +152,7 @@ private fun SearchScreenContent(
is LexiconSearchItemUio -> "$TYPE_LEXICON-${it.id}"
is LocationSearchItemUio -> "$TYPE_LOCATION-${it.id}"
is QuestSearchItemUio -> "$TYPE_QUEST-${it.id}"
is SpellSearchItemUio -> "$TYPE_SPELL-${it.id}"
}
},
contentType = {
@ -152,6 +160,7 @@ private fun SearchScreenContent(
is LexiconSearchItemUio -> TYPE_LEXICON
is LocationSearchItemUio -> TYPE_LOCATION
is QuestSearchItemUio -> TYPE_QUEST
is SpellSearchItemUio -> TYPE_SPELL
}
}
) { item ->
@ -170,6 +179,11 @@ private fun SearchScreenContent(
item = item,
onQuest = onQuest,
)
is SpellSearchItemUio -> SpellSearchItem(
item = item,
onSpell = onSpell,
)
}
}
}
@ -229,10 +243,12 @@ fun SearchScreenPreview() {
onLexicon = { },
onQuest = { },
onLocation = { },
onSpell = { },
)
}
}
private const val TYPE_LEXICON = "Lexicon"
private const val TYPE_QUEST = "Quest"
private const val TYPE_LOCATION = "Location"
private const val TYPE_LOCATION = "Location"
private const val TYPE_SPELL = "Spell"

View file

@ -17,6 +17,8 @@ import com.pixelized.rplexicon.business.SearchUseCase
import com.pixelized.rplexicon.data.model.Lexicon
import com.pixelized.rplexicon.data.model.Location
import com.pixelized.rplexicon.data.model.Quest
import com.pixelized.rplexicon.data.model.Spell
import com.pixelized.rplexicon.data.repository.character.SpellRepository
import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository
import com.pixelized.rplexicon.data.repository.lexicon.LocationRepository
import com.pixelized.rplexicon.data.repository.lexicon.QuestRepository
@ -39,6 +41,7 @@ class SearchViewModel @Inject constructor(
private val lexiconRepository: LexiconRepository,
private val questRepository: QuestRepository,
private val locationRepository: LocationRepository,
private val spellsRepository: SpellRepository,
private val searchUseCase: SearchUseCase,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
@ -46,6 +49,7 @@ class SearchViewModel @Inject constructor(
private val enableLexicon = MutableStateFlow(savedStateHandle.searchArgument.enableLexicon)
private val enableQuests = MutableStateFlow(savedStateHandle.searchArgument.enableQuests)
private val enableLocations = MutableStateFlow(savedStateHandle.searchArgument.enableLocations)
private val enableSpells = MutableStateFlow(savedStateHandle.searchArgument.enableSpells)
private val search = MutableStateFlow("")
private val _data = mutableStateOf<List<SearchItemUio>>(emptyList())
@ -68,6 +72,9 @@ class SearchViewModel @Inject constructor(
.combine(locationRepository.data) { _, locations ->
scope.locations = locations
}
.combine(spellsRepository.spellsBook) { _, spells ->
scope.spells = spells
}
.combine(enableLexicon) { _, enable ->
scope.isLexiconEnable = enable
}
@ -77,6 +84,9 @@ class SearchViewModel @Inject constructor(
.combine(enableLocations) { _, enable ->
scope.isLocationsEnable = enable
}
.combine(enableSpells) { _, enable ->
scope.isSpellsEnable = enable
}
.combine(search) { _, search ->
scope.search = search.searchCriterion()
}
@ -112,7 +122,18 @@ class SearchViewModel @Inject constructor(
} else {
emptyList()
}
val data = (lexicon + quests + locations).sortedBy { it.sort }
val spells = if (scope.isSpellsEnable) {
searchUseCase.filterSpells(
context = context,
typography = typography,
spells = scope.spells,
criterion = scope.search,
)
} else {
emptyList<SearchItemUio.SpellSearchItemUio>()
}
val data = (lexicon + quests + locations + spells).sortedBy { it.sort }
withContext(Dispatchers.Main) {
_data.value = data
}
@ -129,6 +150,7 @@ class SearchViewModel @Inject constructor(
val enableLexicon = this.enableLexicon.collectAsState()
val enableQuests = this.enableQuests.collectAsState()
val enableLocations = this.enableLocations.collectAsState()
val enableSpells = this.enableSpells.collectAsState()
return remember {
SearchFormUio(
search = TextFieldUio(
@ -139,6 +161,7 @@ class SearchViewModel @Inject constructor(
SearchFilterUio.Lexicon(selected = enableLexicon),
SearchFilterUio.Quest(selected = enableQuests),
SearchFilterUio.Location(selected = enableLocations),
SearchFilterUio.Spell(selected = enableSpells),
)
)
}
@ -151,7 +174,7 @@ class SearchViewModel @Inject constructor(
is SearchFilterUio.Lexicon -> enableLexicon.emit(chip.selected.value.not())
is SearchFilterUio.Location -> enableLocations.emit(chip.selected.value.not())
is SearchFilterUio.Quest -> enableQuests.emit(chip.selected.value.not())
is SearchFilterUio.Spell -> TODO()
is SearchFilterUio.Spell -> enableSpells.emit(chip.selected.value.not())
}
}
}
@ -172,6 +195,8 @@ class SearchViewModel @Inject constructor(
var quests: List<Quest> = emptyList()
var isLocationsEnable: Boolean = false
var locations: List<Location> = emptyList()
var isSpellsEnable: Boolean = false
var spells: List<Spell> = emptyList()
var search: List<String> = emptyList()
}
}

View file

@ -75,13 +75,53 @@ private fun QuestSearchItemPreview() {
QuestSearchItem(
item = SearchItemUio.QuestSearchItemUio(
id = "",
category = AnnotatedString(text = "Quest $PUC_FULL Les cartes de la destinée"),
title = AnnotatedString(text = "La Brume"),
owner = AnnotatedString(text = ""),
location = AnnotatedString(text = ""),
individualReward = AnnotatedString(text = ""),
groupReward = AnnotatedString(text = ""),
description = AnnotatedString(text = ""),
category = AnnotatedString(
text = "Quest $PUC_FULL Les cartes de la destinée",
),
title = AnnotatedString(
text = "La Brume",
),
owner = AnnotatedString(
text = "Commanditaire : Eva (Mdme)",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Commanditaire".length,
),
),
),
location = AnnotatedString(
text = "Lieu : Campement Vistani",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Lieu".length,
),
),
),
individualReward = null,
groupReward = AnnotatedString(
text = "Récompense de groupe : Un allié",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Récompense de groupe".length,
),
),
),
description = AnnotatedString(
text = "Description : Cette carte nous révèle un allié, un autre qui vous aidera grandement dans votre combat contre l'obscurité. Une Vistana ère seule dans ces terres isolées,cherchant son mentor. Elle est souvent en mouvement. Cherchez sa trace à l'abbaye de sainte Markovia, près de la brume.",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Description".length,
),
),
),
),
onQuest = { },
)

View file

@ -44,4 +44,19 @@ sealed class SearchItemUio(
val groupReward: AnnotatedString?,
val description: AnnotatedString?,
) : SearchItemUio(sort = title.text)
@Stable
data class SpellSearchItemUio(
val id: String,
override val category: AnnotatedString?,
val name: AnnotatedString,
val translated: AnnotatedString?,
val level: AnnotatedString?,
val school: AnnotatedString?,
val castingTime: AnnotatedString?,
val range: AnnotatedString?,
val requirement: AnnotatedString?,
val duration: AnnotatedString?,
val description: AnnotatedString?,
) : SearchItemUio(sort = name.text)
}

View file

@ -0,0 +1,187 @@
package com.pixelized.rplexicon.ui.screens.search.item
import android.content.res.Configuration
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.PUC_FULL
import com.pixelized.rplexicon.utilitary.extentions.lexicon
@Composable
fun SpellSearchItem(
modifier: Modifier = Modifier,
item: SearchItemUio.SpellSearchItemUio,
onSpell: (String) -> Unit,
) {
SearchItemLayout(
modifier = modifier.clickable { onSpell(item.id) },
category = item.category,
content = {
Row(
horizontalArrangement = Arrangement.spacedBy(space = 4.dp)
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lexicon.typography.search.title,
fontWeight = FontWeight.Bold,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
text = item.name,
)
item.translated?.let {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.lexicon.typography.search.extract,
fontStyle = FontStyle.Italic,
fontWeight = FontWeight.Light,
maxLines = 1,
text = it,
)
}
}
item.level?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
item.school?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
item.castingTime?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
item.range?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
item.requirement?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
item.duration?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
item.description?.let {
Text(
style = MaterialTheme.lexicon.typography.search.extract,
text = it,
)
}
}
)
}
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SpellSearchItemPreview() {
LexiconTheme {
Surface {
SpellSearchItem(
item = SearchItemUio.SpellSearchItemUio(
id = "",
category = AnnotatedString(text = "Sortilège $PUC_FULL Illusion"),
name = AnnotatedString(text = "Espièglerie de Nathair"),
translated = AnnotatedString(text = "Nathair's Mischief"),
level = AnnotatedString(
text = "Niveau : 2",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Niveau".length,
),
)
),
school = AnnotatedString(
text = "École : Illusion - Ritual",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "École".length,
),
)
),
castingTime = AnnotatedString(
text = "Temps d\'incantation : 1 action",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Temps".length,
),
)
),
range = AnnotatedString(
text = "Portée : 18 mètres",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Portée".length,
),
)
),
requirement = AnnotatedString(
text = "Composante : S, M (un morceau de croûte de tarde aux pommes)",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Composante".length,
),
)
),
duration = AnnotatedString(
text = "Durée : concentration, jusqu\'à 1 minutes",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Durée".length,
),
)
),
description = AnnotatedString(
text = "Description : Remplit un cube de 6m d'un effet magique. L\'effet aléatoire peut être charmé, aveuglé, incapable d\'agir ou terrain difficile.",
spanStyles = listOf(
AnnotatedString.Range(
item = MaterialTheme.lexicon.typography.search.extractBold,
start = 0,
end = "Description".length,
),
)
),
),
onSpell = { },
)
}
}
}

View file

@ -44,7 +44,13 @@ import com.pixelized.rplexicon.ui.composable.BackgroundImage
import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
import com.pixelized.rplexicon.ui.theme.LexiconTheme
import com.pixelized.rplexicon.utilitary.annotate
import com.pixelized.rplexicon.utilitary.dropCapRegex
import com.pixelized.rplexicon.utilitary.extentions.lexicon
import com.pixelized.rplexicon.utilitary.extentions.scrollOffset
import com.pixelized.rplexicon.utilitary.extentions.searchCriterion
import com.pixelized.rplexicon.utilitary.highlightRegex
import com.pixelized.rplexicon.utilitary.styleWith
@Stable
class SpellDetailUio(
@ -72,6 +78,7 @@ fun SpellDetailScreen(
SpellDetailContent(
modifier = Modifier.fillMaxSize(),
spell = viewModel.spell,
highlight = viewModel.highlight,
onBack = { screen.popBackStack() },
)
@ -86,9 +93,14 @@ fun SpellDetailScreen(
private fun SpellDetailContent(
modifier: Modifier,
state: ScrollState = rememberScrollState(),
highlight: String?,
spell: State<SpellDetailUio?>,
onBack: () -> Unit,
) {
val typography = MaterialTheme.lexicon.typography
val highlightRegex = remember(highlight) { highlightRegex(terms = highlight.searchCriterion()) }
val dropCapRegex = remember { dropCapRegex() }
Scaffold(
modifier = modifier,
containerColor = Color.Transparent,
@ -132,14 +144,21 @@ private fun SpellDetailContent(
) {
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.headlineSmall,
text = detail.name,
style = typography.base.headlineSmall,
text = annotate(
text = detail.name,
dropCapRegex styleWith typography.headlineSmallDropCap,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
Text(
modifier = Modifier.alignByBaseline(),
fontStyle = FontStyle.Italic,
style = MaterialTheme.typography.bodyMedium,
text = detail.translated,
text = annotate(
text = detail.translated,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
@ -155,7 +174,10 @@ private fun SpellDetailContent(
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodyMedium,
text = stringResource(id = detail.school),
text = annotate(
text = stringResource(id = detail.school),
highlightRegex styleWith typography.detail.highlightStyle,
),
)
if (detail.ritual) {
Text(
@ -166,7 +188,10 @@ private fun SpellDetailContent(
Text(
modifier = Modifier.alignByBaseline(),
style = MaterialTheme.typography.bodyMedium,
text = stringResource(id = R.string.spell_detail_ritual),
text = annotate(
text = stringResource(id = R.string.spell_detail_ritual),
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
}
@ -182,7 +207,10 @@ private fun SpellDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = detail.level,
text = annotate(
text = detail.level,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
@ -196,7 +224,10 @@ private fun SpellDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = detail.castingTime,
text = annotate(
text = detail.castingTime,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
FlowRow(
@ -209,7 +240,10 @@ private fun SpellDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = detail.range,
text = annotate(
text = detail.range,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
FlowRow(
@ -222,7 +256,10 @@ private fun SpellDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = detail.requirement,
text = annotate(
text = detail.requirement,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
FlowRow(
@ -235,7 +272,10 @@ private fun SpellDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = detail.duration,
text = annotate(
text = detail.duration,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
Text(
@ -246,7 +286,10 @@ private fun SpellDetailContent(
)
Text(
style = MaterialTheme.typography.bodyMedium,
text = detail.description,
text = annotate(
text = detail.description,
highlightRegex styleWith typography.detail.highlightStyle,
),
)
}
}
@ -280,6 +323,7 @@ private fun SpellDetailPreview() {
)
)
},
highlight = "créature cure a e Vv",
onBack = { },
)
}

View file

@ -8,6 +8,7 @@ import androidx.lifecycle.viewModelScope
import com.pixelized.rplexicon.data.repository.character.DescriptionRepository
import com.pixelized.rplexicon.data.repository.character.SpellRepository
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
import com.pixelized.rplexicon.ui.navigation.screens.lexiconDetailArgument
import com.pixelized.rplexicon.ui.navigation.screens.spellDetailArgument
import com.pixelized.rplexicon.utilitary.extentions.local.icon
import com.pixelized.rplexicon.utilitary.extentions.local.label
@ -23,6 +24,8 @@ class SpellDetailViewModel @Inject constructor(
descriptionRepository: DescriptionRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel() {
val highlight: String? = savedStateHandle.spellDetailArgument.highlight
val spell: State<SpellDetailUio?>
val error: Flow<FetchErrorUio>
@ -31,24 +34,22 @@ class SpellDetailViewModel @Inject constructor(
val description = descriptionRepository.find(
name = argument.spell,
)
val spell = spellRepository.find(
character = argument.character,
spell = argument.spell,
val spell = spellRepository.findSpell(
name = argument.spell,
)
val assignedSpell = if (spell != null && description != null) {
SpellDetailUio(
icon = spell.spell.school.icon,
name = spell.spell.name,
icon = spell.school.icon,
name = spell.name,
translated = description.original,
level = "${spell.spell.level}",
school = spell.spell.school.label,
castingTime = spell.spell.castingTime,
range = spell.spell.range,
requirement = spell.spell.requirement,
duration = spell.spell.duration,
level = "${spell.level}",
school = spell.school.label,
castingTime = spell.castingTime,
range = spell.range,
requirement = spell.requirement,
duration = spell.duration,
description = description.description,
ritual = spell.spell.ritual,
ritual = spell.ritual,
)
} else {
null

View file

@ -53,6 +53,7 @@
<string name="search_category_prefix_lexicon">Lexicon</string>
<string name="search_category_prefix_quest">Quest</string>
<string name="search_category_prefix_location">Location</string>
<string name="search_category_prefix_spell">Spell</string>
<string name="search_lexicon_item_status">Status:</string>
<string name="search_lexicon_item_location">Location:</string>
<string name="search_lexicon_item_description">Description:</string>
@ -65,6 +66,13 @@
<string name="search_quest_item_description">Description:</string>
<string name="search_location_item_description">Description:</string>
<string name="search_location_item_destination">Destination:</string>
<string name="search_spell_item_level">Level:</string>
<string name="search_spell_item_school">School:</string>
<string name="search_spell_item_castingTime">Casting Time:</string>
<string name="search_spell_item_range">Range:</string>
<string name="search_spell_item_requirement">Components:</string>
<string name="search_spell_item_duration">Duration:</string>
<string name="search_spell_item_description">Description:</string>
<string name="quest_detail_title">Quest details</string>
<string name="quest_detail_completed">Completed</string>