Category management - lexicon
This commit is contained in:
parent
020af02c29
commit
fa7fcbeae6
24 changed files with 266 additions and 699 deletions
|
|
@ -7,39 +7,14 @@ import androidx.compose.runtime.Stable
|
|||
data class Lexicon(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val category: String?,
|
||||
val diminutive: String?,
|
||||
val gender: Gender,
|
||||
val race: Race,
|
||||
val gender: String?,
|
||||
val race: String?,
|
||||
val status: String?,
|
||||
val location: String?,
|
||||
val portrait: List<Uri>,
|
||||
val description: String?,
|
||||
val history: String?,
|
||||
val tags: String?,
|
||||
) {
|
||||
|
||||
@Stable
|
||||
enum class Gender {
|
||||
MALE,
|
||||
FEMALE,
|
||||
UNDETERMINED,
|
||||
}
|
||||
|
||||
@Stable
|
||||
enum class Race {
|
||||
ELF,
|
||||
HALFLING,
|
||||
HUMAN,
|
||||
DWARF,
|
||||
HALF_ELF,
|
||||
HALF_ORC,
|
||||
DRAGONBORN,
|
||||
GNOME,
|
||||
TIEFLING,
|
||||
AARAKOCRA,
|
||||
GENASI,
|
||||
DEEP_GNOME,
|
||||
GOLIATH,
|
||||
UNDETERMINED,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -6,7 +6,7 @@ import androidx.compose.runtime.Stable
|
|||
@Stable
|
||||
data class Quest(
|
||||
val id: String,
|
||||
val group: String?,
|
||||
val category: String?,
|
||||
val title: String,
|
||||
val entries: List<QuestEntry>,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
package com.pixelized.rplexicon.data.parser
|
||||
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import javax.inject.Inject
|
||||
|
||||
class GenderParser @Inject constructor() {
|
||||
|
||||
fun parse(gender: String?): Lexicon.Gender {
|
||||
return when (gender?.takeIf { it.isNotBlank() }) {
|
||||
Gender.MALE -> Lexicon.Gender.MALE
|
||||
Gender.FEMALE -> Lexicon.Gender.FEMALE
|
||||
else -> Lexicon.Gender.UNDETERMINED
|
||||
}
|
||||
}
|
||||
|
||||
private object Gender {
|
||||
const val MALE = "Male"
|
||||
const val FEMALE = "Femelle"
|
||||
}
|
||||
}
|
||||
|
|
@ -7,8 +7,6 @@ import javax.inject.Inject
|
|||
|
||||
class LexiconParser @Inject constructor(
|
||||
private val portraitParser: PortraitParser,
|
||||
private val genderParser: GenderParser,
|
||||
private val raceParser: RaceParser,
|
||||
) {
|
||||
@Throws(IncompatibleSheetStructure::class)
|
||||
fun parse(sheet: ValueRange): List<Lexicon> = parserScope {
|
||||
|
|
@ -26,9 +24,10 @@ class LexiconParser @Inject constructor(
|
|||
val lexicon = Lexicon(
|
||||
id = "$name-${ids[name]}",
|
||||
name = name,
|
||||
category = row.parse(column = CATEGORY),
|
||||
diminutive = row.parse(column = SHORT),
|
||||
gender = genderParser.parse(row.parse(column = GENDER)),
|
||||
race = raceParser.parser(row.parse(column = RACE)),
|
||||
gender = row.parse(column = GENDER),
|
||||
race = row.parse(column = RACE),
|
||||
status = row.parse(column = STATUS),
|
||||
location = row.parse(column = LOCATION),
|
||||
portrait = portraitParser.parse(row.parse(column = PORTRAIT)),
|
||||
|
|
@ -47,6 +46,7 @@ class LexiconParser @Inject constructor(
|
|||
|
||||
companion object {
|
||||
private val NAME = column("Nom")
|
||||
private val CATEGORY = column("Catégorie")
|
||||
private val SHORT = column("Diminutif")
|
||||
private val GENDER = column("Sexe")
|
||||
private val RACE = column("Race")
|
||||
|
|
@ -60,6 +60,7 @@ class LexiconParser @Inject constructor(
|
|||
private val COLUMNS
|
||||
get() = listOf(
|
||||
NAME,
|
||||
CATEGORY,
|
||||
SHORT,
|
||||
GENDER,
|
||||
RACE,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ class QuestParser @Inject constructor(
|
|||
val entry = QuestEntry(
|
||||
sheetIndex = index,
|
||||
title = quest,
|
||||
group = item.parse(column = GROUP),
|
||||
group = item.parse(column = CATEGORY),
|
||||
subtitle = item.parse(column = SUB_TITLE),
|
||||
complete = item.parseBool(column = COMPLETED) ?: false,
|
||||
questGiver = item.parse(column = QUEST_GIVER),
|
||||
|
|
@ -44,7 +44,7 @@ class QuestParser @Inject constructor(
|
|||
Quest(
|
||||
id = "$quest-1", // TODO refactor that when quest have ids in the google sheet.
|
||||
title = quest,
|
||||
group = relatedEntries.firstNotNullOfOrNull { it.group },
|
||||
category = relatedEntries.firstNotNullOfOrNull { it.group },
|
||||
entries = relatedEntries,
|
||||
)
|
||||
}
|
||||
|
|
@ -54,7 +54,7 @@ class QuestParser @Inject constructor(
|
|||
|
||||
companion object {
|
||||
private val TITLE = column("Titre")
|
||||
private val GROUP = column("Groupe")
|
||||
private val CATEGORY = column("Catégorie")
|
||||
private val SUB_TITLE = column("Sous Titre")
|
||||
private val COMPLETED = column("Compléter")
|
||||
private val QUEST_GIVER = column("Commanditaire")
|
||||
|
|
@ -68,7 +68,7 @@ class QuestParser @Inject constructor(
|
|||
private val COLUMNS
|
||||
get() = listOf(
|
||||
TITLE,
|
||||
GROUP,
|
||||
CATEGORY,
|
||||
SUB_TITLE,
|
||||
COMPLETED,
|
||||
QUEST_GIVER,
|
||||
|
|
|
|||
|
|
@ -1,42 +0,0 @@
|
|||
package com.pixelized.rplexicon.data.parser
|
||||
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import javax.inject.Inject
|
||||
|
||||
class RaceParser @Inject constructor() {
|
||||
|
||||
fun parser(race: String?): Lexicon.Race {
|
||||
return when (race?.takeIf { it.isNotBlank() }) {
|
||||
Race.ELF -> Lexicon.Race.ELF
|
||||
Race.HALFLING -> Lexicon.Race.HALFLING
|
||||
Race.HUMAN -> Lexicon.Race.HUMAN
|
||||
Race.DWARF -> Lexicon.Race.DWARF
|
||||
Race.HALF_ELF -> Lexicon.Race.HALF_ELF
|
||||
Race.HALF_ORC -> Lexicon.Race.HALF_ORC
|
||||
Race.DRAGONBORN -> Lexicon.Race.DRAGONBORN
|
||||
Race.GNOME -> Lexicon.Race.GNOME
|
||||
Race.TIEFLING -> Lexicon.Race.TIEFLING
|
||||
Race.AARAKOCRA -> Lexicon.Race.AARAKOCRA
|
||||
Race.GENASI -> Lexicon.Race.GENASI
|
||||
Race.DEEP_GNOME -> Lexicon.Race.DEEP_GNOME
|
||||
Race.GOLIATH -> Lexicon.Race.GOLIATH
|
||||
else -> Lexicon.Race.UNDETERMINED
|
||||
}
|
||||
}
|
||||
|
||||
private object Race {
|
||||
const val ELF = "Elfe"
|
||||
const val HALFLING = "Halfelin"
|
||||
const val HUMAN = "Humain"
|
||||
const val DWARF = "Nain"
|
||||
const val HALF_ELF = "Demi-Elfe"
|
||||
const val HALF_ORC = "Demi-Orc"
|
||||
const val DRAGONBORN = "Drakéide"
|
||||
const val GNOME = "Gnome"
|
||||
const val TIEFLING = "Tieffelin"
|
||||
const val AARAKOCRA = "Aarakocra"
|
||||
const val GENASI = "Génasi"
|
||||
const val DEEP_GNOME = "Gnome des Profondeurs"
|
||||
const val GOLIATH = "Goliath"
|
||||
}
|
||||
}
|
||||
|
|
@ -21,7 +21,7 @@ class CategoryOrderRepository @Inject constructor(
|
|||
var lastSuccessFullUpdate: Update = Update.INITIAL
|
||||
private set
|
||||
|
||||
fun finLexiconOrder(quest: String?): Int {
|
||||
fun findLexiconOrder(quest: String?): Int {
|
||||
return _data.value[LEXICON]?.get(quest) ?: Int.MAX_VALUE
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,176 +0,0 @@
|
|||
package com.pixelized.rplexicon.ui.composable.form
|
||||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
|
||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
||||
|
||||
@Stable
|
||||
data class DropDownFieldUio<T>(
|
||||
@StringRes val label: Int,
|
||||
val values: List<T>,
|
||||
val value: State<T?>,
|
||||
val valueLabel: @Composable (T) -> String,
|
||||
val onValueChange: (T?) -> Unit,
|
||||
) {
|
||||
companion object {
|
||||
fun <T> preview(
|
||||
@StringRes label: Int,
|
||||
id: T?,
|
||||
valueLabel: @Composable (T) -> String,
|
||||
) = DropDownFieldUio(
|
||||
label = label,
|
||||
values = emptyList(),
|
||||
value = mutableStateOf(id),
|
||||
valueLabel = valueLabel,
|
||||
onValueChange = { },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun <T> DropDownField(
|
||||
modifier: Modifier = Modifier,
|
||||
field: DropDownFieldUio<T>,
|
||||
) {
|
||||
var expended by remember(field) { mutableStateOf(false) }
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
modifier = modifier,
|
||||
expanded = expended,
|
||||
onExpandedChange = { expended = !expended && field.value.value == null },
|
||||
) {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.menuAnchor(),
|
||||
shape = MaterialTheme.lexicon.shapes.textField,
|
||||
readOnly = true,
|
||||
singleLine = true,
|
||||
label = {
|
||||
Text(
|
||||
text = stringResource(id = field.label)
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
AnimatedContent(
|
||||
modifier = Modifier.size(size = 48.dp),
|
||||
targetState = field.value.value != null,
|
||||
transitionSpec = { fadeIn() togetherWith fadeOut() },
|
||||
label = "DropDownFieldTrailingIconAnimation",
|
||||
) {
|
||||
when (it) {
|
||||
true -> IconButton(
|
||||
onClick = { field.onValueChange(null) },
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(size = 18.dp),
|
||||
painter = painterResource(id = R.drawable.ic_clear_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
|
||||
else -> Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.size(size = 24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_arrow_down_24),
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = TextFieldDefaults.colors(
|
||||
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||
),
|
||||
value = field.value.value?.let { field.valueLabel(it) } ?: "",
|
||||
onValueChange = {},
|
||||
)
|
||||
|
||||
DropdownMenu(
|
||||
expanded = expended,
|
||||
onDismissRequest = { expended = false },
|
||||
) {
|
||||
field.values.forEach {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
expended = false
|
||||
field.onValueChange(it)
|
||||
},
|
||||
text = {
|
||||
Text(text = field.valueLabel(it))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Preview(uiMode = UI_MODE_NIGHT_NO)
|
||||
@Preview(uiMode = UI_MODE_NIGHT_YES)
|
||||
private fun DropDownFieldPreview(
|
||||
@PreviewParameter(DropDownFieldPreviewProvider::class) preview: Lexicon.Race?,
|
||||
) {
|
||||
LexiconTheme {
|
||||
Surface {
|
||||
DropDownField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 8.dp),
|
||||
field = DropDownFieldUio(
|
||||
label = R.string.search_field_race,
|
||||
values = emptyList(),
|
||||
value = remember { mutableStateOf(preview) },
|
||||
valueLabel = { stringResource(id = it) },
|
||||
onValueChange = { },
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DropDownFieldPreviewProvider : PreviewParameterProvider<Lexicon.Race?> {
|
||||
override val values: Sequence<Lexicon.Race?> = sequenceOf(null, Lexicon.Race.HALF_ORC)
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ import androidx.navigation.NavHostController
|
|||
import androidx.navigation.NavOptionsBuilder
|
||||
import androidx.navigation.NavType
|
||||
import androidx.navigation.navArgument
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.ui.navigation.NavigationAnimation
|
||||
import com.pixelized.rplexicon.ui.navigation.animatedComposable
|
||||
import com.pixelized.rplexicon.ui.screens.lexicon.detail.LexiconDetailScreen
|
||||
|
|
@ -17,42 +16,21 @@ import com.pixelized.rplexicon.utilitary.extentions.ARG
|
|||
private const val ROUTE = "LexiconDetail"
|
||||
private const val ARG_ID = "id"
|
||||
private const val ARG_HIGHLIGHT = "highlight"
|
||||
private const val ARG_RACE = "race"
|
||||
private const val ARG_HIGHLIGHT_RACE = "highlightRace"
|
||||
private const val ARG_GENDER = "gender"
|
||||
private const val ARG_HIGHLIGHT_GENDER = "highlightGender"
|
||||
|
||||
val LEXICON_DETAIL_ROUTE = ROUTE +
|
||||
"?${ARG_ID.ARG}" +
|
||||
"&${ARG_HIGHLIGHT.ARG}" +
|
||||
"&${ARG_RACE.ARG}" +
|
||||
"&${ARG_HIGHLIGHT_RACE.ARG}" +
|
||||
"&${ARG_GENDER.ARG}" +
|
||||
"&${ARG_HIGHLIGHT_GENDER.ARG}"
|
||||
"&${ARG_HIGHLIGHT.ARG}"
|
||||
|
||||
@Stable
|
||||
@Immutable
|
||||
data class LexiconDetailArgument(
|
||||
val id: String,
|
||||
val highlight: String?,
|
||||
val race: Lexicon.Race,
|
||||
val highlightRace: Boolean,
|
||||
val gender: Lexicon.Gender,
|
||||
val highlightGender: Boolean,
|
||||
)
|
||||
|
||||
val SavedStateHandle.lexiconDetailArgument: LexiconDetailArgument
|
||||
get() = LexiconDetailArgument(
|
||||
id = get(ARG_ID)
|
||||
?: error("CharacterDetailArgument argument: $ARG_ID"),
|
||||
race = get(ARG_RACE)
|
||||
?: error("CharacterDetailArgument argument: $ARG_RACE"),
|
||||
highlightRace = get(ARG_HIGHLIGHT_RACE)
|
||||
?: error("CharacterDetailArgument argument: $ARG_HIGHLIGHT_RACE"),
|
||||
gender = get(ARG_GENDER)
|
||||
?: error("CharacterDetailArgument argument: $ARG_GENDER"),
|
||||
highlightGender = get(ARG_HIGHLIGHT_GENDER)
|
||||
?: error("CharacterDetailArgument argument: $ARG_HIGHLIGHT_GENDER"),
|
||||
id = get(ARG_ID) ?: error("CharacterDetailArgument argument: $ARG_ID"),
|
||||
highlight = get(ARG_HIGHLIGHT),
|
||||
)
|
||||
|
||||
|
|
@ -67,18 +45,6 @@ fun NavGraphBuilder.composableLexiconDetail() {
|
|||
type = NavType.StringType
|
||||
nullable = true
|
||||
},
|
||||
navArgument(name = ARG_RACE) {
|
||||
type = NavType.EnumType(Lexicon.Race::class.java)
|
||||
},
|
||||
navArgument(name = ARG_HIGHLIGHT_RACE) {
|
||||
type = NavType.BoolType
|
||||
},
|
||||
navArgument(name = ARG_GENDER) {
|
||||
type = NavType.EnumType(Lexicon.Gender::class.java)
|
||||
},
|
||||
navArgument(name = ARG_HIGHLIGHT_GENDER) {
|
||||
type = NavType.BoolType
|
||||
},
|
||||
),
|
||||
animation = NavigationAnimation.Push,
|
||||
) {
|
||||
|
|
@ -89,17 +55,11 @@ fun NavGraphBuilder.composableLexiconDetail() {
|
|||
fun NavHostController.navigateToLexiconDetail(
|
||||
id: String,
|
||||
highlight: String? = null,
|
||||
race: Lexicon.Race? = null,
|
||||
gender: Lexicon.Gender? = null,
|
||||
option: NavOptionsBuilder.() -> Unit = {},
|
||||
) {
|
||||
val route = ROUTE +
|
||||
"?$ARG_ID=$id" +
|
||||
"&$ARG_HIGHLIGHT=$highlight" +
|
||||
"&$ARG_RACE=${race ?: Lexicon.Race.UNDETERMINED}" +
|
||||
"&$ARG_HIGHLIGHT_RACE=${race != null}" +
|
||||
"&$ARG_GENDER=${gender ?: Lexicon.Gender.UNDETERMINED}" +
|
||||
"&$ARG_HIGHLIGHT_GENDER=${gender != null}"
|
||||
"&$ARG_HIGHLIGHT=$highlight"
|
||||
|
||||
navigate(route = route, builder = option)
|
||||
}
|
||||
|
|
@ -55,7 +55,6 @@ 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.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.ui.composable.AsyncImage
|
||||
import com.pixelized.rplexicon.ui.composable.BackgroundImage
|
||||
import com.pixelized.rplexicon.ui.composable.FullScreenImageHandler
|
||||
|
|
@ -63,7 +62,6 @@ import com.pixelized.rplexicon.ui.composable.FullScreenImageViewModel
|
|||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.navigateToCharacterSheet
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
|
||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
|
||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedString
|
||||
import com.pixelized.rplexicon.utilitary.extentions.highlightRegex
|
||||
|
|
@ -75,16 +73,14 @@ import com.pixelized.rplexicon.utilitary.extentions.searchCriterion
|
|||
data class LexiconDetailUio(
|
||||
val name: String,
|
||||
val diminutive: String?,
|
||||
val gender: Lexicon.Gender,
|
||||
val race: Lexicon.Race,
|
||||
val gender: String?,
|
||||
val race: String?,
|
||||
val status: String?,
|
||||
val location: String?,
|
||||
val portrait: List<Uri>,
|
||||
val description: String?,
|
||||
val history: String?,
|
||||
val search: String?,
|
||||
val highlightGender: Boolean?,
|
||||
val highlightRace: Boolean?,
|
||||
val tags: String?,
|
||||
)
|
||||
|
||||
|
|
@ -92,8 +88,8 @@ data class LexiconDetailUio(
|
|||
data class AnnotatedLexiconDetailUio(
|
||||
val name: AnnotatedString,
|
||||
val diminutive: AnnotatedString?,
|
||||
val gender: AnnotatedString,
|
||||
val race: AnnotatedString,
|
||||
val gender: AnnotatedString?,
|
||||
val race: AnnotatedString?,
|
||||
val portrait: List<Uri>,
|
||||
val status: AnnotatedString?,
|
||||
val location: AnnotatedString?,
|
||||
|
|
@ -110,10 +106,8 @@ fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio {
|
|||
val highlight = remember { SpanStyle(color = colorScheme.primary) }
|
||||
val trimmedSearch = remember(search) { search.searchCriterion() }
|
||||
val highlightRegex = remember(search) { trimmedSearch.highlightRegex }
|
||||
val gender = stringResource(id = gender, short = true)
|
||||
val race = stringResource(id = race)
|
||||
|
||||
return remember(search, race, highlightRace, gender, highlightGender) {
|
||||
return remember(search) {
|
||||
AnnotatedLexiconDetailUio(
|
||||
portrait = portrait,
|
||||
name = AnnotatedString(
|
||||
|
|
@ -132,20 +126,12 @@ fun LexiconDetailUio.annotate(): AnnotatedLexiconDetailUio {
|
|||
diminutive = diminutive?.let {
|
||||
highlightRegex.annotatedString(input = it, spanStyle = highlight)
|
||||
},
|
||||
gender = AnnotatedString(
|
||||
text = gender,
|
||||
spanStyles = when (highlightGender) {
|
||||
true -> listOf(AnnotatedString.Range(highlight, 0, gender.length))
|
||||
else -> emptyList()
|
||||
}
|
||||
),
|
||||
race = AnnotatedString(
|
||||
text = race,
|
||||
spanStyles = when (highlightRace) {
|
||||
true -> listOf(AnnotatedString.Range(highlight, 0, race.length))
|
||||
else -> emptyList()
|
||||
}
|
||||
),
|
||||
gender = gender?.let { gender ->
|
||||
highlightRegex.annotatedString(input = gender, spanStyle = highlight)
|
||||
},
|
||||
race = race?.let { race ->
|
||||
highlightRegex.annotatedString(input = race, spanStyle = highlight)
|
||||
},
|
||||
description = description?.let { description ->
|
||||
highlightRegex.annotatedString(input = description, spanStyle = highlight)
|
||||
},
|
||||
|
|
@ -457,8 +443,8 @@ private fun LexiconDetailPreview() {
|
|||
LexiconDetailUio(
|
||||
name = "Brulkhai",
|
||||
diminutive = "./ Bru",
|
||||
gender = Lexicon.Gender.FEMALE,
|
||||
race = Lexicon.Race.HALF_ORC,
|
||||
gender = "Female",
|
||||
race = "Half-orc",
|
||||
status = "Vivante",
|
||||
location = "Manoir Durst",
|
||||
portrait = listOf(
|
||||
|
|
@ -468,8 +454,6 @@ private fun LexiconDetailPreview() {
|
|||
history = null,
|
||||
tags = "protagoniste, brute",
|
||||
search = "Bru",
|
||||
highlightGender = true,
|
||||
highlightRace = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ class LexiconDetailViewModel @Inject constructor(
|
|||
history = source.history,
|
||||
tags = source.tags,
|
||||
search = argument.highlight,
|
||||
highlightGender = argument.highlightGender && argument.gender == source.gender,
|
||||
highlightRace = argument.highlightRace && argument.race == source.race,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
package com.pixelized.rplexicon.ui.screens.lexicon.list
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
||||
@Stable
|
||||
data class LexiconCategoryUio(
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun LexiconCategory(
|
||||
modifier: Modifier = Modifier,
|
||||
item: LexiconCategoryUio,
|
||||
) {
|
||||
Text(
|
||||
modifier = modifier,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Light,
|
||||
text = item.title,
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.pixelized.rplexicon.ui.screens.lexicon.list
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
|
||||
@Stable
|
||||
class LexiconGroupUio(
|
||||
val category: LexiconCategoryUio?,
|
||||
val items: List<LexiconItemUio>,
|
||||
)
|
||||
|
|
@ -2,10 +2,8 @@ package com.pixelized.rplexicon.ui.screens.lexicon.list
|
|||
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_NO
|
||||
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
|
|
@ -15,7 +13,6 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
|
@ -24,7 +21,6 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
import com.pixelized.rplexicon.utilitary.LOS_FULL
|
||||
import com.pixelized.rplexicon.utilitary.LOS_HOLLOW
|
||||
|
|
@ -37,8 +33,8 @@ data class LexiconItemUio(
|
|||
val id: String,
|
||||
val name: String,
|
||||
val diminutive: String?,
|
||||
@StringRes val gender: Int,
|
||||
@StringRes val race: Int,
|
||||
val gender: String?,
|
||||
val race: String?,
|
||||
val isPlayingCharacter: Boolean = false,
|
||||
val placeholder: Boolean = false,
|
||||
) {
|
||||
|
|
@ -48,8 +44,8 @@ data class LexiconItemUio(
|
|||
id: String = "Brulkhai-1",
|
||||
name: String = "Brulkhai",
|
||||
diminutive: String? = null,
|
||||
@StringRes gender: Int = R.string.gender_female_short,
|
||||
@StringRes race: Int = R.string.race_half_orc,
|
||||
gender: String? = null,
|
||||
race: String? = null,
|
||||
isPlayingCharacter: Boolean = false,
|
||||
placeholder: Boolean = false,
|
||||
) = LexiconItemUio(
|
||||
|
|
@ -122,27 +118,31 @@ fun LexiconItem(
|
|||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.placeholder { item.placeholder },
|
||||
style = typography.base.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontWeight = FontWeight.Light,
|
||||
maxLines = 1,
|
||||
text = stringResource(id = item.gender)
|
||||
)
|
||||
item.gender?.let {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.placeholder { item.placeholder },
|
||||
style = typography.base.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontWeight = FontWeight.Light,
|
||||
maxLines = 1,
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.placeholder { item.placeholder },
|
||||
style = typography.base.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontWeight = FontWeight.Light,
|
||||
maxLines = 1,
|
||||
text = stringResource(id = item.race)
|
||||
)
|
||||
item.race?.let {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.alignByBaseline()
|
||||
.placeholder { item.placeholder },
|
||||
style = typography.base.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
fontWeight = FontWeight.Light,
|
||||
maxLines = 1,
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ import com.pixelized.rplexicon.ui.composable.error.HandleFetchError
|
|||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconSearch
|
||||
import com.pixelized.rplexicon.ui.screens.quest.list.QuestCategory
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
import com.pixelized.rplexicon.utilitary.extentions.cell
|
||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
||||
|
|
@ -102,7 +103,7 @@ private fun LexiconScreenContent(
|
|||
refreshState: PullRefreshState,
|
||||
refreshing: State<Boolean>,
|
||||
isFabExpended: State<Boolean>,
|
||||
items: State<List<LexiconItemUio>>,
|
||||
items: State<List<LexiconGroupUio>>,
|
||||
onSearch: () -> Unit,
|
||||
onItem: (LexiconItemUio) -> Unit,
|
||||
) {
|
||||
|
|
@ -137,17 +138,31 @@ private fun LexiconScreenContent(
|
|||
state = lazyColumnState,
|
||||
contentPadding = MaterialTheme.lexicon.dimens.itemListPadding,
|
||||
) {
|
||||
items(
|
||||
items = items.value,
|
||||
key = { it.id },
|
||||
contentType = { "Lexicon" },
|
||||
) {
|
||||
LexiconItem(
|
||||
modifier = Modifier
|
||||
.clickable { onItem(it) }
|
||||
.cell(),
|
||||
item = it,
|
||||
)
|
||||
items.value.forEachIndexed { index, entry ->
|
||||
entry.category?.let {
|
||||
item(
|
||||
contentType = { "Header" },
|
||||
) {
|
||||
LexiconCategory(
|
||||
modifier = Modifier
|
||||
.padding(top = if (index == 0) 0.dp else 16.dp)
|
||||
.padding(horizontal = 16.dp),
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
items(
|
||||
items = entry.items,
|
||||
key = { it.id },
|
||||
contentType = { "Lexicon" },
|
||||
) {
|
||||
LexiconItem(
|
||||
modifier = Modifier
|
||||
.clickable { onItem(it) }
|
||||
.cell(),
|
||||
item = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -206,13 +221,18 @@ private fun LexiconScreenContentPreview() {
|
|||
items = remember {
|
||||
mutableStateOf(
|
||||
listOf(
|
||||
LexiconItemUio(
|
||||
id = "Brulkhai-1",
|
||||
name = "Brulkhai",
|
||||
diminutive = "Bru",
|
||||
gender = R.string.gender_female_short,
|
||||
race = R.string.race_half_orc,
|
||||
)
|
||||
LexiconGroupUio(
|
||||
category = null,
|
||||
items = listOf(
|
||||
LexiconItemUio(
|
||||
id = "Brulkhai-1",
|
||||
name = "Brulkhai",
|
||||
diminutive = "Bru",
|
||||
gender = "Female",
|
||||
race = "Half-orc",
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,36 +1,44 @@
|
|||
package com.pixelized.rplexicon.ui.screens.lexicon.list
|
||||
|
||||
import android.app.Application
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.data.repository.character.CharacterSheetRepository
|
||||
import com.pixelized.rplexicon.data.repository.lexicon.CategoryOrderRepository
|
||||
import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository
|
||||
import com.pixelized.rplexicon.ui.composable.error.FetchErrorUio
|
||||
import com.pixelized.rplexicon.ui.screens.quest.list.QuestListViewModel
|
||||
import com.pixelized.rplexicon.utilitary.exceptions.IncompatibleSheetStructure
|
||||
import com.pixelized.rplexicon.utilitary.extentions.context
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class LexiconViewModel @Inject constructor(
|
||||
private val lexiconRepository: LexiconRepository,
|
||||
private val characterSheetRepository: CharacterSheetRepository,
|
||||
) : ViewModel() {
|
||||
private val orderRepository: CategoryOrderRepository,
|
||||
application: Application,
|
||||
) : AndroidViewModel(application) {
|
||||
|
||||
private val _isLoading = mutableStateOf(false)
|
||||
val isLoading: State<Boolean> get() = _isLoading
|
||||
|
||||
private val _items = mutableStateOf<List<LexiconItemUio>>(emptyList())
|
||||
val items: State<List<LexiconItemUio>> get() = _items
|
||||
private val _items = mutableStateOf<List<LexiconGroupUio>>(emptyList())
|
||||
val items: State<List<LexiconGroupUio>> get() = _items
|
||||
|
||||
private val _error = MutableSharedFlow<FetchErrorUio>()
|
||||
val error: SharedFlow<FetchErrorUio> get() = _error
|
||||
|
|
@ -38,41 +46,36 @@ class LexiconViewModel @Inject constructor(
|
|||
init {
|
||||
viewModelScope.launch {
|
||||
launch {
|
||||
lexiconRepository.data.collect { items ->
|
||||
_items.value = items
|
||||
.sortedBy { it.name }
|
||||
.sortedBy { !characterSheetRepository.haveSheet(it.name) }
|
||||
.map { item ->
|
||||
LexiconItemUio(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
diminutive = item.diminutive?.takeIf { it.isNotBlank() }
|
||||
?.let { "./ $it" },
|
||||
gender = when (item.gender) {
|
||||
Lexicon.Gender.MALE -> R.string.gender_male_short
|
||||
Lexicon.Gender.FEMALE -> R.string.gender_female_short
|
||||
Lexicon.Gender.UNDETERMINED -> R.string.gender_undetermined_short
|
||||
orderRepository.data.combine(lexiconRepository.data) { _, lexicon -> lexicon }
|
||||
.collect { items ->
|
||||
_items.value = items
|
||||
.sortedBy { it.name }
|
||||
.groupBy(
|
||||
keySelector = {
|
||||
LexiconCategoryUio(
|
||||
title = it.category
|
||||
?: context.getString(R.string.default_category_other)
|
||||
)
|
||||
},
|
||||
race = when (item.race) {
|
||||
Lexicon.Race.ELF -> R.string.race_elf
|
||||
Lexicon.Race.HALFLING -> R.string.race_halfling
|
||||
Lexicon.Race.HUMAN -> R.string.race_human
|
||||
Lexicon.Race.DWARF -> R.string.race_dwarf
|
||||
Lexicon.Race.HALF_ELF -> R.string.race_half_elf
|
||||
Lexicon.Race.HALF_ORC -> R.string.race_half_orc
|
||||
Lexicon.Race.DRAGONBORN -> R.string.race_dragonborn
|
||||
Lexicon.Race.GNOME -> R.string.race_gnome
|
||||
Lexicon.Race.TIEFLING -> R.string.race_tiefling
|
||||
Lexicon.Race.AARAKOCRA -> R.string.race_aarakocra
|
||||
Lexicon.Race.GENASI -> R.string.race_genasi
|
||||
Lexicon.Race.DEEP_GNOME -> R.string.race_deep_gnome
|
||||
Lexicon.Race.GOLIATH -> R.string.race_goliath
|
||||
Lexicon.Race.UNDETERMINED -> R.string.race_undetermined
|
||||
valueTransform = { item ->
|
||||
LexiconItemUio(
|
||||
id = item.id,
|
||||
name = item.name,
|
||||
diminutive = item.diminutive?.let { "./ $it" },
|
||||
gender = item.gender,
|
||||
race = item.race,
|
||||
isPlayingCharacter = characterSheetRepository.haveSheet(item.name)
|
||||
)
|
||||
},
|
||||
isPlayingCharacter = characterSheetRepository.haveSheet(item.name)
|
||||
)
|
||||
}
|
||||
}
|
||||
.map { item ->
|
||||
LexiconGroupUio(
|
||||
category = item.key,
|
||||
items = item.value,
|
||||
)
|
||||
}
|
||||
.sortedBy { orderRepository.findLexiconOrder(quest = it.category?.title) }
|
||||
}
|
||||
}
|
||||
launch {
|
||||
updateLexicon(force = false)
|
||||
|
|
@ -81,15 +84,19 @@ class LexiconViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
suspend fun updateLexicon(force: Boolean) = coroutineScope {
|
||||
_isLoading.value = true
|
||||
val lexicon = async {
|
||||
fetchLexicon(force = force)
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = true
|
||||
}
|
||||
val sheets = async {
|
||||
fetchCharacterSheet(force = force)
|
||||
withContext(Dispatchers.IO) {
|
||||
awaitAll(
|
||||
async { fetchLexicon(force = force) },
|
||||
async { fetchCharacterSheet(force = force) },
|
||||
async { fetchCategoryOrder(force = force) },
|
||||
)
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = false
|
||||
}
|
||||
awaitAll(lexicon, sheets)
|
||||
_isLoading.value = false
|
||||
}
|
||||
|
||||
private suspend fun fetchLexicon(force: Boolean) {
|
||||
|
|
@ -128,6 +135,24 @@ class LexiconViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchCategoryOrder(force: Boolean) {
|
||||
try {
|
||||
if (force || orderRepository.lastSuccessFullUpdate.shouldUpdate()) {
|
||||
orderRepository.fetchCategoryOrder()
|
||||
}
|
||||
}
|
||||
// the data sheet structure is not as expected
|
||||
catch (exception: IncompatibleSheetStructure) {
|
||||
Log.e(QuestListViewModel.TAG, exception.message, exception)
|
||||
_error.emit(FetchErrorUio.Structure(type = FetchErrorUio.Structure.Type.CATEGORY_ORDER))
|
||||
}
|
||||
// default exception
|
||||
catch (exception: Exception) {
|
||||
Log.e(QuestListViewModel.TAG, exception.message, exception)
|
||||
_error.emit(FetchErrorUio.Default)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "LexiconViewModel"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,8 @@ import androidx.compose.animation.animateContentSize
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
|
|
@ -30,9 +27,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
|||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
|
||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedSpan
|
||||
import com.pixelized.rplexicon.utilitary.extentions.annotatedString
|
||||
import com.pixelized.rplexicon.utilitary.extentions.finderRegex
|
||||
|
|
@ -46,31 +41,27 @@ class SearchItemUio(
|
|||
val id: String,
|
||||
val name: String,
|
||||
val diminutive: String?,
|
||||
val gender: Lexicon.Gender,
|
||||
val race: Lexicon.Race,
|
||||
val gender: String?,
|
||||
val race: String?,
|
||||
val status: String?,
|
||||
val location: String?,
|
||||
val description: String?,
|
||||
val history: String?,
|
||||
val tags: String?,
|
||||
val search: String,
|
||||
val highlightGender: Boolean,
|
||||
val highlightRace: Boolean,
|
||||
) {
|
||||
companion object {
|
||||
fun preview(
|
||||
id: String = "Brulkhai-1",
|
||||
name: String = "Brulkhai",
|
||||
diminutive: String? = "Bru",
|
||||
gender: Lexicon.Gender = Lexicon.Gender.FEMALE,
|
||||
race: Lexicon.Race = Lexicon.Race.HALF_ORC,
|
||||
gender: String? = "Female",
|
||||
race: String? = "Half-Orc",
|
||||
description: String? = "Brulkhai, ou plus simplement Bru, est une demi-orc de 38 ans solidement bâti. Elle mesure 192 cm pour 110 kg de muscles lorsqu’elle est en bonne santé. Elle a les cheveux châtains, les yeux noisettes et la peau couleur gris-vert typique de son espèce. D’un tempérament taciturne, elle parle peu et de façon concise. Elle est parfois brutale, aussi bien physiquement que verbalement, Elle ne prend cependant aucun plaisir à malmener ceux qu’elle considère plus faibles qu’elle.\n" +
|
||||
"D’une nature simple et honnête, elle ne mâche pas ses mots et ne dissimule généralement pas ses pensées. Son intelligence modeste est plus le reflet d’un manque d’éducation et d’une capacité limitée à gérer ses émotions qu’à une débilité congénitale.\n" +
|
||||
"Elle voue à la force un culte car c’est par son expression qu’elle se sent vraiment vivante et éprouve de grandes difficultés vis à vis de ceux qu’elle nomme foshnu (bébé, chouineur en commun).",
|
||||
history: String? = null,
|
||||
search: String = " ",
|
||||
highlightGender: Boolean = false,
|
||||
highlightRace: Boolean = false,
|
||||
search: String = "",
|
||||
): SearchItemUio {
|
||||
return SearchItemUio(
|
||||
id = id,
|
||||
|
|
@ -84,8 +75,6 @@ class SearchItemUio(
|
|||
history = history,
|
||||
tags = null,
|
||||
search = search,
|
||||
highlightGender = highlightGender,
|
||||
highlightRace = highlightRace,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -96,8 +85,8 @@ class AnnotatedSearchItemUio(
|
|||
val id: String,
|
||||
val name: AnnotatedString,
|
||||
val diminutive: AnnotatedString?,
|
||||
val gender: AnnotatedString,
|
||||
val race: AnnotatedString,
|
||||
val gender: AnnotatedString?,
|
||||
val race: AnnotatedString?,
|
||||
val status: AnnotatedString?,
|
||||
val location: AnnotatedString?,
|
||||
val description: AnnotatedString?,
|
||||
|
|
@ -113,10 +102,8 @@ private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
|
|||
val trimmedSearch = remember(search) { search.searchCriterion() }
|
||||
val highlightRegex = remember(search) { trimmedSearch.highlightRegex }
|
||||
val finderRegex = remember(search) { trimmedSearch.finderRegex }
|
||||
val gender = stringResource(id = gender, short = true)
|
||||
val race = stringResource(id = race)
|
||||
|
||||
return remember(trimmedSearch, race, highlightRace, gender, highlightGender) {
|
||||
return remember(trimmedSearch) {
|
||||
AnnotatedSearchItemUio(
|
||||
id = id,
|
||||
name = AnnotatedString(
|
||||
|
|
@ -130,23 +117,11 @@ private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
|
|||
input = diminutive ?: "",
|
||||
spanStyle = highlight
|
||||
),
|
||||
gender = gender.let {
|
||||
AnnotatedString(
|
||||
text = it,
|
||||
spanStyles = when (highlightGender) {
|
||||
true -> listOf(AnnotatedString.Range(highlight, 0, it.length))
|
||||
else -> emptyList()
|
||||
}
|
||||
)
|
||||
gender = finderRegex?.foldAll(gender)?.let { gender ->
|
||||
highlightRegex?.annotatedString(gender, spanStyle = highlight)
|
||||
},
|
||||
race = race.let {
|
||||
AnnotatedString(
|
||||
text = it,
|
||||
spanStyles = when (highlightRace) {
|
||||
true -> listOf(AnnotatedString.Range(highlight, 0, it.length))
|
||||
else -> emptyList()
|
||||
}
|
||||
)
|
||||
race = finderRegex?.foldAll(race)?.let { race ->
|
||||
highlightRegex?.annotatedString(race, spanStyle = highlight)
|
||||
},
|
||||
status = finderRegex?.foldAll(status)?.let { status ->
|
||||
highlightRegex?.annotatedString(status, spanStyle = highlight)
|
||||
|
|
@ -167,7 +142,6 @@ private fun SearchItemUio.annotate(): AnnotatedSearchItemUio {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun SearchItem(
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -186,10 +160,10 @@ fun SearchItem(
|
|||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
||||
.animateContentSize(),
|
||||
verticalArrangement = Arrangement.spacedBy(6.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(space = 4.dp),
|
||||
) {
|
||||
FlowRow(
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(space = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
|
|
@ -206,26 +180,24 @@ fun SearchItem(
|
|||
text = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Row(
|
||||
modifier = Modifier.offset(y = (-2).dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
style = typography.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
maxLines = 1,
|
||||
text = annotatedItem.gender
|
||||
)
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
style = typography.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
maxLines = 1,
|
||||
text = annotatedItem.race
|
||||
)
|
||||
annotatedItem.gender?.let {
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
style = typography.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
maxLines = 1,
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
annotatedItem.race?.let {
|
||||
Text(
|
||||
modifier = Modifier.alignByBaseline(),
|
||||
style = typography.labelMedium,
|
||||
fontStyle = FontStyle.Italic,
|
||||
maxLines = 1,
|
||||
text = it,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(
|
||||
|
|
@ -346,7 +318,7 @@ private class SearchItemPreviewProvider : PreviewParameterProvider<SearchItemUio
|
|||
SearchItemUio.preview(search = "bru"),
|
||||
SearchItemUio.preview(search = "Brulkhai"),
|
||||
SearchItemUio.preview(search = "elle"),
|
||||
SearchItemUio.preview(highlightGender = true),
|
||||
SearchItemUio.preview(highlightRace = true),
|
||||
SearchItemUio.preview(search = "female"),
|
||||
SearchItemUio.preview(search = "orc"),
|
||||
)
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
|
|
@ -41,23 +40,17 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.pixelized.rplexicon.NO_WINDOW_INSETS
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.ui.composable.CollapsingHeader
|
||||
import com.pixelized.rplexicon.ui.composable.form.DropDownField
|
||||
import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
|
||||
import com.pixelized.rplexicon.ui.composable.form.TextField
|
||||
import com.pixelized.rplexicon.ui.composable.form.TextFieldUio
|
||||
import com.pixelized.rplexicon.ui.navigation.LocalScreenNavHost
|
||||
import com.pixelized.rplexicon.ui.navigation.screens.navigateToLexiconDetail
|
||||
import com.pixelized.rplexicon.ui.theme.LexiconTheme
|
||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
|
||||
import com.pixelized.rplexicon.utilitary.extentions.lexicon
|
||||
|
||||
@Stable
|
||||
data class SearchFormUio(
|
||||
val search: TextFieldUio,
|
||||
val gender: DropDownFieldUio<Lexicon.Gender>,
|
||||
val race: DropDownFieldUio<Lexicon.Race>,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
@ -81,8 +74,6 @@ fun SearchScreen(
|
|||
screen.navigateToLexiconDetail(
|
||||
id = item.id,
|
||||
highlight = form.search.value.value.takeIf { it.isNotEmpty() },
|
||||
race = form.race.value.value,
|
||||
gender = form.gender.value.value,
|
||||
)
|
||||
},
|
||||
onBack = {
|
||||
|
|
@ -172,19 +163,6 @@ private fun SearchBox(
|
|||
.padding(horizontal = 16.dp),
|
||||
field = form.search,
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
DropDownField(
|
||||
modifier = Modifier.weight(weight = 1f, fill = true),
|
||||
field = form.gender,
|
||||
)
|
||||
DropDownField(
|
||||
modifier = Modifier.weight(weight = 1f, fill = true),
|
||||
field = form.race,
|
||||
)
|
||||
}
|
||||
Divider(
|
||||
modifier = Modifier.padding(top = 16.dp),
|
||||
color = MaterialTheme.lexicon.colorScheme.placeholder,
|
||||
|
|
@ -203,16 +181,6 @@ private fun SearchScreenContentPreview() {
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
form = SearchFormUio(
|
||||
search = TextFieldUio.preview(R.string.search_field_search),
|
||||
gender = DropDownFieldUio.preview(
|
||||
id = Lexicon.Gender.FEMALE,
|
||||
label = R.string.search_field_gender,
|
||||
valueLabel = { stringResource(id = Lexicon.Gender.FEMALE) },
|
||||
),
|
||||
race = DropDownFieldUio.preview(
|
||||
id = null,
|
||||
label = R.string.search_field_race,
|
||||
valueLabel = { stringResource(id = Lexicon.Race.HALF_ORC) },
|
||||
),
|
||||
),
|
||||
items = remember {
|
||||
mutableStateOf(
|
||||
|
|
@ -221,36 +189,36 @@ private fun SearchScreenContentPreview() {
|
|||
id = "Brulkhai-1",
|
||||
name = "Brulkhai",
|
||||
diminutive = "Bru",
|
||||
gender = Lexicon.Gender.FEMALE,
|
||||
race = Lexicon.Race.HALF_ORC,
|
||||
gender = "Female",
|
||||
race = "Half-orc",
|
||||
),
|
||||
SearchItemUio.preview(
|
||||
id = "Léandre-1",
|
||||
name = "Léandre",
|
||||
diminutive = null,
|
||||
gender = Lexicon.Gender.MALE,
|
||||
race = Lexicon.Race.HUMAN,
|
||||
gender = "Male",
|
||||
race = "Human",
|
||||
),
|
||||
SearchItemUio.preview(
|
||||
id = "Nélia-1",
|
||||
name = "Nélia",
|
||||
diminutive = null,
|
||||
gender = Lexicon.Gender.FEMALE,
|
||||
race = Lexicon.Race.ELF,
|
||||
gender = "Female",
|
||||
race = "Elf",
|
||||
),
|
||||
SearchItemUio.preview(
|
||||
id = "Tigrane-1",
|
||||
name = "Tigrane",
|
||||
diminutive = null,
|
||||
gender = Lexicon.Gender.MALE,
|
||||
race = Lexicon.Race.TIEFLING,
|
||||
gender = "Male",
|
||||
race = "Tiefling",
|
||||
),
|
||||
SearchItemUio.preview(
|
||||
id = "Unathana-1",
|
||||
name = "Unathana",
|
||||
diminutive = "Una",
|
||||
gender = Lexicon.Gender.FEMALE,
|
||||
race = Lexicon.Race.HALF_ELF,
|
||||
gender = "Female",
|
||||
race = "Half-elf",
|
||||
),
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.lifecycle.ViewModel
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import com.pixelized.rplexicon.data.model.Lexicon.Gender
|
||||
import com.pixelized.rplexicon.data.model.Lexicon.Race
|
||||
import com.pixelized.rplexicon.data.repository.lexicon.LexiconRepository
|
||||
import com.pixelized.rplexicon.ui.composable.form.DropDownFieldUio
|
||||
import com.pixelized.rplexicon.ui.composable.form.TextFieldUio
|
||||
import com.pixelized.rplexicon.utilitary.composable.stringResource
|
||||
import com.pixelized.rplexicon.utilitary.extentions.searchCriterion
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import javax.inject.Inject
|
||||
|
|
@ -20,8 +16,6 @@ class SearchViewModel @Inject constructor(
|
|||
repository: LexiconRepository,
|
||||
) : ViewModel() {
|
||||
private val _search = mutableStateOf("")
|
||||
private val _gender = mutableStateOf<Gender?>(null)
|
||||
private val _race = mutableStateOf<Race?>(null)
|
||||
|
||||
val form = SearchFormUio(
|
||||
search = TextFieldUio(
|
||||
|
|
@ -31,39 +25,25 @@ class SearchViewModel @Inject constructor(
|
|||
_search.value = it
|
||||
}
|
||||
),
|
||||
gender = DropDownFieldUio(
|
||||
label = R.string.search_field_gender,
|
||||
values = Lexicon.Gender.values().toList(),
|
||||
value = _gender,
|
||||
valueLabel = { stringResource(id = it) },
|
||||
onValueChange = { id -> _gender.value = id },
|
||||
),
|
||||
race = DropDownFieldUio(
|
||||
label = R.string.search_field_race,
|
||||
values = Lexicon.Race.values().toList(),
|
||||
value = _race,
|
||||
valueLabel = { stringResource(id = it) },
|
||||
onValueChange = { id -> _race.value = id },
|
||||
),
|
||||
)
|
||||
|
||||
private var data: List<Lexicon> = repository.data.value
|
||||
|
||||
val filter = derivedStateOf {
|
||||
data.filter { item ->
|
||||
val gender = _gender.value?.let { it == item.gender }
|
||||
val race = _race.value?.let { it == item.race }
|
||||
val search = _search.value.searchCriterion().map { criteria ->
|
||||
val name = item.name.contains(criteria, true)
|
||||
val gender = item.gender?.contains(criteria, true) == true
|
||||
val race = item.race?.contains(criteria, true) == true
|
||||
val diminutive = item.diminutive?.contains(criteria, true) == true
|
||||
val status = item.status?.contains(criteria, true) == true
|
||||
val location = item.location?.contains(criteria, true) == true
|
||||
val description = item.description?.contains(criteria, true) == true
|
||||
val history = item.history?.contains(criteria, true) == true
|
||||
val tag = item.tags?.contains(criteria, true) == true
|
||||
name || diminutive || status || location || description || history || tag
|
||||
name || gender || race || diminutive || status || location || description || history || tag
|
||||
}
|
||||
(gender == null || gender) && (race == null || race) && (search.all { it })
|
||||
search.all { it }
|
||||
}.map {
|
||||
it.toSearchUio()
|
||||
}.sortedBy {
|
||||
|
|
@ -73,8 +53,6 @@ class SearchViewModel @Inject constructor(
|
|||
|
||||
private fun Lexicon.toSearchUio(
|
||||
search: String = _search.value,
|
||||
highlightGender: Boolean = this.gender == _gender.value,
|
||||
highlightRace: Boolean = this.race == _race.value,
|
||||
) = SearchItemUio(
|
||||
id = this.id,
|
||||
name = this.name,
|
||||
|
|
@ -86,8 +64,6 @@ class SearchViewModel @Inject constructor(
|
|||
description = this.description,
|
||||
history = this.history,
|
||||
search = search,
|
||||
highlightGender = highlightGender,
|
||||
highlightRace = highlightRace,
|
||||
tags = tags,
|
||||
)
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.SharedFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
|
|
@ -51,7 +52,7 @@ class QuestListViewModel @Inject constructor(
|
|||
.groupBy(
|
||||
keySelector = {
|
||||
QuestCategoryUio(
|
||||
title = it.group
|
||||
title = it.category
|
||||
?: context.getString(R.string.default_category_other),
|
||||
)
|
||||
},
|
||||
|
|
@ -74,36 +75,27 @@ class QuestListViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = true
|
||||
}
|
||||
awaitAll(
|
||||
async { updateQuests(force = false) },
|
||||
async { updateCategoryOrder(force = false) },
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = false
|
||||
}
|
||||
update(force = false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun update(force: Boolean) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = true
|
||||
}
|
||||
suspend fun update(force: Boolean) = coroutineScope {
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = true
|
||||
}
|
||||
withContext(Dispatchers.IO) {
|
||||
awaitAll(
|
||||
async { updateQuests(force = force) },
|
||||
async { updateCategoryOrder(force = force) },
|
||||
async { fetchQuests(force = force) },
|
||||
async { fetchCategoryOrder(force = force) },
|
||||
)
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
_isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun updateQuests(force: Boolean) {
|
||||
private suspend fun fetchQuests(force: Boolean) {
|
||||
try {
|
||||
if (force || repository.lastSuccessFullUpdate.shouldUpdate()) {
|
||||
repository.fetchQuests()
|
||||
|
|
@ -121,7 +113,7 @@ class QuestListViewModel @Inject constructor(
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun updateCategoryOrder(force: Boolean) {
|
||||
private suspend fun fetchCategoryOrder(force: Boolean) {
|
||||
try {
|
||||
if (force || order.lastSuccessFullUpdate.shouldUpdate()) {
|
||||
order.fetchCategoryOrder()
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
package com.pixelized.rplexicon.utilitary.composable
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import androidx.compose.ui.res.stringResource as androidStringResource
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun stringResource(id: Lexicon.Gender, short: Boolean = false): String {
|
||||
return androidStringResource(
|
||||
id = when (short) {
|
||||
true -> when (id) {
|
||||
Lexicon.Gender.MALE -> R.string.gender_male_short
|
||||
Lexicon.Gender.FEMALE -> R.string.gender_female_short
|
||||
Lexicon.Gender.UNDETERMINED -> R.string.gender_undetermined_short
|
||||
}
|
||||
|
||||
else -> when (id) {
|
||||
Lexicon.Gender.MALE -> R.string.gender_male
|
||||
Lexicon.Gender.FEMALE -> R.string.gender_female
|
||||
Lexicon.Gender.UNDETERMINED -> R.string.gender_undetermined
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
package com.pixelized.rplexicon.utilitary.composable
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import com.pixelized.rplexicon.R
|
||||
import com.pixelized.rplexicon.data.model.Lexicon
|
||||
import androidx.compose.ui.res.stringResource as androidStringResource
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
fun stringResource(id: Lexicon.Race): String {
|
||||
return androidStringResource(
|
||||
id = when (id) {
|
||||
Lexicon.Race.ELF -> R.string.race_elf
|
||||
Lexicon.Race.HALFLING -> R.string.race_halfling
|
||||
Lexicon.Race.HUMAN -> R.string.race_human
|
||||
Lexicon.Race.DWARF -> R.string.race_dwarf
|
||||
Lexicon.Race.HALF_ELF -> R.string.race_half_elf
|
||||
Lexicon.Race.HALF_ORC -> R.string.race_half_orc
|
||||
Lexicon.Race.DRAGONBORN -> R.string.race_dragonborn
|
||||
Lexicon.Race.GNOME -> R.string.race_gnome
|
||||
Lexicon.Race.TIEFLING -> R.string.race_tiefling
|
||||
Lexicon.Race.AARAKOCRA -> R.string.race_aarakocra
|
||||
Lexicon.Race.GENASI -> R.string.race_genasi
|
||||
Lexicon.Race.DEEP_GNOME -> R.string.race_deep_gnome
|
||||
Lexicon.Race.GOLIATH -> R.string.race_goliath
|
||||
Lexicon.Race.UNDETERMINED -> R.string.race_undetermined
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -25,28 +25,6 @@
|
|||
<string name="generic_failure">Échec</string>
|
||||
<string name="generic_failure_critical">ÉCHEC CRITIQUE</string>
|
||||
|
||||
<string name="gender_male">Mâle</string>
|
||||
<string name="gender_female">Femelle</string>
|
||||
<string name="gender_undetermined">Indéterminé</string>
|
||||
<string name="gender_male_short">m.</string>
|
||||
<string name="gender_female_short">f.</string>
|
||||
<string name="gender_undetermined_short">i.</string>
|
||||
|
||||
<string name="race_elf">Elfe</string>
|
||||
<string name="race_halfling">Halfelin</string>
|
||||
<string name="race_human">Humain</string>
|
||||
<string name="race_dwarf">Nain</string>
|
||||
<string name="race_half_elf">Demi-Elfe</string>
|
||||
<string name="race_half_orc">Demi-Orc</string>
|
||||
<string name="race_dragonborn">Drakéide</string>
|
||||
<string name="race_gnome">Gnome</string>
|
||||
<string name="race_tiefling">Tieffelin</string>
|
||||
<string name="race_aarakocra">Aarakocra</string>
|
||||
<string name="race_genasi">Génasi</string>
|
||||
<string name="race_deep_gnome">Gnome des Profondeurs</string>
|
||||
<string name="race_goliath">Goliath</string>
|
||||
<string name="race_undetermined">Indéterminé</string>
|
||||
|
||||
<string name="spell_school_abjuration">Abjuration</string>
|
||||
<string name="spell_school_divination">Divination</string>
|
||||
<string name="spell_school_enchantment">Enchantement</string>
|
||||
|
|
|
|||
|
|
@ -25,28 +25,6 @@
|
|||
<string name="generic_failure">Failure</string>
|
||||
<string name="generic_failure_critical">CRITICAL FAILURE</string>
|
||||
|
||||
<string name="gender_male">Male</string>
|
||||
<string name="gender_female">Female</string>
|
||||
<string name="gender_undetermined">Undetermined</string>
|
||||
<string name="gender_male_short">m.</string>
|
||||
<string name="gender_female_short">f.</string>
|
||||
<string name="gender_undetermined_short">u.</string>
|
||||
|
||||
<string name="race_elf">Elf</string>
|
||||
<string name="race_halfling">Halfling</string>
|
||||
<string name="race_human">Human</string>
|
||||
<string name="race_dwarf">Dwarf</string>
|
||||
<string name="race_half_elf">Half-elf</string>
|
||||
<string name="race_half_orc">Half-Orc</string>
|
||||
<string name="race_dragonborn">Dragonborn</string>
|
||||
<string name="race_gnome">Gnome</string>
|
||||
<string name="race_tiefling">Tiefling</string>
|
||||
<string name="race_aarakocra">Aarakocra</string>
|
||||
<string name="race_genasi">Genasi</string>
|
||||
<string name="race_deep_gnome">Deep Gnome</string>
|
||||
<string name="race_goliath">Goliath</string>
|
||||
<string name="race_undetermined">Undetermined</string>
|
||||
|
||||
<string name="spell_school_abjuration">Abjuration</string>
|
||||
<string name="spell_school_divination">Divination</string>
|
||||
<string name="spell_school_enchantment">Enchantment</string>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue